Skip to content

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

Conversation

Avasam
Copy link
Contributor

@Avasam Avasam commented Aug 25, 2024

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.

@@ -18,7 +18,7 @@ jobs:
- name: run the tests
run: python3 tests.py
- name: verify type hints
run: mypy datemath
run: mypy . --strict
Copy link
Contributor Author

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
Copy link
Contributor Author

@Avasam Avasam Aug 25, 2024

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
Copy link
Contributor Author

@Avasam Avasam Aug 25, 2024

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:
Copy link
Contributor Author

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):
Copy link
Contributor Author

@Avasam Avasam Aug 25, 2024

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))
Copy link
Contributor Author

@Avasam Avasam Aug 25, 2024

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".

Suggested change
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)
Copy link
Contributor Author

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.

Comment on lines +189 to +190
now = now.floor(unit) # type: ignore[arg-type]
else:
now = now.ceil(unit)
now = now.ceil(unit) # type: ignore[arg-type]
Copy link
Contributor Author

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
Copy link
Contributor Author

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.

Copy link
Owner

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

@Avasam Avasam force-pushed the complete-typing-with-strict-checking branch 2 times, most recently from 2fa344b to c90f0bc Compare August 25, 2024 03:41
@@ -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))
Copy link
Contributor Author

@Avasam Avasam Aug 25, 2024

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".

Suggested change
return cast(Arrow, getattr(rettime, type))
return getattr(rettime, type) # type: ignore[no-any-return]

@Avasam Avasam force-pushed the complete-typing-with-strict-checking branch from c90f0bc to 5d1bb8f Compare August 25, 2024 03:49
@nickmaccarthy
Copy link
Owner

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.

@Avasam Avasam force-pushed the complete-typing-with-strict-checking branch from 5d1bb8f to 360cc11 Compare August 26, 2024 21:46
@@ -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
Copy link
Contributor Author

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

Copy link
Owner

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

@nickmaccarthy nickmaccarthy merged commit c4c6265 into nickmaccarthy:master Aug 27, 2024
1 check passed
@Avasam Avasam deleted the complete-typing-with-strict-checking branch August 27, 2024 14:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Completing type annotations
2 participants