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

@@ -8,7 +8,7 @@ import ddt
from crum import set_current_request
from django.test import RequestFactory
from django.utils import timezone
from pytz import UTC
from zoneinfo import ZoneInfo
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from common.djangoapps.course_modes.models import CourseMode
@@ -34,9 +34,12 @@ class TestAccess(ModuleStoreTestCase):
def setUp(self):
super().setUp() # lint-amnesty, pylint: disable=super-with-arguments
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=UTC))
CourseDurationLimitConfig.objects.create(
enabled=True,
enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))
)
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
self.course = CourseOverviewFactory.create(start=datetime(2018, 1, 1, tzinfo=UTC), self_paced=True)
self.course = CourseOverviewFactory.create(start=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC")), self_paced=True)
def assertDateInMessage(self, date, message): # lint-amnesty, pylint: disable=missing-function-docstring
# First, check that the formatted version is in there
@@ -148,7 +151,7 @@ class TestAccess(ModuleStoreTestCase):
course_id=enrollment.course.id,
mode_slug=CourseMode.AUDIT,
)
Schedule.objects.update(start_date=datetime(2017, 1, 1, tzinfo=UTC))
Schedule.objects.update(start_date=datetime(2017, 1, 1, tzinfo=ZoneInfo("UTC")))
content_availability_date = max(enrollment.created, enrollment.course.start)
access_duration = get_user_course_duration(enrollment.user, enrollment.course)

View File

@@ -8,7 +8,7 @@ from unittest.mock import Mock
import ddt
import pytest
import pytz
from zoneinfo import ZoneInfo
from django.utils import timezone
from edx_django_utils.cache import RequestCache
from opaque_keys.edx.locator import CourseLocator
@@ -178,13 +178,18 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
def test_all_current_course_configs(self):
# Set up test objects
for global_setting in (True, False, None):
CourseDurationLimitConfig.objects.create(enabled=global_setting, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) # lint-amnesty, pylint: disable=line-too-long
CourseDurationLimitConfig.objects.create(
enabled=global_setting,
enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))
)
for site_setting in (True, False, None):
test_site_cfg = SiteConfigurationFactory.create(
site_values={'course_org_filter': []}
)
CourseDurationLimitConfig.objects.create(
site=test_site_cfg.site, enabled=site_setting, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)
site=test_site_cfg.site,
enabled=site_setting,
enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))
)
for org_setting in (True, False, None):
@@ -193,7 +198,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
test_site_cfg.save()
CourseDurationLimitConfig.objects.create(
org=test_org, enabled=org_setting, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)
org=test_org, enabled=org_setting, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))
)
for course_setting in (True, False, None):
@@ -202,7 +207,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
id=CourseLocator(test_org, 'test_course', f'run-{course_setting}')
)
CourseDurationLimitConfig.objects.create(
course=test_course, enabled=course_setting, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC) # lint-amnesty, pylint: disable=line-too-long
course=test_course, enabled=course_setting, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC")) # lint-amnesty, pylint: disable=line-too-long
)
with self.assertNumQueries(4):
@@ -216,22 +221,25 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
# Point-test some of the final configurations
assert all_configs[CourseLocator('7-True', 'test_course', 'run-None')] == {
'enabled': (True, Provenance.org),
'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC),
'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=ZoneInfo("UTC")),
Provenance.run)
}
assert all_configs[CourseLocator('7-True', 'test_course', 'run-False')] == {
'enabled': (False, Provenance.run),
'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC),
'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=ZoneInfo("UTC")),
Provenance.run)
}
assert all_configs[CourseLocator('7-None', 'test_course', 'run-None')] == {
'enabled': (True, Provenance.site),
'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC),
'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=ZoneInfo("UTC")),
Provenance.run)
}
def test_caching_global(self):
global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC))
global_config = CourseDurationLimitConfig(
enabled=True,
enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))
)
global_config.save()
RequestCache.clear_all_namespaces()
@@ -257,7 +265,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
def test_caching_site(self):
site_cfg = SiteConfigurationFactory()
site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) # lint-amnesty, pylint: disable=line-too-long
site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))) # lint-amnesty, pylint: disable=line-too-long
site_config.save()
RequestCache.clear_all_namespaces()
@@ -281,7 +289,10 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
with self.assertNumQueries(1):
assert not CourseDurationLimitConfig.current(site=site_cfg.site).enabled
global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC))
global_config = CourseDurationLimitConfig(
enabled=True,
enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))
)
global_config.save()
RequestCache.clear_all_namespaces()
@@ -295,7 +306,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
site_cfg = SiteConfigurationFactory.create(
site_values={'course_org_filter': course.org}
)
org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) # lint-amnesty, pylint: disable=line-too-long
org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))) # lint-amnesty, pylint: disable=line-too-long
org_config.save()
RequestCache.clear_all_namespaces()
@@ -319,7 +330,10 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
with self.assertNumQueries(2):
assert not CourseDurationLimitConfig.current(org=course.org).enabled
global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC))
global_config = CourseDurationLimitConfig(
enabled=True,
enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))
)
global_config.save()
RequestCache.clear_all_namespaces()
@@ -328,7 +342,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
with self.assertNumQueries(0):
assert not CourseDurationLimitConfig.current(org=course.org).enabled
site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) # lint-amnesty, pylint: disable=line-too-long
site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))) # lint-amnesty, pylint: disable=line-too-long
site_config.save()
RequestCache.clear_all_namespaces()
@@ -342,7 +356,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
site_cfg = SiteConfigurationFactory.create(
site_values={'course_org_filter': course.org}
)
course_config = CourseDurationLimitConfig(course=course, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) # lint-amnesty, pylint: disable=line-too-long
course_config = CourseDurationLimitConfig(course=course, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))) # lint-amnesty, pylint: disable=line-too-long
course_config.save()
RequestCache.clear_all_namespaces()
@@ -366,7 +380,10 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
with self.assertNumQueries(2):
assert not CourseDurationLimitConfig.current(course_key=course.id).enabled
global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC))
global_config = CourseDurationLimitConfig(
enabled=True,
enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))
)
global_config.save()
RequestCache.clear_all_namespaces()
@@ -375,7 +392,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
with self.assertNumQueries(0):
assert not CourseDurationLimitConfig.current(course_key=course.id).enabled
site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) # lint-amnesty, pylint: disable=line-too-long
site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))) # lint-amnesty, pylint: disable=line-too-long
site_config.save()
RequestCache.clear_all_namespaces()
@@ -384,7 +401,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
with self.assertNumQueries(0):
assert not CourseDurationLimitConfig.current(course_key=course.id).enabled
org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=pytz.UTC)) # lint-amnesty, pylint: disable=line-too-long
org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=ZoneInfo("UTC"))) # lint-amnesty, pylint: disable=line-too-long
org_config.save()
RequestCache.clear_all_namespaces()