Skip to content

Commit

Permalink
Merge branch '4.0-dev' into feature/allow-passing-config
Browse files Browse the repository at this point in the history
  • Loading branch information
agronholm committed Mar 22, 2023
2 parents e9d97c8 + a66d170 commit 0c931ea
Show file tree
Hide file tree
Showing 12 changed files with 286 additions and 80 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ repos:
exclude: "^tests/mypy/negative.py"

- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.255
rev: v0.0.257
hooks:
- id: ruff
args: [--fix, --show-fixes]
Expand Down
8 changes: 8 additions & 0 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,17 @@ This library adheres to `Semantic Versioning 2.0 <https://semver.org/#semantic-v
look up the function object at run time
- Added support for type checking against nonlocal classes defined within the same
parent function as the instrumented function

**3.0.2** (2023-03-22)

- Improved warnings by ensuring that they target user code and not Typeguard internal
code
- Fixed ``warn_on_error()`` not showing where the type violation actually occurred
- Fixed local assignment to ``*args`` or ``**kwargs`` being type checked incorrectly
- Fixed ``TypeError`` on ``check_type(..., None)``
- Fixed unpacking assignment not working with a starred variable (``x, *y = ...``) in
the target tuple
- Fixed variable multi-assignment (``a = b = c = ...``) being type checked incorrectly

**3.0.1** (2023-03-16)

Expand Down
13 changes: 9 additions & 4 deletions src/typeguard/_checkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from ._config import ForwardRefPolicy
from ._exceptions import TypeCheckError, TypeHintWarning
from ._memo import TypeCheckMemo
from ._utils import evaluate_forwardref, get_type_name, qualified_name
from ._utils import evaluate_forwardref, get_stacklevel, get_type_name, qualified_name

if sys.version_info >= (3, 11):
from typing import (
Expand Down Expand Up @@ -629,7 +629,8 @@ def check_protocol(
warnings.warn(
f"Typeguard cannot check the {origin_type.__qualname__} protocol because "
f"it is a non-runtime protocol. If you would like to type check this "
f"protocol, please use @typing.runtime_checkable"
f"protocol, please use @typing.runtime_checkable",
stacklevel=get_stacklevel(),
)


Expand Down Expand Up @@ -710,6 +711,7 @@ def check_type_internal(
warnings.warn(
f"Cannot resolve forward reference {annotation.__forward_arg__!r}",
TypeHintWarning,
stacklevel=get_stacklevel(),
)

return
Expand Down Expand Up @@ -843,12 +845,15 @@ def load_plugins() -> None:
plugin = ep.load()
except Exception as exc:
warnings.warn(
f"Failed to load plugin {ep.name!r}: " f"{qualified_name(exc)}: {exc}"
f"Failed to load plugin {ep.name!r}: " f"{qualified_name(exc)}: {exc}",
stacklevel=2,
)
continue

if not callable(plugin):
warnings.warn(f"Plugin {ep} returned a non-callable object: {plugin!r}")
warnings.warn(
f"Plugin {ep} returned a non-callable object: {plugin!r}", stacklevel=2
)
continue

checker_lookup_functions.insert(0, plugin)
3 changes: 2 additions & 1 deletion src/typeguard/_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from ._exceptions import InstrumentationWarning
from ._functions import TypeCheckFailCallback
from ._transformer import TypeguardTransformer
from ._utils import Unset, function_name, is_method_of, unset
from ._utils import Unset, function_name, get_stacklevel, is_method_of, unset

if TYPE_CHECKING:
from typeshed.stdlib.types import _Cell
Expand Down Expand Up @@ -207,6 +207,7 @@ def typechecked(
warn(
f"{retval} -- not typechecking {function_name(target)}",
InstrumentationWarning,
stacklevel=get_stacklevel(),
)
return target

Expand Down
70 changes: 49 additions & 21 deletions src/typeguard/_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from collections.abc import Generator
from contextlib import contextmanager
from threading import Lock
from typing import Any, Callable, Iterable, NoReturn, TypeVar, cast, overload
from typing import Any, Callable, NoReturn, TypeVar, overload

from ._checkers import BINARY_MAGIC_METHODS, check_type_internal
from ._config import (
Expand All @@ -15,7 +15,7 @@
)
from ._exceptions import TypeCheckError, TypeCheckWarning
from ._memo import TypeCheckMemo
from ._utils import qualified_name
from ._utils import get_stacklevel, qualified_name

if sys.version_info >= (3, 11):
from typing import Literal, Never, TypeAlias
Expand Down Expand Up @@ -246,31 +246,59 @@ def check_yield_type(


def check_variable_assignment(
value: object, expected_annotations: dict[str, Any], memo: TypeCheckMemo
value: object, varname: str, annotation: Any, memo: TypeCheckMemo
) -> Any:
if type_checks_suppressed:
return

if len(expected_annotations) > 1:
source_values = cast("Iterable[Any]", value)
else:
source_values = (value,)
try:
check_type_internal(value, annotation, memo)
except TypeCheckError as exc:
qualname = qualified_name(value, add_class_prefix=True)
exc.append_path_element(f"value assigned to {varname} ({qualname})")
if memo.config.typecheck_fail_callback:
memo.config.typecheck_fail_callback(exc, memo)
else:
raise

iterated_values = []
for obj, (argname, expected_type) in zip(
source_values, expected_annotations.items()
):
iterated_values.append(obj)
try:
check_type_internal(obj, expected_type, memo)
except TypeCheckError as exc:
exc.append_path_element(argname)
if memo.config.typecheck_fail_callback:
memo.config.typecheck_fail_callback(exc, memo)
return value


def check_multi_variable_assignment(
value: Any, targets: list[dict[str, Any]], memo: TypeCheckMemo
) -> Any:
if type_checks_suppressed:
return

if max(len(target) for target in targets) == 1:
iterated_values = [value]
else:
iterated_values = list(value)

for expected_types in targets:
value_index = 0
for ann_index, (varname, expected_type) in enumerate(expected_types.items()):
if varname.startswith("*"):
varname = varname[1:]
keys_left = len(expected_types) - 1 - ann_index
next_value_index = len(iterated_values) - keys_left
obj: object = iterated_values[value_index:next_value_index]
value_index = next_value_index
else:
raise
obj = iterated_values[value_index]
value_index += 1

try:
check_type_internal(obj, expected_type, memo)
except TypeCheckError as exc:
qualname = qualified_name(obj, add_class_prefix=True)
exc.append_path_element(f"value assigned to {varname} ({qualname})")
if memo.config.typecheck_fail_callback:
memo.config.typecheck_fail_callback(exc, memo)
else:
raise

return iterated_values if len(iterated_values) > 1 else iterated_values[0]
return iterated_values[0] if len(iterated_values) == 1 else iterated_values


def warn_on_error(exc: TypeCheckError, memo: TypeCheckMemo) -> None:
Expand All @@ -281,7 +309,7 @@ def warn_on_error(exc: TypeCheckError, memo: TypeCheckMemo) -> None:
:attr:`TypeCheckConfiguration.typecheck_fail_callback`.
"""
warnings.warn(TypeCheckWarning(str(exc)), stacklevel=3)
warnings.warn(TypeCheckWarning(str(exc)), stacklevel=get_stacklevel())


@contextmanager
Expand Down
1 change: 1 addition & 0 deletions src/typeguard/_pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def pytest_configure(config: Config) -> None:
f"typeguard cannot check these packages because they are already "
f"imported: {', '.join(already_imported_packages)}",
InstrumentationWarning,
stacklevel=1,
)

install_import_hook(packages=packages)
Expand Down
112 changes: 81 additions & 31 deletions src/typeguard/_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import ast
import sys
from _ast import List
from ast import (
Add,
AnnAssign,
Expand Down Expand Up @@ -38,6 +39,7 @@
Pow,
Return,
RShift,
Starred,
Store,
Str,
Sub,
Expand Down Expand Up @@ -788,12 +790,14 @@ def visit_AnnAssign(self, node: AnnAssign) -> Any:
func_name = self._get_import(
"typeguard._functions", "check_variable_assignment"
)
expected_types = Dict(
keys=[Constant(node.target.id)], values=[node.annotation]
)
node.value = Call(
func_name,
[node.value, expected_types, self._memo.get_memo_name()],
[
node.value,
Constant(node.target.id),
node.annotation,
self._memo.get_memo_name(),
],
[],
)

Expand All @@ -809,37 +813,75 @@ def visit_Assign(self, node: Assign) -> Any:

# Only instrument function-local assignments
if isinstance(self._memo.node, (FunctionDef, AsyncFunctionDef)):
annotations_: dict[str, Any] = {}
targets: list[dict[Constant, expr | None]] = []
check_required = False
for target in node.targets:
names: list[Name]
elts: Sequence[expr]
if isinstance(target, Name):
names = [target]
elts = [target]
elif isinstance(target, Tuple):
names = [exp for exp in target.elts if isinstance(exp, Name)]
elts = target.elts
else:
continue

for name in names:
annotation = self._memo.variable_annotations.get(name.id)
annotations_[name.id] = self._convert_annotation(annotation)
annotations_: dict[Constant, expr | None] = {}
for exp in elts:
prefix = ""
if isinstance(exp, Starred):
exp = exp.value
prefix = "*"

if isinstance(exp, Name):
name = prefix + exp.id
annotation = self._memo.variable_annotations.get(exp.id)
if annotation:
annotations_[Constant(name)] = self._convert_annotation(
annotation
)
check_required = True
else:
annotations_[Constant(name)] = None

if any(ann for ann in annotations_.values()):
# Replace untyped variables with Any
for key, ann in annotations_.items():
if ann is None:
annotations_[key] = self._get_import("typing", "Any")
targets.append(annotations_)

func_name = self._get_import(
"typeguard._functions", "check_variable_assignment"
)
keys = [Constant(argname) for argname in annotations_.keys()]
values = list(annotations_.values())
expected_types = Dict(keys=keys, values=values)
node.value = Call(
func_name,
[node.value, expected_types, self._memo.get_memo_name()],
[],
)
if check_required:
# Replace missing annotations with typing.Any
for item in targets:
for key, expression in item.items():
if expression is None:
item[key] = self._get_import("typing", "Any")

if len(targets) == 1 and len(targets[0]) == 1:
func_name = self._get_import(
"typeguard._functions", "check_variable_assignment"
)
target_varname = next(iter(targets[0]))
node.value = Call(
func_name,
[
node.value,
target_varname,
targets[0][target_varname],
self._memo.get_memo_name(),
],
[],
)
elif targets:
func_name = self._get_import(
"typeguard._functions", "check_multi_variable_assignment"
)
targets_arg = List(
[
Dict(keys=list(target), values=list(target.values()))
for target in targets
],
ctx=Load(),
)
node.value = Call(
func_name,
[node.value, targets_arg, self._memo.get_memo_name()],
[],
)

return node

Expand All @@ -859,10 +901,14 @@ def visit_NamedExpr(self, node: NamedExpr) -> Any:
func_name = self._get_import(
"typeguard._functions", "check_variable_assignment"
)
expected_types = Dict(keys=[Constant(node.target.id)], values=[annotation])
node.value = Call(
func_name,
[node.value, expected_types, self._memo.get_memo_name()],
[
node.value,
Constant(node.target.id),
annotation,
self._memo.get_memo_name(),
],
[],
)

Expand Down Expand Up @@ -894,10 +940,14 @@ def visit_AugAssign(self, node: AugAssign) -> Any:
operator_call = Call(
operator_func, [Name(node.target.id, ctx=Load()), node.value], []
)
expected_types = Dict(keys=[Constant(node.target.id)], values=[annotation])
check_call = Call(
self._get_import("typeguard._functions", "check_variable_assignment"),
[operator_call, expected_types, self._memo.get_memo_name()],
[
operator_call,
Constant(node.target.id),
annotation,
self._memo.get_memo_name(),
],
[],
)
return Assign(targets=[node.target], value=check_call)
Expand Down
15 changes: 13 additions & 2 deletions src/typeguard/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import inspect
import sys
from importlib import import_module
from types import CodeType, FunctionType
from typing import TYPE_CHECKING, Any, Callable, ForwardRef, Union
from inspect import currentframe
from types import CodeType, FrameType, FunctionType
from typing import TYPE_CHECKING, Any, Callable, ForwardRef, Union, cast
from weakref import WeakValueDictionary

if TYPE_CHECKING:
Expand Down Expand Up @@ -147,6 +148,16 @@ def is_method_of(obj: object, cls: type) -> bool:
)


def get_stacklevel() -> int:
level = 1
frame = cast(FrameType, currentframe()).f_back
while frame and frame.f_globals.get("__name__", "").startswith("typeguard."):
level += 1
frame = frame.f_back

return level


@final
class Unset:
__slots__ = ()
Expand Down

0 comments on commit 0c931ea

Please sign in to comment.