Skip to content
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

Rust extension module #101

Merged
merged 26 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
94f31ed
Rust extension module with maturin
ariebovenberg Mar 29, 2024
18a83b4
Make rust extension optional
ariebovenberg Apr 1, 2024
7b81dd6
First classes in rust extension
ariebovenberg Apr 2, 2024
2fc7ccf
Add other datetime types, redesign DateDelta
ariebovenberg Apr 29, 2024
fc2f77f
Improve error handling
ariebovenberg May 20, 2024
a060e69
Add missing methods, improve error handling
ariebovenberg May 28, 2024
7b1d82c
patch up GC/ref handling, 3.13 support
ariebovenberg Jun 10, 2024
5d7ce10
Improve docs, get pure-Python version working again
ariebovenberg Jun 10, 2024
0c39c8c
rationalize method names
ariebovenberg Jun 13, 2024
2e6e53a
fix leftover todos and coverage
ariebovenberg Jun 13, 2024
6cc336c
Fixes for wheels across platforms
ariebovenberg Jun 16, 2024
def6619
fix image link in README
ariebovenberg Jun 19, 2024
3b43929
Additional wheels for other archs
ariebovenberg Jun 19, 2024
a4c0e8f
add flag to see whether rust extension is loaded
ariebovenberg Jun 20, 2024
e5bfa10
additional testing for internal function
ariebovenberg Jun 20, 2024
857caab
add benchmark readme
ariebovenberg Jun 20, 2024
1ef0ab7
docs improvements
ariebovenberg Jun 20, 2024
d517867
small refactorings across extension module
ariebovenberg Jun 20, 2024
9475917
improve robustness of conversions near bounds
ariebovenberg Jun 23, 2024
14876cf
Drop "local" terminolgy from system timezone
ariebovenberg Jun 23, 2024
38cfb27
fix failing tests
ariebovenberg Jun 24, 2024
d93a57f
Replace UTCDateTime with Instant
ariebovenberg Jun 25, 2024
f18b5ba
Rename naive to local, cleanup docs
ariebovenberg Jun 28, 2024
1d4b722
make disambiguate argument required for relevant methods
ariebovenberg Jun 28, 2024
7c35f86
Add ignore_dst and disambiguate arg to relevant methods
ariebovenberg Jun 29, 2024
ce7885a
prepare next release
ariebovenberg Jul 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Drop "local" terminolgy from system timezone
  • Loading branch information
ariebovenberg committed Jun 23, 2024
commit 14876cf7e11b41e75cdc5dee78d26159f32496b6
2 changes: 1 addition & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
**Rationale**: Consistency with other methods.

- Renamed ``as_utc``, ``as_offset``, ``as_zoned``, ``as_local`` to
``to_utc``, ``to_fixed_offset``, ``to_tz``, ``to_local_system``,
``to_utc``, ``to_fixed_offset``, ``to_tz``, ``to_system_tz``,
and the ``NaiveDateTime.assume_*`` methods accordingly

**Rationale**: "to" better clarifies a conversion is being made (not a replacement),
Expand Down
4 changes: 2 additions & 2 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Concrete classes
:special-members: __add__, __sub__
:member-order: bysource

.. autoclass:: whenever.LocalSystemDateTime
.. autoclass:: whenever.SystemDateTime
:members:
now,
from_timestamp,
Expand All @@ -98,7 +98,7 @@ Concrete classes
assume_utc,
assume_fixed_offset,
assume_tz,
assume_local_system,
assume_system_tz,
strptime,
replace,
replace_date,
Expand Down
12 changes: 6 additions & 6 deletions docs/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,21 @@ In the second, you communicate that you also store the user's local time.
This intent is crucial for reasoning about the code,
and extending it correctly (e.g. with migrations, API endpoints, etc).

.. _faq-why-local:
.. _faq-why-system-tz:

Why does :class:`~whenever.LocalSystemDateTime` exist?
Why does :class:`~whenever.SystemDateTime` exist?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

While it may not make sense for server-type applications to use the local system time,
While it may not make sense for server-type applications to use the system timezone,
it's often useful for CLI tools or desktop applications.

Using :class:`~whenever.LocalSystemDateTime` has the following advantages:
Using :class:`~whenever.SystemDateTime` has the following advantages:

- In contrast to :class:`~whenever.OffsetDateTime`,
:class:`~whenever.LocalSystemDateTime` knows about the system's timezone changes,
:class:`~whenever.SystemDateTime` knows about the system's timezone changes,
enabling DST-safe arithmetic.
- In contrast to :class:`~whenever.ZonedDateTime`,
:class:`~whenever.LocalSystemDateTime` doesn't require the system be configured with an IANA timezone.
:class:`~whenever.SystemDateTime` doesn't require the system be configured with an IANA timezone.
While this is often the case, it's not guaranteed.

Of course, feel free to work with :class:`~whenever.ZonedDateTime` if
Expand Down
71 changes: 36 additions & 35 deletions docs/overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ Read on to find out which one is right for you.
.. code-block:: python

from whenever import (
UTCDateTime, OffsetDateTime, ZonedDateTime, LocalSystemDateTime, NaiveDateTime
UTCDateTime, OffsetDateTime, ZonedDateTime, SystemDateTime, NaiveDateTime
)

Here's a summary of how you can use them:

+-----------------------+-----+--------+-------+-------+-------+
| Feature | "Aware" | Naive |
+ +-----+--------+-------+-------+ +
| | UTC | Offset | Zoned | Local | |
| | UTC | Offset | Zoned | System| |
+=======================+=====+========+=======+=======+=======+
| comparison | ✅ | ✅ | ✅ | ✅ | ✅ |
+-----------------------+-----+--------+-------+-------+-------+
Expand Down Expand Up @@ -100,21 +100,21 @@ ZonedDateTime(2024-12-08 11:00:00+00:00[Europe/London])
>>> ZonedDateTime(2023, 10, 29, 1, 15, tz="Europe/London", disambiguate="later")
ZonedDateTime(2023-10-29 01:15:00+00:00[Europe/London])

:class:`~whenever.LocalSystemDateTime`
:class:`~whenever.SystemDateTime`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This is a datetime in the system local timezone.
Unless you're building a system that specifically runs on the user's local
This is a datetime in the timezone of the system running the code.
Unless you're building a system that specifically runs on the user's
machine (such as a CLI), you should avoid using this type.

>>> # assuming system timezone is America/New_York
>>> backup_performed = LocalSystemDateTime(2023, 12, 28, hour=2)
LocalSystemDateTime(2023-12-28 02:00:00-05:00)
>>> backup_performed = SystemDateTime(2023, 12, 28, hour=2)
SystemDateTime(2023-12-28 02:00:00-05:00)

.. seealso::

- :ref:`Why does LocalSystemDateTime exist? <faq-why-local>`
- :ref:`Working with the local system timezone <localtime>`
- :ref:`Why does SystemDateTime exist? <faq-why-system>`
- :ref:`Working with the system timezone <systemtime>`

:class:`~whenever.NaiveDateTime`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -141,7 +141,7 @@ Aware types
~~~~~~~~~~~

For aware types (:class:`~whenever.UTCDateTime`, :class:`~whenever.OffsetDateTime`,
:class:`~whenever.ZonedDateTime`, and :class:`~whenever.LocalSystemDateTime`),
:class:`~whenever.ZonedDateTime`, and :class:`~whenever.SystemDateTime`),
comparison and equality are based on whether they represent the same moment in
time. This means that two datetimes with different values can be equal:

Expand Down Expand Up @@ -253,7 +253,7 @@ Between aware types

You can convert between aware datetimes with the :meth:`~whenever._AwareDateTime.to_utc`,
:meth:`~whenever._AwareDateTime.to_fixed_offset`, :meth:`~whenever._AwareDateTime.to_tz`,
and :meth:`~whenever._AwareDateTime.to_local_system` methods. These methods return a new
and :meth:`~whenever._AwareDateTime.to_system_tz` methods. These methods return a new
instance of the appropriate type, representing the same moment in time.
This means the results will always compare equal to the original datetime.

Expand All @@ -264,8 +264,8 @@ UTCDateTime(2023-12-28 10:30:00Z)
OffsetDateTime(2023-12-28 15:30:00+05:00)
>>> d.to_tz("America/New_York") # same moment in New York
ZonedDateTime(2023-12-28 05:30:00-05:00[America/New_York])
>>> d.to_local_system() # same moment in the system timezone (e.g. Europe/Paris)
LocalSystemDateTime(2023-12-28 11:30:00+01:00)
>>> d.to_system_tz() # same moment in the system timezone (e.g. Europe/Paris)
SystemDateTime(2023-12-28 11:30:00+01:00)
>>> d.to_fixed_offset(4) == d
True # always the same moment in time

Expand All @@ -283,7 +283,7 @@ NaiveDateTime(2023-12-28 11:30:00)
You can convert from naïve types with the :meth:`~whenever.NaiveDateTime.assume_utc`,
:meth:`~whenever.NaiveDateTime.assume_fixed_offset`, and
:meth:`~whenever.NaiveDateTime.assume_tz`, and
:meth:`~whenever.NaiveDateTime.assume_local_system` methods.
:meth:`~whenever.NaiveDateTime.assume_system_tz` methods.

>>> n = NaiveDateTime(2023, 12, 28, 11, 30)
>>> n.assume_utc()
Expand Down Expand Up @@ -367,7 +367,7 @@ Ambiguity in timezones
In real-world 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.LocalSystemDateTime` types:
and :class:`~whenever.SystemDateTime` types:

- When the clock moves backwards, there is a period of time that occurs twice.
For example, Sunday October 29th 2:30am occured twice in Paris.
Expand Down Expand Up @@ -455,7 +455,7 @@ Here are the ISO formats for each type:
+-----------------------------------------+------------------------------------------------+
| :class:`~whenever.ZonedDateTime` | ``YYYY-MM-DDTHH:MM:SS±HH:MM[IANA TZ ID]`` [1]_ |
+-----------------------------------------+------------------------------------------------+
| :class:`~whenever.LocalSystemDateTime` | ``YYYY-MM-DDTHH:MM:SS±HH:MM`` |
| :class:`~whenever.SystemDateTime` | ``YYYY-MM-DDTHH:MM:SS±HH:MM`` |
+-----------------------------------------+------------------------------------------------+
| :class:`~whenever.NaiveDateTime` | ``YYYY-MM-DDTHH:MM:SS`` |
+-----------------------------------------+------------------------------------------------+
Expand Down Expand Up @@ -572,19 +572,20 @@ The same `formatting rules <https://docs.python.org/3/library/datetime.html#form
OffsetDateTime.strptime("2023-01-01+05:00", "%Y-%m-%d%z") # 2023-01-01 00:00:00+05:00
NaiveDateTime.strptime("2023-01-01 00:00", "%Y-%m-%d %H:%M") # 2023-01-01 00:00:00

:class:`~whenever.ZonedDateTime` and :class:`~whenever.LocalSystemDateTime` do not (yet)
:class:`~whenever.ZonedDateTime` and :class:`~whenever.SystemDateTime` do not (yet)
implement ``strptime()`` methods, because they require disambiguation.
If you'd like to parse into these types,
use :meth:`NaiveDateTime.strptime() <whenever.NaiveDateTime.strptime>`
to parse them, and then use the :meth:`~whenever.NaiveDateTime.assume_utc`,
:meth:`~whenever.NaiveDateTime.assume_fixed_offset`,
:meth:`~whenever.NaiveDateTime.assume_tz`, or :meth:`~whenever.NaiveDateTime.assume_local_system`
:meth:`~whenever.NaiveDateTime.assume_tz`,
or :meth:`~whenever.NaiveDateTime.assume_system_tz`
methods to convert them.
This makes it explicit what information is being assumed.

.. code-block:: python

NaiveDateTime.strptime("2023-01-01 12:00", "%Y-%m-%d %H:%M").assume_local_system()
NaiveDateTime.strptime("2023-01-01 12:00", "%Y-%m-%d %H:%M").assume_system_tz()

# handling ambiguity
NaiveDateTime.strptime("2023-10-29 02:30:00", "%Y-%m-%d %H:%M:%S").assume_tz(
Expand Down Expand Up @@ -649,14 +650,14 @@ DateDelta(P3M16D)

See the :ref:`API reference <date-and-time-api>` for more details.

.. _localtime:
.. _systemtime:

The local system timezone
-------------------------
The system timezone
-------------------

The local timezone is the timezone of the system running the code.
It's important to be aware that the local timezone can change.
Instances of :class:`~whenever.LocalSystemDateTime` have the fixed offset
The system running the code also has a timezone configured.
It's important to be aware that the system timezone can change.
Instances of :class:`~whenever.SystemDateTime` have the fixed offset
of the system timezone at the time of initialization.
The system timezone may change afterwards,
but instances of this type will not reflect that change.
Expand All @@ -669,40 +670,40 @@ This is because:
This would make it harder to reason about and use.

>>> # initialization where the system timezone is America/New_York
>>> d = LocalSystemDateTime(2020, 8, 15, hour=8)
LocalSystemDateTime(2020-08-15 08:00:00-04:00)
>>> d = SystemDateTime(2020, 8, 15, hour=8)
SystemDateTime(2020-08-15 08:00:00-04:00)
...
>>> # we change the system timezone to Amsterdam
>>> os.environ["TZ"] = "Europe/Amsterdam"
>>> time.tzset()
...
>>> d # object remains unchanged
LocalSystemDateTime(2020-08-15 08:00:00-04:00)
SystemDateTime(2020-08-15 08:00:00-04:00)

If you'd like to preserve the moment in time
and calculate the new local time, simply call
:meth:`~whenever._AwareDateTime.to_local_system`.
:meth:`~whenever._AwareDateTime.to_system_tz`.

>>> # same moment, but now with the clock time in Amsterdam
>>> d.to_local_system()
LocalSystemDateTime(2020-08-15 14:00:00+02:00)
>>> d.to_system_tz()
DateTime(2020-08-15 14:00:00+02:00)

On the other hand, if you'd like to preserve the local time on the clock
and calculate the corresponding moment in time:

>>> # take the wall clock time and assume the (new) system timezone (Amsterdam)
>>> d.naive().assume_local_system()
LocalSystemDateTime(2020-08-15 08:00:00+02:00)
>>> d.naive().assume_system_tz()
SystemDateTime(2020-08-15 08:00:00+02:00)

.. note::

Remember that :meth:`~whenever.NaiveDateTime.assume_local_system` may
Remember that :meth:`~whenever.NaiveDateTime.assume_system_tz` may
require disambiguation, if the wall clock time is ambiguous in
the system timezone.

.. seealso::

:ref:`Why does LocalSystemDateTime exist? <faq-why-local>`
:ref:`Why does SystemDateTime exist? <faq-why-system>`

.. [1] The timezone ID is not part of the core ISO 8601 standard,
but is part of the RFC 9557 extension.
Expand Down
4 changes: 2 additions & 2 deletions pysrc/whenever/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
_unpkl_date,
_unpkl_ddelta,
_unpkl_dtdelta,
_unpkl_local,
_unpkl_naive,
_unpkl_offset,
_unpkl_system,
_unpkl_tdelta,
_unpkl_time,
_unpkl_utc,
Expand All @@ -27,9 +27,9 @@
_unpkl_date,
_unpkl_ddelta,
_unpkl_dtdelta,
_unpkl_local,
_unpkl_naive,
_unpkl_offset,
_unpkl_system,
_unpkl_tdelta,
_unpkl_time,
_unpkl_utc,
Expand Down
38 changes: 19 additions & 19 deletions pysrc/whenever/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ __all__ = [
"UTCDateTime",
"OffsetDateTime",
"ZonedDateTime",
"LocalSystemDateTime",
"SystemDateTime",
"NaiveDateTime",
"DateDelta",
"TimeDelta",
Expand Down Expand Up @@ -292,7 +292,7 @@ class _AwareDateTime(_DateTime, metaclass=abc.ABCMeta):
self, offset: int | TimeDelta, /
) -> OffsetDateTime: ...
def to_tz(self, tz: str, /) -> ZonedDateTime: ...
def to_local_system(self) -> LocalSystemDateTime: ...
def to_system_tz(self) -> SystemDateTime: ...
def naive(self) -> NaiveDateTime: ...
def timestamp(self) -> int: ...
def timestamp_millis(self) -> int: ...
Expand Down Expand Up @@ -534,7 +534,7 @@ class ZonedDateTime(_AwareDateTime):
def __sub__(self, other: Delta) -> ZonedDateTime: ...

@final
class LocalSystemDateTime(_AwareDateTime):
class SystemDateTime(_AwareDateTime):
def __init__(
self,
year: int,
Expand All @@ -550,18 +550,18 @@ class LocalSystemDateTime(_AwareDateTime):
@property
def offset(self) -> TimeDelta: ...
@classmethod
def now(cls) -> LocalSystemDateTime: ...
def now(cls) -> SystemDateTime: ...
@classmethod
def from_timestamp(cls, i: int, /) -> LocalSystemDateTime: ...
def from_timestamp(cls, i: int, /) -> SystemDateTime: ...
@classmethod
def from_timestamp_millis(cls, i: int, /) -> LocalSystemDateTime: ...
def from_timestamp_millis(cls, i: int, /) -> SystemDateTime: ...
@classmethod
def from_timestamp_nanos(cls, i: int, /) -> LocalSystemDateTime: ...
def from_timestamp_nanos(cls, i: int, /) -> SystemDateTime: ...
@classmethod
def from_py_datetime(cls, d: _datetime, /) -> LocalSystemDateTime: ...
def from_py_datetime(cls, d: _datetime, /) -> SystemDateTime: ...
@classmethod
def parse_common_iso(cls, s: str, /) -> LocalSystemDateTime: ...
def exact_eq(self, other: LocalSystemDateTime, /) -> bool: ...
def parse_common_iso(cls, s: str, /) -> SystemDateTime: ...
def exact_eq(self, other: SystemDateTime, /) -> bool: ...
def replace(
self,
*,
Expand All @@ -573,13 +573,13 @@ class LocalSystemDateTime(_AwareDateTime):
second: int | _UNSET = ...,
nanosecond: int | _UNSET = ...,
disambiguate: Disambiguate | _UNSET = ...,
) -> LocalSystemDateTime: ...
) -> SystemDateTime: ...
def replace_date(
self, d: Date, /, *, disambiguate: Disambiguate = "raise"
) -> LocalSystemDateTime: ...
) -> SystemDateTime: ...
def replace_time(
self, t: Time, /, *, disambiguate: Disambiguate = "raise"
) -> LocalSystemDateTime: ...
) -> SystemDateTime: ...
def add(
self,
*,
Expand All @@ -594,7 +594,7 @@ class LocalSystemDateTime(_AwareDateTime):
microseconds: float = 0,
nanoseconds: int = 0,
disambiguate: Disambiguate = "raise",
) -> LocalSystemDateTime: ...
) -> SystemDateTime: ...
def subtract(
self,
*,
Expand All @@ -609,12 +609,12 @@ class LocalSystemDateTime(_AwareDateTime):
microseconds: float = 0,
nanoseconds: int = 0,
disambiguate: Disambiguate = "raise",
) -> LocalSystemDateTime: ...
def __add__(self, delta: Delta) -> LocalSystemDateTime: ...
) -> SystemDateTime: ...
def __add__(self, delta: Delta) -> SystemDateTime: ...
@overload
def __sub__(self, other: _AwareDateTime) -> TimeDelta: ...
@overload
def __sub__(self, other: Delta) -> LocalSystemDateTime: ...
def __sub__(self, other: Delta) -> SystemDateTime: ...

@final
class NaiveDateTime(_DateTime):
Expand All @@ -638,9 +638,9 @@ class NaiveDateTime(_DateTime):
def assume_tz(
self, tz: str, /, *, disambiguate: Disambiguate = "raise"
) -> ZonedDateTime: ...
def assume_local_system(
def assume_system_tz(
self, *, disambiguate: Disambiguate = "raise"
) -> LocalSystemDateTime: ...
) -> SystemDateTime: ...
@classmethod
def from_py_datetime(cls, d: _datetime, /) -> NaiveDateTime: ...
@classmethod
Expand Down
Loading