-
Notifications
You must be signed in to change notification settings - Fork 15
Complete typing with strict type-checking #43
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
Complete typing with strict type-checking #43
Conversation
@@ -18,7 +18,7 @@ jobs: | |||
- name: run the tests | |||
run: python3 tests.py | |||
- name: verify type hints | |||
run: mypy datemath | |||
run: mypy . --strict |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Running on tests is important too. This ensures expected usage is correctly typed. In fact, it found that parse
was missing the int
annotation on its first parameter.
@@ -1,9 +1,18 @@ | |||
from .helpers import parse, DateMathException | |||
from __future__ import annotations |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This allows the use of |
in Pyton < 3.10, and use symbols that won't be present at runtime, in annotations.
from typing_extensions import Unpack | ||
from arrow import Arrow | ||
|
||
from .helpers import ParseParams, parse as parse, DateMathException as DateMathException |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as
imports without renaming is a typing convention understood by type-checkers to mean that the symbol is purposefully / explicitly re-exported (the other option is to use __all__
)
|
||
from .helpers import ParseParams, parse as parse, DateMathException as DateMathException | ||
|
||
def dm(expr: str | int, **kwargs: Unpack[ParseParams]) -> Arrow: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By using an unpacked TypedDict
to annotate kwargs
, you don't have to duplicate all your default keyword argument parameter's default values to have these functions fully typed.
@@ -78,7 +77,13 @@ def unitMap(c: str) -> str: | |||
else: | |||
raise DateMathException("Not a valid timeunit: {0}".format(c)) | |||
|
|||
def parse(expression: str, now: Any = None, tz: str = 'UTC', type: Any = None, roundDown: bool = True) -> Arrow: | |||
class ParseParams(TypedDict, total=False): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment in datemath/__init__.py
total=False
means all members are optional (since we use this to annotate optional kwargs)
if expression == 'now': | ||
if debug: print("parse() - Now, no dm: {0}".format(now)) | ||
if type: | ||
return getattr(now, type) | ||
return cast(Arrow, getattr(now, type)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In strict mode, mypy will whine about returning Any
from a typed function.
This can either be cast or type-ignored to accept the "type unsafety".
return cast(Arrow, getattr(now, type)) | |
return getattr(now, type) # type: ignore[no-any-return] |
@@ -101,15 +106,16 @@ def parse(expression: str, now: Any = None, tz: str = 'UTC', type: Any = None, r | |||
if debug: print("parse() - will now convert tz to {0}".format(tz)) | |||
now = now.to(tz) | |||
|
|||
expression = str(expression) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Less runtime casting in some cases. But more importantly, this is now type-safe.
now = now.floor(unit) # type: ignore[arg-type] | ||
else: | ||
now = now.ceil(unit) | ||
now = now.ceil(unit) # type: ignore[arg-type] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
arrow
types these only with the literal strings they accept. I figured you'd probably prefer keeping the types and checks simpler with a more general str
, as any usage of roundDate
would also have to comply to only using statically known literals.
@@ -1,4 +1,4 @@ | |||
import unittest2 as unittest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unittest2
is untyped, but also no longer needed without Python 2 support.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file is going to get removed in the next release anyways since this package is only going to be python3 going forward
2fa344b
to
c90f0bc
Compare
@@ -142,7 +148,7 @@ def parse(expression: str, now: Any = None, tz: str = 'UTC', type: Any = None, r | |||
rettime = evaluate(math, time, tz, roundDown) | |||
|
|||
if type: | |||
return getattr(rettime, type) | |||
return cast(Arrow, getattr(rettime, type)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In strict mode, mypy will whine about returning Any
from a typed function.
This can either be cast or type-ignored to accept the "type unsafety".
return cast(Arrow, getattr(rettime, type)) | |
return getattr(rettime, type) # type: ignore[no-any-return] |
c90f0bc
to
5d1bb8f
Compare
Thank you so much for your contribution @Avasam ! This is quite awesome!! I've been writing so much golang lately that I have taking the typing for granted and it seems folks really needed it for this module. I'll approve it and merge it. I'll get a release out soon. There are few other things I'd like to tidy up before the next one first related to the legacy python 2.7 support we had. |
5d1bb8f
to
360cc11
Compare
@@ -9,8 +9,7 @@ docutils==0.15.2 | |||
freezegun==1.2.2 | |||
idna==2.7 | |||
linecache2==1.0.0 | |||
mypy==1.5.1 | |||
mypy-extensions==1.0.0 | |||
mypy==1.7.1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd personally bump mypy all the way to latest. 1.7
is the minimum where Unpack
is no longer an incomplete feature https://mypy-lang.blogspot.com/2023/11/mypy-17-released.html
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for this. mypy is only used in this package to do the type checking so its probably good that its at the latest anyways. That got the tests to pass now
Closes #42
I figured this library is small enough I could lend a hand.
This is everything to have complete type annotations, be on par with typeshed's stubs, and use strict type-checking. I can split into smaller PRs if needed.
More details in GitHub file comments.