jenkins-bot submitted this change.

View Change


Approvals: jenkins-bot: Verified Xqt: Looks good to me, approved
[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(-)

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 change 934696. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: Ibbae55a4459155ea0f71f2fc2a033f7e4b513419
Gerrit-Change-Number: 934696
Gerrit-PatchSet: 29
Gerrit-Owner: Xqt <info@gno.de>
Gerrit-Reviewer: Xqt <info@gno.de>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged