-
-
Notifications
You must be signed in to change notification settings - Fork 2k
Make datetime generic over tzinfo #11844
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4c32d0c
e2a3800
814df90
a5492ba
d176a1d
040dbcb
7ce96ea
ce3ee77
c59db10
c0d5cc1
eb4f7d8
14aa5d2
d8d9d41
f656dbb
25a3e48
ce3c470
b148970
f9d5875
605f598
510c87f
2b2c314
cbf66a8
4974026
d7b69b1
9b8d191
a903506
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from datetime import date, datetime, time, timedelta, timezone, tzinfo | ||
| from typing import Union, cast | ||
| from typing_extensions import Never, assert_type | ||
|
|
||
| UTC: timezone = timezone.utc | ||
|
|
||
| dt_none = cast(datetime[None], None) | ||
| dt_tz = cast(datetime[tzinfo], None) | ||
| dt_both = cast(datetime[Union[tzinfo, None]], None) | ||
|
|
||
| # Constructors | ||
|
|
||
| assert_type(datetime(2000, 1, 1), datetime[None]) | ||
| assert_type(datetime(2000, 1, 1, tzinfo=None), datetime[None]) | ||
| assert_type(datetime(2000, 1, 1, tzinfo=UTC), datetime[tzinfo]) | ||
|
|
||
| assert_type(datetime.fromtimestamp(0), datetime[None]) | ||
| assert_type(datetime.fromtimestamp(0, None), datetime[None]) | ||
| assert_type(datetime.fromtimestamp(0, UTC), datetime[tzinfo]) | ||
| assert_type(datetime.utcfromtimestamp(0), datetime[None]) # pyright: ignore[reportDeprecated] | ||
|
|
||
| assert_type(datetime.now(), datetime[None]) | ||
| assert_type(datetime.now(None), datetime[None]) | ||
| assert_type(datetime.now(UTC), datetime[tzinfo]) | ||
| assert_type(datetime.today(), datetime[None]) | ||
| assert_type(datetime.utcnow(), datetime[None]) # pyright: ignore[reportDeprecated] | ||
|
|
||
| assert_type(datetime.fromisoformat("2000-01-01"), datetime[Union[tzinfo, None]]) | ||
|
|
||
| # Comparisons | ||
|
|
||
| assert_type(dt_none < dt_none, bool) | ||
| assert_type(dt_tz < dt_tz, bool) | ||
| assert_type(dt_both < dt_both, bool) | ||
|
|
||
| assert_type(dt_none < dt_tz, Never) | ||
| assert_type(dt_tz < dt_none, Never) | ||
| assert_type(dt_both < dt_none, bool) | ||
| assert_type(dt_both < dt_tz, bool) | ||
| assert_type(dt_none < dt_both, bool) | ||
|
|
||
| # Sub | ||
|
|
||
| assert_type(dt_none - dt_none, timedelta) | ||
| assert_type(dt_tz - dt_tz, timedelta) | ||
| assert_type(dt_both - dt_both, timedelta) | ||
|
|
||
| assert_type(dt_none - dt_tz, Never) | ||
| assert_type(dt_tz - dt_none, Never) | ||
| assert_type(dt_both - dt_none, timedelta) | ||
| assert_type(dt_both - dt_tz, timedelta) | ||
| assert_type(dt_none - dt_both, timedelta) | ||
| assert_type(dt_tz - dt_both, timedelta) | ||
|
|
||
| # Combine | ||
|
|
||
| assert_type(datetime.combine(date(2000, 1, 1), time(12, 0)), datetime[None]) | ||
| assert_type(datetime.combine(date(2000, 1, 1), time(12, 0), tzinfo=None), datetime[None]) | ||
| assert_type(datetime.combine(date(2000, 1, 1), time(12, 0), tzinfo=UTC), datetime[tzinfo]) | ||
|
|
||
| # Replace | ||
|
|
||
| assert_type(dt_none.replace(year=2001), datetime[None]) | ||
| assert_type(dt_none.replace(year=2001, tzinfo=None), datetime[None]) | ||
| assert_type(dt_none.replace(year=2001, tzinfo=UTC), datetime[tzinfo]) | ||
| assert_type(dt_tz.replace(year=2001), datetime[tzinfo]) | ||
| assert_type(dt_tz.replace(year=2001, tzinfo=None), datetime[None]) | ||
| assert_type(dt_tz.replace(year=2001, tzinfo=UTC), datetime[tzinfo]) | ||
| assert_type(dt_both.replace(year=2001), datetime[Union[tzinfo, None]]) | ||
| assert_type(dt_both.replace(year=2001, tzinfo=None), datetime[None]) | ||
| assert_type(dt_both.replace(year=2001, tzinfo=UTC), datetime[tzinfo]) | ||
|
|
||
| # Attributes | ||
|
|
||
| assert_type(dt_none.tzinfo, None) | ||
| assert_type(dt_tz.tzinfo, tzinfo) | ||
| assert_type(dt_both.tzinfo, Union[tzinfo, None]) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| import sys | ||
| from abc import abstractmethod | ||
| from time import struct_time | ||
| from typing import ClassVar, Final, NoReturn, SupportsIndex, final, overload, type_check_only | ||
| from typing import Any, ClassVar, Final, Generic, NoReturn, SupportsIndex, TypeVar, final, overload, type_check_only | ||
| from typing_extensions import CapsuleType, Self, TypeAlias, deprecated, disjoint_base | ||
|
|
||
| if sys.version_info >= (3, 11): | ||
|
|
@@ -242,10 +242,13 @@ class timedelta: | |
| def __bool__(self) -> bool: ... | ||
| def __hash__(self) -> int: ... | ||
|
|
||
| _TzInfoT = TypeVar("_TzInfoT", bound=tzinfo | None, default=Any) | ||
|
|
||
| @disjoint_base | ||
| class datetime(date): | ||
| class datetime(date, Generic[_TzInfoT]): | ||
| min: ClassVar[datetime] | ||
| max: ClassVar[datetime] | ||
| @overload | ||
| def __new__( | ||
| cls, | ||
| year: SupportsIndex, | ||
|
|
@@ -255,10 +258,40 @@ class datetime(date): | |
| minute: SupportsIndex = 0, | ||
| second: SupportsIndex = 0, | ||
| microsecond: SupportsIndex = 0, | ||
| tzinfo: _TzInfo | None = None, | ||
| tzinfo: None = None, | ||
| *, | ||
| fold: int = 0, | ||
| ) -> Self: ... | ||
| ) -> datetime[None]: ... | ||
| @overload | ||
| def __new__( | ||
| cls, | ||
| year: SupportsIndex, | ||
| month: SupportsIndex, | ||
| day: SupportsIndex, | ||
| hour: SupportsIndex = 0, | ||
| minute: SupportsIndex = 0, | ||
| second: SupportsIndex = 0, | ||
| microsecond: SupportsIndex = 0, | ||
| *, | ||
| tzinfo: _TzInfo, | ||
| fold: int = 0, | ||
| ) -> datetime[_TzInfo]: ... | ||
| @overload | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this extra overload needed compared to making |
||
| def __new__( | ||
| cls, | ||
| year: SupportsIndex, | ||
| month: SupportsIndex, | ||
| day: SupportsIndex, | ||
| hour: SupportsIndex, | ||
| minute: SupportsIndex, | ||
| second: SupportsIndex, | ||
| microsecond: SupportsIndex, | ||
| tzinfo: _TzInfo, | ||
| *, | ||
| fold: int = 0, | ||
| ) -> datetime[_TzInfo]: ... | ||
| @classmethod | ||
| def fromisoformat(cls, date_string: str, /) -> datetime[_TzInfo | None]: ... # type: ignore[override] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the fact that this will now start returning import datetime
class mydatetime(datetime.datetime):
pass
print(type(mydatetime.fromisoformat('2020-01-01')))A difficulty is that _DT = TypeVar("_DT", bound="datetime")
@classmethod
def fromisoformat(cls: type[_DT], date_string: str, /) -> _DT[_TzInfo | None]: ... # type: ignore[override] |
||
| @property | ||
| def hour(self) -> int: ... | ||
| @property | ||
|
|
@@ -268,29 +301,47 @@ class datetime(date): | |
| @property | ||
| def microsecond(self) -> int: ... | ||
| @property | ||
| def tzinfo(self) -> _TzInfo | None: ... | ||
| def tzinfo(self) -> _TzInfoT: ... | ||
| @property | ||
| def fold(self) -> int: ... | ||
| # On <3.12, the name of the first parameter in the pure-Python implementation | ||
| # didn't match the name in the C implementation, | ||
| # meaning it is only *safe* to pass it as a keyword argument on 3.12+ | ||
| if sys.version_info >= (3, 12): | ||
| @overload # type: ignore[override] | ||
| @classmethod | ||
| def fromtimestamp(cls, timestamp: float, tz: _TzInfo | None = None) -> Self: ... | ||
| def fromtimestamp(cls, timestamp: float, tz: None = None) -> datetime[None]: ... | ||
| @overload | ||
| @classmethod | ||
| def fromtimestamp(cls, timestamp: float, tz: _TzInfo) -> datetime[_TzInfo]: ... | ||
| else: | ||
| @overload # type: ignore[override] | ||
| @classmethod | ||
| def fromtimestamp(cls, timestamp: float, /, tz: None = None) -> datetime[None]: ... | ||
| @overload | ||
| @classmethod | ||
| def fromtimestamp(cls, timestamp: float, /, tz: _TzInfo | None = None) -> Self: ... | ||
| def fromtimestamp(cls, timestamp: float, /, tz: _TzInfo) -> datetime[_TzInfo]: ... | ||
|
|
||
| @classmethod | ||
| @deprecated("Use timezone-aware objects to represent datetimes in UTC; e.g. by calling .fromtimestamp(datetime.timezone.utc)") | ||
| def utcfromtimestamp(cls, t: float, /) -> Self: ... | ||
| @deprecated("Use timezone-aware objects to represent datetimes in UTC; e.g. by calling .fromtimestamp(datetime.UTC)") | ||
| def utcfromtimestamp(cls, t: float, /) -> datetime[None]: ... | ||
| @overload | ||
| @classmethod | ||
| def now(cls, tz: None = None) -> datetime[None]: ... | ||
| @overload | ||
| @classmethod | ||
| def now(cls, tz: _TzInfo) -> datetime[_TzInfo]: ... | ||
| @classmethod | ||
| def now(cls, tz: _TzInfo | None = None) -> Self: ... | ||
| def today(cls) -> datetime[None]: ... # type: ignore[override] | ||
| @classmethod | ||
| @deprecated("Use timezone-aware objects to represent datetimes in UTC; e.g. by calling .now(datetime.timezone.utc)") | ||
| def utcnow(cls) -> Self: ... | ||
| @deprecated("Use timezone-aware objects to represent datetimes in UTC; e.g. by calling .now(datetime.UTC)") | ||
| def utcnow(cls) -> datetime[None]: ... | ||
| @overload | ||
| @classmethod | ||
| def combine(cls, date: _Date, time: _Time, tzinfo: None = None) -> datetime[None]: ... | ||
| @overload | ||
| @classmethod | ||
| def combine(cls, date: _Date, time: _Time, tzinfo: _TzInfo | None = ...) -> Self: ... | ||
| def combine(cls, date: _Date, time: _Time, tzinfo: _TzInfo) -> datetime[_TzInfo]: ... | ||
| def timestamp(self) -> float: ... | ||
| def utctimetuple(self) -> struct_time: ... | ||
| def date(self) -> _Date: ... | ||
|
|
@@ -312,6 +363,7 @@ class datetime(date): | |
| fold: int = ..., | ||
| ) -> Self: ... | ||
|
|
||
| @overload | ||
| def replace( | ||
| self, | ||
| year: SupportsIndex = ..., | ||
|
|
@@ -321,25 +373,116 @@ class datetime(date): | |
| minute: SupportsIndex = ..., | ||
| second: SupportsIndex = ..., | ||
| microsecond: SupportsIndex = ..., | ||
| tzinfo: _TzInfo | None = ..., | ||
| *, | ||
| fold: int = ..., | ||
| ) -> Self: ... | ||
| @overload | ||
| def replace( | ||
| self, | ||
| year: SupportsIndex, | ||
| month: SupportsIndex, | ||
| day: SupportsIndex, | ||
| hour: SupportsIndex, | ||
| minute: SupportsIndex, | ||
| second: SupportsIndex, | ||
| microsecond: SupportsIndex, | ||
| tzinfo: _TzInfo, | ||
| *, | ||
| fold: int = ..., | ||
| ) -> datetime[_TzInfo]: ... | ||
| @overload | ||
| def replace( | ||
| self, | ||
| year: SupportsIndex = ..., | ||
| month: SupportsIndex = ..., | ||
| day: SupportsIndex = ..., | ||
| hour: SupportsIndex = ..., | ||
| minute: SupportsIndex = ..., | ||
| second: SupportsIndex = ..., | ||
| microsecond: SupportsIndex = ..., | ||
| *, | ||
| tzinfo: _TzInfo, | ||
| fold: int = ..., | ||
| ) -> datetime[_TzInfo]: ... | ||
| @overload | ||
| def replace( | ||
| self, | ||
| year: SupportsIndex, | ||
| month: SupportsIndex, | ||
| day: SupportsIndex, | ||
| hour: SupportsIndex, | ||
| minute: SupportsIndex, | ||
| second: SupportsIndex, | ||
| microsecond: SupportsIndex, | ||
| tzinfo: None, | ||
| *, | ||
| fold: int = ..., | ||
| ) -> datetime[None]: ... | ||
| @overload | ||
| def replace( | ||
| self, | ||
| year: SupportsIndex = ..., | ||
| month: SupportsIndex = ..., | ||
| day: SupportsIndex = ..., | ||
| hour: SupportsIndex = ..., | ||
| minute: SupportsIndex = ..., | ||
| second: SupportsIndex = ..., | ||
| microsecond: SupportsIndex = ..., | ||
| *, | ||
| tzinfo: None, | ||
| fold: int = ..., | ||
| ) -> datetime[None]: ... | ||
| def astimezone(self, tz: _TzInfo | None = None) -> Self: ... | ||
| def isoformat(self, sep: str = "T", timespec: str = "auto") -> str: ... | ||
| @classmethod | ||
| def strptime(cls, date_string: str, format: str, /) -> Self: ... | ||
| def strptime(cls, date_string: str, format: str, /) -> datetime[_TzInfo | None]: ... # type: ignore[override] | ||
| def utcoffset(self) -> timedelta | None: ... | ||
| def tzname(self) -> str | None: ... | ||
| def dst(self) -> timedelta | None: ... | ||
| def __le__(self, value: datetime, /) -> bool: ... # type: ignore[override] | ||
| def __lt__(self, value: datetime, /) -> bool: ... # type: ignore[override] | ||
| def __ge__(self, value: datetime, /) -> bool: ... # type: ignore[override] | ||
| def __gt__(self, value: datetime, /) -> bool: ... # type: ignore[override] | ||
| @overload # type: ignore[override] | ||
| def __le__( # type: ignore[overload-overlap] | ||
| self: datetime[_TzInfo] | datetime[_TzInfo | None], value: datetime[_TzInfo] | datetime[_TzInfo | None], / | ||
| ) -> bool: ... # type: ignore[misc] | ||
| @overload | ||
| def __le__(self: datetime[None] | datetime[_TzInfo | None], value: datetime[None] | datetime[_TzInfo | None], /) -> bool: ... # type: ignore[misc] | ||
| @overload | ||
| def __le__(self: datetime[Any], value: datetime[Any], /) -> NoReturn: ... | ||
| @overload # type: ignore[override] | ||
| def __lt__( # type: ignore[overload-overlap] | ||
| self: datetime[_TzInfo] | datetime[_TzInfo | None], value: datetime[_TzInfo] | datetime[_TzInfo | None], / | ||
| ) -> bool: ... # type: ignore[misc] | ||
| @overload | ||
| def __lt__(self: datetime[None] | datetime[_TzInfo | None], value: datetime[None] | datetime[_TzInfo | None], /) -> bool: ... # type: ignore[misc] | ||
| @overload | ||
| def __lt__(self: datetime[Any], value: datetime[Any], /) -> NoReturn: ... | ||
| @overload # type: ignore[override] | ||
| def __ge__( # type: ignore[overload-overlap] | ||
| self: datetime[_TzInfo] | datetime[_TzInfo | None], value: datetime[_TzInfo] | datetime[_TzInfo | None], / | ||
| ) -> bool: ... # type: ignore[misc] | ||
| @overload | ||
| def __ge__(self: datetime[None] | datetime[_TzInfo | None], value: datetime[None] | datetime[_TzInfo | None], /) -> bool: ... # type: ignore[misc] | ||
| @overload | ||
| def __ge__(self: datetime[Any], value: datetime[Any], /) -> NoReturn: ... | ||
| @overload # type: ignore[override] | ||
| def __gt__( # type: ignore[overload-overlap] | ||
| self: datetime[_TzInfo] | datetime[_TzInfo | None], value: datetime[_TzInfo] | datetime[_TzInfo | None], / | ||
| ) -> bool: ... # type: ignore[misc] | ||
| @overload | ||
| def __gt__(self: datetime[None] | datetime[_TzInfo | None], value: datetime[None] | datetime[_TzInfo | None], /) -> bool: ... # type: ignore[misc] | ||
| @overload | ||
| def __gt__(self: datetime[Any], value: datetime[Any], /) -> NoReturn: ... | ||
| def __eq__(self, value: object, /) -> bool: ... | ||
| def __hash__(self) -> int: ... | ||
| @overload # type: ignore[override] | ||
| def __sub__(self, value: Self, /) -> timedelta: ... | ||
| def __sub__( # type: ignore[overload-overlap] | ||
| self: datetime[_TzInfo] | datetime[_TzInfo | None], value: datetime[_TzInfo] | datetime[_TzInfo | None], / | ||
| ) -> timedelta: ... | ||
| @overload | ||
| def __sub__( # type: ignore[overload-overlap] | ||
| self: datetime[None] | datetime[_TzInfo | None], value: datetime[None] | datetime[_TzInfo | None], / | ||
| ) -> timedelta: ... | ||
| @overload | ||
| def __sub__(self: datetime[Any], value: datetime[Any], /) -> NoReturn: ... | ||
| @overload | ||
| def __sub__(self, value: timedelta, /) -> Self: ... | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if, when using the
default=Any, willdt.tzinfobetzinfo | None(the bound) orAny?Running the following with mypy, ty and pyrefly reveals
Any:So maybe it would be better to use
default=tzinfo | None? Or is there a reason whyAnyis better?(Added after I remembered I asked this already) Previously you answered:
If both are equally non-working, wouldn't it be better to go with
tzinfo | Nonethen?