Skip to content

Commit aabc570

Browse files
committed
it's better than nothing!
This commit was sponsored by Anika Huhn, and my other patrons. If you want to join them, you can support my work at https://glyph.im/patrons/.
1 parent a3adbc6 commit aabc570

3 files changed

Lines changed: 108 additions & 5 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
build
22
dist
3-
.tox
3+
.tox
4+
_build

docs/conf.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@
1313
# -- General configuration ---------------------------------------------------
1414
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
1515

16-
extensions = []
16+
extensions = [
17+
"sphinx.ext.intersphinx",
18+
]
1719

1820
templates_path = ['_templates']
1921
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
2022

21-
23+
intersphinx_mapping = {
24+
"py3": ("https://docs.python.org/3", None),
25+
}
2226

2327
# -- Options for HTML output -------------------------------------------------
2428
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output

docs/index.rst

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
DateType documentation
77
======================
88

9-
A Workaround
10-
------------
9+
What Is DateType?
10+
-----------------
1111

1212
DateType is a `workaround for this
1313
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
2828
construction on the ``Naive`` and ``Aware`` types, and a few functions that do
2929
runtime checks to convert to/from stdlib types).
3030

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``:
78+
79+
.. code-block::
80+
81+
import datetime
82+
83+
def seconds_between(then: datetime.datetime, now: datetime.datetime) -> float:
84+
return (now - then).total_seconds()
85+
86+
from time import sleep
87+
a = datetime.datetime.now()
88+
sleep(2)
89+
b = datetime.datetime.now(datetime.UTC)
90+
print(seconds_between(a, b))
91+
92+
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:
93+
94+
.. code-block::
95+
96+
from datetype import DateTime
97+
from datetime import UTC, tzinfo
98+
99+
def seconds_between(then: DateTime[tzinfo], now: DateTime[tzinfo]) -> float:
100+
return (now - then).total_seconds()
101+
102+
from time import sleep
103+
a = DateTime.now()
104+
sleep(2)
105+
b = DateTime.now(UTC)
106+
print(seconds_between(a, b))
107+
108+
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).
31129

32130
.. toctree::
33131
:maxdepth: 2

0 commit comments

Comments
 (0)