From 3f24fa7bfd613b17a73e0d0bd3f1cf3341ba187c Mon Sep 17 00:00:00 2001 From: Arie Bovenberg Date: Fri, 25 Oct 2024 14:16:26 +0200 Subject: [PATCH] Improve docstrings and autocomplete --- .github/PULL_REQUEST_TEMPLATE.md | 1 + .github/workflows/checks.yml | 8 +- .readthedocs.yml | 2 +- CHANGELOG.rst | 12 + Cargo.lock | 20 +- docs/api.rst | 9 +- docs/faq.rst | 14 + docs/overview.rst | 6 +- pyproject.toml | 7 +- pysrc/whenever/__init__.pyi | 103 +++-- pysrc/whenever/_pywhenever.py | 624 +++++++++++++++++++++++++------ requirements/docs.txt | 2 +- requirements/test.txt | 6 +- src/common.rs | 27 +- src/date.rs | 15 +- src/date_delta.rs | 9 +- src/instant.rs | 41 +- src/local_datetime.rs | 63 +++- src/offset_datetime.rs | 77 ++-- src/system_datetime.rs | 53 ++- src/time.rs | 15 +- src/zoned_datetime.rs | 70 ++-- tests/test_main.py | 45 +++ 23 files changed, 943 insertions(+), 286 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2c083b1e..98df1cc0 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,6 +7,7 @@ - [ ] Build runs successfully - [ ] Type stubs updated - [ ] Docs updated +- [ ] If docstrings were affected, check if they appear correctly in the docs as well as autocomplete # Release checklist (maintainers only) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 4519a1c6..850ea15d 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -19,7 +19,7 @@ jobs: "3.10", "3.11", "3.12", - "3.13-dev", + "3.13", ] steps: - uses: actions/checkout@v4 @@ -62,14 +62,14 @@ jobs: - uses: actions/setup-python@v5 if: ${{ !(matrix.os == 'windows-latest') }} with: - python-version: '3.12' + python-version: '3.13' # ensure 32-bit target is tested # FUTURE: Add a linux 32-bit target - uses: actions/setup-python@v5 if: ${{ matrix.os == 'windows-latest' }} with: - python-version: '3.12' + python-version: '3.13' architecture: x86 @@ -91,7 +91,7 @@ jobs: "3.10", "3.11", "3.12", - "3.13-dev", + "3.13", # # NOTE: pypy/pytest fails sometimes (https://github.com/pypy/pypy/issues/3959) "pypy3.9", "pypy3.10" diff --git a/.readthedocs.yml b/.readthedocs.yml index 10853a3b..c22136aa 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -8,7 +8,7 @@ sphinx: build: os: ubuntu-22.04 tools: - python: "3.11" + python: "3.12" # rust shouldn't be needed as we disable building the extension # in the readthedocs configuration diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fba7b839..729ec12e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,18 @@ 🚀 Changelog ============ +0.6.10 (2024-10-30) +------------------- + +**Improved** + +- Improve method documentation and autocomplete support (#172, #173, #176) + +**Fixed** + +- Remove lingering undocumented ``offset`` on ``Instant`` +- Fix incorrect ``LocalDateTime.difference`` return type annotation + 0.6.9 (2024-09-12) ------------------ diff --git a/Cargo.lock b/Cargo.lock index 68337940..9f98914e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,21 +4,21 @@ version = 3 [[package]] name = "libc" -version = "0.2.155" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "pyo3-build-config" -version = "0.22.2" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8730e591b14492a8945cdff32f089250b05f5accecf74aeddf9e8272ce1fa8" +checksum = "bc38c5feeb496c8321091edf3d63e9a6829eab4b863b4a6a65f26f3e9cc6b179" dependencies = [ "once_cell", "target-lexicon", @@ -26,9 +26,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.22.2" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e97e919d2df92eb88ca80a037969f44e5e70356559654962cbb3316d00300c6" +checksum = "94845622d88ae274d2729fcefc850e63d7a3ddff5e3ce11bd88486db9f1d357d" dependencies = [ "libc", "pyo3-build-config", @@ -36,9 +36,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.15" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4873307b7c257eddcb50c9bedf158eb669578359fb28428bef438fec8e6ba7c2" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "whenever" diff --git a/docs/api.rst b/docs/api.rst index 99d9e853..007cd6ed 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -41,16 +41,20 @@ Concrete classes .. autoclass:: whenever.Instant :members: + from_utc, format_rfc3339, parse_rfc3339, format_rfc2822, parse_rfc2822, add, - subtract + subtract, :special-members: __add__, __sub__ :member-order: bysource :show-inheritance: + .. autoattribute:: MIN + .. autoattribute:: MAX + .. autoclass:: whenever.LocalDateTime :members: assume_utc, @@ -63,6 +67,9 @@ Concrete classes :member-order: bysource :show-inheritance: + .. autoattribute:: MIN + .. autoattribute:: MAX + .. autoclass:: whenever.OffsetDateTime :members: format_rfc3339, diff --git a/docs/faq.rst b/docs/faq.rst index 2732e048..83dac2c2 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -179,3 +179,17 @@ Additional advantages of ``pyo3_ffi`` are: - Its API is more stable than PyO3's, which is still evolving. - It allows support for per-interpreter GIL, and free-threaded Python, which are not yet supported by PyO3. + +Why can't I subclass Whenever classes? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Whenever classes aren't meant to be subclassed. +There's no plan to change this due to the following reasons: + +1. The benefits of subclassing are limited. + If you want to extend the classes, composition is a better way to do it. + Alternatively, you can use Python's dynamic features to create + something that "quacks" like a subclass. +2. For a class to support subclassing properly, a lot of extra work is needed. + It also adds many subtle ways to misuse the API, that are hard to control. +3. Enabling subclassing would undo some performance optimizations. diff --git a/docs/overview.rst b/docs/overview.rst index 27fc768f..ca461ef1 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -361,8 +361,8 @@ Ambiguity in timezones In timezones, local clocks are often moved backwards and forwards due to Daylight Saving Time (DST) or political decisions. -This creates two types of situations for the :class:`~whenever.ZonedDateTime` -and :class:`~whenever.SystemDateTime` types: +This makes it complicated to map a local datetime to a point on the timeline. +Two common situations arise: - When the clock moves backwards, there is a period of time that repeats. For example, Sunday October 29th 2023 2:30am occurred twice in Paris. @@ -374,7 +374,7 @@ and :class:`~whenever.SystemDateTime` types: Common approaches are to extrapolate the time forward or backwards to 1:30am or 3:30am. - .. important:: + .. note:: You may wonder why skipped time is "extrapolated" like this, and not truncated. Why turn 2:30am into 3:30am and not cut diff --git a/pyproject.toml b/pyproject.toml index ad341ceb..43f99c6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,9 +3,12 @@ name = "whenever" authors = [ {name = "Arie Bovenberg", email = "a.c.bovenberg@gmail.com"}, ] +maintainers = [ + {name = "Arie Bovenberg", email = "a.c.bovenberg@gmail.com"}, +] readme = "README.md" -version = "0.6.9" -description = "Modern datetime library for Python, written in Rust" +version = "0.6.10" +description = "Modern datetime library for Python" requires-python = ">=3.9" classifiers = [ "Development Status :: 4 - Beta", diff --git a/pysrc/whenever/__init__.pyi b/pysrc/whenever/__init__.pyi index b48d572b..54b62c3c 100644 --- a/pysrc/whenever/__init__.pyi +++ b/pysrc/whenever/__init__.pyi @@ -40,7 +40,7 @@ __all__ = [ "SUNDAY", ] -Disambiguate = Literal["raise", "earlier", "later", "compatible"] +_EXTENSION_LOADED: bool @final class Date: @@ -257,15 +257,7 @@ Delta = DateTimeDelta | TimeDelta | DateDelta _T = TypeVar("_T") -class _BasicConversions(ABC): - @classmethod - def from_py_datetime(cls: type[_T], d: _datetime, /) -> _T: ... - def py_datetime(self) -> _datetime: ... - def format_common_iso(self) -> str: ... - @classmethod - def parse_common_iso(cls: type[_T], s: str, /) -> _T: ... - -class _KnowsLocal(_BasicConversions, ABC): +class _KnowsLocal(ABC): @property def year(self) -> int: ... @property @@ -283,7 +275,7 @@ class _KnowsLocal(_BasicConversions, ABC): def date(self) -> Date: ... def time(self) -> Time: ... -class _KnowsInstant(_BasicConversions, ABC): +class _KnowsInstant(ABC): def timestamp(self) -> int: ... def timestamp_millis(self) -> int: ... def timestamp_nanos(self) -> int: ... @@ -333,6 +325,7 @@ class Instant(_KnowsInstant): def from_timestamp_nanos(cls, i: int, /) -> Instant: ... @classmethod def from_py_datetime(cls, d: _datetime, /) -> Instant: ... + def py_datetime(self) -> _datetime: ... def format_rfc2822(self) -> str: ... @classmethod def parse_rfc2822(cls, s: str, /) -> Instant: ... @@ -406,11 +399,13 @@ class OffsetDateTime(_KnowsInstantAndLocal): ) -> OffsetDateTime: ... @classmethod def from_py_datetime(cls, d: _datetime, /) -> OffsetDateTime: ... + def py_datetime(self) -> _datetime: ... @classmethod def strptime(cls, s: str, fmt: str, /) -> OffsetDateTime: ... def format_rfc2822(self) -> str: ... @classmethod def parse_rfc2822(cls, s: str, /) -> OffsetDateTime: ... + def format_common_iso(self) -> str: ... def format_rfc3339(self) -> str: ... @classmethod def parse_rfc3339(cls, s: str, /) -> OffsetDateTime: ... @@ -491,7 +486,9 @@ class ZonedDateTime(_KnowsInstantAndLocal): *, nanosecond: int = 0, tz: str, - disambiguate: Disambiguate = "raise", + disambiguate: Literal[ + "compatible", "raise", "earlier", "later" + ] = "raise", ) -> None: ... @property def tz(self) -> str: ... @@ -500,6 +497,7 @@ class ZonedDateTime(_KnowsInstantAndLocal): def now(cls, tz: str, /) -> ZonedDateTime: ... @classmethod def from_py_datetime(cls, d: _datetime, /) -> ZonedDateTime: ... + def py_datetime(self) -> _datetime: ... @classmethod def from_timestamp( cls, i: int | float, /, *, tz: str @@ -508,6 +506,7 @@ class ZonedDateTime(_KnowsInstantAndLocal): def from_timestamp_millis(cls, i: int, /, *, tz: str) -> ZonedDateTime: ... @classmethod def from_timestamp_nanos(cls, i: int, /, *, tz: str) -> ZonedDateTime: ... + def format_common_iso(self) -> str: ... @classmethod def parse_common_iso(cls, s: str, /) -> ZonedDateTime: ... def exact_eq(self, other: ZonedDateTime, /) -> bool: ... @@ -522,13 +521,21 @@ class ZonedDateTime(_KnowsInstantAndLocal): second: int = ..., nanosecond: int = ..., tz: str = ..., - disambiguate: Disambiguate, + disambiguate: Literal["compatible", "raise", "earlier", "later"], ) -> ZonedDateTime: ... def replace_date( - self, d: Date, /, *, disambiguate: Disambiguate + self, + d: Date, + /, + *, + disambiguate: Literal["compatible", "raise", "earlier", "later"], ) -> ZonedDateTime: ... def replace_time( - self, t: Time, /, *, disambiguate: Disambiguate + self, + t: Time, + /, + *, + disambiguate: Literal["compatible", "raise", "earlier", "later"], ) -> ZonedDateTime: ... @overload def add( @@ -544,7 +551,7 @@ class ZonedDateTime(_KnowsInstantAndLocal): milliseconds: float = 0, microseconds: float = 0, nanoseconds: int = 0, - disambiguate: Disambiguate, + disambiguate: Literal["compatible", "raise", "earlier", "later"], ) -> ZonedDateTime: ... @overload def add( @@ -561,7 +568,11 @@ class ZonedDateTime(_KnowsInstantAndLocal): def add(self, d: TimeDelta, /) -> ZonedDateTime: ... @overload def add( - self, d: DateDelta | DateTimeDelta, /, *, disambiguate: Disambiguate + self, + d: DateDelta | DateTimeDelta, + /, + *, + disambiguate: Literal["compatible", "raise", "earlier", "later"], ) -> ZonedDateTime: ... @overload def subtract( @@ -577,7 +588,7 @@ class ZonedDateTime(_KnowsInstantAndLocal): milliseconds: float = 0, microseconds: float = 0, nanoseconds: int = 0, - disambiguate: Disambiguate, + disambiguate: Literal["compatible", "raise", "earlier", "later"], ) -> ZonedDateTime: ... @overload def subtract( @@ -594,7 +605,11 @@ class ZonedDateTime(_KnowsInstantAndLocal): def subtract(self, d: TimeDelta, /) -> ZonedDateTime: ... @overload def subtract( - self, d: DateDelta | DateTimeDelta, /, *, disambiguate: Disambiguate + self, + d: DateDelta | DateTimeDelta, + /, + *, + disambiguate: Literal["compatible", "raise", "earlier", "later"], ) -> ZonedDateTime: ... def __add__(self, delta: TimeDelta) -> ZonedDateTime: ... @overload @@ -614,7 +629,9 @@ class SystemDateTime(_KnowsInstantAndLocal): second: int = 0, *, nanosecond: int = 0, - disambiguate: Disambiguate = "raise", + disambiguate: Literal[ + "compatible", "raise", "earlier", "later" + ] = "raise", ) -> None: ... @classmethod def now(cls) -> SystemDateTime: ... @@ -626,8 +643,10 @@ class SystemDateTime(_KnowsInstantAndLocal): def from_timestamp_nanos(cls, i: int, /) -> SystemDateTime: ... @classmethod def from_py_datetime(cls, d: _datetime, /) -> SystemDateTime: ... + def py_datetime(self) -> _datetime: ... @classmethod def parse_common_iso(cls, s: str, /) -> SystemDateTime: ... + def format_common_iso(self) -> str: ... def exact_eq(self, other: SystemDateTime, /) -> bool: ... def replace( self, @@ -639,13 +658,21 @@ class SystemDateTime(_KnowsInstantAndLocal): minute: int = ..., second: int = ..., nanosecond: int = ..., - disambiguate: Disambiguate, + disambiguate: Literal["compatible", "raise", "earlier", "later"], ) -> SystemDateTime: ... def replace_date( - self, d: Date, /, *, disambiguate: Disambiguate + self, + d: Date, + /, + *, + disambiguate: Literal["compatible", "raise", "earlier", "later"], ) -> SystemDateTime: ... def replace_time( - self, t: Time, /, *, disambiguate: Disambiguate + self, + t: Time, + /, + *, + disambiguate: Literal["compatible", "raise", "earlier", "later"], ) -> SystemDateTime: ... @overload def add( @@ -661,7 +688,7 @@ class SystemDateTime(_KnowsInstantAndLocal): milliseconds: float = 0, microseconds: float = 0, nanoseconds: int = 0, - disambiguate: Disambiguate, + disambiguate: Literal["compatible", "raise", "earlier", "later"], ) -> SystemDateTime: ... @overload def add( @@ -678,7 +705,11 @@ class SystemDateTime(_KnowsInstantAndLocal): def add(self, d: TimeDelta, /) -> SystemDateTime: ... @overload def add( - self, d: DateDelta | DateTimeDelta, /, *, disambiguate: Disambiguate + self, + d: DateDelta | DateTimeDelta, + /, + *, + disambiguate: Literal["compatible", "raise", "earlier", "later"], ) -> SystemDateTime: ... @overload def subtract( @@ -694,7 +725,7 @@ class SystemDateTime(_KnowsInstantAndLocal): milliseconds: float = 0, microseconds: float = 0, nanoseconds: int = 0, - disambiguate: Disambiguate, + disambiguate: Literal["compatible", "raise", "earlier", "later"], ) -> SystemDateTime: ... @overload def subtract( @@ -711,7 +742,11 @@ class SystemDateTime(_KnowsInstantAndLocal): def subtract(self, d: TimeDelta, /) -> SystemDateTime: ... @overload def subtract( - self, d: DateDelta | DateTimeDelta, /, *, disambiguate: Disambiguate + self, + d: DateDelta | DateTimeDelta, + /, + *, + disambiguate: Literal["compatible", "raise", "earlier", "later"], ) -> SystemDateTime: ... def __add__(self, delta: TimeDelta) -> SystemDateTime: ... @overload @@ -739,15 +774,23 @@ class LocalDateTime(_KnowsLocal): self, offset: int | TimeDelta, / ) -> OffsetDateTime: ... def assume_tz( - self, tz: str, /, *, disambiguate: Disambiguate + self, + tz: str, + /, + *, + disambiguate: Literal["compatible", "raise", "earlier", "later"], ) -> ZonedDateTime: ... def assume_system_tz( - self, *, disambiguate: Disambiguate + self, + *, + disambiguate: Literal["compatible", "raise", "earlier", "later"], ) -> SystemDateTime: ... @classmethod def from_py_datetime(cls, d: _datetime, /) -> LocalDateTime: ... + def py_datetime(self) -> _datetime: ... @classmethod def parse_common_iso(cls, s: str, /) -> LocalDateTime: ... + def format_common_iso(self) -> str: ... @classmethod def strptime(cls, s: str, fmt: str, /) -> LocalDateTime: ... def replace( @@ -817,7 +860,7 @@ class LocalDateTime(_KnowsLocal): ) -> LocalDateTime: ... def difference( self, other: LocalDateTime, /, *, ignore_dst: Literal[True] - ) -> DateTimeDelta: ... + ) -> TimeDelta: ... def __add__(self, delta: DateDelta) -> LocalDateTime: ... def __sub__(self, other: DateDelta) -> LocalDateTime: ... def __lt__(self, other: LocalDateTime) -> bool: ... diff --git a/pysrc/whenever/_pywhenever.py b/pysrc/whenever/_pywhenever.py index a08af24f..930456ea 100644 --- a/pysrc/whenever/_pywhenever.py +++ b/pysrc/whenever/_pywhenever.py @@ -32,7 +32,7 @@ # - It saves some overhead from __future__ import annotations -__version__ = "0.6.9" +__version__ = "0.6.10" import enum import re @@ -1931,6 +1931,7 @@ def format_common_iso(self) -> str: See :ref:`here ` for more information. """ + raise NotImplementedError() @classmethod @abstractmethod @@ -2053,7 +2054,8 @@ def replace(self: _T, /, **kwargs: Any) -> _T: The same exceptions as the constructor may be raised. For system and zoned datetimes, The ``disambiguate=`` keyword argument is **required** to - resolve ambiguities. + resolve ambiguities. For more information, see + whenever.rtfd.io/en/latest/overview.html#ambiguity-in-timezones Example ------- @@ -2083,6 +2085,8 @@ def replace_date(self: _T, date: Date, /, **kwargs) -> _T: The same exceptions as the constructor may be raised. For system and zoned datetimes, you will need to pass ``disambiguate=`` to resolve ambiguities. + For more information, see + whenever.rtfd.io/en/latest/overview.html#ambiguity-in-timezones """ def replace_time(self: _T, time: Time, /, **kwargs) -> _T: @@ -2122,7 +2126,11 @@ def add( ) -> _T: """Add date and time units to this datetime. - See :ref:`the docs on arithmetic ` for more information. + Arithmetic on datetimes is complicated. + Additional keyword arguments ``ignore_dst`` and ``disambiguate`` + may be needed for certain types and situations. + See :ref:`the docs on arithmetic ` for more information + and the reasoning behind it. """ @abstractmethod @@ -2141,10 +2149,7 @@ def subtract( nanoseconds: int = 0, **kwargs, ) -> _T: - """Subtract date and time units to this datetime. - - See :ref:`the docs on arithmetic ` for more information. - """ + """Inverse of :meth:`add`.""" class _KnowsInstant(_BasicConversions): @@ -2530,21 +2535,19 @@ class Instant(_KnowsInstant): Example ------- >>> from whenever import Instant - >>> py311_release_livestream = Instant.from_utc(2022, 10, 24, hour=17) + >>> py311_release = Instant.from_utc(2022, 10, 24, hour=17) Instant(2022-10-24 17:00:00Z) - >>> py311_release_livestream.add(hours=3).timestamp() + >>> py311_release.add(hours=3).timestamp() 1666641600 - - Note - ---- - The corresponding :class:`~datetime.datetime` object is always - timezone-aware and has a fixed :attr:`~datetime.UTC` tzinfo. """ __slots__ = () def __init__(self) -> None: - raise TypeError("Instant instances cannot be created") + raise TypeError( + "Instant instances cannot be created through the constructor. " + "Use `Instant.from_utc` or `Instant.now` instead." + ) @classmethod def from_utc( @@ -2558,6 +2561,7 @@ def from_utc( *, nanosecond: int = 0, ) -> Instant: + """Create an Instant defined by a UTC date and time.""" if nanosecond < 0 or nanosecond >= 1_000_000_000: raise ValueError(f"nanosecond out of range: {nanosecond}") return cls._from_py_unchecked( @@ -2565,18 +2569,24 @@ def from_utc( nanosecond, ) - offset = TimeDelta.ZERO - MIN: ClassVar[Instant] + """The minimum representable instant.""" + MAX: ClassVar[Instant] + """The maximum representable instant.""" @classmethod def now(cls) -> Instant: + """Create an Instant from the current time.""" secs, nanos = divmod(time_ns(), 1_000_000_000) return cls._from_py_unchecked(_fromtimestamp(secs, _UTC), nanos) @classmethod def from_timestamp(cls, i: int | float, /) -> Instant: + """Create an Instant from a UNIX timestamp (in seconds). + + The inverse of the ``timestamp()`` method. + """ secs, fract = divmod(i, 1) return cls._from_py_unchecked( _fromtimestamp(secs, _UTC), int(fract * 1_000_000_000) @@ -2584,6 +2594,10 @@ def from_timestamp(cls, i: int | float, /) -> Instant: @classmethod def from_timestamp_millis(cls, i: int, /) -> Instant: + """Create an Instant from a UNIX timestamp (in milliseconds). + + The inverse of the ``timestamp_millis()`` method. + """ if not isinstance(i, int): raise TypeError("method requires an integer") secs, millis = divmod(i, 1_000) @@ -2593,6 +2607,10 @@ def from_timestamp_millis(cls, i: int, /) -> Instant: @classmethod def from_timestamp_nanos(cls, i: int, /) -> Instant: + """Create an Instant from a UNIX timestamp (in nanoseconds). + + The inverse of the ``timestamp_nanos()`` method. + """ if not isinstance(i, int): raise TypeError("method requires an integer") secs, nanos = divmod(i, 1_000_000_000) @@ -2600,6 +2618,17 @@ def from_timestamp_nanos(cls, i: int, /) -> Instant: @classmethod def from_py_datetime(cls, d: _datetime, /) -> Instant: + """Create an Instant from a standard library ``datetime`` object + with ``tzinfo=datetime.timezone.utc``. + + The inverse of the ``py_datetime()`` method. + + Important + --------- + If the datetime tzinfo is *not* UTC, a ``ValueError`` is raised. + If you want to convert a datetime with a different timezone to UTC, + use ``ZonedDateTime`` or ``OffsetDateTime``. + """ if d.tzinfo is not _UTC: raise ValueError( "Can only create Instant from UTC datetime, " @@ -2610,6 +2639,10 @@ def from_py_datetime(cls, d: _datetime, /) -> Instant: ) def format_common_iso(self) -> str: + """Convert to the popular ISO format ``YYYY-MM-DDTHH:MM:SSZ`` + + The inverse of the ``parse_common_iso()`` method. + """ return ( self._py_dt.isoformat()[:-6] + bool(self._nanos) * f".{self._nanos:09d}".rstrip("0") @@ -2618,28 +2651,15 @@ def format_common_iso(self) -> str: @classmethod def parse_common_iso(cls, s: str, /) -> Instant: - """Parse a UTC datetime in common ISO 8601 format. + """Parse the popular ISO format ``YYYY-MM-DDTHH:MM:SSZ`` - Inverse of :meth:`~_DateTime.format_common_iso` - - Example - ------- - >>> Instant.parse_common_iso("2020-08-15T23:12:00Z") - Instant(2020-08-15 23:12:00Z) - >>> - >>> # also valid: - >>> Instant.parse_common_iso("2020-08-15T23:12:00+00:00") - >>> Instant.parse_common_iso("2020-08-15T23:12:00.34Z") - >>> - >>> # not valid - >>> Instant.parse_common_iso("2020-08-15T23:12:00+02:00") - >>> Instant.parse_common_iso("2020-08-15 23:12:00Z") - >>> Instant.parse_common_iso("2020-08-15T23:12:00-00:00") + The inverse of the ``format_common_iso()`` method. - Warning - ------- - Nonzero offsets will not be implicitly converted to UTC. - Use :meth:`OffsetDateTime.parse_common_iso` if you'd like to + Important + --------- + Nonzero offsets will *not* be implicitly converted to UTC, + but will raise a ``ValueError``. + Use ``OffsetDateTime.parse_common_iso`` if you'd like to parse an ISO 8601 string with a nonzero offset. """ if ( @@ -2656,7 +2676,7 @@ def parse_common_iso(cls, s: str, /) -> Instant: def format_rfc2822(self) -> str: """Format as an RFC 2822 string. - The inverse of :meth:`parse_rfc2822`. + The inverse of the ``parse_rfc2822()`` method. Example ------- @@ -2669,7 +2689,7 @@ def format_rfc2822(self) -> str: def parse_rfc2822(cls, s: str, /) -> Instant: """Parse a UTC datetime in RFC 2822 format. - The inverse of :meth:`format_rfc2822`. + The inverse of the ``format_rfc2822()`` method. Example ------- @@ -2685,11 +2705,11 @@ def parse_rfc2822(cls, s: str, /) -> Instant: >>> # Error: includes offset. Use OffsetDateTime.parse_rfc2822() instead >>> Instant.parse_rfc2822("Sat, 15 Aug 2020 23:12:00 +0200") - Warning - ------- - * Nonzero offsets will not be implicitly converted to UTC. - Use :meth:`OffsetDateTime.parse_rfc2822` if you'd like to - parse an RFC 2822 string with a nonzero offset. + Important + --------- + Nonzero offsets will not be implicitly converted to UTC. + Use ``OffsetDateTime.parse_rfc2822()`` if you'd like to + parse an RFC 2822 string with a nonzero offset. """ # FUTURE: disallow +0000 parsed = _parse_rfc2822(s) @@ -2712,7 +2732,7 @@ def parse_rfc2822(cls, s: str, /) -> Instant: def format_rfc3339(self) -> str: """Format as an RFC 3339 string - Inverse of :meth:`parse_rfc3339`. + The inverse of the ``parse_rfc3339()`` method. Example ------- @@ -2729,7 +2749,7 @@ def format_rfc3339(self) -> str: def parse_rfc3339(cls, s: str, /) -> Instant: """Parse a UTC datetime in RFC 3339 format. - Inverse of :meth:`format_rfc3339`. + The inverse of the ``format_rfc3339()`` method. Example ------- @@ -2744,9 +2764,10 @@ def parse_rfc3339(cls, s: str, /) -> Instant: >>> # not valid (nonzero offset): >>> Instant.parse_rfc3339("2020-08-15T23:12:00+02:00") - Warning - ------- - Nonzero offsets will not be implicitly converted to UTC. + Important + --------- + Nonzero offsets will not be implicitly converted to UTC, + but will raise a ValueError. Use :meth:`OffsetDateTime.parse_rfc3339` if you'd like to parse an RFC 3339 string with a nonzero offset. """ @@ -2771,7 +2792,7 @@ def add( ) -> Instant: """Add a time amount to this instant. - See :ref:`the docs on arithmetic ` for more information. + See the `docs on arithmetic `_ for more information. """ return self + TimeDelta( hours=hours, @@ -2794,7 +2815,7 @@ def subtract( ) -> Instant: """Subtract a time amount from this instant. - See :ref:`the docs on arithmetic ` for more information. + See the `docs on arithmetic `_ for more information. """ return self.add( hours=-hours, @@ -2808,7 +2829,7 @@ def subtract( def __add__(self, delta: TimeDelta) -> Instant: """Add a time amount to this datetime. - See :ref:`the docs on arithmetic ` for more information. + See the `docs on arithmetic `_ for more information. """ if isinstance(delta, TimeDelta): delta_secs, nanos = divmod( @@ -2830,8 +2851,10 @@ def __sub__(self, other: TimeDelta) -> Instant: ... def __sub__(self, other: TimeDelta | _KnowsInstant) -> Instant | TimeDelta: """Subtract another exact time or timedelta - Subtraction of deltas happens in the same way as :meth:`subtract`. - Subtraction of instants happens the same way as :meth:`~_KnowsInstant.difference`. + Subtraction of deltas happens in the same way as the :meth:`subtract` method. + Subtraction of instants happens the same way as the :meth:`~_KnowsInstant.difference` method. + + See the `docs on arithmetic `_ for more information. Example ------- @@ -2886,20 +2909,16 @@ class OffsetDateTime(_KnowsInstantAndLocal): Example ------- - >>> # 9 AM in Salt Lake City, with the UTC offset at the time - >>> pycon23_start = OffsetDateTime(2023, 4, 21, hour=9, offset=-6) - OffsetDateTime(2023-04-21 09:00:00-06:00) - - Warning - ------- - This class does *not* account for daylight saving time. - If you need to adjust an offset datetime and account for DST, - convert to a :class:`~ZonedDateTime`. + >>> # Midnight in Salt Lake City + >>> OffsetDateTime(2023, 4, 21, offset=-6) + OffsetDateTime(2023-04-21 00:00:00-06:00) Note ---- - The corresponding :class:`~datetime.datetime` object is always - timezone-aware and has a fixed :class:`datetime.timezone` tzinfo. + Adjusting instances of this class do *not* account for daylight saving time. + If you need to add or subtract durations from an offset datetime + and account for DST, convert to a ``ZonedDateTime`` first, + This class knows when the offset changes. """ __slots__ = () @@ -2936,6 +2955,17 @@ def __init__( def now( cls, offset: int | TimeDelta, /, *, ignore_dst: bool = False ) -> OffsetDateTime: + """Create an instance from the current time. + + Important + --------- + Getting the current time with a fixed offset implicitly ignores DST + and other timezone changes. Instead, use ``Instant.now()`` or + ``ZonedDateTime.now()`` if you know the timezone. + Or, if you want to ignore DST and accept potentially incorrect offsets, + pass ``ignore_dst=True`` to this method. For more information, see + `the documentation `_. + """ if ignore_dst is not True: raise ImplicitlyIgnoringDST( "Getting the current time with a fixed offset implicitly ignores DST " @@ -2951,6 +2981,10 @@ def now( ) def format_common_iso(self) -> str: + """Convert to the popular ISO format ``YYYY-MM-DDTHH:MM:SS±HH:MM`` + + The inverse of the ``parse_common_iso()`` method. + """ iso_without_fracs = self._py_dt.isoformat() return ( iso_without_fracs[:19] @@ -2960,16 +2994,14 @@ def format_common_iso(self) -> str: @classmethod def parse_common_iso(cls, s: str, /) -> OffsetDateTime: - """Parse a *popular version* of the ISO 8601 datetime format. + """Parse the popular ISO format ``YYYY-MM-DDTHH:MM:SS±HH:MM`` - Inverse of :meth:`~_DateTime.format_common_iso` + The inverse of the ``format_common_iso()`` method. Example ------- >>> OffsetDateTime.parse_common_iso("2020-08-15T23:12:00+02:00") OffsetDateTime(2020-08-15 23:12:00+02:00) - >>> # also valid: - >>> OffsetDateTime.parse_common_iso("2020-08-15T23:12:00Z") """ if (match := _match_offset_str(s)) is None: raise ValueError(f"Invalid format: {s!r}") @@ -2999,6 +3031,21 @@ def parse_common_iso(cls, s: str, /) -> OffsetDateTime: def from_timestamp( cls, i: int, /, *, offset: int | TimeDelta, ignore_dst: bool = False ) -> OffsetDateTime: + """Create an instance from a UNIX timestamp (in seconds). + + The inverse of the ``timestamp()`` method. + + Important + --------- + Creating an instance from a UNIX timestamp implicitly ignores DST + and other timezone changes. This because you don't strictly + know if the given offset is correct for an arbitrary timestamp. + Instead, use ``Instant.from_timestamp()`` + or ``ZonedDateTime.from_timestamp()`` if you know the timezone. + Or, if you want to ignore DST and accept potentially incorrect offsets, + pass ``ignore_dst=True`` to this method. For more information, see + `the documentation `_. + """ if ignore_dst is not True: raise _EXC_TIMESTAMP_DST secs, fract = divmod(i, 1) @@ -3011,6 +3058,21 @@ def from_timestamp( def from_timestamp_millis( cls, i: int, /, *, offset: int | TimeDelta, ignore_dst: bool = False ) -> OffsetDateTime: + """Create an instance from a UNIX timestamp (in milliseconds). + + The inverse of the ``timestamp_millis()`` method. + + Important + --------- + Creating an instance from a UNIX timestamp implicitly ignores DST + and other timezone changes. This because you don't strictly + know if the given offset is correct for an arbitrary timestamp. + Instead, use ``Instant.from_timestamp_millis()`` + or ``ZonedDateTime.from_timestamp_millis()`` if you know the timezone. + Or, if you want to ignore DST and accept potentially incorrect offsets, + pass ``ignore_dst=True`` to this method. For more information, see + `the documentation `_. + """ if ignore_dst is not True: raise _EXC_TIMESTAMP_DST if not isinstance(i, int): @@ -3024,6 +3086,21 @@ def from_timestamp_millis( def from_timestamp_nanos( cls, i: int, /, *, offset: int | TimeDelta, ignore_dst: bool = False ) -> OffsetDateTime: + """Create an instance from a UNIX timestamp (in nanoseconds). + + The inverse of the ``timestamp_nanos()`` method. + + Important + --------- + Creating an instance from a UNIX timestamp implicitly ignores DST + and other timezone changes. This because you don't strictly + know if the given offset is correct for an arbitrary timestamp. + Instead, use ``Instant.from_timestamp_nanos()`` + or ``ZonedDateTime.from_timestamp_nanos()`` if you know the timezone. + Or, if you want to ignore DST and accept potentially incorrect offsets, + pass ``ignore_dst=True`` to this method. For more information, see + `the documentation `_. + """ if ignore_dst is not True: raise _EXC_TIMESTAMP_DST if not isinstance(i, int): @@ -3035,6 +3112,16 @@ def from_timestamp_nanos( @classmethod def from_py_datetime(cls, d: _datetime, /) -> OffsetDateTime: + """Create an instance from a standard library ``datetime`` object. + + The inverse of the ``py_datetime()`` method. + + Important + --------- + If the datetime tzinfo is *not* a fixed offset, + a ``ValueError`` is raised. If you want to convert a datetime + with a ``ZoneInfo`` tzinfo, use ``ZonedDateTime.from_py_datetime()`` instead. + """ if not isinstance(d.tzinfo, _timezone): raise ValueError( "Datetime's tzinfo is not a datetime.timezone, " @@ -3048,6 +3135,17 @@ def from_py_datetime(cls, d: _datetime, /) -> OffsetDateTime: def replace( self, /, ignore_dst: bool = False, **kwargs: Any ) -> OffsetDateTime: + """Construct a new instance with the given fields replaced. + + Important + --------- + Replacing fields of an offset datetime implicitly ignores DST + and other timezone changes. This because it isn't guaranteed that + the same offset will be valid at the new time. + If you want to account for DST, convert to a ``ZonedDateTime`` first. + Or, if you want to ignore DST and accept potentially incorrect offsets, + pass ``ignore_dst=True`` to this method. + """ _check_invalid_replace_kwargs(kwargs) if ignore_dst is not True: raise _EXC_ADJUST_OFFSET_DATETIME @@ -3063,6 +3161,17 @@ def replace( def replace_date( self, date: Date, /, *, ignore_dst: bool = False ) -> OffsetDateTime: + """Construct a new instance with the date replaced. + + Important + --------- + Replacing the date of an offset datetime implicitly ignores DST + and other timezone changes. This because it isn't guaranteed that + the same offset will be valid at the new date. + If you want to account for DST, convert to a ``ZonedDateTime`` first. + Or, if you want to ignore DST and accept potentially incorrect offsets, + pass ``ignore_dst=True`` to this method. + """ if ignore_dst is not True: raise _EXC_ADJUST_OFFSET_DATETIME return self._from_py_unchecked( @@ -3075,6 +3184,17 @@ def replace_date( def replace_time( self, time: Time, /, *, ignore_dst: bool = False ) -> OffsetDateTime: + """Construct a new instance with the time replaced. + + Important + --------- + Replacing the time of an offset datetime implicitly ignores DST + and other timezone changes. This because it isn't guaranteed that + the same offset will be valid at the new time. + If you want to account for DST, convert to a ``ZonedDateTime`` first. + Or, if you want to ignore DST and accept potentially incorrect offsets, + pass ``ignore_dst=True`` to this method. + """ if ignore_dst is not True: raise _EXC_ADJUST_OFFSET_DATETIME return self._from_py_unchecked( @@ -3090,6 +3210,7 @@ def __hash__(self) -> int: return hash((self._py_dt, self._nanos)) def __sub__(self, other: _KnowsInstant) -> TimeDelta: + """Calculate the duration relative to another exact time.""" if isinstance(other, (TimeDelta, DateDelta, DateTimeDelta)): raise _EXC_ADJUST_OFFSET_DATETIME return super().__sub__(other) # type: ignore[misc, no-any-return] @@ -3104,11 +3225,11 @@ def strptime(cls, s: str, /, fmt: str) -> OffsetDateTime: >>> OffsetDateTime.strptime("2020-08-15+0200", "%Y-%m-%d%z") OffsetDateTime(2020-08-15 00:00:00+02:00) - Note - ---- + Important + --------- The parsed ``tzinfo`` must be a fixed offset - (:class:`~datetime.timezone` instance). - This means you need to include the directive ``%z``, ``%Z``, or ``%:z`` + (``datetime.timezone`` instance). + This means you MUST include the directive ``%z``, ``%Z``, or ``%:z`` in the format string. """ parsed = _datetime.strptime(s, fmt) @@ -3127,7 +3248,7 @@ def strptime(cls, s: str, /, fmt: str) -> OffsetDateTime: def format_rfc2822(self) -> str: """Format as an RFC 2822 string. - Inverse of :meth:`parse_rfc2822`. + The inverse of the ``parse_rfc2822()`` method. Example ------- @@ -3140,7 +3261,7 @@ def format_rfc2822(self) -> str: def parse_rfc2822(cls, s: str, /) -> OffsetDateTime: """Parse an offset datetime in RFC 2822 format. - Inverse of :meth:`format_rfc2822`. + The inverse of the ``format_rfc2822()`` method. Example ------- @@ -3168,7 +3289,7 @@ def parse_rfc2822(cls, s: str, /) -> OffsetDateTime: def format_rfc3339(self) -> str: """Format as an RFC 3339 string - Inverse of :meth:`parse_rfc3339`. + The inverse of the ``parse_rfc3339()`` method. Example ------- @@ -3184,9 +3305,9 @@ def format_rfc3339(self) -> str: @classmethod def parse_rfc3339(cls, s: str, /) -> OffsetDateTime: - """Parse a UTC datetime in RFC 3339 format. + """Parse a fixed-offset datetime in RFC 3339 format. - Inverse of :meth:`format_rfc3339`. + The inverse of the ``format_rfc3339()`` method. Example ------- @@ -3223,10 +3344,38 @@ def parse_rfc3339(cls, s: str, /) -> OffsetDateTime: @no_type_check def add(self, *args, **kwargs) -> OffsetDateTime: + """Add a time amount to this datetime. + + Important + --------- + Shifting a fixed-offset datetime implicitly ignore DST + and other timezone changes. This because it isn't guaranteed that + the same offset will be valid at the resulting time. + If you want to account for DST, convert to a ``ZonedDateTime`` first. + Or, if you want to ignore DST and accept potentially incorrect offsets, + pass ``ignore_dst=True`` to this method. + + For more information, see + `the documentation `_. + """ return self._shift(1, *args, **kwargs) @no_type_check def subtract(self, *args, **kwargs) -> OffsetDateTime: + """Subtract a time amount from this datetime. + + Important + --------- + Shifting a fixed-offset datetime implicitly ignore DST + and other timezone changes. This because it isn't guaranteed that + the same offset will be valid at the resulting time. + If you want to account for DST, convert to a ``ZonedDateTime`` first. + Or, if you want to ignore DST and accept potentially incorrect offsets, + pass ``ignore_dst=True`` to this method. + + For more information, see + `the documentation `_. + """ return self._shift(-1, *args, **kwargs) @no_type_check @@ -3326,19 +3475,21 @@ def _unpkl_offset(data: bytes) -> OffsetDateTime: @final class ZonedDateTime(_KnowsInstantAndLocal): - """A datetime associated with a IANA timezone ID. - Useful for representing the local time bound to a specific location. + """A datetime associated with a timezone in the IANA database. + Useful for representing the exact time at a specific location. Example ------- - >>> changing_the_guard = ZonedDateTime(2024, 12, 8, hour=11, tz="Europe/London") - >>> # Explicitly resolve ambiguities when clocks are set backwards. - >>> night_shift = ZonedDateTime(2023, 10, 29, 1, 15, tz="Europe/London", disambiguate="later") + >>> ZonedDateTime(2024, 12, 8, hour=11, tz="Europe/Paris") + ZonedDateTime(2024-12-08 11:00:00+01:00[Europe/Paris]) + >>> # Explicitly resolve ambiguities during DST transitions + >>> ZonedDateTime(2023, 10, 29, 1, 15, tz="Europe/London", disambiguate="earlier") + ZonedDateTime(2023-10-29 01:15:00+01:00[Europe/London]) - - Attention + Important --------- - To use this type properly, read more about :ref:`ambiguity `. + To use this type properly, read more about + `ambiguity in timezones `_. """ __slots__ = () @@ -3377,12 +3528,28 @@ def __init__( @classmethod def now(cls, tz: str, /) -> ZonedDateTime: + """Create an instance from the current time in the given timezone.""" secs, nanos = divmod(time_ns(), 1_000_000_000) return cls._from_py_unchecked( _fromtimestamp(secs, ZoneInfo(tz)), nanos ) def format_common_iso(self) -> str: + """Convert to the popular ISO format ``YYYY-MM-DDTHH:MM:SS±HH:MM[TZ_ID]`` + + The inverse of the ``parse_common_iso()`` method. + + Example + ------- + >>> ZonedDateTime(2020, 8, 15, hour=23, minute=12, tz="Europe/London") + ZonedDateTime(2020-08-15 23:12:00+01:00[Europe/London]) + + Important + --------- + The timezone ID is a recent extension to the ISO 8601 format (RFC 9557). + Althought it is gaining popularity, it is not yet widely supported + by ISO 8601 parsers. + """ py_isofmt = self._py_dt.isoformat() return ( py_isofmt[:19] # without the offset @@ -3393,15 +3560,19 @@ def format_common_iso(self) -> str: @classmethod def parse_common_iso(cls, s: str, /) -> ZonedDateTime: - """Parse from the common ISO 8601 format. + """Parse from the popular ISO format ``YYYY-MM-DDTHH:MM:SS±HH:MM[TZ_ID]`` - Inverse of :meth:`~_DateTime.format_common_iso`. + The inverse of the ``format_common_iso()`` method. Example ------- - >>> ZonedDateTime.parse_common_iso("2020-08-15T23:12:00+01:00[Europe/London]") ZonedDateTime(2020-08-15 23:12:00+01:00[Europe/London]) + + Important + --------- + The timezone ID is a recent extension to the ISO 8601 format (RFC 9557). + Althought it is gaining popularity, it is not yet widely supported. """ if (match := _match_zoned_str(s)) is None: raise ValueError(f"Invalid format: {s!r}") @@ -3433,6 +3604,10 @@ def parse_common_iso(cls, s: str, /) -> ZonedDateTime: @classmethod def from_timestamp(cls, i: int, /, *, tz: str) -> ZonedDateTime: + """Create an instance from a UNIX timestamp (in seconds). + + The inverse of the ``timestamp()`` method. + """ secs, fract = divmod(i, 1) return cls._from_py_unchecked( _fromtimestamp(secs, ZoneInfo(tz)), int(fract * 1_000_000_000) @@ -3440,6 +3615,10 @@ def from_timestamp(cls, i: int, /, *, tz: str) -> ZonedDateTime: @classmethod def from_timestamp_millis(cls, i: int, /, *, tz: str) -> ZonedDateTime: + """Create an instance from a UNIX timestamp (in milliseconds). + + The inverse of the ``timestamp_millis()`` method. + """ if not isinstance(i, int): raise TypeError("method requires an integer") secs, millis = divmod(i, 1_000) @@ -3449,6 +3628,10 @@ def from_timestamp_millis(cls, i: int, /, *, tz: str) -> ZonedDateTime: @classmethod def from_timestamp_nanos(cls, i: int, /, *, tz: str) -> ZonedDateTime: + """Create an instance from a UNIX timestamp (in nanoseconds). + + The inverse of the ``timestamp_nanos()`` method. + """ if not isinstance(i, int): raise TypeError("method requires an integer") secs, nanos = divmod(i, 1_000_000_000) @@ -3459,6 +3642,16 @@ def from_timestamp_nanos(cls, i: int, /, *, tz: str) -> ZonedDateTime: # FUTURE: optional `disambiguate` to override fold? @classmethod def from_py_datetime(cls, d: _datetime, /) -> ZonedDateTime: + """Create an instance from a standard library ``datetime`` object + with a ``ZoneInfo`` tzinfo. + + The inverse of the ``py_datetime()`` method. + + Attention + --------- + If the datetime is ambiguous (e.g. during a DST transition), + the ``fold`` attribute is used to disambiguate the time. + """ if type(d.tzinfo) is not ZoneInfo: raise ValueError( "Can only create ZonedDateTime from tzinfo=ZoneInfo (exactly), " @@ -3474,6 +3667,17 @@ def from_py_datetime(cls, d: _datetime, /) -> ZonedDateTime: def replace_date( self, date: Date, /, disambiguate: Disambiguate ) -> ZonedDateTime: + """Construct a new instance with the date replaced. + + Important + --------- + Replacing the date of a ZonedDateTime may result in an ambiguous time + (e.g. during a DST transition). Therefore, you must explicitly + specify how to handle such a situation using the ``disambiguate`` argument. + + See `the documentation `_ + for more information. + """ return self._from_py_unchecked( _resolve_ambiguity( _datetime.combine(date._py_date, self._py_dt.timetz()).replace( @@ -3489,6 +3693,17 @@ def replace_date( def replace_time( self, time: Time, /, disambiguate: Disambiguate ) -> ZonedDateTime: + """Construct a new instance with the time replaced. + + Important + --------- + Replacing the time of a ZonedDateTime may result in an ambiguous time + (e.g. during a DST transition). Therefore, you must explicitly + specify how to handle such a situation using the ``disambiguate`` argument. + + See `the documentation `_ + for more information. + """ return self._from_py_unchecked( _resolve_ambiguity( _datetime.combine( @@ -3504,6 +3719,17 @@ def replace_time( def replace( self, /, disambiguate: Disambiguate, **kwargs: Any ) -> ZonedDateTime: + """Construct a new instance with the given fields replaced. + + Important + --------- + Replacing fields of a ZonedDateTime may result in an ambiguous time + (e.g. during a DST transition). Therefore, you must explicitly + specify how to handle such a situation using the ``disambiguate`` argument. + + See `the documentation `_ + for more information. + """ _check_invalid_replace_kwargs(kwargs) try: tz = kwargs.pop("tz") @@ -3532,7 +3758,8 @@ def __hash__(self) -> int: def __add__(self, delta: TimeDelta) -> ZonedDateTime: """Add an amount of time, accounting for timezone changes (e.g. DST). - See :ref:`the docs on arithmetic ` for more information. + See `the docs `_ + for more information. """ if isinstance(delta, TimeDelta): delta_secs, nanos = divmod( @@ -3564,7 +3791,8 @@ def __sub__( ) -> _KnowsInstant | TimeDelta: """Subtract another datetime or duration. - See :ref:`the docs on arithmetic ` for more information. + See `the docs `_ + for more information. """ if isinstance(other, _KnowsInstant): return super().__sub__(other) # type: ignore[misc, no-any-return] @@ -3574,10 +3802,34 @@ def __sub__( @no_type_check def add(self, *args, **kwargs) -> ZonedDateTime: + """Add a time amount to this datetime. + + Important + --------- + Shifting a ``ZonedDateTime`` with **calendar units** (e.g. months, weeks) + may result in an ambiguous time (e.g. during a DST transition). + Therefore, when adding calendar units, you must explicitly + specify how to handle such a situation using the ``disambiguate`` argument. + + See `the documentation `_ + for more information. + """ return self._shift(1, *args, **kwargs) @no_type_check def subtract(self, *args, **kwargs) -> ZonedDateTime: + """Subtract a time amount from this datetime. + + Important + --------- + Shifting a ``ZonedDateTime`` with **calendar units** (e.g. months, weeks) + may result in an ambiguous time (e.g. during a DST transition). + Therefore, when adding calendar units, you must explicitly + specify how to handle such a situation using the ``disambiguate`` argument. + + See `the documentation `_ + for more information. + """ return self._shift(-1, *args, **kwargs) @no_type_check @@ -3699,8 +3951,8 @@ def _unpkl_zoned( @final class SystemDateTime(_KnowsInstantAndLocal): """Represents a time in the system timezone. - Unlike :class:`~OffsetDateTime`, - it knows about the system timezone and its DST transitions. + It is similar to ``OffsetDateTime``, + but it knows about the system timezone and its DST transitions. Example ------- @@ -3716,13 +3968,8 @@ class SystemDateTime(_KnowsInstantAndLocal): Attention --------- - To use this type properly, read more about :ref:`ambiguity ` and - :ref:`working with the system timezone `. - - Note - ---- - The corresponding :class:`~datetime.datetime` object has - a fixed :class:`~datetime.timezone` tzinfo. + To use this type properly, read more about `ambiguity `_ + and `working with the system timezone `_. """ __slots__ = () @@ -3758,23 +4005,41 @@ def __init__( @classmethod def now(cls) -> SystemDateTime: + """Create an instance from the current time in the system timezone.""" secs, nanos = divmod(time_ns(), 1_000_000_000) return cls._from_py_unchecked( _fromtimestamp(secs, _UTC).astimezone(None), nanos ) format_common_iso = OffsetDateTime.format_common_iso + """Convert to the popular ISO format ``YYYY-MM-DDTHH:MM:SS±HH:MM`` + + The inverse of the ``parse_common_iso()`` method. + + Important + --------- + Information about the system timezone name is *not* included in the output. + """ @classmethod def parse_common_iso(cls, s: str, /) -> SystemDateTime: - """Parse from the common ISO 8601 format, + """Parse from the popular ISO format ``YYYY-MM-DDTHH:MM:SS±HH:MM`` - similar to that of ``OffsetDateTime``.""" + Important + --------- + The offset isn't adjusted to the current system timezone. + See `the docs `_ + for more information. + """ odt = OffsetDateTime.parse_common_iso(s) return cls._from_py_unchecked(odt._py_dt, odt._nanos) @classmethod def from_timestamp(cls, i: int | float, /) -> SystemDateTime: + """Create an instance from a UNIX timestamp (in seconds). + + The inverse of the ``timestamp()`` method. + """ secs, fract = divmod(i, 1) return cls._from_py_unchecked( _fromtimestamp(secs, _UTC).astimezone(), int(fract * 1_000_000_000) @@ -3782,6 +4047,10 @@ def from_timestamp(cls, i: int | float, /) -> SystemDateTime: @classmethod def from_timestamp_millis(cls, i: int, /) -> SystemDateTime: + """Create an instance from a UNIX timestamp (in milliseconds). + + The inverse of the ``timestamp_millis()`` method. + """ if not isinstance(i, int): raise TypeError("method requires an integer") secs, millis = divmod(i, 1_000) @@ -3791,6 +4060,10 @@ def from_timestamp_millis(cls, i: int, /) -> SystemDateTime: @classmethod def from_timestamp_nanos(cls, i: int, /) -> SystemDateTime: + """Create an instance from a UNIX timestamp (in nanoseconds). + + The inverse of the ``timestamp_nanos()`` method. + """ if not isinstance(i, int): raise TypeError("method requires an integer") secs, nanos = divmod(i, 1_000_000_000) @@ -3798,8 +4071,13 @@ def from_timestamp_nanos(cls, i: int, /) -> SystemDateTime: _fromtimestamp(secs, _UTC).astimezone(), nanos ) + # TODO py_datetime docstring + @classmethod def from_py_datetime(cls, d: _datetime, /) -> SystemDateTime: + """Create an instance from a standard library ``datetime`` object + with fixed-offset tzinfo. + """ odt = OffsetDateTime.from_py_datetime(d) return cls._from_py_unchecked(odt._py_dt, odt._nanos) @@ -3811,6 +4089,17 @@ def __repr__(self) -> str: def replace_date( self, date: Date, /, disambiguate: Disambiguate ) -> SystemDateTime: + """Construct a new instance with the date replaced. + + Important + --------- + Replacing the date of a SystemDateTime may result in an ambiguous time + (e.g. during a DST transition). Therefore, you must explicitly + specify how to handle such a situation using the ``disambiguate`` argument. + + See `the documentation `_ + for more information. + """ return self._from_py_unchecked( _resolve_system_ambiguity( _datetime.combine(date._py_date, self._py_dt.time()).replace( @@ -3824,6 +4113,17 @@ def replace_date( def replace_time( self, time: Time, /, disambiguate: Disambiguate ) -> SystemDateTime: + """Construct a new instance with the time replaced. + + Important + --------- + Replacing the time of a SystemDateTime may result in an ambiguous time + (e.g. during a DST transition). Therefore, you must explicitly + specify how to handle such a situation using the ``disambiguate`` argument. + + See `the documentation `_ + for more information. + """ return self._from_py_unchecked( _resolve_system_ambiguity( _datetime.combine(self._py_dt, time._py_time).replace( @@ -3837,6 +4137,17 @@ def replace_time( def replace( self, /, disambiguate: Disambiguate, **kwargs: Any ) -> SystemDateTime: + """Construct a new instance with the given fields replaced. + + Important + --------- + Replacing fields of a SystemDateTime may result in an ambiguous time + (e.g. during a DST transition). Therefore, you must explicitly + specify how to handle such a situation using the ``disambiguate`` argument. + + See `the documentation `_ + for more information. + """ _check_invalid_replace_kwargs(kwargs) nanos = _pop_nanos_kwarg(kwargs, self._nanos) return self._from_py_unchecked( @@ -3855,7 +4166,8 @@ def __hash__(self) -> int: def __add__(self, delta: TimeDelta) -> SystemDateTime: """Add an amount of time, accounting for timezone changes (e.g. DST). - See :ref:`the docs on arithmetic ` for more information. + See `the docs `_ + for more information. """ if isinstance(delta, TimeDelta): py_dt = self._py_dt @@ -3884,7 +4196,8 @@ def __sub__( ) -> _KnowsInstant | Delta: """Subtract another datetime or duration - See :ref:`the docs on arithmetic ` for more information. + See `the docs `_ + for more information. """ if isinstance(other, _KnowsInstant): return super().__sub__(other) # type: ignore[misc, no-any-return] @@ -3894,10 +4207,34 @@ def __sub__( @no_type_check def add(self, *args, **kwargs) -> SystemDateTime: + """Add a time amount to this datetime. + + Important + --------- + Shifting a ``SystemDateTime`` with **calendar units** (e.g. months, weeks) + may result in an ambiguous time (e.g. during a DST transition). + Therefore, when adding calendar units, you must explicitly + specify how to handle such a situation using the ``disambiguate`` argument. + + See `the documentation `_ + for more information. + """ return self._shift(1, *args, **kwargs) @no_type_check def subtract(self, *args, **kwargs) -> SystemDateTime: + """Subtract a time amount from this datetime. + + Important + --------- + Shifting a ``SystemDateTime`` with **calendar units** (e.g. months, weeks) + may result in an ambiguous time (e.g. during a DST transition). + Therefore, when adding calendar units, you must explicitly + specify how to handle such a situation using the ``disambiguate`` argument. + + See `the documentation `_ + for more information. + """ return self._shift(-1, *args, **kwargs) @no_type_check @@ -4024,6 +4361,10 @@ def __init__( self._nanos = nanosecond def format_common_iso(self) -> str: + """Convert to the popular ISO format ``YYYY-MM-DDTHH:MM:SS`` + + The inverse of the ``parse_common_iso()`` method. + """ return ( (self._py_dt.isoformat() + f".{self._nanos:09d}").rstrip("0") if self._nanos @@ -4032,10 +4373,9 @@ def format_common_iso(self) -> str: @classmethod def parse_common_iso(cls, s: str, /) -> LocalDateTime: - """Parse from the commonly used ISO 8601 format - ``YYYY-MM-DDTHH:MM:SS``, where seconds may be fractional. + """Parse the popular ISO format ``YYYY-MM-DDTHH:MM:SS`` - Inverse of :meth:`~_DateTime.format_common_iso`. + The inverse of the ``format_common_iso()`` method. Example ------- @@ -4052,6 +4392,7 @@ def parse_common_iso(cls, s: str, /) -> LocalDateTime: @classmethod def from_py_datetime(cls, d: _datetime, /) -> LocalDateTime: + """Create an instance from a "naive" standard library ``datetime`` object""" if d.tzinfo is not None: raise ValueError( "Can only create LocalDateTime from a naive datetime, " @@ -4062,6 +4403,7 @@ def from_py_datetime(cls, d: _datetime, /) -> LocalDateTime: ) def replace(self, /, **kwargs: Any) -> LocalDateTime: + """Construct a new instance with the given fields replaced.""" if not _no_tzinfo_fold_or_ms(kwargs): raise TypeError( "tzinfo, fold, or microsecond are not allowed arguments" @@ -4070,11 +4412,13 @@ def replace(self, /, **kwargs: Any) -> LocalDateTime: return self._from_py_unchecked(self._py_dt.replace(**kwargs), nanos) def replace_date(self, d: Date, /) -> LocalDateTime: + """Construct a new instance with the date replaced.""" return self._from_py_unchecked( _datetime.combine(d._py_date, self._py_dt.time()), self._nanos ) def replace_time(self, t: Time, /) -> LocalDateTime: + """Construct a new instance with the time replaced.""" return self._from_py_unchecked( _datetime.combine(self._py_dt.date(), t._py_time), t._nanos ) @@ -4111,7 +4455,9 @@ def __eq__(self, other: object) -> bool: return (self._py_dt, self._nanos) == (other._py_dt, other._nanos) MIN: ClassVar[LocalDateTime] + """The minimum representable value of this type.""" MAX: ClassVar[LocalDateTime] + """The maximum representable value of this type.""" def __lt__(self, other: LocalDateTime) -> bool: if not isinstance(other, LocalDateTime): @@ -4175,7 +4521,15 @@ def difference( ) -> TimeDelta: """Calculate the difference between two local datetimes. - See :ref:`the docs on arithmetic ` for more information. + Important + --------- + The difference between two local datetimes implicitly ignores + DST transitions and other timezone changes. + To perform DST-safe operations, convert to a ``ZonedDateTime`` first. + Or, if you don't know the timezone and accept potentially incorrect results + during DST transitions, pass ``ignore_dst=True``. + For more information, + see `the docs `_. """ if ignore_dst is not True: raise ImplicitlyIgnoringDST( @@ -4192,10 +4546,36 @@ def difference( @no_type_check def add(self, *args, **kwargs) -> LocalDateTime: + """Add a time amount to this datetime. + + Important + --------- + Shifting a ``LocalDateTime`` with **exact units** (e.g. hours, seconds) + implicitly ignores DST transitions and other timezone changes. + If you need to account for these, convert to a ``ZonedDateTime`` first. + Or, if you don't know the timezone and accept potentially incorrect results + during DST transitions, pass ``ignore_dst=True``. + + See `the documentation `_ + for more information. + """ return self._shift(1, *args, **kwargs) @no_type_check def subtract(self, *args, **kwargs) -> LocalDateTime: + """Subtract a time amount from this datetime. + + Important + --------- + Shifting a ``LocalDateTime`` with **exact units** (e.g. hours, seconds) + implicitly ignores DST transitions and other timezone changes. + If you need to account for these, convert to a ``ZonedDateTime`` first. + Or, if you don't know the timezone and accept potentially incorrect results + during DST transitions, pass ``ignore_dst=True``. + + See `the documentation `_ + for more information. + """ return self._shift(-1, *args, **kwargs) @no_type_check @@ -4278,7 +4658,7 @@ def strptime(cls, s: str, /, fmt: str) -> LocalDateTime: Note ---- The parsed ``tzinfo`` must be be ``None``. - This means you can't include the directives ``%z``, ``%Z``, or ``%:z`` + This means you CANNOT include the directives ``%z``, ``%Z``, or ``%:z`` in the format string. """ parsed = _datetime.strptime(s, fmt) @@ -4292,8 +4672,7 @@ def strptime(cls, s: str, /, fmt: str) -> LocalDateTime: ) def assume_utc(self) -> Instant: - """Assume the datetime is in UTC, - creating a :class:`~whenever.Instant` instance. + """Assume the datetime is in UTC, creating an ``Instant``. Example ------- @@ -4307,8 +4686,7 @@ def assume_utc(self) -> Instant: def assume_fixed_offset( self, offset: int | TimeDelta, / ) -> OffsetDateTime: - """Assume the datetime is in the given offset, - creating a :class:`~whenever.OffsetDateTime` instance. + """Assume the datetime has the given offset, creating an ``OffsetDateTime``. Example ------- @@ -4323,7 +4701,15 @@ def assume_tz( self, tz: str, /, disambiguate: Disambiguate ) -> ZonedDateTime: """Assume the datetime is in the given timezone, - creating a :class:`~whenever.ZonedDateTime` instance. + creating a ``ZonedDateTime``. + + Note + ---- + The local datetime may be ambiguous in the given timezone + (e.g. during a DST transition). Therefore, you must explicitly + specify how to handle such a situation using the ``disambiguate`` argument. + See `the documentation `_ + for more information. Example ------- @@ -4344,7 +4730,15 @@ def assume_tz( def assume_system_tz(self, disambiguate: Disambiguate) -> SystemDateTime: """Assume the datetime is in the system timezone, - creating a :class:`~whenever.SystemDateTime` instance. + creating a ``SystemDateTime``. + + Note + ---- + The local datetime may be ambiguous in the system timezone + (e.g. during a DST transition). Therefore, you must explicitly + specify how to handle such a situation using the ``disambiguate`` argument. + See `the documentation `_ + for more information. Example ------- diff --git a/requirements/docs.txt b/requirements/docs.txt index 41b3b36d..40e087a8 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -1,5 +1,5 @@ sphinx<9 -furo>=2024.4.27,<2024.9 +furo>=2024.8.6,<2024.9 sphinx-copybutton~=0.5 myst-parser>=3,<5 enum-tools[sphinx]==0.12.0 diff --git a/requirements/test.txt b/requirements/test.txt index 63453c5b..bc9dfa28 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,9 +1,5 @@ pytest>=7,<9 -pytest-cov>=4,<6 +pytest-cov>=5,<7 pytest-benchmark[histogram]>=4,<6 hypothesis>=6,<7 time_machine>=2,<3; implementation_name == 'cpython' - -# FUTURE: remove these constraints once rdps-py supports python 3.13 -referencing>=0.23,<0.24.0; python_version == '3.13' -jsonschema>=4.17,<4.18.0; python_version == '3.13' diff --git a/src/common.rs b/src/common.rs index 11958631..cb7a00a9 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,4 +1,4 @@ -use core::ffi::c_long; +use core::ffi::{c_char, c_long, CStr}; use core::mem; use core::ptr::null_mut as NULL; use pyo3_ffi::*; @@ -1103,6 +1103,31 @@ pub(crate) const fn hash_combine(lhs: Py_hash_t, rhs: Py_hash_t) -> Py_hash_t { .wrapping_add(lhs >> 2)) } +// FUTURE: create arg lists statically +#[cfg(Py_3_13)] +pub(crate) fn arg_vec(fields: &[&CStr]) -> Vec<*const c_char> { + [ + fields + .iter() + .map(|&field| field.as_ptr()) + .collect::>(), + vec![NULL()], + ] + .concat() +} + +#[cfg(not(Py_3_13))] +pub(crate) fn arg_vec(fields: &[&CStr]) -> Vec<*mut c_char> { + [ + fields + .iter() + .map(|&field| field.as_ptr() as *mut _) + .collect::>(), + vec![NULL()], + ] + .concat() +} + pub(crate) static S_PER_DAY: i32 = 86_400; pub(crate) static NS_PER_DAY: i128 = 86_400 * 1_000_000_000; diff --git a/src/date.rs b/src/date.rs index 61fcf728..ecadf591 100644 --- a/src/date.rs +++ b/src/date.rs @@ -703,11 +703,20 @@ static mut METHODS: &[PyMethodDef] = &[ method!(day_of_week, "Return the day of the week"), method!(at, "Combine with a time to create a datetime", METH_O), method!(__reduce__, ""), - method_kwargs!(add, "Add various units to the date"), - method_kwargs!(subtract, "Subtract various units from the date"), + method_kwargs!( + add, + "add($self, *, years=0, months=0, weeks=0, days=0)\n--\n\n\ + Add various units to the date" + ), + method_kwargs!( + subtract, + "subtract($self, *, years=0, months=0, weeks=0, days=0)\n--\n\n\ + Subtract various units from the date" + ), method_kwargs!( replace, - "Return a new date with the specified components replaced" + "replace($self, *, year=None, month=None, day=None)\n--\n\n\ + Return a new date with the specified components replaced" ), PyMethodDef::zeroed(), ]; diff --git a/src/date_delta.rs b/src/date_delta.rs index 8f8331b4..c6bd2013 100644 --- a/src/date_delta.rs +++ b/src/date_delta.rs @@ -168,14 +168,7 @@ unsafe fn __new__(cls: *mut PyTypeObject, args: *mut PyObject, kwargs: *mut PyOb args, kwargs, c"|$llll:DateDelta".as_ptr(), - vec![ - c"years".as_ptr() as *mut _, - c"months".as_ptr() as *mut _, - c"weeks".as_ptr() as *mut _, - c"days".as_ptr() as *mut _, - NULL(), - ] - .as_mut_ptr(), + arg_vec(&[c"years", c"months", c"weeks", c"days"]).as_mut_ptr(), &mut years, &mut months, &mut weeks, diff --git a/src/instant.rs b/src/instant.rs index 5f434564..aebdda5a 100644 --- a/src/instant.rs +++ b/src/instant.rs @@ -282,16 +282,15 @@ unsafe fn from_utc(cls: *mut PyTypeObject, args: *mut PyObject, kwargs: *mut PyO args, kwargs, c"lll|lll$l:Instant.from_utc".as_ptr(), - vec![ - c"year".as_ptr() as *mut _, - c"month".as_ptr() as *mut _, - c"day".as_ptr() as *mut _, - c"hour".as_ptr() as *mut _, - c"minute".as_ptr() as *mut _, - c"second".as_ptr() as *mut _, - c"nanosecond".as_ptr() as *mut _, - NULL(), - ] + arg_vec(&[ + c"year", + c"month", + c"day", + c"hour", + c"minute", + c"second", + c"nanosecond", + ]) .as_mut_ptr(), &mut year, &mut month, @@ -832,7 +831,7 @@ static mut METHODS: &[PyMethodDef] = &[ }, }, ml_flags: METH_CLASS | METH_VARARGS | METH_KEYWORDS, - ml_doc: c"Create an instance from a UTC date and time".as_ptr(), + ml_doc: c"from_utc()\n--\n\nCreate an instance from a UTC date and time".as_ptr(), }, method!( from_timestamp_millis, @@ -876,14 +875,28 @@ static mut METHODS: &[PyMethodDef] = &[ "Create an instance from the common ISO8601 format", METH_O | METH_CLASS ), - method_kwargs!(add, "Add various time units to the instance"), - method_kwargs!(subtract, "Subtract various time units from the instance"), + method_kwargs!( + add, + "add($self, *, hours=0, minutes=0, seconds=0, milliseconds=0, \ + microseconds=0, nanoseconds=0)\n--\n\n\ + Add various time units to the instance" + ), + method_kwargs!( + subtract, + "subtract($self, *, hours=0, minutes=0, seconds=0, milliseconds=0, \ + microseconds=0, nanoseconds=0)\n--\n\n\ + Subtract various time units from the instance" + ), method!(to_tz, "Convert to an equivalent ZonedDateTime", METH_O), method!( to_system_tz, "Convert to an equivalent datetime in the system timezone" ), - method_vararg!(to_fixed_offset, "Convert to an equivalent OffsetDateTime"), + method_vararg!( + to_fixed_offset, + "to_fixed_offset($self, offset=0, /)\n--\n\n\ + Convert to an equivalent OffsetDateTime" + ), method!( difference, "Calculate the difference between two instances", diff --git a/src/local_datetime.rs b/src/local_datetime.rs index 9cf78e58..a82586f4 100644 --- a/src/local_datetime.rs +++ b/src/local_datetime.rs @@ -1,4 +1,4 @@ -use core::ffi::{c_char, c_int, c_long, c_void, CStr}; +use core::ffi::{c_int, c_long, c_void, CStr}; use core::{mem, ptr::null_mut as NULL}; use pyo3_ffi::*; @@ -153,16 +153,15 @@ unsafe fn __new__(cls: *mut PyTypeObject, args: *mut PyObject, kwargs: *mut PyOb args, kwargs, c"lll|lll$l:LocalDateTime".as_ptr(), - vec![ - c"year".as_ptr() as *mut c_char, - c"month".as_ptr() as *mut c_char, - c"day".as_ptr() as *mut c_char, - c"hour".as_ptr() as *mut c_char, - c"minute".as_ptr() as *mut c_char, - c"second".as_ptr() as *mut c_char, - c"nanosecond".as_ptr() as *mut c_char, - NULL(), - ] + arg_vec(&[ + c"year", + c"month", + c"day", + c"hour", + c"minute", + c"second", + c"nanosecond", + ]) .as_mut_ptr(), &mut year, &mut month, @@ -851,10 +850,17 @@ static mut METHODS: &[PyMethodDef] = &[ METH_O | METH_CLASS ), method!(__reduce__, ""), - method_vararg!(strptime, "Parse a string into a LocalDateTime", METH_CLASS), + method_vararg!( + strptime, + "strptime($type, string, fmt, /)\n--\n\n\ + Parse a string into a LocalDateTime", + METH_CLASS + ), method_kwargs!( replace, - "Return a new instance with the specified fields replaced" + "replace($self, *, year=None, month=None, day=None, hour=None, \ + minute=None, second=None, nanosecond=None)\n--\n\n\ + Return a new instance with the specified fields replaced" ), method!(assume_utc, "Assume the datetime is in UTC"), method!( @@ -862,10 +868,15 @@ static mut METHODS: &[PyMethodDef] = &[ "Assume the datetime has a fixed offset", METH_O ), - method_kwargs!(assume_tz, "Assume the datetime is in a timezone"), + method_kwargs!( + assume_tz, + "assume_tz($self, tz, /, *, disambiguate)\n--\n\n\ + Assume the datetime is in a timezone" + ), method_kwargs!( assume_system_tz, - "Assume the datetime is in the system timezone" + "assume_system_tz($self, *, disambiguate)\n--\n\n\ + Assume the datetime is in the system timezone" ), method!( replace_date, @@ -877,9 +888,25 @@ static mut METHODS: &[PyMethodDef] = &[ "Return a new instance with the time replaced", METH_O ), - method_kwargs!(add, "Add various time and/or calendar units"), - method_kwargs!(subtract, "Subtract various time and/or calendar units"), - method_kwargs!(difference, "Get the difference between two local datetimes"), + method_kwargs!( + add, + "add($self, delta=None, /, *, years=0, months=0, days=0, \ + hours=0, minutes=0, seconds=0, milliseconds=0, microseconds=0, nanoseconds=0, \ + ignore_dst=False)\n--\n\n\ + Add various time and/or calendar units" + ), + method_kwargs!( + subtract, + "subtract($self, delta=None, /, *, years=0, months=0, days=0, \ + hours=0, minutes=0, seconds=0, milliseconds=0, microseconds=0, nanoseconds=0, \ + ignore_dst=False)\n--\n\n\ + Subtract various time and/or calendar units" + ), + method_kwargs!( + difference, + "difference($self, other, /, *, ignore_dst)\n--\n\n\ + Get the difference between two local datetimes" + ), PyMethodDef::zeroed(), ]; diff --git a/src/offset_datetime.rs b/src/offset_datetime.rs index 8d90af8f..6d0097a3 100644 --- a/src/offset_datetime.rs +++ b/src/offset_datetime.rs @@ -219,17 +219,16 @@ unsafe fn __new__(cls: *mut PyTypeObject, args: *mut PyObject, kwargs: *mut PyOb args, kwargs, c"lll|lll$lO:OffsetDateTime".as_ptr(), - vec![ - c"year".as_ptr() as *mut _, - c"month".as_ptr() as *mut _, - c"day".as_ptr() as *mut _, - c"hour".as_ptr() as *mut _, - c"minute".as_ptr() as *mut _, - c"second".as_ptr() as *mut _, - c"nanosecond".as_ptr() as *mut _, - c"offset".as_ptr() as *mut _, - NULL(), - ] + arg_vec(&[ + c"year", + c"month", + c"day", + c"hour", + c"minute", + c"second", + c"nanosecond", + c"offset", + ]) .as_mut_ptr(), &mut year, &mut month, @@ -1133,10 +1132,11 @@ static mut METHODS: &[PyMethodDef] = &[ method!(__reduce__, ""), method_kwargs!( now, - "Create a new instance representing the current time", + "now($type, offset, /, *, ignore_dst)\n--\n\n\ + Create a new instance representing the current time", METH_CLASS ), - method!(exact_eq, "Exact equality", METH_O), + method!(exact_eq, "exact_eq()\n--\n\nExact equality", METH_O), method!(py_datetime, "Convert to a `datetime.datetime`"), method!( from_py_datetime, @@ -1148,7 +1148,8 @@ static mut METHODS: &[PyMethodDef] = &[ method!(to_tz, "Convert to a `ZonedDateTime` with given tz", METH_O), method_vararg!( to_fixed_offset, - "Convert to a new instance with a different offset" + "to_fixed_offset($self, offset=None, /)\n--\n\n\ + Convert to a new instance with a different offset" ), method!(to_system_tz, "Convert to a datetime to the system timezone"), method!(date, "The date component"), @@ -1185,28 +1186,58 @@ static mut METHODS: &[PyMethodDef] = &[ ), method_kwargs!( from_timestamp, - "Create a new instance from a UNIX timestamp", + "from_timestamp($type, timestamp, /, *, offset, ignore_dst)\n--\n\n\ + Create a new instance from a UNIX timestamp", METH_CLASS ), method_kwargs!( from_timestamp_millis, - "Create a new instance from a UNIX timestamp in milliseconds", + "from_timestamp_millis($type, timestamp, /, *, offset, ignore_dst)\n--\n\n\ + Create a new instance from a UNIX timestamp in milliseconds", METH_CLASS ), method_kwargs!( from_timestamp_nanos, - "Create a new instance from a UNIX timestamp", + "from_timestamp_nanos($type, timestamp, /, *, offset, ignore_dst)\n--\n\n\ + Create a new instance from a UNIX timestamp", METH_CLASS ), method_kwargs!( replace, - "Return a new instance with the specified fields replaced" + "replace($self, /, *, year=None, month=None, day=None, hour=None, \ + minute=None, second=None, nanosecond=None, offset=None, ignore_dst)\n--\n\n\ + Return a new instance with the specified fields replaced" + ), + method_kwargs!( + replace_date, + "replace_date($self, date, /, *, ignore_dst)\n--\n\n\ + Return a new instance with the date replaced" + ), + method_kwargs!( + replace_time, + "replace_time($self, time, /, *, ignore_dst)\n--\n\n\ + Return a new instance with the time replaced" + ), + method_vararg!( + strptime, + "strptime($type, date_string, format, /)\n--\n\n\ + Parse a string with strptime", + METH_CLASS + ), + method_kwargs!( + add, + "add($self, delta=None, /, *, years=0, months=0, weeks=0, days=0, \ + hours=0, minutes=0, seconds=0, milliseconds=0, microseconds=0, nanoseconds=0, \ + ignore_dst=False)\n--\n\n\ + Add time units" + ), + method_kwargs!( + subtract, + "subtract($self, delta=None, /, *, years=0, months=0, weeks=0, days=0, \ + hours=0, minutes=0, seconds=0, milliseconds=0, microseconds=0, nanoseconds=0, \ + ignore_dst=False)\n--\n\n\ + Subtract time units" ), - method_kwargs!(replace_date, "Return a new instance with the date replaced"), - method_kwargs!(replace_time, "Return a new instance with the time replaced"), - method_vararg!(strptime, "Parse a string with strptime", METH_CLASS), - method_kwargs!(add, "Add time units"), - method_kwargs!(subtract, "Subtract time units"), method!( difference, "Calculate the difference between two instances", diff --git a/src/system_datetime.rs b/src/system_datetime.rs index 8e82fb77..42c6215f 100644 --- a/src/system_datetime.rs +++ b/src/system_datetime.rs @@ -119,17 +119,16 @@ unsafe fn __new__(cls: *mut PyTypeObject, args: *mut PyObject, kwargs: *mut PyOb args, kwargs, c"lll|lll$lU:SystemDateTime".as_ptr(), - vec![ - c"year".as_ptr() as *mut _, - c"month".as_ptr() as *mut _, - c"day".as_ptr() as *mut _, - c"hour".as_ptr() as *mut _, - c"minute".as_ptr() as *mut _, - c"second".as_ptr() as *mut _, - c"nanosecond".as_ptr() as *mut _, - c"disambiguate".as_ptr() as *mut _, - NULL(), - ] + arg_vec(&[ + c"year", + c"month", + c"day", + c"hour", + c"minute", + c"second", + c"nanosecond", + c"disambiguate", + ]) .as_mut_ptr(), &mut year, &mut month, @@ -870,18 +869,38 @@ static mut METHODS: &[PyMethodDef] = &[ ), method_kwargs!( replace, - "Return a new instance with the specified fields replaced" + "replace($self, *, year=None, month=None, day=None, hour=None, \ + minute=None, second=None, nanosecond=None, disambiguate=None)\n--\n\n\ + Return a new instance with the specified fields replaced" ), method_vararg!( to_fixed_offset, - "Return an equivalent instance with the given offset" + "to_fixed_offset($self, offset=None, /)\n--\n\n\ + Return an equivalent instance with the given offset" + ), + method_kwargs!( + replace_date, + "replace_date($self, date, /, *, disambiguate)\n--\n\n\ + Return a new instance with the date replaced" + ), + method_kwargs!( + replace_time, + "replace_time($self, time, /, *, disambiguate)\n--\n\n\ + Return a new instance with the time replaced" + ), + method_kwargs!( + add, + "add($self, delta=None, /, *, years=0, months=0, days=0, hours=0, \ + minutes=0, seconds=0, milliseconds=0, microseconds=0, nanoseconds=0, \ + disambiguate)\n--\n\n\ + Return a new instance with the given time units added" ), - method_kwargs!(replace_date, "Return a new instance with the date replaced"), - method_kwargs!(replace_time, "Return a new instance with the time replaced"), - method_kwargs!(add, "Return a new instance with the given time units added"), method_kwargs!( subtract, - "Return a new instance with the given time units subtracted" + "subtract($self, delta=None, /, *, years=0, months=0, days=0, hours=0, \ + minutes=0, seconds=0, milliseconds=0, microseconds=0, nanoseconds=0, \ + disambiguate)\n--\n\n\ + Return a new instance with the given time units subtracted" ), method!( difference, diff --git a/src/time.rs b/src/time.rs index c150b7bf..fc665019 100644 --- a/src/time.rs +++ b/src/time.rs @@ -226,14 +226,7 @@ unsafe fn __new__(cls: *mut PyTypeObject, args: *mut PyObject, kwargs: *mut PyOb args, kwargs, c"|lll$l:Time".as_ptr(), - vec![ - c"hour".as_ptr() as *mut _, - c"minute".as_ptr() as *mut _, - c"second".as_ptr() as *mut _, - c"nanosecond".as_ptr() as *mut _, - NULL(), - ] - .as_mut_ptr(), + arg_vec(&[c"hour", c"minute", c"second", c"nanosecond"]).as_mut_ptr(), &mut hour, &mut minute, &mut second, @@ -450,7 +443,11 @@ unsafe fn replace( static mut METHODS: &[PyMethodDef] = &[ method!(py_time, "Convert to a Python datetime.time"), - method_kwargs!(replace, "Replace one or more components of the time"), + method_kwargs!( + replace, + "replace($self, *, hour=None, minute=None, second=None, nanosecond=None)\n--\n\n\ + Replace one or more components of the time" + ), method!( format_common_iso, "Return the time in the common ISO 8601 format" diff --git a/src/zoned_datetime.rs b/src/zoned_datetime.rs index f6df5d23..77237a6f 100644 --- a/src/zoned_datetime.rs +++ b/src/zoned_datetime.rs @@ -210,18 +210,17 @@ unsafe fn __new__(cls: *mut PyTypeObject, args: *mut PyObject, kwargs: *mut PyOb args, kwargs, c"lll|lll$lUU:ZonedDateTime".as_ptr(), - vec![ - c"year".as_ptr() as *mut _, - c"month".as_ptr() as *mut _, - c"day".as_ptr() as *mut _, - c"hour".as_ptr() as *mut _, - c"minute".as_ptr() as *mut _, - c"second".as_ptr() as *mut _, - c"nanosecond".as_ptr() as *mut _, - c"tz".as_ptr() as *mut _, - c"disambiguate".as_ptr() as *mut _, - NULL(), - ] + arg_vec(&[ + c"year", + c"month", + c"day", + c"hour", + c"minute", + c"second", + c"nanosecond", + c"tz", + c"disambiguate", + ]) .as_mut_ptr(), &mut year, &mut month, @@ -1293,28 +1292,57 @@ static mut METHODS: &[PyMethodDef] = &[ method!(is_ambiguous, "Check if the datetime is ambiguous"), method_kwargs!( from_timestamp, - "Create a new instance from a UNIX timestamp", + "from_timestamp($type, ts, /, *, tz)\n--\n\n\ + Create a new instance from a UNIX timestamp", METH_CLASS ), method_kwargs!( from_timestamp_millis, - "Create a new instance from a UNIX timestamp in milliseconds", + "from_timestamp_millis($type, ts, /, *, tz)\n--\n\n\ + Create a new instance from a UNIX timestamp in milliseconds", METH_CLASS ), method_kwargs!( from_timestamp_nanos, - "Create a new instance from a UNIX timestamp in nanoseconds", + "from_timestamp_nanos($type, ts, /, *, tz)\n--\n\n\ + Create a new instance from a UNIX timestamp in nanoseconds", METH_CLASS ), method_kwargs!( replace, - "Return a new instance with the specified fields replaced" + "replace($self, /, *, year=None, month=None, day=None, hour=None, \ + minute=None, second=None, nanosecond=None, tz=None, disambiguate)\n--\n\n\ + Return a new instance with the specified fields replaced" + ), + method_kwargs!( + replace_date, + "replace_date($self, date, /, *, disambiguate)\n--\n\n\ + Return a new instance with the date replaced" + ), + method_kwargs!( + replace_time, + "replace_time($self, time, /, *, disambiguate)\n--\n\n\ + Return a new instance with the time replaced" + ), + method_vararg!( + to_fixed_offset, + "to_fixed_offset($self, offset=None, /)\n--\n\n\ + Convert to an equivalent offset datetime" + ), + method_kwargs!( + add, + "add($self, delta=None, /, *, years=0, months=0, days=0, hours=0, \ + minutes=0, seconds=0, milliseconds=0, microseconds=0, nanoseconds=0, \ + disambiguate=None)\n--\n\n\ + Add various time/calendar units" + ), + method_kwargs!( + subtract, + "subtract($self, delta=None, /, *, years=0, months=0, days=0, hours=0, \ + minutes=0, seconds=0, milliseconds=0, microseconds=0, nanoseconds=0, \ + disambiguate=None)\n--\n\n\ + Subtract various time/calendar units" ), - method_kwargs!(replace_date, "Return a new instance with the date replaced"), - method_kwargs!(replace_time, "Return a new instance with the time replaced"), - method_vararg!(to_fixed_offset, "Convert to an equivalent offset datetime"), - method_kwargs!(add, "Add various time/calendar units"), - method_kwargs!(subtract, "Subtract various time/calendar units"), method!( difference, "Get the difference between two instances", diff --git a/tests/test_main.py b/tests/test_main.py index 82fc3a5e..160c93fd 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,12 +1,24 @@ import sys +from inspect import signature +from itertools import chain from time import sleep import pytest from whenever import ( + _EXTENSION_LOADED, + Date, + DateDelta, + DateTimeDelta, ImplicitlyIgnoringDST, Instant, InvalidOffset, + LocalDateTime, + OffsetDateTime, + SystemDateTime, + Time, + TimeDelta, + ZonedDateTime, hours, patch_current_time, seconds, @@ -71,3 +83,36 @@ def test_patch_time(): assert hours(50) < (Instant.now() - i) < hours(50.1) assert Instant.now() - i > hours(40_000) + + +@pytest.mark.skipif( + not ( + _EXTENSION_LOADED + # only in 3.13+, __text_signature__ is autogenerated for 1-argument methods + and sys.version_info > (3, 13) + ), + reason="text signatures only relevant for the Rust extension", +) +def test_text_signature(): + classes = [ + Instant, + OffsetDateTime, + ZonedDateTime, + SystemDateTime, + LocalDateTime, + Date, + Time, + TimeDelta, + DateDelta, + DateTimeDelta, + ] + methods = ( + m + for m in chain.from_iterable(cls.__dict__.values() for cls in classes) + if callable(m) + ) + + for m in methods: + sig = m.__text_signature__ + assert sig is not None + signature(m) # raises ValueError if invalid