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)

--
To view, visit 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/934696?usp=email
To unsubscribe, or for help writing mail filters, visit 
https://gerrit.wikimedia.org/r/settings

Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: Ibbae55a4459155ea0f71f2fc2a033f7e4b513419
Gerrit-Change-Number: 934696
Gerrit-PatchSet: 29
Gerrit-Owner: Xqt <[email protected]>
Gerrit-Reviewer: Xqt <[email protected]>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged
_______________________________________________
Pywikibot-commits mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to