Replace pytz with standard library timezone (#16197)

Refactored code to use Python's built-in datetime.timezone and zoneinfo instead of pytz for timezone handling. This modernizes the codebase and removes the dependency on pytz, aligning with current best practices for timezone-aware datetime objects.
This commit is contained in:
Hao Liu 2026-01-09 16:05:08 -05:00 committed by GitHub
parent dbe979b425
commit fee71b8917
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 23 additions and 30 deletions

View File

@ -50,7 +50,7 @@ from rest_framework_yaml.renderers import YAMLRenderer
# ansi2html
from ansi2html import Ansi2HTMLConverter
import pytz
from datetime import timezone as dt_timezone
from wsgiref.util import FileWrapper
# django-ansible-base
@ -648,7 +648,7 @@ class SchedulePreview(GenericAPIView):
continue
schedule.append(event)
return Response({'local': schedule, 'utc': [s.astimezone(pytz.utc) for s in schedule]})
return Response({'local': schedule, 'utc': [s.astimezone(dt_timezone.utc) for s in schedule]})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

View File

@ -4,7 +4,6 @@
# Python
import datetime
import logging
import pytz
import re
@ -43,7 +42,7 @@ def partition_name_dt(part_name):
if not m:
return m
dt_str = f"{m.group(3)}_{m.group(4)}"
dt = datetime.datetime.strptime(dt_str, '%Y%m%d_%H').replace(tzinfo=pytz.UTC)
dt = datetime.datetime.strptime(dt_str, '%Y%m%d_%H').replace(tzinfo=datetime.timezone.utc)
return dt

View File

@ -24,8 +24,6 @@ from awx.main.models.jobs import LaunchTimeConfig
from awx.main.utils import ignore_inventory_computed_fields
from awx.main.consumers import emit_channel_notification
import pytz
logger = logging.getLogger('awx.main.models.schedule')
@ -255,7 +253,7 @@ class Schedule(PrimordialModel, LaunchTimeConfig):
# Coerce the datetime to UTC and format it as a string w/ Zulu format
# utc_until = UNTIL=20200601T220000Z
utc_until = 'UNTIL=' + localized_until.astimezone(pytz.utc).strftime('%Y%m%dT%H%M%SZ')
utc_until = 'UNTIL=' + localized_until.astimezone(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ')
# rule was: DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T170000
# rule is now: DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T220000Z
@ -310,7 +308,7 @@ class Schedule(PrimordialModel, LaunchTimeConfig):
# If we made it this far we should have an end date and can ask the ruleset what the last date is
# However, if the until/count is before dtstart we will get an IndexError when trying to get [-1]
try:
return ruleset[-1].astimezone(pytz.utc)
return ruleset[-1].astimezone(datetime.timezone.utc)
except IndexError:
return None
@ -328,14 +326,14 @@ class Schedule(PrimordialModel, LaunchTimeConfig):
if not datetime_exists(next_run_actual):
# skip imaginary dates, like 2:30 on DST boundaries
next_run_actual = future_rs.after(next_run_actual)
next_run_actual = next_run_actual.astimezone(pytz.utc)
next_run_actual = next_run_actual.astimezone(datetime.timezone.utc)
else:
next_run_actual = None
self.next_run = next_run_actual
if not self.dtstart:
try:
self.dtstart = future_rs[0].astimezone(pytz.utc)
self.dtstart = future_rs[0].astimezone(datetime.timezone.utc)
except IndexError:
self.dtstart = None
self.dtend = Schedule.get_end_date(future_rs)

View File

@ -1,11 +1,11 @@
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from contextlib import contextmanager
from zoneinfo import ZoneInfo
from django.utils.timezone import now
from django.db.utils import IntegrityError
from unittest import mock
import pytest
import pytz
from awx.main.models import JobTemplate, Schedule, ActivityStream
@ -76,7 +76,7 @@ class TestComputedFields:
with self.assert_no_unwanted_stuff(s):
# force update of next_run, as if schedule re-calculation had not happened
# since this time
old_next_run = datetime(2009, 3, 13, tzinfo=pytz.utc)
old_next_run = datetime(2009, 3, 13, tzinfo=timezone.utc)
Schedule.objects.filter(pk=s.pk).update(next_run=old_next_run)
s.next_run = old_next_run
prior_modified = s.modified
@ -259,7 +259,7 @@ def test_utc_until_in_the_past(job_template):
@pytest.mark.django_db
@mock.patch('awx.main.models.schedules.now', lambda: datetime(2030, 3, 5, tzinfo=pytz.utc))
@mock.patch('awx.main.models.schedules.now', lambda: datetime(2030, 3, 5, tzinfo=timezone.utc))
def test_dst_phantom_hour(job_template):
# The DST period in the United States begins at 02:00 (2 am) local time, so
# the hour from 2:00:00 to 2:59:59 does not exist in the night of the
@ -456,15 +456,15 @@ def test_skip_sundays():
RRULE:INTERVAL=1;FREQ=DAILY
EXRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=SU
'''
timezone = pytz.timezone("America/New_York")
friday_apr_29th = datetime(2022, 4, 29, 0, 0, 0, 0, timezone)
monday_may_2nd = datetime(2022, 5, 2, 23, 59, 59, 999, timezone)
tz = ZoneInfo("America/New_York")
friday_apr_29th = datetime(2022, 4, 29, 0, 0, 0, 0, tz)
monday_may_2nd = datetime(2022, 5, 2, 23, 59, 59, 999, tz)
ruleset = Schedule.rrulestr(rrule)
gen = ruleset.between(friday_apr_29th, monday_may_2nd, True)
# We should only get Fri, Sat and Mon (skipping Sunday)
assert len(list(gen)) == 3
saturday_night = datetime(2022, 4, 30, 23, 59, 59, 9999, timezone)
monday_morning = datetime(2022, 5, 2, 0, 0, 0, 0, timezone)
saturday_night = datetime(2022, 4, 30, 23, 59, 59, 9999, tz)
monday_morning = datetime(2022, 5, 2, 0, 0, 0, 0, tz)
gen = ruleset.between(saturday_night, monday_morning, True)
assert len(list(gen)) == 0
@ -476,17 +476,17 @@ def test_skip_sundays():
[
pytest.param(
'DTSTART;TZID=America/New_York:20210310T150000 RRULE:INTERVAL=1;FREQ=DAILY;UNTIL=20210430T150000Z EXRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=SU;COUNT=5',
datetime(2021, 4, 29, 19, 0, 0, tzinfo=pytz.utc),
datetime(2021, 4, 29, 19, 0, 0, tzinfo=timezone.utc),
id="Single rule in rule set with UTC TZ aware until",
),
pytest.param(
'DTSTART;TZID=America/New_York:20220310T150000 RRULE:INTERVAL=1;FREQ=DAILY;UNTIL=20220430T150000 EXRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=SU;COUNT=5',
datetime(2022, 4, 30, 19, 0, tzinfo=pytz.utc),
datetime(2022, 4, 30, 19, 0, tzinfo=timezone.utc),
id="Single rule in ruleset with naive until",
),
pytest.param(
'DTSTART;TZID=America/New_York:20220310T150000 RRULE:INTERVAL=1;FREQ=DAILY;COUNT=4 EXRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=SU;COUNT=5',
datetime(2022, 3, 12, 20, 0, tzinfo=pytz.utc),
datetime(2022, 3, 12, 20, 0, tzinfo=timezone.utc),
id="Single rule in ruleset with count",
),
pytest.param(
@ -501,12 +501,12 @@ def test_skip_sundays():
),
pytest.param(
'DTSTART;TZID=America/New_York:20220310T150000 RRULE:INTERVAL=1;FREQ=DAILY;UNTIL=20220430T150000Z',
datetime(2022, 4, 29, 19, 0, tzinfo=pytz.utc),
datetime(2022, 4, 29, 19, 0, tzinfo=timezone.utc),
id="Single rule in rule with UTZ TZ aware until",
),
pytest.param(
'DTSTART;TZID=America/New_York:20220310T150000 RRULE:INTERVAL=1;FREQ=DAILY;UNTIL=20220430T150000',
datetime(2022, 4, 30, 19, 0, tzinfo=pytz.utc),
datetime(2022, 4, 30, 19, 0, tzinfo=timezone.utc),
id="Single rule in rule with naive until",
),
pytest.param(
@ -521,12 +521,12 @@ def test_skip_sundays():
),
pytest.param(
'DTSTART;TZID=America/New_York:20220310T150000 RRULE:INTERVAL=1;FREQ=DAILY;BYDAY=SU;UNTIL=20220430T1500Z RRULE:INTERVAL=1;FREQ=DAILY;BYDAY=MO;COUNT=4',
datetime(2022, 4, 24, 19, 0, tzinfo=pytz.utc),
datetime(2022, 4, 24, 19, 0, tzinfo=timezone.utc),
id="Multi rule one with until and one with an count",
),
pytest.param(
'DTSTART;TZID=America/New_York:20010430T1500 RRULE:INTERVAL=1;FREQ=DAILY;BYDAY=SU;COUNT=1',
datetime(2001, 5, 6, 19, 0, tzinfo=pytz.utc),
datetime(2001, 5, 6, 19, 0, tzinfo=timezone.utc),
id="Rule with count but ends in the past",
),
pytest.param(

View File

@ -22,10 +22,6 @@ filterwarnings =
once:datetime.datetime.utcfromtimestamp\(\) is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC:DeprecationWarning
# NOTE: the following are present using python 3.11
# FIXME: Set `USE_TZ` to `True`.
# Note: RemovedInDjango50Warning may not exist in newer Django versions
ignore:The default value of USE_TZ will change from False to True in Django 5.0. Set USE_TZ to False in your project settings if you want to keep the current default behavior.
# FIXME: Delete this entry once `pyparsing` is updated.
once:module 'sre_constants' is deprecated:DeprecationWarning:_pytest.assertion.rewrite