Skip to content

Commit b73f6f2

Browse files
authored
strict typing (#10)
1 parent c8ca42b commit b73f6f2

File tree

7 files changed

+59
-35
lines changed

7 files changed

+59
-35
lines changed

.pre-commit-config.yaml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ repos:
88
- id: trailing-whitespace
99
- id: end-of-file-fixer
1010
- repo: https://github.com/psf/black
11-
rev: "23.9.1"
11+
rev: "23.10.0"
1212
hooks:
1313
- id: black
1414
language_version: python3
1515
- repo: https://github.com/astral-sh/ruff-pre-commit
16-
rev: v0.0.292
16+
rev: v0.1.1
1717
hooks:
1818
- id: ruff
1919
args: ["--fix"]
@@ -29,3 +29,9 @@ repos:
2929
hooks:
3030
- id: taplo
3131
language_version: stable
32+
- repo: https://github.com/pre-commit/mirrors-mypy
33+
rev: v1.6.1
34+
hooks:
35+
- id: mypy
36+
additional_dependencies:
37+
- pytest

.vscode/settings.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"python.testing.unittestEnabled": false,
33
"python.testing.pytestEnabled": true,
4-
"python.testing.pytestArgs": [],
4+
"python.testing.pytestArgs": ["--color=yes"],
55
"[python]": {
66
"editor.formatOnSave": true,
77
"editor.defaultFormatter": "ms-python.black-formatter",
88
"editor.codeActionsOnSave": {
9-
"source.fixAll.ruff": true,
10-
"source.organizeImports.ruff": true,
9+
"source.fixAll": true,
10+
"source.organizeImports": true,
1111
},
1212
},
1313
"[toml]": {

pyproject.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ ignore = [
3333
]
3434
allowed-confusables = [""]
3535
[tool.ruff.isort]
36-
3736
known-first-party = ["legacy_api_wrap"]
37+
required-imports = ["from __future__ import annotations"]
3838
[tool.ruff.extend-per-file-ignores]
3939
"src/testing/*.py" = ["INP001"]
4040
"tests/**/test_*.py" = [
@@ -45,6 +45,11 @@ known-first-party = ["legacy_api_wrap"]
4545
"S101", # tests use `assert`
4646
]
4747

48+
[tool.mypy]
49+
strict = true
50+
explicit_package_bases = true
51+
mypy_path = "src"
52+
4853
[tool.pytest.ini_options]
4954
addopts = [
5055
"--import-mode=importlib",

src/legacy_api_wrap/__init__.py

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010

1111
from __future__ import annotations
1212

13+
import sys
1314
from functools import wraps
1415
from inspect import Parameter, signature
15-
from typing import TYPE_CHECKING, Callable, TypeVar
16+
from typing import TYPE_CHECKING
1617
from warnings import warn
1718

1819
if TYPE_CHECKING:
19-
from typing import ParamSpec
20+
from collections.abc import Callable
21+
from typing import ParamSpec, TypeVar
2022

2123
P = ParamSpec("P")
2224
R = TypeVar("R")
@@ -25,6 +27,8 @@
2527
POS_TYPES = {Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD}
2628

2729

30+
# The actual returned Callable of course accepts more positional parameters,
31+
# but we want the type to lie so end users don’t rely on the deprecated API.
2832
def legacy_api(*old_positionals: str) -> Callable[[Callable[P, R]], Callable[P, R]]:
2933
"""Legacy API wrapper.
3034
@@ -57,30 +61,33 @@ def wrapper(fn: Callable[P, R]) -> Callable[P, R]:
5761
par_types = [p.kind for p in sig.parameters.values()]
5862
has_var = Parameter.VAR_POSITIONAL in par_types
5963
n_required = sum(1 for p in sig.parameters.values() if p.default is Parameter.empty)
60-
n_positional = INF if has_var else sum(1 for p in par_types if p in POS_TYPES)
64+
n_positional = sys.maxsize if has_var else sum(1 for p in par_types if p in POS_TYPES)
6165

6266
@wraps(fn)
63-
def fn_compatible(*args: P.args, **kw: P.kwargs) -> R:
64-
if len(args) > n_positional:
65-
args, args_rest = args[:n_positional], args[n_positional:]
66-
if args_rest:
67-
if len(args_rest) > len(old_positionals):
68-
n_max = n_positional + len(old_positionals)
69-
msg = (
70-
f"{fn.__name__}() takes from {n_required} to {n_max} parameters, "
71-
f"but {len(args) + len(args_rest)} were given."
72-
)
73-
raise TypeError(msg)
74-
warn(
75-
f"The specified parameters {old_positionals[:len(args_rest)]!r} are "
76-
"no longer positional. "
77-
f"Please specify them like `{old_positionals[0]}={args_rest[0]!r}`",
78-
DeprecationWarning,
79-
stacklevel=2,
80-
)
81-
kw = {**kw, **dict(zip(old_positionals, args_rest))}
82-
83-
return fn(*args, **kw)
67+
def fn_compatible(*args_all: P.args, **kw: P.kwargs) -> R:
68+
if len(args_all) <= n_positional:
69+
return fn(*args_all, **kw)
70+
71+
args_pos: P.args
72+
args_pos, args_rest = args_all[:n_positional], args_all[n_positional:]
73+
74+
if len(args_rest) > len(old_positionals):
75+
n_max = n_positional + len(old_positionals)
76+
msg = (
77+
f"{fn.__name__}() takes from {n_required} to {n_max} parameters, "
78+
f"but {len(args_pos) + len(args_rest)} were given."
79+
)
80+
raise TypeError(msg)
81+
warn(
82+
f"The specified parameters {old_positionals[:len(args_rest)]!r} are "
83+
"no longer positional. "
84+
f"Please specify them like `{old_positionals[0]}={args_rest[0]!r}`",
85+
DeprecationWarning,
86+
stacklevel=2,
87+
)
88+
kw_new: P.kwargs = {**kw, **dict(zip(old_positionals, args_rest))}
89+
90+
return fn(*args_pos, **kw_new)
8491

8592
return fn_compatible
8693

src/testing/legacy_api_wrap/py.typed

Whitespace-only changes.

src/testing/legacy_api_wrap/pytest.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
"""Pytest plugin for legacy_api_wrap."""
22

3+
from __future__ import annotations
4+
35
import sys
46
import warnings
7+
from typing import TYPE_CHECKING
58

69
import pytest
710

11+
if TYPE_CHECKING:
12+
from collections.abc import Generator
13+
814
__all__ = ["_doctest_env", "pytest_itemcollected"]
915

1016

1117
@pytest.fixture()
12-
def _doctest_env() -> None:
18+
def _doctest_env() -> Generator[None, None, None]:
1319
"""Pytest fixture to make doctests not error on expected warnings."""
1420
sys.stderr, stderr_orig = sys.stdout, sys.stderr
1521
with warnings.catch_warnings():

tests/test_basic.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# def old(a, b=None, d=1, c=2):
1111
# pass
1212
@legacy_api("d", "c")
13-
def new(a, b=None, *, c=2, d=1, e=3): # noqa: ANN001, ANN201
13+
def new(a, b=None, *, c=2, d=1, e=3): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201
1414
return {"a": a, "b": b, "c": c, "d": d, "e": e}
1515

1616

@@ -24,13 +24,13 @@ def test_new_param_available() -> None:
2424

2525
def test_old_positional_order() -> None:
2626
with pytest.deprecated_call():
27-
res = new(12, 13, 14)
27+
res = new(12, 13, 14) # type: ignore[misc]
2828
assert res["d"] == 14
2929

3030

3131
def test_warning_stack() -> None:
3232
with pytest.deprecated_call() as record:
33-
new(12, 13, 14)
33+
new(12, 13, 14) # type: ignore[misc]
3434
w = record.pop()
3535
assert w.filename == __file__
3636

@@ -40,4 +40,4 @@ def test_too_many_args() -> None:
4040
TypeError,
4141
match=r"new\(\) takes from 1 to 4 parameters, but 5 were given\.",
4242
):
43-
new(1, 2, 3, 4, 5)
43+
new(1, 2, 3, 4, 5) # type: ignore[misc]

0 commit comments

Comments
 (0)