Skip to content

Commit ae53b8e

Browse files
committed
Remove pytz
1 parent 86b1c4c commit ae53b8e

File tree

5 files changed

+66
-59
lines changed

5 files changed

+66
-59
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,6 @@ jobs:
9090
run: |
9191
uv pip uninstall typing-extensions
9292
uv run --no-sync python examples/basic.py
93-
- name: Test No pytz
94-
if: matrix.python-version == 'py3.12'
95-
run: |
96-
uv pip uninstall pytz
97-
uv run --no-sync pytest $PYTEST_ARGS tests/
98-
env:
99-
PYTHONDEVMODE: 1
100-
PYTEST_ARGS: "-n auto --cov=tortoise --cov-append --cov-branch --tb=native -q"
10193
- name: Upload Coverage
10294
run: |
10395
uvx coveralls --service=github

CHANGELOG.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Changed
4343
- **Shell command now uses optional dependencies**: Install with ``pip install tortoise-orm[ipython]`` (recommended) or ``pip install tortoise-orm[ptpython]`` to enable. IPython is preferred over ptpython. Both shells are supported.
4444
- **DatetimeField and TimeField behavior change**: Fields with ``auto_now=True`` no longer incorrectly set ``auto_now_add=True`` internally.
4545
- feat: foreignkey to model type (#2027)
46-
- refactor: move pytz to optional dependencies (#2023)
46+
- refactor: remove `pytz` and use standard library `zoneinfo` (#2023)
4747

4848
Deprecated
4949
^^^^^^^^^^

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ accel = [
4949
"ciso8601; sys_platform != 'win32' and implementation_name == 'cpython'",
5050
"uvloop; sys_platform != 'win32' and implementation_name == 'cpython'",
5151
"orjson",
52-
"pytz",
5352
]
5453
asyncpg = ["asyncpg"]
5554
aiomysql = ["aiomysql"]

tests/fields/test_time.py

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@
33
from datetime import timezone as dt_timezone
44
from time import sleep
55
from unittest.mock import patch
6+
from zoneinfo import ZoneInfoNotFoundError
67

78
import pytest
89
from iso8601 import ParseError
910

1011
from tests import testmodels
1112
from tortoise import fields, timezone
1213
from tortoise.contrib import test
13-
from tortoise.contrib.test.condition import NotIn
14+
from tortoise.contrib.test.condition import In, NotIn
1415
from tortoise.exceptions import ConfigurationError, IntegrityError
1516
from tortoise.expressions import F
16-
from tortoise.timezone import get_default_timezone, parse_timezone
17+
from tortoise.timezone import UTC, get_default_timezone, parse_timezone
1718

1819
# ============================================================================
1920
# TestEmpty -> test_empty_*
@@ -229,8 +230,7 @@ async def test_datetime_filter_by_year_month_day(db):
229230

230231

231232
@pytest.mark.asyncio
232-
@test.requireCapability(dialect="sqlite")
233-
@test.requireCapability(dialect="postgres")
233+
@test.requireCapability(dialect=In("sqlite", "postgres"))
234234
async def test_time_create(db):
235235
"""Test creating time fields (sqlite/postgres)."""
236236
model = testmodels.TimeFields
@@ -241,8 +241,7 @@ async def test_time_create(db):
241241

242242

243243
@pytest.mark.asyncio
244-
@test.requireCapability(dialect="sqlite")
245-
@test.requireCapability(dialect="postgres")
244+
@test.requireCapability(dialect=In("sqlite", "postgres"))
246245
async def test_time_cast(db):
247246
"""Test time field accepts ISO format string (sqlite/postgres)."""
248247
model = testmodels.TimeFields
@@ -252,8 +251,7 @@ async def test_time_cast(db):
252251

253252

254253
@pytest.mark.asyncio
255-
@test.requireCapability(dialect="sqlite")
256-
@test.requireCapability(dialect="postgres")
254+
@test.requireCapability(dialect=In("sqlite", "postgres"))
257255
async def test_time_values(db):
258256
"""Test time field in values() query (sqlite/postgres)."""
259257
model = testmodels.TimeFields
@@ -264,8 +262,7 @@ async def test_time_values(db):
264262

265263

266264
@pytest.mark.asyncio
267-
@test.requireCapability(dialect="sqlite")
268-
@test.requireCapability(dialect="postgres")
265+
@test.requireCapability(dialect=In("sqlite", "postgres"))
269266
async def test_time_values_list(db):
270267
"""Test time field in values_list() query (sqlite/postgres)."""
271268
model = testmodels.TimeFields
@@ -276,8 +273,7 @@ async def test_time_values_list(db):
276273

277274

278275
@pytest.mark.asyncio
279-
@test.requireCapability(dialect="sqlite")
280-
@test.requireCapability(dialect="postgres")
276+
@test.requireCapability(dialect=In("sqlite", "postgres"))
281277
async def test_time_get(db):
282278
"""Test getting by time field (sqlite/postgres)."""
283279
model = testmodels.TimeFields
@@ -503,3 +499,28 @@ async def test_timedelta_get(db):
503499
await model.create(timedelta=delta)
504500
obj = await model.get(timedelta=delta)
505501
assert obj.timedelta == delta
502+
503+
504+
def test_zoneinfo():
505+
tz = parse_timezone("Asia/Shanghai")
506+
tz2 = parse_timezone("asia/shanghai")
507+
tz3 = parse_timezone("asia/ShangHai")
508+
now = datetime.now()
509+
assert now.replace(tzinfo=tz) == now.replace(tzinfo=tz2) == now.replace(tzinfo=tz3)
510+
tz = parse_timezone("US/central")
511+
tz2 = parse_timezone("US/Central")
512+
assert now.replace(tzinfo=tz) == now.replace(tzinfo=tz2)
513+
tz_utc = parse_timezone("UTC")
514+
tz_utc2 = parse_timezone("utc")
515+
tz_utc3 = parse_timezone("Utc")
516+
assert tz_utc.key == tz_utc2.zone == "UTC"
517+
assert (
518+
now.replace(tzinfo=UTC)
519+
== now.replace(tzinfo=tz_utc)
520+
== now.replace(tzinfo=tz_utc2)
521+
== now.replace(tzinfo=tz_utc3)
522+
)
523+
with pytest.raises(ZoneInfoNotFoundError):
524+
parse_timezone("invalid-zone-name")
525+
with pytest.raises(ZoneInfoNotFoundError):
526+
parse_timezone("Invalid/Zonename")

tortoise/timezone.py

Lines changed: 32 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,45 +4,40 @@
44
import os
55
import sys
66
from datetime import datetime, time, tzinfo
7+
from zoneinfo import ZoneInfo as _ZoneInfo
8+
from zoneinfo import ZoneInfoNotFoundError
79

8-
try:
9-
import pytz
10-
except ImportError:
11-
from zoneinfo import ZoneInfo as _ZoneInfo
12-
from zoneinfo import ZoneInfoNotFoundError
13-
14-
if sys.version_info >= (3, 12):
15-
from datetime import UTC
16-
else:
17-
from datetime import timezone
18-
19-
UTC = timezone.utc
20-
21-
class ZoneInfo(_ZoneInfo):
22-
@property
23-
def zone(self) -> str:
24-
# Compatible with pytz:
25-
# >>> ZoneInfo('UTC').key == pytz.timezone('UTC').zone == 'UTC'
26-
return self.key
27-
28-
def parse_timezone(zone: str) -> tzinfo:
29-
if zone.upper() == "UTC":
30-
return ZoneInfo("UTC")
31-
try:
32-
return ZoneInfo(zone)
33-
except ZoneInfoNotFoundError as e:
34-
words = zone.split("/")
35-
# Compatible with `pytz.timezone`:
36-
# US/central -> US/Central
37-
# Europe/moscow -> Europe/Moscow
38-
# asia/ShangHai -> Asia/Shanghai
39-
styled = "/".join([i if i.isupper() else i.title() for i in words])
40-
if styled != zone:
41-
return ZoneInfo(zone)
42-
raise e
10+
if sys.version_info >= (3, 12):
11+
from datetime import UTC
4312
else:
44-
UTC = pytz.utc # type: ignore[assignment]
45-
parse_timezone = pytz.timezone
13+
from datetime import timezone
14+
15+
UTC = timezone.utc
16+
17+
18+
class ZoneInfo(_ZoneInfo):
19+
@property
20+
def zone(self) -> str:
21+
# Compatible with pytz:
22+
# >>> ZoneInfo('UTC').key == pytz.timezone('UTC').zone == 'UTC'
23+
return self.key
24+
25+
26+
def parse_timezone(zone: str) -> tzinfo:
27+
if zone.upper() == "UTC":
28+
return ZoneInfo("UTC")
29+
try:
30+
return ZoneInfo(zone)
31+
except ZoneInfoNotFoundError as e:
32+
words = zone.split("/")
33+
# Compatible with `pytz.timezone`:
34+
# US/central -> US/Central
35+
# Europe/moscow -> Europe/Moscow
36+
# asia/ShangHai -> Asia/Shanghai
37+
styled = "/".join([i if i.isupper() else i.title() for i in words])
38+
if styled != zone:
39+
return ZoneInfo(styled)
40+
raise e
4641

4742

4843
@functools.cache

0 commit comments

Comments
 (0)