-
Notifications
You must be signed in to change notification settings - Fork 214
Add type hints #467
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
base: master
Are you sure you want to change the base?
Add type hints #467
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,10 @@ __pycache__ | |
|
||
# / | ||
/.coverage | ||
/.mypy_cache | ||
/.ruff_cache | ||
/.tox | ||
/.venv | ||
/build | ||
/coverage | ||
/dist | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
[mypy] | ||
python_version = 3.9 | ||
strict = True | ||
warn_unreachable = True |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
# _types.py - module for defining custom types | ||
# | ||
# Copyright (C) 2025 David Salvisberg | ||
# | ||
# This library is free software; you can redistribute it and/or | ||
# modify it under the terms of the GNU Lesser General Public | ||
# License as published by the Free Software Foundation; either | ||
# version 2.1 of the License, or (at your option) any later version. | ||
# | ||
# This library is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
# Lesser General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU Lesser General Public | ||
# License along with this library; if not, write to the Free Software | ||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA | ||
# 02110-1301 USA | ||
|
||
"""Module containing custom types. | ||
|
||
This module is designed to be accessed through `stdnum._typing` so | ||
you only have the overhead and runtime requirement of Python 3.9 | ||
and `typing_extensions`, when that type is introspected at runtime. | ||
|
||
As such this module should never be accessed directly. | ||
""" | ||
|
||
from __future__ import annotations | ||
|
||
from typing import Protocol | ||
from typing_extensions import Required, TypedDict | ||
|
||
|
||
class NumberValidationModule(Protocol): | ||
"""Minimal interface for a number validation module.""" | ||
|
||
def compact(self, number: str) -> str: | ||
"""Convert the number to the minimal representation.""" | ||
|
||
def validate(self, number: str) -> str: | ||
"""Check if the number provided is a valid number of its type.""" | ||
|
||
def is_valid(self, number: str) -> bool: | ||
"""Check if the number provided is a valid number of its type.""" | ||
|
||
|
||
class IMSIInfo(TypedDict, total=False): | ||
"""Info `dict` returned by `stdnum.imsi.info`.""" | ||
|
||
number: Required[str] | ||
mcc: Required[str] | ||
mnc: Required[str] | ||
msin: Required[str] | ||
country: str | ||
cc: str | ||
brand: str | ||
operator: str | ||
status: str | ||
bands: str | ||
|
||
|
||
class GSTINInfo(TypedDict): | ||
"""Info `dict` returned by `stdnum.in_.gstin.info`.""" | ||
|
||
state: str | None | ||
pan: str | ||
holder_type: str | ||
initial: str | ||
registration_count: int | ||
|
||
|
||
class PANInfo(TypedDict): | ||
"""Info `dict` returned by `stdnum.in_.pan.info`.""" | ||
|
||
holder_type: str | ||
initial: str |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
# _typing.py - module for typing shims with reduced runtime overhead | ||
# | ||
# Copyright (C) 2025 David Salvisberg | ||
# | ||
# This library is free software; you can redistribute it and/or | ||
# modify it under the terms of the GNU Lesser General Public | ||
# License as published by the Free Software Foundation; either | ||
# version 2.1 of the License, or (at your option) any later version. | ||
# | ||
# This library is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
# Lesser General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU Lesser General Public | ||
# License along with this library; if not, write to the Free Software | ||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA | ||
# 02110-1301 USA | ||
|
||
"""Compatibility shims for the Python typing module. | ||
|
||
This module is designed in a way, such, that runtime use of | ||
annotations is possible starting with Python 3.9, but it still | ||
supports Python 3.6 - 3.8 if the package is used normally without | ||
introspecting the annotations of the API. | ||
|
||
You should never import *from* this module, you should always import | ||
the entire module and then access the members via attribute access. | ||
|
||
I.e. use the module like this: | ||
```python | ||
from stdnum import _typing as t | ||
|
||
foo: t.Any = ... | ||
``` | ||
|
||
Instead of like this: | ||
```python | ||
from stdnum._typing import Any | ||
|
||
foo: Any = ... | ||
``` | ||
|
||
The exception to that rule are `TYPE_CHECKING` `cast` and `deprecated` | ||
which can be used at runtime. | ||
""" | ||
|
||
from __future__ import annotations | ||
|
||
|
||
TYPE_CHECKING = False | ||
if TYPE_CHECKING: | ||
from collections.abc import Generator as Generator | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't really understand why this code is here. I don't think there is any situation where you can have this if branch be executed. This code appears to have another origin and more generic than is needed for python-stdnum, where did it come from? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This branch is for type checkers only, conversely the The entire point of this file is to defer the We could avoid the branch by shipping a separate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm still struggling with getting this merged. The whole Are imports from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The only real friction I see with this approach is, that if you need a type that hasn't been added to There are however alternative approaches. We could simplify things, if you don't care to support runtime introspection of type hints at all we could just put all the typing only imports in a There are however still things with runtime effects like It is unfortunately what you have to do currently if you want to minimize the runtime impact of type annotations. If you don't care to minimize the runtime impact we can add Other than that the only option with no runtime overhead is separate stub files. But then you lose both runtime introspection of the type hints and the ability to type check the implementation. So it will not help you find any bugs in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also just in case this wasn't clear: The The only real hack is the module-level
What happens is that, A module-level The more commonly used style would look like this:
In this case |
||
from collections.abc import Iterable as Iterable | ||
from collections.abc import Mapping as Mapping | ||
from collections.abc import Sequence as Sequence | ||
from typing import Any as Any | ||
from typing import IO as IO | ||
from typing import Literal as Literal | ||
from typing import cast as cast | ||
from typing_extensions import TypeAlias as TypeAlias | ||
from typing_extensions import deprecated as deprecated | ||
|
||
from stdnum._types import GSTINInfo as GSTINInfo | ||
from stdnum._types import IMSIInfo as IMSIInfo | ||
from stdnum._types import NumberValidationModule as NumberValidationModule | ||
from stdnum._types import PANInfo as PANInfo | ||
else: | ||
def cast(typ, val): | ||
"""Cast a value to a type.""" | ||
return val | ||
|
||
class deprecated: # noqa: N801 | ||
"""Simplified backport of `warnings.deprecated`. | ||
|
||
This backport doesn't handle classes or async functions. | ||
""" | ||
|
||
def __init__(self, message, category=DeprecationWarning, stacklevel=1): # noqa: D107 | ||
self.message = message | ||
self.category = category | ||
self.stacklevel = stacklevel | ||
|
||
def __call__(self, func): # noqa: D102 | ||
func.__deprecated__ = self.message | ||
|
||
if self.category is None: | ||
return func | ||
|
||
import functools | ||
import warnings | ||
|
||
@functools.wraps(func) | ||
def wrapper(*args, **kwargs): | ||
warnings.warn(self.message, category=self.category, stacklevel=self.stacklevel + 1) | ||
return func(*args, **kwargs) | ||
|
||
wrapper.__deprecated__ = self.message | ||
return wrapper | ||
|
||
def __getattr__(name): | ||
if name in {'Generator', 'Iterable', 'Mapping', 'Sequence'}: | ||
import collections.abc | ||
return getattr(collections.abc, name) | ||
elif name in {'Any', 'IO', 'Literal'}: | ||
import typing | ||
return getattr(typing, name) | ||
elif name == 'TypeAlias': | ||
import sys | ||
if sys.version_info >= (3, 10): | ||
import typing | ||
else: | ||
import typing_extensions as typing | ||
return getattr(typing, name) | ||
elif name in {'GSTINInfo', 'IMSIInfo', 'NumberValidationModule', 'PANInfo'}: | ||
import stdnum._types | ||
return getattr(stdnum._types, name) | ||
else: | ||
raise AttributeError(name) |
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.
Type checkers don't have great support for the
try
/except
style of providing backwards compatibility. So instead of relying onpkg_resources
, which may be missing even for Python versions 3.7 and 3.8 if a tool likeuv
was used to setup the environment, it seemed better to just rely on theimportlib_resources
backport with the minimum version that supports thefiles
function. That way we don't have a soft-dependency onsetuptools
.