Skip to content

Commit

Permalink
Fixed wrapper checks not being done when @typechecked is used on a …
Browse files Browse the repository at this point in the history
…class

Fixes #309.
  • Loading branch information
agronholm committed Mar 15, 2023
1 parent bc215ed commit 216a7b9
Show file tree
Hide file tree
Showing 3 changed files with 21 additions and 16 deletions.
2 changes: 2 additions & 0 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ This library adheres to `Semantic Versioning 2.0 <https://semver.org/#semantic-v

- Improved the documentation
- Fixed assignment unpacking (``a, b = ...``) being checked incorrectly
- Fixed ``@typechecked`` attempting to instrument wrapper decorators such as
``@contextmanager`` when applied to a class

**3.0.0** (2023-03-15)

Expand Down
23 changes: 7 additions & 16 deletions src/typeguard/_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ def instrument(f: T_CallableOrType) -> FunctionType | str:
return "__module__ attribute is not set"
elif f.__code__.co_filename == "<stdin>":
return "cannot instrument functions defined in a REPL"
elif hasattr(f, "__wrapped__"):
return (
"@typechecked only supports instrumenting functions wrapped with "
"@classmethod, @staticmethod or @property"
)

target_path = [item for item in f.__qualname__.split(".") if item != "<locals>"]
module_source = inspect.getsource(sys.modules[f.__module__])
Expand Down Expand Up @@ -162,29 +167,15 @@ def typechecked(target: T_CallableOrType | None = None) -> Any:
return target

# Find either the first Python wrapper or the actual function
func: FunctionType
wrapper_class: type[classmethod[Any]] | type[staticmethod[Any]] | None = None
if isinstance(target, (classmethod, staticmethod)):
wrapper_class = target.__class__
target = target.__func__

if hasattr(target, "__wrapped__"):
warn(
f"Cannot instrument {function_name(target)} -- @typechecked only supports "
f"instrumenting functions wrapped with @classmethod, @staticmethod or "
f"@property",
InstrumentationWarning,
)
return target
elif isfunction(target):
func = target
else:
raise TypeError("target is not a function or a supported wrapper")

retval = instrument(func)
retval = instrument(target)
if isinstance(retval, str):
warn(
f"{retval} -- not typechecking {function_name(func)}",
f"{retval} -- not typechecking {function_name(target)}",
InstrumentationWarning,
)
return target
Expand Down
12 changes: 12 additions & 0 deletions tests/test_typechecked.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import sys
from contextlib import contextmanager
from textwrap import dedent
from typing import (
Any,
Expand Down Expand Up @@ -580,3 +581,14 @@ def foo() -> Internal:
return Internal()

assert isinstance(foo(), Internal)


def test_existing_method_decorator():
@typechecked
class Foo:
@contextmanager
def method(self, x: int) -> None:
yield x + 1

with Foo().method(6) as value:
assert value == 7

0 comments on commit 216a7b9

Please sign in to comment.