chore: Replace pytz with zoneinfo for UTC handling - Part 1 (#37523)

First PR to replace pytz with zoneinfo for UTC handling across codebase.

This PR migrates all UTC timezone handling from pytz to Python’s standard
library zoneinfo. The pytz library is now deprecated, and its documentation
recommends using zoneinfo for all new code. This update modernizes our
codebase, removes legacy pytz usage, and ensures compatibility with
current best practices for timezone management in Python 3.9+. No functional
changes to timezone logic - just a direct replacement for UTC handling.

https://github.com/openedx/edx-platform/issues/33980
This commit is contained in:
Tarun Tak
2025-10-29 01:53:22 +05:30
committed by GitHub
parent 6deb4f8d05
commit 18d5abb2f6
70 changed files with 263 additions and 234 deletions

View File

@@ -10,7 +10,7 @@ from unittest import mock
import ddt
import httpretty
import pytest
import pytz
from zoneinfo import ZoneInfo
import requests
from celery.exceptions import MaxRetriesExceededError
from django.conf import settings
@@ -520,7 +520,7 @@ class AwardCourseCertificatesTestCase(CredentialsApiConfigMixin, TestCase):
def setUp(self):
super().setUp()
self.available_date = datetime.now(pytz.UTC) + timedelta(days=1)
self.available_date = datetime.now(ZoneInfo("UTC")) + timedelta(days=1)
self.course = CourseOverviewFactory.create(
self_paced=True, # Any option to allow the certificate to be viewable for the course
certificate_available_date=self.available_date,
@@ -1023,7 +1023,7 @@ class UpdateCertificateAvailableDateOnCourseUpdateTestCase(CredentialsApiConfigM
def setUp(self):
super().setUp()
self.end_date = datetime.now(pytz.UTC) + timedelta(days=90)
self.end_date = datetime.now(ZoneInfo("UTC")) + timedelta(days=90)
self.credentials_api_config = self.create_credentials_config(enabled=False)
def tearDown(self):
@@ -1135,7 +1135,7 @@ class UpdateCertificateAvailableDateOnCourseUpdateTestCase(CredentialsApiConfigM
explicitly set as part of the course overview.
"""
self._update_credentials_api_config(True)
certificate_available_date = datetime.now(pytz.UTC) + timedelta(days=120)
certificate_available_date = datetime.now(ZoneInfo("UTC")) + timedelta(days=120)
course_overview = self._create_course_overview(
False,
@@ -1168,7 +1168,7 @@ class UpdateCertificateAvailableDateOnCourseUpdateTestCase(CredentialsApiConfigM
invalid data is set in a course overview, we don't pass it to Credentials.
"""
self._update_credentials_api_config(True)
certificate_available_date = datetime.now(pytz.UTC) + timedelta(days=120)
certificate_available_date = datetime.now(ZoneInfo("UTC")) + timedelta(days=120)
course_overview = self._create_course_overview(
True,

View File

@@ -15,7 +15,7 @@ from django.test.utils import override_settings
from django.urls import reverse
from edx_toggles.toggles.testutils import override_waffle_switch
from opaque_keys.edx.keys import CourseKey # lint-amnesty, pylint: disable=wrong-import-order
from pytz import utc
from zoneinfo import ZoneInfo
from testfixtures import LogCapture
from common.djangoapps.course_modes.models import CourseMode
@@ -209,7 +209,7 @@ class TestProgramProgressMeter(ModuleStoreTestCase):
CourseEntitlementFactory.create(
user=self.user,
course_uuid=course_uuid,
expired_at=datetime.datetime.now(utc),
expired_at=datetime.datetime.now(ZoneInfo("UTC")),
mode=CourseMode.VERIFIED,
enrollment_course_run=enrollment
@@ -308,7 +308,7 @@ class TestProgramProgressMeter(ModuleStoreTestCase):
the right type for which the upgrade deadline has not passed.
"""
course_run_key = generate_course_run_key()
now = datetime.datetime.now(utc)
now = datetime.datetime.now(ZoneInfo("UTC"))
upgrade_deadline = None if not offset else str(now + datetime.timedelta(days=offset))
required_seat = SeatFactory(type=CourseMode.VERIFIED, upgrade_deadline=upgrade_deadline)
enrolled_seat = SeatFactory(type=CourseMode.AUDIT)
@@ -488,7 +488,7 @@ class TestProgramProgressMeter(ModuleStoreTestCase):
def test_simulate_progress(self, mock_get_programs): # lint-amnesty, pylint: disable=too-many-statements
"""Simulate the entirety of a user's progress through a program."""
today = datetime.datetime.now(utc)
today = datetime.datetime.now(ZoneInfo("UTC"))
two_days_ago = today - datetime.timedelta(days=2)
three_days_ago = today - datetime.timedelta(days=3)
yesterday = today - datetime.timedelta(days=1)
@@ -862,8 +862,8 @@ def _create_course(self, course_price, course_run_count=1, make_entitlement=Fals
course_runs = []
for x in range(course_run_count):
course = ModuleStoreCourseFactory.create(run='Run_' + str(x))
course.start = datetime.datetime.now(utc) - datetime.timedelta(days=1)
course.end = datetime.datetime.now(utc) + datetime.timedelta(days=1)
course.start = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=1)
course.end = datetime.datetime.now(ZoneInfo("UTC")) + datetime.timedelta(days=1)
course.instructor_info = self.instructors
course = self.update_course(course, self.user.id)
@@ -899,8 +899,8 @@ class TestProgramDataExtender(ModuleStoreTestCase):
super().setUp()
self.course = ModuleStoreCourseFactory()
self.course.start = datetime.datetime.now(utc) - datetime.timedelta(days=1)
self.course.end = datetime.datetime.now(utc) + datetime.timedelta(days=1)
self.course.start = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=1)
self.course.end = datetime.datetime.now(ZoneInfo("UTC")) + datetime.timedelta(days=1)
self.course = self.update_course(self.course, self.user.id)
self.course_run = CourseRunFactory(key=str(self.course.id))
@@ -941,7 +941,7 @@ class TestProgramDataExtender(ModuleStoreTestCase):
Verify that changes to the course run end date do not affect our
assessment of the course run being open for enrollment.
"""
self.course.end = datetime.datetime.now(utc) + datetime.timedelta(days=days_offset)
self.course.end = datetime.datetime.now(ZoneInfo("UTC")) + datetime.timedelta(days=days_offset)
self.course = self.update_course(self.course, self.user.id)
data = ProgramDataExtender(self.program, self.user).extend()
@@ -1022,8 +1022,8 @@ class TestProgramDataExtender(ModuleStoreTestCase):
"""
Verify that course run enrollment status is reflected correctly.
"""
self.course.enrollment_start = datetime.datetime.now(utc) - datetime.timedelta(days=start_offset)
self.course.enrollment_end = datetime.datetime.now(utc) - datetime.timedelta(days=end_offset)
self.course.enrollment_start = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=start_offset)
self.course.enrollment_end = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=end_offset)
self.course = self.update_course(self.course, self.user.id)
@@ -1040,7 +1040,7 @@ class TestProgramDataExtender(ModuleStoreTestCase):
Verify that a closed course run with no explicit enrollment start date
doesn't cause an error. Regression test for ECOM-4973.
"""
self.course.enrollment_end = datetime.datetime.now(utc) - datetime.timedelta(days=1)
self.course.enrollment_end = datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=1)
self.course = self.update_course(self.course, self.user.id)
data = ProgramDataExtender(self.program, self.user).extend()

View File

@@ -14,7 +14,7 @@ from django.core.cache import cache
from django.urls import reverse
from django.utils.functional import cached_property
from opaque_keys.edx.keys import CourseKey
from pytz import utc
from zoneinfo import ZoneInfo
from requests.exceptions import RequestException
from common.djangoapps.course_modes.api import get_paid_modes_for_course
@@ -43,7 +43,7 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_
from xmodule.modulestore.django import modulestore
# The datetime module's strftime() methods require a year >= 1900.
DEFAULT_ENROLLMENT_START_DATE = datetime.datetime(1900, 1, 1, tzinfo=utc)
DEFAULT_ENROLLMENT_START_DATE = datetime.datetime(1900, 1, 1, tzinfo=ZoneInfo("UTC"))
log = logging.getLogger(__name__)
@@ -286,7 +286,7 @@ class ProgramProgressMeter:
list of dict, each containing information about a user's progress
towards completing a program.
"""
now = datetime.datetime.now(utc)
now = datetime.datetime.now(ZoneInfo("UTC"))
progress = []
programs = programs or self.engaged_programs
@@ -598,15 +598,17 @@ class ProgramDataExtender:
run_mode["enrollment_open_date"] = strftime_localized(self.enrollment_start, "SHORT_DATE")
def _attach_course_run_is_course_ended(self, run_mode):
end_date = self.course_overview.end or datetime.datetime.max.replace(tzinfo=utc)
run_mode["is_course_ended"] = end_date < datetime.datetime.now(utc)
end_date = self.course_overview.end or datetime.datetime.max.replace(tzinfo=ZoneInfo("UTC"))
run_mode["is_course_ended"] = end_date < datetime.datetime.now(ZoneInfo("UTC"))
def _attach_course_run_is_enrolled(self, run_mode):
run_mode["is_enrolled"] = CourseEnrollment.is_enrolled(self.user, self.course_run_key)
def _attach_course_run_is_enrollment_open(self, run_mode):
enrollment_end = self.course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=utc)
run_mode["is_enrollment_open"] = self.enrollment_start <= datetime.datetime.now(utc) < enrollment_end
enrollment_end = self.course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=ZoneInfo("UTC"))
run_mode["is_enrollment_open"] = (
self.enrollment_start <= datetime.datetime.now(ZoneInfo("UTC")) < enrollment_end
)
def _attach_course_run_advertised_start(self, run_mode):
"""