Browse Source

ref: replace deprecated pytz with stdlib zoneinfo (#63849)

going to wait a few days before merging since this solidifies in django
4.x

<!-- Describe your PR here. -->
anthony sottile 1 year ago
parent
commit
a32270c8ab

+ 4 - 3
src/sentry/api/endpoints/user_details.py

@@ -1,7 +1,7 @@
 import logging
+import zoneinfo
 from datetime import datetime
 
-import pytz
 from django.conf import settings
 from django.contrib.auth import logout
 from django.db import router, transaction
@@ -29,6 +29,7 @@ from sentry.models.user import User
 from sentry.services.hybrid_cloud.organization import organization_service
 from sentry.services.hybrid_cloud.organization.model import RpcOrganizationDeleteState
 from sentry.services.hybrid_cloud.user.serial import serialize_generic_user
+from sentry.utils.dates import AVAILABLE_TIMEZONES
 
 audit_logger = logging.getLogger("sentry.audit.user")
 delete_logger = logging.getLogger("sentry.deletions.api")
@@ -36,8 +37,8 @@ delete_logger = logging.getLogger("sentry.deletions.api")
 
 def _get_timezone_choices():
     results = []
-    for tz in pytz.all_timezones:
-        now = datetime.now(pytz.timezone(tz))
+    for tz in AVAILABLE_TIMEZONES:
+        now = datetime.now(zoneinfo.ZoneInfo(tz))
         offset = now.strftime("%z")
         results.append((int(offset), tz, f"(UTC{offset}) {tz}"))
     results.sort()

+ 2 - 2
src/sentry/monitors/models.py

@@ -1,12 +1,12 @@
 from __future__ import annotations
 
 import logging
+import zoneinfo
 from datetime import datetime
 from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Sequence, Tuple, Union
 from uuid import uuid4
 
 import jsonschema
-import pytz
 from django.conf import settings
 from django.db import models
 from django.db.models import Q
@@ -270,7 +270,7 @@ class Monitor(Model):
 
     @property
     def timezone(self):
-        return pytz.timezone(self.config.get("timezone") or "UTC")
+        return zoneinfo.ZoneInfo(self.config.get("timezone") or "UTC")
 
     def get_schedule_type_display(self):
         return ScheduleType.get_name(self.config["schedule_type"])

+ 2 - 2
src/sentry/monitors/validators.py

@@ -1,6 +1,5 @@
 from typing import Literal
 
-import pytz
 import sentry_sdk
 from croniter import CroniterBadDateError, croniter
 from django.core.exceptions import ValidationError
@@ -18,6 +17,7 @@ from sentry.constants import ObjectStatus
 from sentry.db.models import BoundedPositiveIntegerField
 from sentry.monitors.constants import MAX_SLUG_LENGTH, MAX_THRESHOLD, MAX_TIMEOUT
 from sentry.monitors.models import CheckInStatus, Monitor, MonitorType, ScheduleType
+from sentry.utils.dates import AVAILABLE_TIMEZONES
 
 MONITOR_TYPES = {"cron_job": MonitorType.CRON_JOB}
 
@@ -126,7 +126,7 @@ class ConfigValidator(serializers.Serializer):
     )
 
     timezone = serializers.ChoiceField(
-        choices=pytz.all_timezones,
+        choices=sorted(AVAILABLE_TIMEZONES),
         required=False,
         allow_blank=True,
         help_text="tz database style timezone string",

+ 3 - 4
src/sentry/notifications/notifications/rules.py

@@ -1,12 +1,11 @@
 from __future__ import annotations
 
 import logging
+import zoneinfo
 from datetime import timezone
 from typing import Any, Iterable, Mapping, MutableMapping
 from urllib.parse import urlencode
 
-import pytz
-
 from sentry import analytics, features
 from sentry.db.models import Model
 from sentry.eventstore.models import GroupEvent
@@ -127,8 +126,8 @@ class AlertRuleNotification(ProjectNotification):
             )
             user_tz = get_option_from_list(user_options, key="timezone", default="UTC")
             try:
-                tz = pytz.timezone(user_tz)
-            except pytz.UnknownTimeZoneError:
+                tz = zoneinfo.ZoneInfo(user_tz)
+            except zoneinfo.ZoneInfoNotFoundError:
                 pass
         return {
             **super().get_recipient_context(recipient, extra_context),

+ 4 - 0
src/sentry/utils/dates.py

@@ -1,4 +1,5 @@
 import re
+import zoneinfo
 from datetime import datetime, timedelta, timezone
 from typing import Any, Mapping, Optional, Tuple, Union, overload
 
@@ -11,6 +12,9 @@ from sentry.constants import MAX_ROLLUP_POINTS
 
 epoch = datetime(1970, 1, 1, tzinfo=timezone.utc)
 
+# Factory is an obscure GMT alias
+AVAILABLE_TIMEZONES = frozenset(zoneinfo.available_timezones() - {"Factory"})
+
 
 def ensure_aware(value: datetime) -> datetime:
     """

+ 4 - 3
src/sentry/web/forms/accounts.py

@@ -1,10 +1,10 @@
 from __future__ import annotations
 
 import re
+import zoneinfo
 from datetime import datetime
 from typing import Any
 
-import pytz
 from django import forms
 from django.conf import settings
 from django.contrib.auth import authenticate, get_user_model
@@ -17,13 +17,14 @@ from sentry import ratelimits as ratelimiter
 from sentry.auth import password_validation
 from sentry.models.user import User
 from sentry.utils.auth import find_users, logger
+from sentry.utils.dates import AVAILABLE_TIMEZONES
 from sentry.web.forms.fields import AllowedEmailField, CustomTypedChoiceField
 
 
 def _get_timezone_choices():
     results = []
-    for tz in pytz.common_timezones:
-        now = datetime.now(pytz.timezone(tz))
+    for tz in AVAILABLE_TIMEZONES:
+        now = datetime.now(zoneinfo.ZoneInfo(tz))
         offset = now.strftime("%z")
         results.append((int(offset), tz, f"(UTC{offset}) {tz}"))
     results.sort()

+ 3 - 2
src/sentry/web/frontend/debug/debug_generic_issue.py

@@ -1,4 +1,5 @@
-import pytz
+import zoneinfo
+
 from django.utils.safestring import mark_safe
 from django.views.generic import View
 
@@ -32,7 +33,7 @@ class DebugGenericIssueEmailView(View):
                 "rules": get_rules([rule], org, project),
                 "group": group,
                 "event": event,
-                "timezone": pytz.timezone("Europe/Vienna"),
+                "timezone": zoneinfo.ZoneInfo("Europe/Vienna"),
                 # http://testserver/organizations/example/issues/<issue-id>/?referrer=alert_email
                 #       &alert_type=email&alert_timestamp=<ts>&alert_rule_id=1
                 "link": get_group_settings_link(group, None, get_rules([rule], org, project), 1337),

+ 2 - 2
src/sentry/web/frontend/debug/mail.py

@@ -6,6 +6,7 @@ import logging
 import time
 import traceback
 import uuid
+import zoneinfo
 from datetime import datetime, timedelta, timezone
 from hashlib import md5
 from random import Random
@@ -13,7 +14,6 @@ from typing import Any, Generator
 from unittest import mock
 from urllib.parse import urlencode
 
-import pytz
 from django.http import HttpRequest, HttpResponse
 from django.shortcuts import redirect
 from django.urls import reverse
@@ -254,7 +254,7 @@ def get_shared_context(rule, org, project: Project, group, event):
         "group": group,
         "group_header": get_group_substatus_text(group),
         "event": event,
-        "timezone": pytz.timezone("Europe/Vienna"),
+        "timezone": zoneinfo.ZoneInfo("Europe/Vienna"),
         # http://testserver/organizations/example/issues/<issue-id>/?referrer=alert_email
         #       &alert_type=email&alert_timestamp=<ts>&alert_rule_id=1
         "link": get_group_settings_link(group, None, rules, 1337),

+ 2 - 2
src/sentry/web/helpers.py

@@ -3,12 +3,12 @@ from __future__ import annotations
 import logging
 from typing import Any, Mapping, Sequence
 
-import pytz
 from django.http import HttpRequest, HttpResponse
 from django.template import loader
 from django.utils import timezone
 
 from sentry.utils.auth import get_login_url  # NOQA: backwards compatibility
+from sentry.utils.dates import AVAILABLE_TIMEZONES
 
 logger = logging.getLogger("sentry")
 
@@ -23,7 +23,7 @@ def render_to_string(
     else:
         context = dict(context)
 
-    if "timezone" in context and context["timezone"] in pytz.all_timezones_set:
+    if "timezone" in context and context["timezone"] in AVAILABLE_TIMEZONES:
         timezone.activate(context["timezone"])
 
     rendered = loader.render_to_string(template, context=context, request=request)

+ 3 - 3
tests/sentry/mail/test_adapter.py

@@ -1,4 +1,5 @@
 import uuid
+import zoneinfo
 from collections import Counter
 from datetime import datetime, timedelta, timezone
 from functools import cached_property
@@ -6,7 +7,6 @@ from typing import Mapping, Sequence
 from unittest import mock
 from unittest.mock import ANY
 
-import pytz
 from django.contrib.auth.models import AnonymousUser
 from django.core import mail
 from django.core.mail.message import EmailMultiAlternatives
@@ -482,7 +482,7 @@ class MailAdapterNotifyTest(BaseMailAdapterTest):
         recipient_context = notification.get_recipient_context(
             RpcActor.from_orm_user(self.user), {}
         )
-        assert recipient_context["timezone"] == pytz.timezone("Europe/Vienna")
+        assert recipient_context["timezone"] == zoneinfo.ZoneInfo("Europe/Vienna")
 
         self.assertEqual(notification.project, self.project)
         self.assertEqual(notification.reference, group)
@@ -620,7 +620,7 @@ class MailAdapterNotifyTest(BaseMailAdapterTest):
         from django.template.defaultfilters import date
 
         timestamp = datetime.now(tz=timezone.utc)
-        local_timestamp_s = django_timezone.localtime(timestamp, pytz.timezone("Europe/Vienna"))
+        local_timestamp_s = django_timezone.localtime(timestamp, zoneinfo.ZoneInfo("Europe/Vienna"))
         local_timestamp = date(local_timestamp_s, "N j, Y, g:i:s a e")
 
         with assume_test_silo_mode(SiloMode.CONTROL):

Some files were not shown because too many files changed in this diff