🔥 Remove Python 2 datetime compat fallbacks

This patch marks the `ansible.module_utils.compat.datetime` module as deprecated, including `UTC`, `utcfromtimestamp()`  and `utcnow` shims that it provides, scheduling its removal for v2.21.

It also replaces any uses of the compatibility helpers with non-deprecated calls to CPython stdlib.

PR #81874

Co-authored-by: Matt Clay <matt@mystile.com>
pull/84812/head
🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) 10 months ago committed by GitHub
parent 50b4e0d279
commit df08ed3ef3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,11 @@
---
deprecated_features:
- >-
``ansible.module_utils.compat.datetime`` - The datetime compatibility
shims are now deprecated. They are scheduled to be removed in
``ansible-core`` v2.21. This includes ``UTC``, ``utcfromtimestamp()``
and ``utcnow`` importable from said module
(https://github.com/ansible/ansible/pull/81874).
...

@ -3,36 +3,48 @@
from __future__ import annotations
from ansible.module_utils.six import PY3
import datetime as _datetime
import typing as t
import datetime
from ansible.module_utils.common.warnings import deprecate
if PY3:
UTC = datetime.timezone.utc
else:
_ZERO = datetime.timedelta(0)
_UTC = _datetime.timezone.utc
class _UTC(datetime.tzinfo):
__slots__ = ()
def utcoffset(self, dt):
return _ZERO
def _utcfromtimestamp(timestamp: float) -> _datetime.datetime:
"""Construct an aware UTC datetime from a POSIX timestamp."""
return _datetime.datetime.fromtimestamp(timestamp, _UTC)
def dst(self, dt):
return _ZERO
def tzname(self, dt):
return "UTC"
def _utcnow() -> _datetime.datetime:
"""Construct an aware UTC datetime from time.time()."""
return _datetime.datetime.now(_UTC)
UTC = _UTC()
_deprecated_shims_map: dict[str, t.Callable[..., object] | _datetime.timezone] = {
'UTC': _UTC,
'utcfromtimestamp': _utcfromtimestamp,
'utcnow': _utcnow,
}
def utcfromtimestamp(timestamp): # type: (float) -> datetime.datetime
"""Construct an aware UTC datetime from a POSIX timestamp."""
return datetime.datetime.fromtimestamp(timestamp, UTC)
__all__ = tuple(_deprecated_shims_map)
def utcnow(): # type: () -> datetime.datetime
"""Construct an aware UTC datetime from time.time()."""
return datetime.datetime.now(UTC)
def __getattr__(importable_name: str) -> t.Callable[..., object] | _datetime.timezone:
"""Inject import-time deprecation warnings.
Specifically, for ``UTC``, ``utcfromtimestamp()`` and ``utcnow()``.
"""
try:
importable = _deprecated_shims_map[importable_name]
except KeyError as key_err:
raise AttributeError(f"module {__name__!r} has no attribute {key_err}") from None
deprecate(
msg=f'The `ansible.module_utils.compat.datetime.{importable_name}` '
'function is deprecated.',
version='2.21',
)
return importable

@ -22,7 +22,6 @@ import time
import ansible.module_utils.compat.typing as t
from ansible.module_utils.facts.collector import BaseFactCollector
from ansible.module_utils.compat.datetime import utcfromtimestamp
class DateTimeFactCollector(BaseFactCollector):
@ -36,7 +35,10 @@ class DateTimeFactCollector(BaseFactCollector):
# Store the timestamp once, then get local and UTC versions from that
epoch_ts = time.time()
now = datetime.datetime.fromtimestamp(epoch_ts)
utcnow = utcfromtimestamp(epoch_ts).replace(tzinfo=None)
utcnow = datetime.datetime.fromtimestamp(
epoch_ts,
tz=datetime.timezone.utc,
)
date_time_facts['year'] = now.strftime('%Y')
date_time_facts['month'] = now.strftime('%m')

@ -373,10 +373,10 @@ import re
import shutil
import tempfile
import traceback
from datetime import datetime, timezone
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six.moves.urllib.parse import urlsplit
from ansible.module_utils.compat.datetime import utcnow, utcfromtimestamp
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.urls import fetch_url, url_argument_spec
@ -399,10 +399,10 @@ def url_get(module, url, dest, use_proxy, last_mod_time, force, timeout=10, head
Return (tempfile, info about the request)
"""
start = utcnow()
start = datetime.now(timezone.utc)
rsp, info = fetch_url(module, url, use_proxy=use_proxy, force=force, last_mod_time=last_mod_time, timeout=timeout, headers=headers, method=method,
unredirected_headers=unredirected_headers, decompress=decompress, ciphers=ciphers, use_netrc=use_netrc)
elapsed = (utcnow() - start).seconds
elapsed = (datetime.now(timezone.utc) - start).seconds
if info['status'] == 304:
module.exit_json(url=url, dest=dest, changed=False, msg=info.get('msg', ''), status_code=info['status'], elapsed=elapsed)
@ -608,7 +608,7 @@ def main():
# If the file already exists, prepare the last modified time for the
# request.
mtime = os.path.getmtime(dest)
last_mod_time = utcfromtimestamp(mtime)
last_mod_time = datetime.fromtimestamp(mtime, timezone.utc)
# If the checksum does not match we have to force the download
# because last_mod_time may be newer than on remote
@ -616,11 +616,11 @@ def main():
force = True
# download to tmpsrc
start = utcnow()
start = datetime.now(timezone.utc)
method = 'HEAD' if module.check_mode else 'GET'
tmpsrc, info = url_get(module, url, dest, use_proxy, last_mod_time, force, timeout, headers, tmp_dest, method,
unredirected_headers=unredirected_headers, decompress=decompress, ciphers=ciphers, use_netrc=use_netrc)
result['elapsed'] = (utcnow() - start).seconds
result['elapsed'] = (datetime.now(timezone.utc) - start).seconds
result['src'] = tmpsrc
# Now the request has completed, we can finally generate the final

@ -438,12 +438,12 @@ import os
import re
import shutil
import tempfile
from datetime import datetime, timezone
from ansible.module_utils.basic import AnsibleModule, sanitize_keys
from ansible.module_utils.six import binary_type, iteritems, string_types
from ansible.module_utils.six.moves.urllib.parse import urlencode, urlsplit
from ansible.module_utils.common.text.converters import to_native, to_text
from ansible.module_utils.compat.datetime import utcnow, utcfromtimestamp
from ansible.module_utils.six.moves.collections_abc import Mapping, Sequence
from ansible.module_utils.urls import (
fetch_url,
@ -579,7 +579,10 @@ def uri(module, url, dest, body, body_format, method, headers, socket_timeout, c
kwargs = {}
if dest is not None and os.path.isfile(dest):
# if destination file already exist, only download if file newer
kwargs['last_mod_time'] = utcfromtimestamp(os.path.getmtime(dest))
kwargs['last_mod_time'] = datetime.fromtimestamp(
os.path.getmtime(dest),
tz=timezone.utc,
)
if module.params.get('follow_redirects') in ('no', 'yes'):
module.deprecate(
@ -693,12 +696,12 @@ def main():
module.exit_json(stdout="skipped, since '%s' does not exist" % removes, changed=False)
# Make the request
start = utcnow()
start = datetime.now(timezone.utc)
r, info = uri(module, url, dest, body, body_format, method,
dict_headers, socket_timeout, ca_path, unredirected_headers,
decompress, ciphers, use_netrc)
elapsed = (utcnow() - start).seconds
elapsed = (datetime.now(timezone.utc) - start).seconds
if r and dest is not None and os.path.isdir(dest):
filename = get_response_filename(r) or 'index.html'

@ -224,7 +224,6 @@ match_groupdict:
import binascii
import contextlib
import datetime
import errno
import math
import mmap
@ -234,11 +233,11 @@ import select
import socket
import time
import traceback
from datetime import datetime, timedelta, timezone
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.sys_info import get_platform_subclass
from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible.module_utils.compat.datetime import utcnow
HAS_PSUTIL = False
@ -532,7 +531,7 @@ def main():
except Exception:
module.fail_json(msg="unknown active_connection_state (%s) defined" % _connection_state, elapsed=0)
start = utcnow()
start = datetime.now(timezone.utc)
if delay:
time.sleep(delay)
@ -541,9 +540,9 @@ def main():
time.sleep(timeout)
elif state in ['absent', 'stopped']:
# first wait for the stop condition
end = start + datetime.timedelta(seconds=timeout)
end = start + timedelta(seconds=timeout)
while utcnow() < end:
while datetime.now(timezone.utc) < end:
if path:
try:
if not os.access(b_path, os.F_OK):
@ -560,7 +559,7 @@ def main():
# Conditions not yet met, wait and try again
time.sleep(module.params['sleep'])
else:
elapsed = utcnow() - start
elapsed = datetime.now(timezone.utc) - start
if port:
module.fail_json(msg=msg or "Timeout when waiting for %s:%s to stop." % (host, port), elapsed=elapsed.seconds)
elif path:
@ -568,15 +567,15 @@ def main():
elif state in ['started', 'present']:
# wait for start condition
end = start + datetime.timedelta(seconds=timeout)
while utcnow() < end:
end = start + timedelta(seconds=timeout)
while datetime.now(timezone.utc) < end:
if path:
try:
os.stat(b_path)
except OSError as e:
# If anything except file not present, throw an error
if e.errno != 2:
elapsed = utcnow() - start
elapsed = datetime.now(timezone.utc) - start
module.fail_json(msg=msg or "Failed to stat %s, %s" % (path, e.strerror), elapsed=elapsed.seconds)
# file doesn't exist yet, so continue
else:
@ -611,7 +610,9 @@ def main():
except IOError:
pass
elif port:
alt_connect_timeout = math.ceil(_timedelta_total_seconds(end - utcnow()))
alt_connect_timeout = math.ceil(
_timedelta_total_seconds(end - datetime.now(timezone.utc)),
)
try:
s = socket.create_connection((host, port), min(connect_timeout, alt_connect_timeout))
except Exception:
@ -622,8 +623,12 @@ def main():
if b_compiled_search_re:
b_data = b''
matched = False
while utcnow() < end:
max_timeout = math.ceil(_timedelta_total_seconds(end - utcnow()))
while datetime.now(timezone.utc) < end:
max_timeout = math.ceil(
_timedelta_total_seconds(
end - datetime.now(timezone.utc),
),
)
readable = select.select([s], [], [], max_timeout)[0]
if not readable:
# No new data. Probably means our timeout
@ -667,7 +672,7 @@ def main():
else: # while-else
# Timeout expired
elapsed = utcnow() - start
elapsed = datetime.now(timezone.utc) - start
if port:
if search_regex:
module.fail_json(msg=msg or "Timeout when waiting for search string %s in %s:%s" % (search_regex, host, port), elapsed=elapsed.seconds)
@ -681,19 +686,19 @@ def main():
elif state == 'drained':
# wait until all active connections are gone
end = start + datetime.timedelta(seconds=timeout)
end = start + timedelta(seconds=timeout)
tcpconns = TCPConnectionInfo(module)
while utcnow() < end:
while datetime.now(timezone.utc) < end:
if tcpconns.get_active_connections_count() == 0:
break
# Conditions not yet met, wait and try again
time.sleep(module.params['sleep'])
else:
elapsed = utcnow() - start
elapsed = datetime.now(timezone.utc) - start
module.fail_json(msg=msg or "Timeout when waiting for %s:%s to drain" % (host, port), elapsed=elapsed.seconds)
elapsed = utcnow() - start
elapsed = datetime.now(timezone.utc) - start
module.exit_json(state=state, port=port, search_regex=search_regex, match_groups=match_groups, match_groupdict=match_groupdict, path=path,
elapsed=elapsed.seconds)

@ -2,23 +2,29 @@ from __future__ import annotations
import datetime
from ansible.module_utils.compat.datetime import utcnow, utcfromtimestamp, UTC
import pytest
from ansible.module_utils.compat import datetime as compat_datetime
pytestmark = pytest.mark.usefixtures('capfd') # capture deprecation warnings
def test_utc():
assert UTC.tzname(None) == 'UTC'
assert UTC.utcoffset(None) == datetime.timedelta(0)
assert UTC.dst(None) is None
assert compat_datetime.UTC.tzname(None) == 'UTC'
assert compat_datetime.UTC.utcoffset(None) == datetime.timedelta(0)
assert compat_datetime.UTC.dst(None) is None
def test_utcnow():
assert utcnow().tzinfo is UTC
assert compat_datetime.utcnow().tzinfo is compat_datetime.UTC
def test_utcfometimestamp_zero():
dt = utcfromtimestamp(0)
dt = compat_datetime.utcfromtimestamp(0)
assert dt.tzinfo is UTC
assert dt.tzinfo is compat_datetime.UTC
assert dt.year == 1970
assert dt.month == 1
assert dt.day == 1

@ -5,16 +5,15 @@
from __future__ import annotations
import pytest
import datetime
import string
import time
from datetime import datetime, timezone
from ansible.module_utils.compat.datetime import UTC
from ansible.module_utils.facts.system import date_time
EPOCH_TS = 1594449296.123456
DT = datetime.datetime(2020, 7, 11, 12, 34, 56, 124356)
UTC_DT = datetime.datetime(2020, 7, 11, 2, 34, 56, 124356)
DT = datetime(2020, 7, 11, 12, 34, 56, 124356)
UTC_DT = datetime(2020, 7, 11, 2, 34, 56, 124356)
@pytest.fixture
@ -26,9 +25,13 @@ def fake_now(monkeypatch):
class FakeNow:
@classmethod
def fromtimestamp(cls, timestamp, tz=None):
if tz == UTC:
return UTC_DT.replace(tzinfo=tz)
def fromtimestamp(
cls: type[FakeNow],
timestamp: float,
tz: timezone | None = None,
) -> datetime:
if tz == timezone.utc:
return UTC_DT.replace(tzinfo=None)
return DT.replace(tzinfo=tz)
def _time():

Loading…
Cancel
Save