jenkins-bot has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/core/+/934696?usp=email )
Change subject: [Fix] datetime.utcnow() is deprecated; use Timestamp.nowutc() instead ......................................................................
[Fix] datetime.utcnow() is deprecated; use Timestamp.nowutc() instead
datetime.utcnow() is deprecated and datetime.now(datetime.UTC) should be used instead: https://docs.python.org/3.13/library/datetime.html#datetime.datetime.utcnow
- add new Timestamp method 'nowutc' which is a shorthand for Timestamp.now(datetime.UTC) - override Timestamp.utcnow() to ignore deprecation warnings because this behaviour is needed until other methods like fromISOformat returns a timestamp with timezone. - replace all datetime.utcnow() with Timestamp.utcnow() to omit deprecation warnings. - replace datetime.utcnow() with Timestamp.nowutc() if appropriate e.g. for the CachedRequest timestamp. - rename the api cache from apicache-py3 to apicache. Otherwise a TypeErrror can happen when mixing offset-naive and offset-aware datetimes - update tests, remove Python related test - remove experimental flag in github actions for Python 3.12
Bug: T337748 Change-Id: Ibbae55a4459155ea0f71f2fc2a033f7e4b513419 --- M .github/workflows/pywikibot-ci.yml M pywikibot/backports.py M pywikibot/bot.py M pywikibot/data/api/_requests.py M pywikibot/scripts/generate_user_files.py M pywikibot/site/_siteinfo.py M pywikibot/time.py M pywikibot/tools/__init__.py M scripts/clean_sandbox.py M scripts/dataextend.py M scripts/maintenance/cache.py M scripts/redirect.py M scripts/templatecount.py M setup.py M tests/__init__.py M tests/api_tests.py M tests/dry_api_tests.py M tests/time_tests.py 18 files changed, 286 insertions(+), 121 deletions(-)
Approvals: jenkins-bot: Verified Xqt: Looks good to me, approved
diff --git a/.github/workflows/pywikibot-ci.yml b/.github/workflows/pywikibot-ci.yml index ea447d1..276dc01 100644 --- a/.github/workflows/pywikibot-ci.yml +++ b/.github/workflows/pywikibot-ci.yml @@ -26,7 +26,7 @@ max-parallel: 17
matrix: - python-version: ["pypy3.7", "3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["pypy3.7", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] site: ['wikipedia:en', 'wikisource:zh'] test_prod_only: [true] include: @@ -51,18 +51,9 @@ site: wikidata:wikidata - python-version: 3.8 site: wowwiki:uk - - python-version: "3" # fails due to T337748 + - python-version: "3" site: wikipedia:de os: macOS-latest - experimental: true - - python-version: "3.12" # 3.12 fails due to T337748 - site: wikipedia:en - test_prod_only: true - experimental: true - - python-version: "3.12" # 3.12 fails due to T337748 - site: wikisource:zh - test_prod_only: true - experimental: true - python-version: "3.13-dev" site: wikipedia:test experimental: true diff --git a/pywikibot/backports.py b/pywikibot/backports.py index 5286439..155effd 100644 --- a/pywikibot/backports.py +++ b/pywikibot/backports.py @@ -15,6 +15,7 @@ from typing import Any
+# Placed here to omit circular import in tools PYTHON_VERSION = sys.version_info[:3] SPHINX_RUNNING = 'sphinx' in sys.modules
diff --git a/pywikibot/bot.py b/pywikibot/bot.py index e97c8be..04606a0 100644 --- a/pywikibot/bot.py +++ b/pywikibot/bot.py @@ -91,7 +91,6 @@ import atexit import codecs import configparser -import datetime import json import logging import logging.handlers @@ -444,10 +443,12 @@
def writelogheader() -> None: - """ - Save additional version, system and status info to the log file in use. + """Save additional version, system and status info to the log file in use.
This may help the user to track errors or report bugs. + + .. versionchanged:: 9.0 + ignore milliseconds with timestamp. """ log('') log(f'=== Pywikibot framework v{pywikibot.__version__} -- Logging header' @@ -457,7 +458,7 @@ log(f'COMMAND: {sys.argv}')
# script call time stamp - log(f'DATE: {datetime.datetime.utcnow()} UTC') + log(f'DATE: {pywikibot.Timestamp.nowutc()} UTC')
# new framework release/revision? (handle_args needs to be called first) try: diff --git a/pywikibot/data/api/_requests.py b/pywikibot/data/api/_requests.py index 4dd995d..ee9b4de 100644 --- a/pywikibot/data/api/_requests.py +++ b/pywikibot/data/api/_requests.py @@ -38,7 +38,7 @@ ) from pywikibot.login import LoginStatus from pywikibot.textlib import removeDisabledParts, removeHTMLParts -from pywikibot.tools import PYTHON_VERSION, deprecated +from pywikibot.tools import deprecated
__all__ = ('CachedRequest', 'Request', 'encode_url') @@ -1133,7 +1133,11 @@
class CachedRequest(Request):
- """Cached request.""" + """Cached request. + + .. versionchanged:: 9.0 + timestamp with timezone is used to determine expiry. + """
def __init__(self, expiry, *args, **kwargs) -> None: """Initialize a CachedRequest object. @@ -1162,10 +1166,12 @@
.. versionchanged:: 8.0 return a `pathlib.Path` object. + .. versionchanged:: 9.0 + remove Python main version from directoy name
:return: base directory path for cache entries """ - path = Path(config.base_dir, f'apicache-py{PYTHON_VERSION[0]:d}') + path = Path(config.base_dir, 'apicache') cls._make_dir(path) cls._get_cache_dir = classmethod(lambda c: path) # cache the result return path @@ -1226,7 +1232,8 @@ return CachedRequest._get_cache_dir() / self._create_file_name()
def _expired(self, dt): - return dt + self.expiry < datetime.datetime.utcnow() + """Check whether the timestamp is expired.""" + return dt + self.expiry < pywikibot.Timestamp.nowutc()
def _load_cache(self) -> bool: """Load cache entry for request, if available. @@ -1262,7 +1269,7 @@
def _write_cache(self, data) -> None: """Write data to self._cachefile_path().""" - data = (self._uniquedescriptionstr(), data, datetime.datetime.utcnow()) + data = self._uniquedescriptionstr(), data, pywikibot.Timestamp.nowutc() path = self._cachefile_path() with suppress(OSError), path.open('wb') as f: pickle.dump(data, f, protocol=config.pickle_protocol) diff --git a/pywikibot/scripts/generate_user_files.py b/pywikibot/scripts/generate_user_files.py index a58ebc3..2864f4b 100755 --- a/pywikibot/scripts/generate_user_files.py +++ b/pywikibot/scripts/generate_user_files.py @@ -26,9 +26,6 @@ from pywikibot.scripts import _import_with_no_user_config
-PYTHON_VERSION = sys.version_info[:2] - - # DISABLED_SECTIONS cannot be copied; variables must be set manually DISABLED_SECTIONS = { 'USER INTERFACE SETTINGS', # uses sys diff --git a/pywikibot/site/_siteinfo.py b/pywikibot/site/_siteinfo.py index 3283d54..5f58640 100644 --- a/pywikibot/site/_siteinfo.py +++ b/pywikibot/site/_siteinfo.py @@ -159,7 +159,7 @@ if 'query' in data: # If the request is a CachedRequest, use the _cachetime attr. cache_time = getattr( - request, '_cachetime', None) or datetime.datetime.utcnow() + request, '_cachetime', None) or pywikibot.Timestamp.nowutc() for prop in props: if prop in data['query']: self._post_process(prop, data['query'][prop]) @@ -176,7 +176,7 @@ return True
# cached date + expiry are in the past if it's expired - return cache_date + expire < datetime.datetime.utcnow() + return cache_date + expire < pywikibot.Timestamp.nowutc()
def _get_general(self, key: str, expiry): """ diff --git a/pywikibot/time.py b/pywikibot/time.py index 4761535..5df6e82 100644 --- a/pywikibot/time.py +++ b/pywikibot/time.py @@ -3,7 +3,7 @@ .. versionadded:: 7.5 """ # -# (C) Pywikibot team, 2007-2023 +# (C) Pywikibot team, 2007-2024 # # Distributed under the terms of the MIT license. # @@ -12,12 +12,18 @@ import datetime import math import re +import time as _time import types from contextlib import suppress from typing import overload
import pywikibot -from pywikibot.tools import classproperty, deprecated +from pywikibot.tools import ( + classproperty, + deprecated, + PYTHON_VERSION, + SPHINX_RUNNING, +)
__all__ = ( @@ -43,24 +49,29 @@
"""Class for handling MediaWiki timestamps.
- This inherits from datetime.datetime, so it can use all of the methods - and operations of a datetime object. To ensure that the results of any - operation are also a Timestamp object, be sure to use only Timestamp - objects (and datetime.timedeltas) in any operation. + This inherits from :python:`datetime.datetime + <library/datetime.html#datetime.datetime>`, so it can use all of + the methods and operations of a ``datetime`` object. To ensure that + the results of any operation are also a Timestamp object, be sure to + use only Timestamp objects (and :python:`datetime.timedelta + <library/datetime.html#datetime.timedelta>`) in any operation.
- Use Timestamp.fromISOformat() and Timestamp.fromtimestampformat() to - create Timestamp objects from MediaWiki string formats. - As these constructors are typically used to create objects using data - passed provided by site and page methods, some of which return a Timestamp - when previously they returned a MediaWiki string representation, these - methods also accept a Timestamp object, in which case they return a clone. + Use :meth:`Timestamp.fromISOformat` and + :meth:`Timestamp.fromtimestampformat` to create Timestamp objects + from MediaWiki string formats. As these constructors are typically + used to create objects using data passed provided by site and page + methods, some of which return a Timestamp when previously they + returned a MediaWiki string representation, these methods also + accept a Timestamp object, in which case they return a clone.
- Alternatively, Timestamp.set_timestamp() can create Timestamp objects from - Timestamp, datetime.datetime object, or strings compliant with ISO8601, - MW, or POSIX formats. + Alternatively, :meth:`Timestamp.set_timestamp` can create Timestamp + objects from :class:`Timestamp`, ``datetime.datetime`` object, or + strings compliant with ISO8601, MediaWiki, or POSIX formats.
- Use Site.server_time() for the current time; this is more reliable - than using Timestamp.utcnow(). + Use :meth:`Site.server_time() + <pywikibot.site._apisite.APISite.server_time>` for the current time; + this is more reliable than using :meth:`Timestamp.utcnow` or + :meth:`Timestamp.nowutc`.
.. versionchanged:: 7.5 moved to :mod:`time` module @@ -373,6 +384,120 @@ return self._from_datetime(newdt) return newdt
+ @classmethod + def nowutc(cls, *, with_tz: bool = True) -> 'Timestamp': + """Return the current UTC date and time. + + If *with_tz* is True it returns an aware :class:`Timestamp` + object with UTC timezone by calling ``now(datetime.UTC)``. As + such this is just a short way to get it. + + Otherwise the UTC timestamp is returned as a naive Timestamp + object with timezone of None. This is equal to the + :meth:`Timestamp.utcnow`. + + .. warning:: + Because naive datetime objects are treated by many datetime + methods as local times, it is preferred to use aware + Timestamps or datetimes to represent times in UTC. As such, + it is not recommended to set *with_tz* to False. + + .. caution:: + You cannot compare, add or subtract offset-naive and offset- + aware Timestamps/datetimes (i.e. missing or having timezone). + A TypeError will be raised in such cases. + + .. versionadded:: 9.0 + .. seealso:: + - :python:`datetime.now() + <library/datetime.html#datetime.datetime.now>` + - :python:`datetime.utcnow() + <library/datetime.html#datetime.datetime.utcnow>` + - :python:`datetime.UTC<library/datetime.html#datetime.UTC>` + + :param with_tz: Whether to include UTC timezone or not + """ + if with_tz: + # datetime.UTC can only be used with Python 3.11+ + return cls.now(datetime.timezone.utc) + + ts = _time.time() + gmt = _time.gmtime(ts) + us = round(ts % 1 * 1e6) + return cls(*gmt[:6], us) + + @classmethod + def utcnow(cls) -> 'Timestamp': + """Return the current UTC date and time, with `tzinfo` ``None``. + + This is like :meth:`Timestamp.now`, but returns the current UTC + date and time, as a naive :class:`Timestamp` object. An aware + current UTC datetime can be obtained by calling + :meth:`Timestamp.nowutc`. + + .. note:: This method is deprecated since Python 3.12 but held + here for backward compatibility because ``utcnow`` is widely + used inside the framework to compare MediaWiki timestamps + which are UTC-based. Neither :meth:`fromisoformatformat` + implementations of Python < 3.11 nor :class:`Timestamp`specific + :meth:`fromISOformat` supports timezone. + + .. warning:: + Because naive datetime objects are treated by many datetime + methods as local times, it is preferred to use aware + Timestamps or datetimes to represent times in UTC. As such, + the recommended way to create an object representing the + current time in UTC is by calling :meth:`Timestamp.nowutc`. + + .. hint:: + This method might be deprecated later. + + .. versionadded:: 9.0 + .. seealso:: + :python:`datetime.utcnow() + <library/datetime.html#datetime.datetime.utcnow>` + """ + if PYTHON_VERSION < (3, 12): + return super().utcnow() + return cls.nowutc(with_tz=False) + + if PYTHON_VERSION < (3, 8) or SPHINX_RUNNING: + # methods which does not upcast the right class if tz is given + # but return a datetime.datetime object + @classmethod + def fromtimestamp(cls, timestamp: int | float, tz=None) -> Timestamp: + """Return the local date and time corresponding to the POSIX ts. + + This class method is for Python 3.7 to upcast the class if a + tz is given. + + .. versionadded:: 9.0 + .. seealso:: + - :python:`datetime.fromtimestamp() + <library/datetime.html#datetime.datetime.fromtimestamp>` + """ + ts = super().fromtimestamp(timestamp, tz) + if tz: + ts = cls.set_timestamp(ts) + return ts + + @classmethod + def now(cls, tz=None) -> Timestamp: + """Return the current local date and time. + + This class method is for Python 3.7 to upcast the class if a + tz is given. + + .. versionadded:: 9.0 + .. seealso:: + - :python:`datetime.fromtimestamp() + <library/datetime.html#datetime.datetime.now>` + """ + ts = super().now(tz) + if tz: + ts = cls.set_timestamp(ts) + return ts +
class TZoneFixedOffset(datetime.tzinfo):
diff --git a/pywikibot/tools/__init__.py b/pywikibot/tools/__init__.py index f66ecd8..a5ddcd6 100644 --- a/pywikibot/tools/__init__.py +++ b/pywikibot/tools/__init__.py @@ -16,7 +16,7 @@ Import them from :mod:`tools.threading` instead. """ # -# (C) Pywikibot team, 2008-2023 +# (C) Pywikibot team, 2008-2024 # # Distributed under the terms of the MIT license. # @@ -32,7 +32,6 @@ import re import stat import subprocess -import sys from contextlib import suppress from functools import total_ordering, wraps from types import TracebackType @@ -42,7 +41,12 @@ import packaging.version
import pywikibot # T306760 -from pywikibot.backports import Callable, importlib_metadata +from pywikibot.backports import ( + Callable, + importlib_metadata, + PYTHON_VERSION, + SPHINX_RUNNING, +) from pywikibot.tools._deprecate import ( ModuleDeprecationWrapper, add_decorated_full_name, @@ -75,6 +79,7 @@
# other tools 'PYTHON_VERSION', + 'SPHINX_RUNNING', 'as_filename', 'is_ip_address', 'is_ip_network', @@ -95,9 +100,6 @@ )
-PYTHON_VERSION = sys.version_info[:3] - - def is_ip_address(value: str) -> bool: """Check if a value is a valid IPv4 or IPv6 address.
diff --git a/scripts/clean_sandbox.py b/scripts/clean_sandbox.py index 602ee4f..c0ff4d5 100755 --- a/scripts/clean_sandbox.py +++ b/scripts/clean_sandbox.py @@ -43,7 +43,7 @@ delay: 7 """ # -# (C) Pywikibot team, 2006-2023 +# (C) Pywikibot team, 2006-2024 # # Distributed under the terms of the MIT license. # @@ -260,7 +260,7 @@ pywikibot.info( 'Standard content was changed, sandbox cleaned.') else: - edit_delta = (datetime.datetime.utcnow() + edit_delta = (pywikibot.Timestamp.utcnow() - sandbox_page.latest_revision.timestamp) delta = self.delay_td - edit_delta # Is the last edit more than 'delay' minutes ago? diff --git a/scripts/dataextend.py b/scripts/dataextend.py index 3d6da3e..db5d136 100755 --- a/scripts/dataextend.py +++ b/scripts/dataextend.py @@ -944,7 +944,7 @@ min( datetime.datetime.now( ).strftime('%Y-%m-%d'), - datetime.datetime.utcnow() + pywikibot.Timestamp.nowutc() .strftime('%Y-%m-%d'))))
if not analyzer.showurl: @@ -1062,13 +1062,10 @@ date = None else: date = pywikibot.Claim(self.site, 'P813') - date.setTarget( - self.createdateclaim( - min( - datetime.datetime.now( - ).strftime('%Y-%m-%d'), - datetime.datetime.utcnow() - .strftime('%Y-%m-%d')))) + date.setTarget(self.createdateclaim( + min(datetime.datetime.now().strftime( + '%Y-%m-%d'), + pywikibot.Timestamp.nowutc().strftime('%Y-%m-%d')))) if not analyzer.showurl: url = None
diff --git a/scripts/maintenance/cache.py b/scripts/maintenance/cache.py index c84a904..c76097a 100755 --- a/scripts/maintenance/cache.py +++ b/scripts/maintenance/cache.py @@ -64,7 +64,7 @@ uniquedesc(entry) """ # -# (C) Pywikibot team, 2014-2023 +# (C) Pywikibot team, 2014-2024 # # Distributed under the terms of the MIT license. # @@ -230,13 +230,15 @@ check the filesystem mount options. You may need to remount with 'strictatime'.
+ .. versionchanged:: 9.0 + default cache path to 'apicache' without Python main version. + :param use_accesstime: Whether access times should be used. `None` for detect, `False` for don't use and `True` for always use. :param tests: Only process a test sample of files """ if not cache_path: - cache_path = os.path.join(pywikibot.config.base_dir, - f'apicache-py{PYTHON_VERSION[0]:d}') + cache_path = os.path.join(pywikibot.config.base_dir, 'apicache')
if not os.path.exists(cache_path): pywikibot.error(f'{cache_path}: no such file or directory') @@ -370,15 +372,29 @@
def older_than(entry, interval): """Find older entries.""" - if entry._cachetime + interval < datetime.datetime.utcnow(): + if entry._cachetime.tzinfo is not None \ + and entry._cachetime + interval < pywikibot.Timestamp.nowutc(): return entry + + # old apicache-py2/3 cache + if entry._cachetime.tzinfo is None \ + and entry._cachetime + interval < pywikibot.Timestamp.utcnow(): + return entry + return None
def newer_than(entry, interval): """Find newer entries.""" - if entry._cachetime + interval >= datetime.datetime.utcnow(): + if entry._cachetime.tzinfo is not None \ + and entry._cachetime + interval >= pywikibot.Timestamp.nowutc(): return entry + + # old apicache-py2/3 cache + if entry._cachetime.tzinfo is None \ + and entry._cachetime + interval >= pywikibot.Timestamp.utcnow(): + return entry + return None
diff --git a/scripts/redirect.py b/scripts/redirect.py index bffa83c..5305a29 100755 --- a/scripts/redirect.py +++ b/scripts/redirect.py @@ -349,7 +349,7 @@ # this will run forever, until user interrupts it if self.opt.offset <= 0: self.opt.offset = 1 - start = (datetime.datetime.utcnow() + start = (pywikibot.Timestamp.nowutc() - datetime.timedelta(0, self.opt.offset * 3600)) # self.opt.offset hours ago offset_time = start.strftime('%Y%m%d%H%M%S') diff --git a/scripts/templatecount.py b/scripts/templatecount.py index 527eac0..cd4d744 100755 --- a/scripts/templatecount.py +++ b/scripts/templatecount.py @@ -33,16 +33,14 @@
""" # -# (C) Pywikibot team, 2006-2022 +# (C) Pywikibot team, 2006-2024 # # Distributed under the terms of the MIT license. # from __future__ import annotations
-import datetime -from typing import Generator - import pywikibot +from pywikibot.backports import Generator
class TemplateCountRobot: @@ -72,8 +70,8 @@ pywikibot.stdout(formatstr.format(key, count)) total += count pywikibot.stdout(formatstr.format('TOTAL', total)) - pywikibot.stdout('Report generated on {}' - .format(datetime.datetime.utcnow().isoformat())) + pywikibot.stdout( + f'Report generated on {pywikibot.Timestamp.nowutc().isoformat()}')
@classmethod def list_templates(cls, templates, namespaces) -> None: @@ -99,8 +97,8 @@ pywikibot.stdout(page.title()) total += 1 pywikibot.info(f'Total page count: {total}') - pywikibot.stdout('Report generated on {}' - .format(datetime.datetime.utcnow().isoformat())) + pywikibot.stdout( + f'Report generated on {pywikibot.Timestamp.nowutc().isoformat()}')
@classmethod def template_dict(cls, templates, namespaces) -> dict[ diff --git a/setup.py b/setup.py index 3aa0946..e37e1f1 100755 --- a/setup.py +++ b/setup.py @@ -231,8 +231,9 @@ sys.exit( 'setuptools >= 40.1.0 is required to create a new distribution.') packages = find_namespace_packages(include=[name + '.*']) - with suppress(ValueError): - packages.remove(name + '.apicache-py3') + for cache_variant in ('', '-py3'): + with suppress(ValueError): + packages.remove(f'{name}.apicache{cache_variant}') return [str(name)] + packages
diff --git a/tests/__init__.py b/tests/__init__.py index 7d6c1fc..d8078a6 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -33,7 +33,6 @@ from pywikibot.backports import removesuffix from pywikibot.data.api import CachedRequest from pywikibot.data.api import Request as _original_Request -from pywikibot.tools import PYTHON_VERSION
_root_dir = os.path.split(os.path.split(__file__)[0])[0] @@ -56,8 +55,7 @@
join_root_path.path = 'root' join_tests_path = create_path_func(join_root_path, 'tests') -join_cache_path = create_path_func(join_tests_path, - f'apicache-py{PYTHON_VERSION[0]}') +join_cache_path = create_path_func(join_tests_path, 'apicache') join_data_path = create_path_func(join_tests_path, 'data') join_pages_path = create_path_func(join_tests_path, 'pages')
diff --git a/tests/api_tests.py b/tests/api_tests.py index 6da76fa..e07fed7 100755 --- a/tests/api_tests.py +++ b/tests/api_tests.py @@ -748,11 +748,12 @@ mysite = self.get_site() # Run tests on a missing page unique to this test run so it can # not be cached the first request, but will be cached after. - now = datetime.datetime.utcnow() - params = {'action': 'query', - 'prop': 'info', - 'titles': 'TestCachedRequest_test_internals ' + str(now), - } + now = pywikibot.time.Timestamp.nowutc() + params = { + 'action': 'query', + 'prop': 'info', + 'titles': 'TestCachedRequest_test_internals ' + str(now), + } req = api.CachedRequest(datetime.timedelta(minutes=10), site=mysite, parameters=params) rv = req._load_cache() @@ -770,6 +771,8 @@ self.assertTrue(rv) self.assertIsNotNone(req._data) self.assertIsNotNone(req._cachetime) + self.assertIsNotNone(req._cachetime.tzinfo) + self.assertEqual(req._cachetime.tzinfo, datetime.timezone.utc) self.assertGreater(req._cachetime, now) self.assertEqual(req._data, data)
diff --git a/tests/dry_api_tests.py b/tests/dry_api_tests.py index a948c95..f6cbfa3 100755 --- a/tests/dry_api_tests.py +++ b/tests/dry_api_tests.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """API tests which do not interact with a site.""" # -# (C) Pywikibot team, 2012-2023 +# (C) Pywikibot team, 2012-2024 # # Distributed under the terms of the MIT license. # @@ -21,7 +21,7 @@ from pywikibot.exceptions import Error from pywikibot.family import Family from pywikibot.login import LoginStatus -from pywikibot.tools import PYTHON_VERSION, suppress_warnings +from pywikibot.tools import suppress_warnings from tests import join_images_path from tests.aspects import ( DefaultDrySiteTestCase, @@ -80,7 +80,7 @@
def test_expired(self): """Test if the request is expired.""" - now = datetime.datetime.utcnow() + now = pywikibot.Timestamp.nowutc() self.assertFalse(self.req._expired(now)) self.assertTrue( self.req._expired(now - datetime.timedelta(days=2)), @@ -107,7 +107,7 @@ """Test that 'apicache' is in the cache dir.""" retval = self.req._get_cache_dir() self.assertIsInstance(retval, Path) - self.assertIn(f'apicache-py{PYTHON_VERSION[0]:d}', retval.parts) + self.assertIn('apicache', retval.parts)
def test_create_file_name(self): """Test the file names for the cache.""" diff --git a/tests/time_tests.py b/tests/time_tests.py index 696f15a..bf8db34 100755 --- a/tests/time_tests.py +++ b/tests/time_tests.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Tests for the Timestamp class.""" # -# (C) Pywikibot team, 2014-2023 +# (C) Pywikibot team, 2014-2024 # # Distributed under the terms of the MIT license. # @@ -11,7 +11,7 @@ import re import unittest from contextlib import suppress -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone
from pywikibot.time import Timestamp, parse_duration, str2timedelta from tests.aspects import TestCase @@ -65,18 +65,25 @@ }
def test_set_from_timestamp(self): - """Test creating instance from Timestamp string.""" - t1 = Timestamp.utcnow() - t2 = Timestamp.set_timestamp(t1) - self.assertEqual(t1, t2) - self.assertIsInstance(t2, Timestamp) + """Test creating instance from Timestamp object.""" + for func in Timestamp.utcnow, Timestamp.nowutc: + with self.subTest(func=func.__qualname__): + t1 = func() + t2 = Timestamp.set_timestamp(t1) + self.assertIs(t1, t2) + self.assertIsInstance(t2, Timestamp)
def test_set_from_datetime(self): - """Test creating instance from datetime.datetime string.""" - t1 = datetime.utcnow() - t2 = Timestamp.set_timestamp(t1) - self.assertEqual(t1, t2) - self.assertIsInstance(t2, datetime) + """Test creating instance from datetime.datetime object.""" + for tz in (None, timezone.utc): + with self.subTest(tzinfo=bool(tz)): + t1 = datetime.now(tz) + t2 = Timestamp.set_timestamp(t1) + self.assertEqual(t1, t2) + self.assertIsInstance(t1, datetime) + self.assertNotIsInstance(t1, Timestamp) + self.assertIsInstance(t2, Timestamp) + self.assertEqual(t2.tzinfo, tz)
@staticmethod def _compute_posix(timestr): @@ -119,17 +126,21 @@
def test_instantiate_from_instance(self): """Test passing instance to factory methods works.""" - t1 = Timestamp.utcnow() - self.assertIsNot(t1, Timestamp.fromISOformat(t1)) - self.assertEqual(t1, Timestamp.fromISOformat(t1)) - self.assertIsInstance(Timestamp.fromISOformat(t1), Timestamp) - self.assertIsNot(t1, Timestamp.fromtimestampformat(t1)) - self.assertEqual(t1, Timestamp.fromtimestampformat(t1)) - self.assertIsInstance(Timestamp.fromtimestampformat(t1), Timestamp) + for func in Timestamp.utcnow, Timestamp.nowutc: + with self.subTest(func=func.__qualname__): + t1 = func() + self.assertIsNot(t1, Timestamp.fromISOformat(t1)) + self.assertEqual(t1, Timestamp.fromISOformat(t1)) + self.assertIsInstance(Timestamp.fromISOformat(t1), Timestamp) + self.assertIsNot(t1, Timestamp.fromtimestampformat(t1)) + self.assertEqual(t1, Timestamp.fromtimestampformat(t1)) + self.assertIsInstance(Timestamp.fromtimestampformat(t1), + Timestamp)
def test_iso_format(self): """Test conversion from and to ISO format.""" sep = 'T' + # note: fromISOformat does not respect timezone t1 = Timestamp.utcnow() if not t1.microsecond: # T199179: ensure microsecond is not 0 t1 = t1.replace(microsecond=1) # pragma: no cover @@ -220,40 +231,29 @@
def test_add_timedelta(self): """Test addin a timedelta to a Timestamp.""" - t1 = Timestamp.utcnow() + t1 = Timestamp.nowutc() t2 = t1 + timedelta(days=1) if t1.month != t2.month: self.assertEqual(1, t2.day) else: self.assertEqual(t1.day + 1, t2.day) + self.assertIsInstance(t1, Timestamp) self.assertIsInstance(t2, Timestamp)
- def test_add_timedate(self): - """Test unsupported additions raise NotImplemented.""" - t1 = datetime.utcnow() - t2 = t1 + timedelta(days=1) - t3 = t1.__add__(t2) - self.assertIs(t3, NotImplemented) - - # Now check that the pywikibot sub-class behaves the same way - t1 = Timestamp.utcnow() - t2 = t1 + timedelta(days=1) - t3 = t1.__add__(t2) - self.assertIs(t3, NotImplemented) - def test_sub_timedelta(self): """Test subtracting a timedelta from a Timestamp.""" - t1 = Timestamp.utcnow() + t1 = Timestamp.nowutc() t2 = t1 - timedelta(days=1) if t1.month != t2.month: self.assertEqual(calendar.monthrange(t2.year, t2.month)[1], t2.day) else: self.assertEqual(t1.day - 1, t2.day) + self.assertIsInstance(t1, Timestamp) self.assertIsInstance(t2, Timestamp)
def test_sub_timedate(self): """Test subtracting two timestamps.""" - t1 = Timestamp.utcnow() + t1 = Timestamp.nowutc() t2 = t1 - timedelta(days=1) td = t1 - t2 self.assertIsInstance(td, timedelta)