Skip to content

Commit c5d91e9

Browse files
committed
Fixed @typechecked unable to find the code when PEP 695 type parameters are used
Fixes #500.
1 parent 9ec20d1 commit c5d91e9

4 files changed

Lines changed: 57 additions & 10 deletions

File tree

docs/versionhistory.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ Version history
44
This library adheres to
55
`Semantic Versioning 2.0 <https://semver.org/#semantic-versioning-200>`_.
66

7+
**UNRELEASED**
8+
9+
- Fixed ``@typechecked`` unable to find the target function or method if it or the
10+
containing class had PEP 695 type parameters on them
11+
(`#500 <https://github.com/agronholm/typeguard/issues/500>`_)
12+
713
**4.4.2** (2025-02-16)
814

915
- Fixed ``TypeCheckError`` in unpacking assignment involving properties of a parameter

src/typeguard/_decorators.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,16 @@ def make_cell(value: object) -> _Cell:
3636
def find_target_function(
3737
new_code: CodeType, target_path: Sequence[str], firstlineno: int
3838
) -> CodeType | None:
39-
target_name = target_path[0]
4039
for const in new_code.co_consts:
4140
if isinstance(const, CodeType):
42-
if const.co_name == target_name:
43-
if const.co_firstlineno == firstlineno:
44-
return const
45-
elif len(target_path) > 1:
46-
target_code = find_target_function(
47-
const, target_path[1:], firstlineno
48-
)
49-
if target_code:
50-
return target_code
41+
new_path = (
42+
target_path[1:] if const.co_name == target_path[0] else target_path
43+
)
44+
if not new_path and const.co_firstlineno == firstlineno:
45+
return const
46+
47+
if target_code := find_target_function(const, new_path, firstlineno):
48+
return target_code
5149

5250
return None
5351

tests/pep695.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from typeguard import typechecked
2+
3+
4+
@typechecked
5+
class ParametrizedClass[T]:
6+
def method(self, x: T, y: str) -> T:
7+
return x
8+
9+
10+
@typechecked
11+
def parametrized_func[T](x: T, y: str) -> T:
12+
return x

tests/test_instrumentation.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,18 @@ def dummymodule(method: str):
7878
def deferredannos(method: str):
7979
if sys.version_info < (3, 14):
8080
raise pytest.skip("Deferred annotations are only supported in Python 3.14+")
81+
8182
return _fixture_module("deferredannos", method)
8283

8384

85+
@pytest.fixture(scope="module")
86+
def pep695(method: str):
87+
if sys.version_info < (3, 12):
88+
raise pytest.skip("PEP 695 type parameter syntax requires Python 3.12+")
89+
90+
return _fixture_module("pep695", method)
91+
92+
8493
def test_type_checked_func(dummymodule):
8594
assert dummymodule.type_checked_func(2, 3) == 6
8695

@@ -386,3 +395,25 @@ def test_failure(self, deferredannos):
386395
match=r'argument "x" \(int\) is not an instance of deferredannos.NotYetDefined',
387396
):
388397
deferredannos.uses_forwardref(1)
398+
399+
400+
class TestParametrized:
401+
def test_success_func(self, pep695):
402+
assert pep695.parametrized_func(1, "2") == 1
403+
404+
def test_success_method(self, pep695):
405+
assert pep695.ParametrizedClass[int]().method(1, "2") == 1
406+
407+
def test_failure_func(self, pep695):
408+
with pytest.raises(
409+
TypeCheckError,
410+
match=r'argument "y" \(int\) is not an instance of str',
411+
):
412+
pep695.parametrized_func(1, 2)
413+
414+
def test_failure_method(self, pep695):
415+
with pytest.raises(
416+
TypeCheckError,
417+
match=r'argument "y" \(int\) is not an instance of str',
418+
):
419+
pep695.ParametrizedClass[int]().method("str", 2)

0 commit comments

Comments
 (0)