Skip to content

Commit 152d4c5

Browse files
committed
Add type hints
1 parent fc766bc commit 152d4c5

File tree

315 files changed

+1897
-1246
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

315 files changed

+1897
-1246
lines changed

.github/workflows/test.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
strategy:
1515
fail-fast: false
1616
matrix:
17-
python-version: [3.6, 3.7]
17+
python-version: [3.7]
1818
steps:
1919
- uses: actions/checkout@v3
2020
- name: Set up Python ${{ matrix.python-version }}
@@ -46,7 +46,7 @@ jobs:
4646
strategy:
4747
fail-fast: false
4848
matrix:
49-
tox_job: [docs, flake8, headers]
49+
tox_job: [docs, flake8, mypy, headers]
5050
steps:
5151
- uses: actions/checkout@v3
5252
- name: Set up Python

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ __pycache__
66

77
# /
88
/.coverage
9+
/.mypy_cache
10+
/.ruff_cache
911
/.tox
12+
/.venv
1013
/build
1114
/coverage
1215
/dist

mypy.ini

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[mypy]
2+
python_version = 3.9
3+
strict = True
4+
warn_unreachable = True

setup.cfg

+14-1
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ owner=root
66
group=root
77

88
[tool:pytest]
9-
addopts = --doctest-modules --doctest-glob="*.doctest" stdnum tests --ignore=stdnum/iso9362.py --cov=stdnum --cov-report=term-missing:skip-covered --cov-report=html
9+
addopts = --doctest-modules --doctest-glob="*.doctest" stdnum tests --ignore=stdnum/iso9362.py --ignore=stdnum/_types.py --cov=stdnum --cov-report=term-missing:skip-covered --cov-report=html
1010
doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL
1111

1212
[coverage:run]
1313
branch = true
14+
omit =
15+
_types.py
16+
_typing.py
1417

1518
[coverage:report]
1619
fail_under=100
@@ -32,16 +35,26 @@ ignore =
3235
Q003 # don't force "" strings to avoid escaping quotes
3336
T001,T201 # we use print statements in the update scripts
3437
W504 # we put the binary operator on the preceding line
38+
per-file-ignores =
39+
# typing re-exports
40+
stdnum/_typing.py: F401,I250
3541
max-complexity = 15
3642
max-line-length = 120
3743
extend-exclude =
3844
.github
45+
.mypy_cache
3946
.pytest_cache
47+
.ruff_cache
48+
.venv
4049
build
4150

4251
[isort]
4352
lines_after_imports = 2
4453
multi_line_output = 4
54+
extra_standard_library =
55+
typing_extensions
56+
classes =
57+
IO
4558
known_third_party =
4659
lxml
4760
openpyxl

setup.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@
6363
'Operating System :: OS Independent',
6464
'Programming Language :: Python',
6565
'Programming Language :: Python :: 3',
66-
'Programming Language :: Python :: 3.6',
6766
'Programming Language :: Python :: 3.7',
6867
'Programming Language :: Python :: 3.8',
6968
'Programming Language :: Python :: 3.9',
@@ -77,8 +76,11 @@
7776
'Topic :: Text Processing :: General',
7877
],
7978
packages=find_packages(),
80-
install_requires=[],
81-
package_data={'': ['*.dat', '*.crt']},
79+
python_requires='>=3.7',
80+
install_requires=[
81+
'importlib_resources >= 1.3 ; python_version < "3.9"',
82+
],
83+
package_data={'': ['*.dat', '*.crt', 'py.typed']},
8284
extras_require={
8385
# The SOAP feature is only required for a number of online tests
8486
# of numbers such as the EU VAT VIES lookup, the Dominican Republic

stdnum/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
Apart from the validate() function, many modules provide extra
3737
parsing, validation, formatting or conversion functions.
3838
"""
39+
from __future__ import annotations
3940

4041
from stdnum.util import get_cc_module
4142

stdnum/_types.py

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# _types.py - module for defining custom types
2+
#
3+
# Copyright (C) 2025 David Salvisberg
4+
#
5+
# This library is free software; you can redistribute it and/or
6+
# modify it under the terms of the GNU Lesser General Public
7+
# License as published by the Free Software Foundation; either
8+
# version 2.1 of the License, or (at your option) any later version.
9+
#
10+
# This library is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
# Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public
16+
# License along with this library; if not, write to the Free Software
17+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18+
# 02110-1301 USA
19+
20+
"""Module containing custom types.
21+
22+
This module is designed to be accessed through `stdnum._typing` so
23+
you only have the overhead and runtime requirement of Python 3.9
24+
and `typing_extensions`, when that type is introspected at runtime.
25+
26+
As such this module should never be accessed directly.
27+
"""
28+
29+
from __future__ import annotations
30+
31+
from typing import Protocol
32+
from typing_extensions import Required, TypedDict
33+
34+
35+
class NumberValidationModule(Protocol):
36+
"""Minimal interface for a number validation module."""
37+
38+
def compact(self, number: str) -> str:
39+
"""Convert the number to the minimal representation."""
40+
41+
def validate(self, number: str) -> str:
42+
"""Check if the number provided is a valid number of its type."""
43+
44+
def is_valid(self, number: str) -> bool:
45+
"""Check if the number provided is a valid number of its type."""
46+
47+
48+
class IMSIInfo(TypedDict, total=False):
49+
"""Info `dict` returned by `stdnum.imsi.info`."""
50+
51+
number: Required[str]
52+
mcc: Required[str]
53+
mnc: Required[str]
54+
msin: Required[str]
55+
country: str
56+
cc: str
57+
brand: str
58+
operator: str
59+
status: str
60+
bands: str
61+
62+
63+
class GSTINInfo(TypedDict):
64+
"""Info `dict` returned by `stdnum.in_.gstin.info`."""
65+
66+
state: str | None
67+
pan: str
68+
holder_type: str
69+
initial: str
70+
registration_count: int
71+
72+
73+
class PANInfo(TypedDict):
74+
"""Info `dict` returned by `stdnum.in_.pan.info`."""
75+
76+
holder_type: str
77+
initial: str

stdnum/_typing.py

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# _typing.py - module for typing shims with reduced runtime overhead
2+
#
3+
# Copyright (C) 2025 David Salvisberg
4+
#
5+
# This library is free software; you can redistribute it and/or
6+
# modify it under the terms of the GNU Lesser General Public
7+
# License as published by the Free Software Foundation; either
8+
# version 2.1 of the License, or (at your option) any later version.
9+
#
10+
# This library is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
# Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public
16+
# License along with this library; if not, write to the Free Software
17+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18+
# 02110-1301 USA
19+
20+
"""Compatibility shims for the Python typing module.
21+
22+
This module is designed in a way, such, that runtime use of
23+
annotations is possible starting with Python 3.9, but it still
24+
supports Python 3.6 - 3.8 if the package is used normally without
25+
introspecting the annotations of the API.
26+
27+
You should never import *from* this module, you should always import
28+
the entire module and then access the members via attribute access.
29+
30+
I.e. use the module like this:
31+
```python
32+
from stdnum import _typing as t
33+
34+
foo: t.Any = ...
35+
```
36+
37+
Instead of like this:
38+
```python
39+
from stdnum._typing import Any
40+
41+
foo: Any = ...
42+
```
43+
44+
The exception to that rule are `TYPE_CHECKING` `cast` and `deprecated`
45+
which can be used at runtime.
46+
"""
47+
48+
from __future__ import annotations
49+
50+
51+
TYPE_CHECKING = False
52+
if TYPE_CHECKING:
53+
from collections.abc import Generator as Generator
54+
from collections.abc import Iterable as Iterable
55+
from collections.abc import Mapping as Mapping
56+
from collections.abc import Sequence as Sequence
57+
from typing import Any as Any
58+
from typing import IO as IO
59+
from typing import Literal as Literal
60+
from typing import cast as cast
61+
from typing_extensions import TypeAlias as TypeAlias
62+
from typing_extensions import deprecated as deprecated
63+
64+
from stdnum._types import GSTINInfo as GSTINInfo
65+
from stdnum._types import IMSIInfo as IMSIInfo
66+
from stdnum._types import NumberValidationModule as NumberValidationModule
67+
from stdnum._types import PANInfo as PANInfo
68+
else:
69+
def cast(typ, val):
70+
"""Cast a value to a type."""
71+
return val
72+
73+
class deprecated: # noqa: N801
74+
"""Simplified backport of `warnings.deprecated`.
75+
76+
This backport doesn't handle classes or async functions.
77+
"""
78+
79+
def __init__(self, message, category=DeprecationWarning, stacklevel=1): # noqa: D107
80+
self.message = message
81+
self.category = category
82+
self.stacklevel = stacklevel
83+
84+
def __call__(self, func): # noqa: D102
85+
func.__deprecated__ = self.message
86+
87+
if self.category is None:
88+
return func
89+
90+
import functools
91+
import warnings
92+
93+
@functools.wraps(func)
94+
def wrapper(*args, **kwargs):
95+
warnings.warn(self.message, category=self.category, stacklevel=self.stacklevel + 1)
96+
return func(*args, **kwargs)
97+
98+
wrapper.__deprecated__ = self.message
99+
return wrapper
100+
101+
def __getattr__(name):
102+
if name in {'Generator', 'Iterable', 'Mapping', 'Sequence'}:
103+
import collections.abc
104+
return getattr(collections.abc, name)
105+
elif name in {'Any', 'IO', 'Literal'}:
106+
import typing
107+
return getattr(typing, name)
108+
elif name == 'TypeAlias':
109+
import sys
110+
if sys.version_info >= (3, 10):
111+
import typing
112+
else:
113+
import typing_extensions as typing
114+
return getattr(typing, name)
115+
elif name in {'GSTINInfo', 'IMSIInfo', 'NumberValidationModule', 'PANInfo'}:
116+
import stdnum._types
117+
return getattr(stdnum._types, name)
118+
else:
119+
raise AttributeError(name)

stdnum/ad/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
# 02110-1301 USA
2020

2121
"""Collection of Andorran numbers."""
22+
from __future__ import annotations
2223

2324
# Provide aliases.
2425
from stdnum.ad import nrt as vat # noqa: F401

stdnum/ad/nrt.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,13 @@
4343
>>> format('D059888N')
4444
'D-059888-N'
4545
""" # noqa: E501
46+
from __future__ import annotations
4647

4748
from stdnum.exceptions import *
4849
from stdnum.util import clean, isdigits
4950

5051

51-
def compact(number):
52+
def compact(number: str) -> str:
5253
"""Convert the number to the minimal representation.
5354
5455
This strips the number of any valid separators and removes surrounding
@@ -57,7 +58,7 @@ def compact(number):
5758
return clean(number, ' -.').upper().strip()
5859

5960

60-
def validate(number):
61+
def validate(number: str) -> str:
6162
"""Check if the number is a valid Andorra NRT number.
6263
6364
This checks the length, formatting and other constraints. It does not check
@@ -79,15 +80,15 @@ def validate(number):
7980
return number
8081

8182

82-
def is_valid(number):
83+
def is_valid(number: str) -> bool:
8384
"""Check if the number is a valid Andorra NRT number."""
8485
try:
8586
return bool(validate(number))
8687
except ValidationError:
8788
return False
8889

8990

90-
def format(number):
91+
def format(number: str) -> str:
9192
"""Reformat the number to the standard presentation format."""
9293
number = compact(number)
9394
return '-'.join([number[0], number[1:-1], number[-1]])

stdnum/al/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
# 02110-1301 USA
2020

2121
"""Collection of Albanian numbers."""
22+
from __future__ import annotations
2223

2324
# provide vat as an alias
2425
from stdnum.al import nipt as vat # noqa: F401

stdnum/al/nipt.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
...
5050
InvalidFormat: ...
5151
"""
52+
from __future__ import annotations
5253

5354
import re
5455

@@ -60,7 +61,7 @@
6061
_nipt_re = re.compile(r'^[A-M][0-9]{8}[A-Z]$')
6162

6263

63-
def compact(number):
64+
def compact(number: str) -> str:
6465
"""Convert the number to the minimal representation. This strips the
6566
number of any valid separators and removes surrounding whitespace."""
6667
number = clean(number, ' ').upper().strip()
@@ -71,7 +72,7 @@ def compact(number):
7172
return number
7273

7374

74-
def validate(number):
75+
def validate(number: str) -> str:
7576
"""Check if the number is a valid VAT number. This checks the length and
7677
formatting."""
7778
number = compact(number)
@@ -82,7 +83,7 @@ def validate(number):
8283
return number
8384

8485

85-
def is_valid(number):
86+
def is_valid(number: str) -> bool:
8687
"""Check if the number is a valid VAT number."""
8788
try:
8889
return bool(validate(number))

0 commit comments

Comments
 (0)