You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/index.rst
+100-2Lines changed: 100 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,8 +6,8 @@
6
6
DateType documentation
7
7
======================
8
8
9
-
A Workaround
10
-
------------
9
+
What Is DateType?
10
+
-----------------
11
11
12
12
DateType is a `workaround for this
13
13
bug <https://github.com/python/mypy/issues/9015>`_ to demonstrate that we could
@@ -28,6 +28,104 @@ There's a very small bit of implementation glue (concrete ``@classmethod``\ s fo
28
28
construction on the ``Naive`` and ``Aware`` types, and a few functions that do
29
29
runtime checks to convert to/from stdlib types).
30
30
31
+
What Does It Contain?
32
+
---------------------
33
+
34
+
After you ``pip install datetype``, you can import the ``datetype`` module.
35
+
36
+
In that module, you will find several types, each of which is a :py:class:`typing.Protocol` that abstractly describes an *existing* type within the standard library.
37
+
38
+
The first, ``datetype.Date``, is just an abstract description of :py:class:`datetime.date` ; it has all the same methods and attributes.
39
+
40
+
The other two, ``datetype.Time[TZ]`` and ``datetype.DateTime[TZ]`` are abstract descriptions of :py:class:`datetime.time` and :py:class:`datetime.datetime` respectively, both :py:class:`generic <typing.Generic>` on a timezone type; which is to say, a subclass of :py:class:`datetime.tzinfo`, or ``None``.
41
+
42
+
In the two places that a timezone is used as a return value in one of these :py:mod:`datetime` types, the equivalent ``datetype`` object's method has its ``TZ`` type precisely, rather than a union. These are:
43
+
44
+
1. The ``.tzinfo`` property on both ``DateTime`` and ``Time``, and
45
+
2. The ``timetz()`` method on ``DateTime``.
46
+
47
+
This means that, for example, if you have a ``datetype.DateTime[``\ :py:class:`zoneinfo.ZoneInfo`\ ``]``, you can get its timezone without checking anything, and it will type-check correctly:
48
+
49
+
.. code-block::
50
+
:language: python
51
+
52
+
from datetype import DateTime
53
+
from zoneinfo import ZoneInfo
54
+
55
+
def func(dt: DateTime[ZoneInfo]) -> None:
56
+
print(f"This datetime is in the {dt.tzinfo.key} timezone.")
57
+
58
+
By contrast, the ``datetime`` version of this:
59
+
60
+
.. code-block::
61
+
:language: python
62
+
63
+
from datetime import datetime
64
+
65
+
def func(dt: datetime) -> None:
66
+
print(f"This datetime is in the {dt.tzinfo.key} timezone.")
67
+
68
+
will result in 2 mypy errors:
69
+
70
+
1. ``Item "tzinfo" of "tzinfo | None" has no attribute "key"``, because the abstract ``tzinfo`` type doesn't let you know that it's a ``zoneinfo.ZoneInfo``, so it won't have ``ZoneInfo``'s custom key attribute, and
71
+
2. ``Item "None" of "tzinfo | None" has no attribute "key"``, because the type ``datetime.datetime`` might *always* be a naive datetime with no timezone information at all.
72
+
73
+
This is how datetype lets you describe your ``datetime`` object to avoid spurious errors when you've already made sure that those objects definitely have a timezone already.
74
+
75
+
``datetype`` will also help you by reporting errors any time you accidentally mix naive and aware datetimes.
76
+
77
+
For example, this program will type check cleanly according to ``mypy``, but will result in a runtime ``TypeError: can't subtract offset-naive and offset-aware datetimes``:
The problem here is that a naive datetime and an aware datetime are not actually compatible types, despite sharing the same class in the standard library. With ``datetype``, you would express this as such:
This version of the program will fail the same way at runtime, but, when checking with ``mypy``, now you will get ``Argument 1 to "seconds_between" has incompatible type "DateTime[None]"; expected "DateTime[tzinfo]"`` while type checking. If we were to replace ``DateTime[tzinfo]`` with ``DateTime[None]`` to indicate that ``then`` could be a naive datetime, we would instead accurately get the error ``Unsupported operand types for - ("DateTime[tzinfo]" and "DateTime[None]")``.
109
+
110
+
How Do You Use It?
111
+
------------------
112
+
113
+
One way to use ``datetype`` is already shown in the example above: the classmethod constructors on ``DateTime`` and ``Time`` (i.e.: ``.now(...)``, ``.utcfromtimestamp()``, ``.utcnow()``, ``.fromtimestamp(...)``, ``.combine()``) all have type hints that will give the appropriately specialized type back. So, when you can use those, it works more or less automatically.
114
+
115
+
However, in a real Python program, you are almost certainly going to need to deal with libraries whose type hints are in terms of the :py:mod:`datetime` module. To convert back and forth from those types, ``datetype`` exposes 3 additional functions:
116
+
117
+
- ``datetype.aware(datetime|time, [type[timezone]:TZ]) -> DateType[TZ]`` : If you have a standard library object ``dt``, you can call ``aware(dt)`` to verify that it has a non-``None`` tzinfo, and get back a ``DateType`` object specialized on that timezone. If you pass a specific timezone type, it will verify and specialize on that exact type rather than the base ``tzinfo``.
118
+
- ``datetype.naive(datetime|time) -> DateType[None]`` : If you have a standard library object with *no* timezone, this will verify that, and return a ``DateType[None]``.
119
+
- ``datetype.concrete(datetype.Date | datetype.DateTime | datetype.Time) -> datetime.datedate | datetime.datetime | datetime.time`` : If you have a ``datetype`` object of some type, this will return it as a ``datetime`` object instead.
120
+
121
+
Note that *at runtime*, these types are all the exact same classes. None of these functions actually instantiate anything new. These conversions merely verify any relevant invariants, then return the abstract type represnting those invariants.
122
+
123
+
Thus, the way that you use ``datetype`` as a library is to write all the functions in *your* application using these types, and then at the boundaries where you need to interact with existing Python libraries that use the standard library types, you can convert to and from them as described here.
124
+
125
+
By doing so, you will:
126
+
127
+
1. prevent spurious runtime errors from accidental mixing of naive and aware ``datetime`` objects, instead getting helpful early warnings from your type checker, and
128
+
2. prevent incorrect calculation surprises from accidentally using ``datetime`` objects as ``date`` objects (since ``datetype.Date`` does not type check as a ``datetype.DateTime`` or vice versa).
0 commit comments