diff --git a/analytics/views/activity_common.py b/analytics/views/activity_common.py index 005b90945d..675578d661 100644 --- a/analytics/views/activity_common.py +++ b/analytics/views/activity_common.py @@ -1,9 +1,9 @@ import re +import sys from datetime import datetime from html import escape from typing import Any, Collection, Dict, List, Optional, Sequence -import pytz from django.conf import settings from django.db.backends.utils import CursorWrapper from django.template import loader @@ -12,7 +12,12 @@ from markupsafe import Markup as mark_safe from zerver.models import UserActivity -eastern_tz = pytz.timezone("US/Eastern") +if sys.version_info < (3, 9): # nocoverage + from backports import zoneinfo +else: # nocoverage + import zoneinfo + +eastern_tz = zoneinfo.ZoneInfo("America/New_York") if settings.BILLING_ENABLED: diff --git a/requirements/common.in b/requirements/common.in index 82483487c6..414c8a5d60 100644 --- a/requirements/common.in +++ b/requirements/common.in @@ -3,7 +3,7 @@ # and requirements/prod.txt. # See requirements/README.md for more detail. # Django itself -Django[argon2]==3.2.* +Django[argon2]==4.0.* # needed for NotRequired, ParamSpec typing-extensions @@ -77,7 +77,7 @@ django-bmemcached python-dateutil # Needed for time zone work -pytz +backports.zoneinfo ; python_version < "3.9" # Needed for Redis redis diff --git a/requirements/dev.txt b/requirements/dev.txt index fd317aa6fe..ae2cd84292 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -93,6 +93,26 @@ backoff-stubs==1.11.1 \ --hash=sha256:3fd641261cfe9cd657ebb7fc8a1dc700efa3f1b63e82fe0235d74bb73f8b85da \ --hash=sha256:8b56cf2cfaf64abc1623544bd725b21566b5b92cf790a97d33e7437fb131251e # via -r requirements/mypy.in +backports.zoneinfo==0.2.1 ; python_version < "3.9" \ + --hash=sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf \ + --hash=sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328 \ + --hash=sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546 \ + --hash=sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6 \ + --hash=sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570 \ + --hash=sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9 \ + --hash=sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7 \ + --hash=sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987 \ + --hash=sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722 \ + --hash=sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582 \ + --hash=sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc \ + --hash=sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b \ + --hash=sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1 \ + --hash=sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08 \ + --hash=sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac \ + --hash=sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2 + # via + # -r requirements/common.in + # django beautifulsoup4==4.11.1 \ --hash=sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30 \ --hash=sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693 @@ -439,9 +459,9 @@ distro==1.7.0 \ --hash=sha256:151aeccf60c216402932b52e40ee477a939f8d58898927378a02abbe852c1c39 \ --hash=sha256:d596311d707e692c2160c37807f83e3820c5d539d5a83e87cfb6babd8ba3a06b # via zulip -django[argon2]==3.2.14 \ - --hash=sha256:677182ba8b5b285a4e072f3ac17ceee6aff1b5ce77fd173cc5b6a2d3dc022fcf \ - --hash=sha256:a8681e098fa60f7c33a4b628d6fcd3fe983a0939ff1301ecacac21d0b38bad56 +django[argon2]==4.0.6 \ + --hash=sha256:a67a793ff6827fd373555537dca0da293a63a316fe34cb7f367f898ccca3c3ae \ + --hash=sha256:ca54ebedfcbc60d191391efbf02ba68fb52165b8bf6ccd6fe71f098cac1fe59e # via # -r requirements/common.in # django-auth-ldap @@ -456,9 +476,8 @@ django-auth-ldap==4.1.0 \ --hash=sha256:68870e7921e84b1a9867e268a9c8a3e573e8a0d95ea08bcf31be178f5826ff36 \ --hash=sha256:77f749d3b17807ce8eb56a9c9c8e5746ff316567f81d5ba613495d9c7495a949 # via -r requirements/common.in -django-bitfield==2.1.0 \ - --hash=sha256:158f1056e22cce450d0a49633ea77bfd84b85a2294b1ef03faa775a485f4065d \ - --hash=sha256:a55859fd16ce4269d5ceed3e20cf8fc3c2df866f0a78b90c60a19a0e76aa5fd8 +django-bitfield==2.2.0 \ + --hash=sha256:1b21262acc4ec0af3f82ed04498a056cd9d5452532ac02771e004835a34e0b1b # via -r requirements/common.in django-bmemcached==0.3.0 \ --hash=sha256:4e4b7d97216dbae331c1de10e699ca22804b94ec3a90d2762dd5d146e6986a8a @@ -1606,9 +1625,7 @@ pytz==2022.1 \ --hash=sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7 \ --hash=sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c # via - # -r requirements/common.in # babel - # django # moto # twilio pyuca==1.2 \ @@ -2136,10 +2153,6 @@ types-python-dateutil==2.8.18 \ --hash=sha256:8695c7d7a5b1aef4002f3ab4e1247e23b1d41cd7cc1286d4594c2d8c5593c991 \ --hash=sha256:fd5ed97262b76ae684695ea38ace8dd7c1bc9491aba7eb4edf6654b7ecabc870 # via -r requirements/mypy.in -types-pytz==2022.1.1 \ - --hash=sha256:4e7add70886dc2ee6ee7535c8184a26eeb0ac9dbafae9962cb882d74b9f67330 \ - --hash=sha256:581467742f32f15fff1098698b11fd511057a2a8a7568d33b604083f2b03c24f - # via -r requirements/mypy.in types-pyyaml==6.0.9 \ --hash=sha256:33ae75c84b8f61fddf0c63e9c7e557db9db1694ad3c2ee8628ec5efebb5a5e9b \ --hash=sha256:b738e9ef120da0af8c235ba49d3b72510f56ef9bcc308fc8e7357100ff122284 diff --git a/requirements/mypy.in b/requirements/mypy.in index aaba58df2e..145703b610 100644 --- a/requirements/mypy.in +++ b/requirements/mypy.in @@ -24,7 +24,6 @@ types-Pillow types-psycopg2 types-Pygments types-python-dateutil -types-pytz types-PyYAML types-redis types-requests diff --git a/requirements/mypy.txt b/requirements/mypy.txt index f0e9443270..d3b2bef8c4 100644 --- a/requirements/mypy.txt +++ b/requirements/mypy.txt @@ -245,10 +245,6 @@ types-python-dateutil==2.8.18 \ --hash=sha256:8695c7d7a5b1aef4002f3ab4e1247e23b1d41cd7cc1286d4594c2d8c5593c991 \ --hash=sha256:fd5ed97262b76ae684695ea38ace8dd7c1bc9491aba7eb4edf6654b7ecabc870 # via -r requirements/mypy.in -types-pytz==2022.1.1 \ - --hash=sha256:4e7add70886dc2ee6ee7535c8184a26eeb0ac9dbafae9962cb882d74b9f67330 \ - --hash=sha256:581467742f32f15fff1098698b11fd511057a2a8a7568d33b604083f2b03c24f - # via -r requirements/mypy.in types-pyyaml==6.0.9 \ --hash=sha256:33ae75c84b8f61fddf0c63e9c7e557db9db1694ad3c2ee8628ec5efebb5a5e9b \ --hash=sha256:b738e9ef120da0af8c235ba49d3b72510f56ef9bcc308fc8e7357100ff122284 diff --git a/requirements/prod.txt b/requirements/prod.txt index a987892456..73c86e8f4f 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -64,6 +64,26 @@ backoff==2.1.2 \ --hash=sha256:407f1bc0f22723648a8880821b935ce5df8475cf04f7b6b5017ae264d30f6069 \ --hash=sha256:b135e6d7c7513ba2bfd6895bc32bc8c66c6f3b0279b4c6cd866053cfd7d3126b # via -r requirements/common.in +backports.zoneinfo==0.2.1 ; python_version < "3.9" \ + --hash=sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf \ + --hash=sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328 \ + --hash=sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546 \ + --hash=sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6 \ + --hash=sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570 \ + --hash=sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9 \ + --hash=sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7 \ + --hash=sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987 \ + --hash=sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722 \ + --hash=sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582 \ + --hash=sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc \ + --hash=sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b \ + --hash=sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1 \ + --hash=sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08 \ + --hash=sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac \ + --hash=sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2 + # via + # -r requirements/common.in + # django beautifulsoup4==4.11.1 \ --hash=sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30 \ --hash=sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693 @@ -272,9 +292,9 @@ distro==1.7.0 \ --hash=sha256:151aeccf60c216402932b52e40ee477a939f8d58898927378a02abbe852c1c39 \ --hash=sha256:d596311d707e692c2160c37807f83e3820c5d539d5a83e87cfb6babd8ba3a06b # via zulip -django[argon2]==3.2.14 \ - --hash=sha256:677182ba8b5b285a4e072f3ac17ceee6aff1b5ce77fd173cc5b6a2d3dc022fcf \ - --hash=sha256:a8681e098fa60f7c33a4b628d6fcd3fe983a0939ff1301ecacac21d0b38bad56 +django[argon2]==4.0.6 \ + --hash=sha256:a67a793ff6827fd373555537dca0da293a63a316fe34cb7f367f898ccca3c3ae \ + --hash=sha256:ca54ebedfcbc60d191391efbf02ba68fb52165b8bf6ccd6fe71f098cac1fe59e # via # -r requirements/common.in # django-auth-ldap @@ -289,9 +309,8 @@ django-auth-ldap==4.1.0 \ --hash=sha256:68870e7921e84b1a9867e268a9c8a3e573e8a0d95ea08bcf31be178f5826ff36 \ --hash=sha256:77f749d3b17807ce8eb56a9c9c8e5746ff316567f81d5ba613495d9c7495a949 # via -r requirements/common.in -django-bitfield==2.1.0 \ - --hash=sha256:158f1056e22cce450d0a49633ea77bfd84b85a2294b1ef03faa775a485f4065d \ - --hash=sha256:a55859fd16ce4269d5ceed3e20cf8fc3c2df866f0a78b90c60a19a0e76aa5fd8 +django-bitfield==2.2.0 \ + --hash=sha256:1b21262acc4ec0af3f82ed04498a056cd9d5452532ac02771e004835a34e0b1b # via -r requirements/common.in django-bmemcached==0.3.0 \ --hash=sha256:4e4b7d97216dbae331c1de10e699ca22804b94ec3a90d2762dd5d146e6986a8a @@ -1085,10 +1104,7 @@ python3-saml==1.14.0 \ pytz==2022.1 \ --hash=sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7 \ --hash=sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c - # via - # -r requirements/common.in - # django - # twilio + # via twilio pyuca==1.2 \ --hash=sha256:8a382fe74627f08c0d18908c0713ca4a20aad5385f077579e56208beea2893b2 \ --hash=sha256:abaa12e1bd2c7c68ca8396ff8383bc0654a739cef3ae68fd7af58bf29af0a91e diff --git a/scripts/lib/zulip_tools.py b/scripts/lib/zulip_tools.py index f5a3a39dd3..02c3f500f5 100755 --- a/scripts/lib/zulip_tools.py +++ b/scripts/lib/zulip_tools.py @@ -16,7 +16,7 @@ import subprocess import sys import time import uuid -from typing import Any, Dict, List, Sequence, Set +from typing import IO, Any, Dict, List, Sequence, Set from urllib.parse import SplitResult DEPLOYMENTS_DIR = "/home/zulip/deployments" @@ -445,6 +445,20 @@ def os_families() -> Set[str]: return {distro_info["ID"], *distro_info.get("ID_LIKE", "").split()} +def get_tzdata_zi() -> IO[str]: + if sys.version_info < (3, 9): # nocoverage + from backports import zoneinfo + else: # nocoverage + import zoneinfo + + for path in zoneinfo.TZPATH: + filename = os.path.join(path, "tzdata.zi") + if os.path.exists(filename): + return open(filename) + else: + raise RuntimeError("Missing time zone data (tzdata.zi)") + + def files_and_string_digest(filenames: Sequence[str], extra_strings: Sequence[str]) -> str: # see is_digest_obsolete for more context sha1sum = hashlib.sha1() diff --git a/tools/lib/provision_inner.py b/tools/lib/provision_inner.py index a72685ca69..c77f70eb33 100755 --- a/tools/lib/provision_inner.py +++ b/tools/lib/provision_inner.py @@ -10,13 +10,13 @@ ZULIP_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__f sys.path.append(ZULIP_PATH) import pygments -from pytz import VERSION as timezones_version from scripts.lib import clean_unused_caches from scripts.lib.zulip_tools import ( ENDC, OKBLUE, get_dev_uuid_var_path, + get_tzdata_zi, is_digest_obsolete, run, run_as_root, @@ -28,6 +28,11 @@ from version import PROVISION_VERSION VENV_PATH = "/srv/zulip-py3-venv" UUID_VAR_PATH = get_dev_uuid_var_path() +with get_tzdata_zi() as f: + line = f.readline() + assert line.startswith("# version ") + timezones_version = line[len("# version ") :] + def create_var_directories() -> None: # create var/coverage, var/log, etc. diff --git a/tools/setup/build_timezone_values b/tools/setup/build_timezone_values index 904aefbbde..59415c463d 100755 --- a/tools/setup/build_timezone_values +++ b/tools/setup/build_timezone_values @@ -3,7 +3,10 @@ import json import os import sys -import pytz +if sys.version_info < (3, 9): + from backports import zoneinfo +else: + import zoneinfo ZULIP_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../") sys.path.insert(0, ZULIP_PATH) @@ -13,4 +16,13 @@ from zerver.lib.timezone import get_canonical_timezone_map OUT_PATH = os.path.join(ZULIP_PATH, "static", "generated", "timezones.json") with open(OUT_PATH, "w") as f: - json.dump({"timezones": sorted(pytz.all_timezones_set - set(get_canonical_timezone_map()))}, f) + json.dump( + { + "timezones": sorted( + zoneinfo.available_timezones() + - {"Factory", "localtime"} + - set(get_canonical_timezone_map()) + ) + }, + f, + ) diff --git a/version.py b/version.py index 18c430b4b3..5669db703a 100644 --- a/version.py +++ b/version.py @@ -48,4 +48,4 @@ API_FEATURE_LEVEL = 132 # historical commits sharing the same major version, in which case a # minor version bump suffices. -PROVISION_VERSION = "194.0" +PROVISION_VERSION = "195.0" diff --git a/zerver/lib/compatibility.py b/zerver/lib/compatibility.py index 3859981682..a1190e6179 100644 --- a/zerver/lib/compatibility.py +++ b/zerver/lib/compatibility.py @@ -3,7 +3,6 @@ import os import re from typing import List, Optional, Tuple -import pytz from django.conf import settings from django.utils.timezone import now as timezone_now @@ -16,7 +15,7 @@ from zerver.signals import get_device_browser if settings.PRODUCTION: # nocoverage timestamp = os.path.basename(os.path.abspath(settings.DEPLOY_ROOT)) LAST_SERVER_UPGRADE_TIME = datetime.datetime.strptime(timestamp, "%Y-%m-%d-%H-%M-%S").replace( - tzinfo=pytz.utc + tzinfo=datetime.timezone.utc ) else: LAST_SERVER_UPGRADE_TIME = timezone_now() @@ -31,7 +30,7 @@ def is_outdated_server(user_profile: Optional[UserProfile]) -> bool: git_version_path = os.path.join(settings.DEPLOY_ROOT, "version.py") release_build_time = datetime.datetime.utcfromtimestamp( os.path.getmtime(git_version_path) - ).replace(tzinfo=pytz.utc) + ).replace(tzinfo=datetime.timezone.utc) version_no_newer_than = min(LAST_SERVER_UPGRADE_TIME, release_build_time) deadline = version_no_newer_than + datetime.timedelta( diff --git a/zerver/lib/email_notifications.py b/zerver/lib/email_notifications.py index 32a57a2fb2..123d0c3c8c 100644 --- a/zerver/lib/email_notifications.py +++ b/zerver/lib/email_notifications.py @@ -11,7 +11,6 @@ from email.headerregistry import Address from typing import Any, Dict, Iterable, List, Optional, Tuple import lxml.html -import pytz from bs4 import BeautifulSoup from django.conf import settings from django.contrib.auth import get_backends @@ -46,6 +45,11 @@ from zerver.models import ( get_user_profile_by_id, ) +if sys.version_info < (3, 9): # nocoverage + from backports import zoneinfo +else: # nocoverage + import zoneinfo + logger = logging.getLogger(__name__) @@ -620,7 +624,7 @@ def followup_day2_email_delay(user: UserProfile) -> timedelta: user_tz = user.timezone if user_tz == "": user_tz = "UTC" - signup_day = user.date_joined.astimezone(pytz.timezone(user_tz)).isoweekday() + signup_day = user.date_joined.astimezone(zoneinfo.ZoneInfo(user_tz)).isoweekday() if signup_day == 5: # If the day is Friday then delay should be till Monday days_to_delay = 3 diff --git a/zerver/lib/test_runner.py b/zerver/lib/test_runner.py index 674172e308..3aa064dcf4 100644 --- a/zerver/lib/test_runner.py +++ b/zerver/lib/test_runner.py @@ -102,7 +102,7 @@ def process_instrumented_calls(func: Callable[[Dict[str, Any]], None]) -> None: SerializedSubsuite = Tuple[Type[TestSuite], List[str]] -SubsuiteArgs = Tuple[Type["RemoteTestRunner"], int, SerializedSubsuite, bool] +SubsuiteArgs = Tuple[Type["RemoteTestRunner"], int, SerializedSubsuite, bool, bool] def run_subsuite(args: SubsuiteArgs) -> Tuple[int, Any]: @@ -110,8 +110,8 @@ def run_subsuite(args: SubsuiteArgs) -> Tuple[int, Any]: test_helpers.INSTRUMENTED_CALLS = [] # The first argument is the test runner class but we don't need it # because we run our own version of the runner class. - _, subsuite_index, subsuite, failfast = args - runner = RemoteTestRunner(failfast=failfast) + _, subsuite_index, subsuite, failfast, buffer = args + runner = RemoteTestRunner(failfast=failfast, buffer=buffer) result = runner.run(deserialize_suite(subsuite)) # Now we send instrumentation related events. This data will be # appended to the data structure in the main thread. For Mypy, @@ -237,8 +237,14 @@ class ParallelTestSuite(django_runner.ParallelTestSuite): run_subsuite = run_subsuite init_worker = init_worker - def __init__(self, suite: TestSuite, processes: int, failfast: bool) -> None: - super().__init__(suite, processes, failfast) + def __init__( + self, + subsuites: List[TestSuite], + processes: int, + failfast: bool = False, + buffer: bool = False, + ) -> None: + super().__init__(subsuites=subsuites, processes=processes, failfast=failfast, buffer=buffer) # We can't specify a consistent type for self.subsuites, since # the whole idea here is to monkey-patch that so we can use # most of django_runner.ParallelTestSuite with our own suite diff --git a/zerver/lib/timezone.py b/zerver/lib/timezone.py index f02489e1bc..5c787e5249 100644 --- a/zerver/lib/timezone.py +++ b/zerver/lib/timezone.py @@ -1,16 +1,13 @@ from functools import lru_cache -from io import TextIOWrapper from typing import Dict -import pytz +from scripts.lib.zulip_tools import get_tzdata_zi @lru_cache(maxsize=None) def get_canonical_timezone_map() -> Dict[str, str]: canonical = {} - with TextIOWrapper( - pytz.open_resource("tzdata.zi") # type: ignore[attr-defined] # Unclear if this is part of the public pytz API - ) as f: + with get_tzdata_zi() as f: for line in f: if line.startswith("L "): l, name, alias = line.split() diff --git a/zerver/lib/validator.py b/zerver/lib/validator.py index 94d2cff5b9..66d137296f 100644 --- a/zerver/lib/validator.py +++ b/zerver/lib/validator.py @@ -28,6 +28,7 @@ for any particular type of object. """ import re +import sys from dataclasses import dataclass from datetime import datetime from decimal import Decimal @@ -49,7 +50,6 @@ from typing import ( ) import orjson -import pytz from django.core.exceptions import ValidationError from django.core.validators import URLValidator, validate_email from django.utils.translation import gettext as _ @@ -58,6 +58,11 @@ from zerver.lib.exceptions import InvalidJSONError, JsonableError from zerver.lib.timezone import canonicalize_timezone from zerver.lib.types import ProfileFieldData, Validator +if sys.version_info < (3, 9): # nocoverage + from backports import zoneinfo +else: # nocoverage + import zoneinfo + ResultT = TypeVar("ResultT") @@ -127,6 +132,17 @@ def check_long_string(var_name: str, val: object) -> str: return check_capped_string(500)(var_name, val) +def check_timezone(var_name: str, val: object) -> str: + s = check_string(var_name, val) + try: + zoneinfo.ZoneInfo(s) + except (ValueError, zoneinfo.ZoneInfoNotFoundError): + raise ValidationError( + _("{var_name} is not a recognized time zone").format(var_name=var_name) + ) + return s + + def check_date(var_name: str, val: object) -> str: if not isinstance(val, str): raise ValidationError(_("{var_name} is not a string").format(var_name=var_name)) @@ -564,10 +580,12 @@ def to_decimal(var_name: str, s: str) -> Decimal: def to_timezone_or_empty(var_name: str, s: str) -> str: - if s in pytz.all_timezones_set: - return canonicalize_timezone(s) - else: + try: + zoneinfo.ZoneInfo(s) + except (ValueError, zoneinfo.ZoneInfoNotFoundError): return "" + else: + return canonicalize_timezone(s) def to_converted_or_fallback( diff --git a/zerver/models.py b/zerver/models.py index 55cb432a95..19ec57e26c 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -1810,7 +1810,7 @@ class UserProfile(AbstractBaseUser, PermissionsMixin, UserBaseSettings): ) default_all_public_streams: bool = models.BooleanField(default=False) - # A time zone name from the `tzdata` database, as found in pytz.all_timezones. + # A time zone name from the `tzdata` database, as found in zoneinfo.available_timezones(). # # The longest existing name is 32 characters long, so max_length=40 seems # like a safe choice. diff --git a/zerver/signals.py b/zerver/signals.py index 6c40048753..6efcf23729 100644 --- a/zerver/signals.py +++ b/zerver/signals.py @@ -1,6 +1,6 @@ +import sys from typing import Any, Optional -import pytz from django.conf import settings from django.contrib.auth.signals import user_logged_in, user_logged_out from django.dispatch import receiver @@ -13,6 +13,11 @@ from zerver.lib.queue import queue_json_publish from zerver.lib.send_email import FromAddress from zerver.models import UserProfile +if sys.version_info < (3, 9): # nocoverage + from backports import zoneinfo +else: # nocoverage + import zoneinfo + JUST_CREATED_THRESHOLD = 60 @@ -81,7 +86,7 @@ def email_on_new_login(sender: Any, user: UserProfile, request: Any, **kwargs: A user_tz = user.timezone if user_tz == "": user_tz = timezone_get_current_timezone_name() - local_time = timezone_now().astimezone(pytz.timezone(user_tz)) + local_time = timezone_now().astimezone(zoneinfo.ZoneInfo(user_tz)) if user.twenty_four_hour_time: hhmm_string = local_time.strftime("%H:%M") else: diff --git a/zerver/tests/test_home.py b/zerver/tests/test_home.py index e706a7a911..d2500bfcee 100644 --- a/zerver/tests/test_home.py +++ b/zerver/tests/test_home.py @@ -6,7 +6,6 @@ from typing import TYPE_CHECKING, Any from unittest.mock import patch import orjson -import pytz from django.conf import settings from django.test import override_settings from django.utils.timezone import now as timezone_now @@ -878,7 +877,7 @@ class HomeTest(ZulipTestCase): # Check when server_upgrade_nag_deadline > last_server_upgrade_time hamlet = self.example_user("hamlet") iago = self.example_user("iago") - now = LAST_SERVER_UPGRADE_TIME.replace(tzinfo=pytz.utc) + now = LAST_SERVER_UPGRADE_TIME.replace(tzinfo=datetime.timezone.utc) with patch("zerver.lib.compatibility.timezone_now", return_value=now + timedelta(days=10)): self.assertEqual(is_outdated_server(iago), False) self.assertEqual(is_outdated_server(hamlet), False) diff --git a/zerver/tests/test_message_send.py b/zerver/tests/test_message_send.py index 54222821fb..37922302e5 100644 --- a/zerver/tests/test_message_send.py +++ b/zerver/tests/test_message_send.py @@ -1,9 +1,9 @@ import datetime +import sys from typing import TYPE_CHECKING, Any, List, Mapping, Optional, Set from unittest import mock import orjson -import pytz from django.conf import settings from django.db.models import Q from django.test import override_settings @@ -65,6 +65,11 @@ from zerver.models import ( ) from zerver.views.message_send import InvalidMirrorInput +if sys.version_info < (3, 9): # nocoverage + from backports import zoneinfo +else: # nocoverage + import zoneinfo + if TYPE_CHECKING: from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse @@ -1432,8 +1437,8 @@ class ScheduledMessageTest(ZulipTestCase): message = self.last_scheduled_message() self.assert_json_success(result) self.assertEqual(message.content, "Test message 6") - local_tz = pytz.timezone(tz_guess) - utz_defer_until = local_tz.normalize(local_tz.localize(defer_until)) + local_tz = zoneinfo.ZoneInfo(tz_guess) + utz_defer_until = defer_until.replace(tzinfo=local_tz) self.assertEqual(message.scheduled_timestamp, convert_to_UTC(utz_defer_until)) self.assertEqual(message.delivery_type, ScheduledMessage.SEND_LATER) @@ -1446,8 +1451,8 @@ class ScheduledMessageTest(ZulipTestCase): message = self.last_scheduled_message() self.assert_json_success(result) self.assertEqual(message.content, "Test message 7") - local_tz = pytz.timezone(user.timezone) - utz_defer_until = local_tz.normalize(local_tz.localize(defer_until)) + local_tz = zoneinfo.ZoneInfo(user.timezone) + utz_defer_until = defer_until.replace(tzinfo=local_tz) self.assertEqual(message.scheduled_timestamp, convert_to_UTC(utz_defer_until)) self.assertEqual(message.delivery_type, ScheduledMessage.SEND_LATER) diff --git a/zerver/tests/test_new_users.py b/zerver/tests/test_new_users.py index fd040c21b9..58909805b1 100644 --- a/zerver/tests/test_new_users.py +++ b/zerver/tests/test_new_users.py @@ -1,8 +1,8 @@ import datetime +import sys from typing import Sequence from unittest import mock -import pytz from django.conf import settings from django.core import mail from django.test import override_settings @@ -16,6 +16,11 @@ from zerver.lib.test_classes import ZulipTestCase from zerver.models import Message, Realm, Recipient, Stream, UserProfile, get_realm from zerver.signals import JUST_CREATED_THRESHOLD, get_device_browser, get_device_os +if sys.version_info < (3, 9): # nocoverage + from backports import zoneinfo +else: # nocoverage + import zoneinfo + class SendLoginEmailTest(ZulipTestCase): """ @@ -47,7 +52,7 @@ class SendLoginEmailTest(ZulipTestCase): firefox_windows = ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0" ) - user_tz = pytz.timezone(user.timezone) + user_tz = zoneinfo.ZoneInfo(user.timezone) mock_time = datetime.datetime(year=2018, month=1, day=1, tzinfo=datetime.timezone.utc) reference_time = mock_time.astimezone(user_tz).strftime("%A, %B %d, %Y at %I:%M%p %Z") with mock.patch("zerver.signals.timezone_now", return_value=mock_time): diff --git a/zerver/tests/test_settings.py b/zerver/tests/test_settings.py index e26f122f71..4701273ac3 100644 --- a/zerver/tests/test_settings.py +++ b/zerver/tests/test_settings.py @@ -410,6 +410,8 @@ class ChangeSettingsTest(ZulipTestCase): expected_error_msg = f"Invalid {setting_name}" if setting_name == "notification_sound": expected_error_msg = f"Invalid notification sound '{invalid_value}'" + elif setting_name == "timezone": + expected_error_msg = "timezone is not a recognized time zone" self.assert_json_error(result, expected_error_msg) hamlet = self.example_user("hamlet") self.assertNotEqual(getattr(hamlet, setting_name), invalid_value) diff --git a/zerver/tests/test_timezone.py b/zerver/tests/test_timezone.py index 75fec98a51..88905c84fa 100644 --- a/zerver/tests/test_timezone.py +++ b/zerver/tests/test_timezone.py @@ -1,11 +1,16 @@ +import sys from datetime import datetime -import pytz from django.utils.timezone import now as timezone_now from zerver.lib.test_classes import ZulipTestCase from zerver.lib.timezone import canonicalize_timezone, common_timezones +if sys.version_info < (3, 9): # nocoverage + from backports import zoneinfo +else: # nocoverage + import zoneinfo + class TimeZoneTest(ZulipTestCase): def test_canonicalize_timezone(self) -> None: @@ -32,10 +37,11 @@ class TimeZoneTest(ZulipTestCase): now = timezone_now() dates = [datetime(now.year, 6, 21), datetime(now.year, 12, 21)] extra = {*common_timezones.items(), *ambiguous_abbrevs} - for name in pytz.all_timezones: - tz = pytz.timezone(name) + for name in zoneinfo.available_timezones(): + tz = zoneinfo.ZoneInfo(name) for date in dates: abbrev = tz.tzname(date) + assert abbrev is not None if abbrev.startswith(("-", "+")): continue delta = tz.utcoffset(date) diff --git a/zerver/views/message_send.py b/zerver/views/message_send.py index e032bd3bf8..644559e2ab 100644 --- a/zerver/views/message_send.py +++ b/zerver/views/message_send.py @@ -1,6 +1,6 @@ +import sys from typing import Iterable, Optional, Sequence, Union, cast -import pytz from dateutil.parser import parse as dateparser from django.core import validators from django.core.exceptions import ValidationError @@ -36,6 +36,11 @@ from zerver.models import ( get_user_including_cross_realm, ) +if sys.version_info < (3, 9): # nocoverage + from backports import zoneinfo +else: # nocoverage + import zoneinfo + class InvalidMirrorInput(Exception): pass @@ -159,8 +164,8 @@ def handle_deferred_message( deliver_at_usertz = deliver_at if deliver_at_usertz.tzinfo is None: - user_tz = pytz.timezone(local_tz) - deliver_at_usertz = user_tz.normalize(user_tz.localize(deliver_at)) + user_tz = zoneinfo.ZoneInfo(local_tz) + deliver_at_usertz = deliver_at.replace(tzinfo=user_tz) deliver_at = convert_to_UTC(deliver_at_usertz) if deliver_at <= timezone_now(): diff --git a/zerver/views/user_settings.py b/zerver/views/user_settings.py index 11ff4fb6e7..d526d541f1 100644 --- a/zerver/views/user_settings.py +++ b/zerver/views/user_settings.py @@ -1,6 +1,5 @@ from typing import Any, Dict, Optional -import pytz from django.conf import settings from django.contrib.auth import authenticate, update_session_auth_hash from django.core.exceptions import ValidationError @@ -42,7 +41,13 @@ from zerver.lib.response import json_success from zerver.lib.send_email import FromAddress, send_email from zerver.lib.sounds import get_available_notification_sounds from zerver.lib.upload import upload_avatar_image -from zerver.lib.validator import check_bool, check_int, check_int_in, check_string_in +from zerver.lib.validator import ( + check_bool, + check_int, + check_int_in, + check_string_in, + check_timezone, +) from zerver.models import ( EmailChangeStatus, UserProfile, @@ -164,9 +169,7 @@ def json_change_settings( demote_inactive_streams: Optional[int] = REQ( json_validator=check_int_in(UserProfile.DEMOTE_STREAMS_CHOICES), default=None ), - timezone: Optional[str] = REQ( - str_validator=check_string_in(pytz.all_timezones_set), default=None - ), + timezone: Optional[str] = REQ(str_validator=check_timezone, default=None), email_notifications_batching_period_seconds: Optional[int] = REQ( json_validator=check_int, default=None ),