From fee71b8917564e61d2ab4d4d49f0e96bea5600ed Mon Sep 17 00:00:00 2001 From: Hao Liu <44379968+TheRealHaoLiu@users.noreply.github.com> Date: Fri, 9 Jan 2026 16:05:08 -0500 Subject: [PATCH] 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. --- awx/api/views/__init__.py | 4 +-- awx/main/management/commands/cleanup_jobs.py | 3 +- awx/main/models/schedules.py | 10 +++--- .../tests/functional/models/test_schedule.py | 32 +++++++++---------- pytest.ini | 4 --- 5 files changed, 23 insertions(+), 30 deletions(-) diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 6e91c9ba61..f2a65cb2ba 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -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) diff --git a/awx/main/management/commands/cleanup_jobs.py b/awx/main/management/commands/cleanup_jobs.py index ddd871330c..4c47eadf0a 100644 --- a/awx/main/management/commands/cleanup_jobs.py +++ b/awx/main/management/commands/cleanup_jobs.py @@ -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 diff --git a/awx/main/models/schedules.py b/awx/main/models/schedules.py index 4fbe74048b..0e92960b88 100644 --- a/awx/main/models/schedules.py +++ b/awx/main/models/schedules.py @@ -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) diff --git a/awx/main/tests/functional/models/test_schedule.py b/awx/main/tests/functional/models/test_schedule.py index ba81f424bc..97051ae45f 100644 --- a/awx/main/tests/functional/models/test_schedule.py +++ b/awx/main/tests/functional/models/test_schedule.py @@ -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( diff --git a/pytest.ini b/pytest.ini index 0634837d81..2c8790e6af 100644 --- a/pytest.ini +++ b/pytest.ini @@ -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