Skip to content

Commit 4a6ebdc

Browse files
committed
refactor: use standard library zoneinfo to replace pytz
1 parent fe41c34 commit 4a6ebdc

File tree

9 files changed

+126
-92
lines changed

9 files changed

+126
-92
lines changed

examples/fastapi/_tests.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@
1010

1111
import anyio
1212
import pytest
13-
import pytz
1413
from asgi_lifespan import LifespanManager
1514
from httpx import ASGITransport, AsyncClient
1615

1716
from tortoise.contrib.test import MEMORY_SQLITE
1817
from tortoise.fields.data import JSON_LOADS
18+
from tortoise.timezone import UTC, parse_timezone
1919

2020
os.environ["DB_URL"] = MEMORY_SQLITE
2121
try:
@@ -75,7 +75,7 @@ async def create_user(self, async_client: AsyncClient) -> Users:
7575
return user_obj
7676

7777
async def user_list(self, async_client: AsyncClient) -> tuple[datetime, Users, User_Pydantic]:
78-
utc_now = datetime.now(pytz.utc)
78+
utc_now = datetime.now(UTC)
7979
user_obj = await Users.create(username="test")
8080
response = await async_client.get("/users")
8181
assert response.status_code == 200, response.text
@@ -123,13 +123,13 @@ async def test_create_user_east(self, client_east: AsyncClient) -> None: # nose
123123
created_at = user_obj.created_at
124124

125125
# Verify time zone
126-
asia_tz = pytz.timezone(self.timezone)
127-
asia_now = datetime.now(pytz.utc).astimezone(asia_tz)
126+
asia_tz = parse_timezone(self.timezone)
127+
asia_now = datetime.now(UTC).astimezone(asia_tz)
128128
assert created_at.hour - asia_now.hour == 0
129129

130130
# UTC timezone
131-
utc_tz = pytz.timezone("UTC")
132-
utc_now = datetime.now(pytz.utc).astimezone(utc_tz)
131+
utc_tz = parse_timezone("UTC")
132+
utc_now = datetime.now(UTC).astimezone(utc_tz)
133133
assert (created_at.hour - utc_now.hour) in [self.delta_hours, self.delta_hours - 24]
134134

135135
@pytest.mark.anyio

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ dependencies = [
1111
"pypika-tortoise (>=0.6.1,<1.0.0)",
1212
"iso8601 (>=2.1.0,<3.0.0); python_version < '4.0'",
1313
"aiosqlite (>=0.16.0,<1.0.0)",
14-
"pytz",
1514
]
1615
classifiers = [
1716
"License :: OSI Approved :: Apache Software License",

tests/fields/test_time.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from time import sleep
55
from unittest.mock import patch
66

7-
import pytz
87
from iso8601 import ParseError
98

109
from tests import testmodels
@@ -13,7 +12,7 @@
1312
from tortoise.contrib.test.condition import NotIn
1413
from tortoise.exceptions import ConfigurationError, IntegrityError
1514
from tortoise.expressions import F
16-
from tortoise.timezone import get_default_timezone
15+
from tortoise.timezone import get_default_timezone, parse_timezone
1716

1817

1918
class TestEmpty(test.TestCase):
@@ -126,7 +125,7 @@ async def test_set_timezone(self):
126125
old_tz = os.environ["TIMEZONE"]
127126
tz = "Asia/Shanghai"
128127
os.environ["TIMEZONE"] = tz
129-
now = datetime.now(pytz.timezone(tz))
128+
now = datetime.now(parse_timezone(tz))
130129
obj = await self.model.create(datetime=now)
131130
self.assertEqual(obj.datetime.tzinfo.zone, tz)
132131

@@ -143,7 +142,7 @@ async def test_timezone(self):
143142
os.environ["TIMEZONE"] = tz
144143
os.environ["USE_TZ"] = "True"
145144

146-
now = datetime.now(pytz.timezone(tz))
145+
now = datetime.now(parse_timezone(tz))
147146
obj = await self.model.create(datetime=now)
148147
self.assertEqual(obj.datetime.tzinfo.zone, tz)
149148
obj_get = await self.model.get(pk=obj.pk)

tests/test_default.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import datetime
22
from decimal import Decimal
33

4-
import pytz
5-
64
from tests.testmodels import DefaultModel
75
from tortoise import connections
86
from tortoise.backends.asyncpg import AsyncpgDBClient
@@ -12,6 +10,7 @@
1210
from tortoise.backends.psycopg import PsycopgClient
1311
from tortoise.backends.sqlite import SqliteClient
1412
from tortoise.contrib import test
13+
from tortoise.timezone import UTC
1514

1615

1716
class TestDefault(test.TestCase):
@@ -45,5 +44,5 @@ async def test_default(self):
4544
self.assertEqual(default_model.date_default, datetime.date(year=2020, month=5, day=21))
4645
self.assertEqual(
4746
default_model.datetime_default,
48-
datetime.datetime(year=2020, month=5, day=20, tzinfo=pytz.utc),
47+
datetime.datetime(year=2020, month=5, day=20, tzinfo=UTC),
4948
)

tests/test_update.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from datetime import datetime, timedelta
55
from typing import Any
66

7-
import pytz
87
from pypika_tortoise.terms import Function as PupikaFunction
98

109
from tests.testmodels import (
@@ -26,6 +25,7 @@
2625
from tortoise.contrib.test.condition import In, NotEQ
2726
from tortoise.expressions import Case, F, Q, Subquery, When
2827
from tortoise.functions import Function, Upper
28+
from tortoise.timezone import UTC
2929

3030

3131
class TestUpdate(test.TestCase):
@@ -49,11 +49,11 @@ async def test_bulk_update(self):
4949

5050
async def test_bulk_update_datetime(self):
5151
objs = [
52-
await DatetimeFields.create(datetime=datetime(2021, 1, 1, tzinfo=pytz.utc)),
53-
await DatetimeFields.create(datetime=datetime(2021, 1, 1, tzinfo=pytz.utc)),
52+
await DatetimeFields.create(datetime=datetime(2021, 1, 1, tzinfo=UTC)),
53+
await DatetimeFields.create(datetime=datetime(2021, 1, 1, tzinfo=UTC)),
5454
]
55-
t0 = datetime(2021, 1, 2, tzinfo=pytz.utc)
56-
t1 = datetime(2021, 1, 3, tzinfo=pytz.utc)
55+
t0 = datetime(2021, 1, 2, tzinfo=UTC)
56+
t1 = datetime(2021, 1, 3, tzinfo=UTC)
5757
objs[0].datetime = t0
5858
objs[1].datetime = t1
5959
rows_affected = await DatetimeFields.bulk_update(objs, fields=["datetime"])

tests/testmodels.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from enum import Enum, IntEnum
1414
from typing import Union
1515

16-
import pytz
1716
from pydantic import BaseModel, ConfigDict
1817

1918
from tortoise import fields
@@ -23,6 +22,7 @@
2322
from tortoise.manager import Manager
2423
from tortoise.models import Model
2524
from tortoise.queryset import QuerySet
25+
from tortoise.timezone import UTC
2626
from tortoise.validators import (
2727
CommaSeparatedIntegerListValidator,
2828
MaxValueValidator,
@@ -845,7 +845,7 @@ class DefaultModel(Model):
845845
char_default = fields.CharField(max_length=20, default="tortoise")
846846
date_default = fields.DateField(default=datetime.date(year=2020, month=5, day=21))
847847
datetime_default = fields.DatetimeField(
848-
default=datetime.datetime(year=2020, month=5, day=20, tzinfo=pytz.utc)
848+
default=datetime.datetime(year=2020, month=5, day=20, tzinfo=UTC)
849849
)
850850

851851

tortoise/backends/oracle/client.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
from typing import TYPE_CHECKING, Any, SupportsInt, cast
66

77
import pyodbc
8-
import pytz
9-
108
try:
119
from ciso8601 import parse_datetime
1210
except ImportError: # pragma: nocoverage
@@ -29,6 +27,7 @@
2927
)
3028
from tortoise.backends.oracle.executor import OracleExecutor
3129
from tortoise.backends.oracle.schema_generator import OracleSchemaGenerator
30+
from tortoise.timezone import UTC
3231

3332
if TYPE_CHECKING: # pragma: nocoverage
3433
import asyncodbc # pylint: disable=W0611
@@ -102,7 +101,7 @@ def _timestamp_convert(self, value: bytes) -> datetime.date:
102101
try:
103102
return parse_datetime(value.decode()).date()
104103
except ValueError:
105-
return parse_datetime(value.decode()[:-32]).astimezone(tz=pytz.utc)
104+
return parse_datetime(value.decode()[:-32]).astimezone(tz=UTC)
106105

107106
async def __aenter__(self) -> asyncodbc.Connection:
108107
connection = await super().__aenter__()

tortoise/timezone.py

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,17 @@
22

33
import functools
44
import os
5+
import sys
56
from datetime import datetime, time, tzinfo
7+
from zoneinfo import ZoneInfo as _ZoneInfo
8+
from zoneinfo import ZoneInfoNotFoundError
69

7-
import pytz
10+
if sys.version_info >= (3, 12):
11+
from datetime import UTC
12+
else:
13+
from datetime import timezone
14+
15+
UTC = timezone.utc
816

917

1018
@functools.cache
@@ -28,7 +36,7 @@ def now() -> datetime:
2836
Return an aware datetime.datetime, depending on use_tz and timezone.
2937
"""
3038
if get_use_tz():
31-
return datetime.now(tz=pytz.utc)
39+
return datetime.now(tz=UTC)
3240
else:
3341
return datetime.now(get_default_timezone())
3442

@@ -40,7 +48,32 @@ def get_default_timezone() -> tzinfo:
4048
4149
This is the time zone defined by Tortoise config.
4250
"""
43-
return pytz.timezone(get_timezone())
51+
return parse_timezone(get_timezone())
52+
53+
54+
class ZoneInfo(_ZoneInfo):
55+
@property
56+
def zone(self) -> str:
57+
# Compatible with pytz:
58+
# >>> ZoneInfo('UTC').key == pytz.timezone('UTC').zone == 'UTC'
59+
return self.key
60+
61+
62+
def parse_timezone(zone: str) -> tzinfo:
63+
if zone.upper() == "UTC":
64+
return ZoneInfo("UTC")
65+
try:
66+
return ZoneInfo(zone)
67+
except ZoneInfoNotFoundError as e:
68+
words = zone.split("/")
69+
# Compatible with `pytz.timezone`:
70+
# US/central -> US/Central
71+
# Europe/moscow -> Europe/Moscow
72+
# asia/ShangHai -> Asia/Shanghai
73+
styled = "/".join([i if i.isupper() else i.title() for i in words])
74+
if styled != zone:
75+
return ZoneInfo(zone)
76+
raise e
4477

4578

4679
def _reset_timezone_cache() -> None:
@@ -64,7 +97,7 @@ def localtime(value: datetime | None = None, timezone: str | None = None) -> dat
6497
"""
6598
if value is None:
6699
value = now()
67-
tz = get_default_timezone() if timezone is None else pytz.timezone(timezone)
100+
tz = get_default_timezone() if timezone is None else parse_timezone(timezone)
68101
if is_naive(value):
69102
raise ValueError("localtime() cannot be applied to a naive datetime")
70103
return value.astimezone(tz)
@@ -104,7 +137,7 @@ def make_aware(
104137
105138
:raises ValueError: when value is not naive datetime
106139
"""
107-
tz = get_default_timezone() if timezone is None else pytz.timezone(timezone)
140+
tz = get_default_timezone() if timezone is None else parse_timezone(timezone)
108141
if hasattr(tz, "localize"):
109142
return tz.localize(value, is_dst=is_dst)
110143
if is_aware(value):
@@ -119,7 +152,7 @@ def make_naive(value: datetime, timezone: str | None = None) -> datetime:
119152
120153
:raises ValueError: when value is naive datetime
121154
"""
122-
tz = get_default_timezone() if timezone is None else pytz.timezone(timezone)
155+
tz = get_default_timezone() if timezone is None else parse_timezone(timezone)
123156
if is_naive(value):
124157
raise ValueError("make_naive() cannot be applied to a naive datetime")
125158
return value.astimezone(tz).replace(tzinfo=None)

0 commit comments

Comments
 (0)