Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ repos:
- id: trailing-whitespace

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.2
rev: v0.8.6
hooks:
- id: ruff
args: [--fix, --show-fixes]
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.13.0
rev: v1.14.1
hooks:
- id: mypy
additional_dependencies: [ "typing_extensions" ]
Expand Down
8 changes: 8 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
python-typeguard (4.4.2-1) unstable; urgency=medium

* Team upload.
* New upstream release.
- Switched to JSON output when running mypy (closes: #1098615).

-- Colin Watson <[email protected]> Mon, 24 Feb 2025 01:03:04 +0000

python-typeguard (4.4.1-1) unstable; urgency=medium

* Team upload.
Expand Down
18 changes: 13 additions & 5 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ Version history
This library adheres to
`Semantic Versioning 2.0 <https://semver.org/#semantic-versioning-200>`_.

**4.4.2** (2025-02-16)

- Fixed ``TypeCheckError`` in unpacking assignment involving properties of a parameter
of the function (`#506 <https://github.com/agronholm/typeguard/issues/506>`_;
regression introduced in v4.4.1)
- Fixed display of module name for forward references
(`#492 <https://github.com/agronholm/typeguard/pull/492>`_; PR by @JelleZijlstra)
- Fixed ``TypeError`` when using an assignment expression
(`#510 <https://github.com/agronholm/typeguard/issues/510>`_; PR by @JohannesK71083)
- Fixed ``ValueError: no signature found for builtin`` when checking against a protocol
and a matching attribute in the subject is a built-in function
(`#504 <https://github.com/agronholm/typeguard/issues/504>`_)

**4.4.1** (2024-11-03)

- Dropped Python 3.8 support
Expand All @@ -22,18 +35,13 @@ This library adheres to
- Fixed checks against annotations wrapped in ``NotRequired`` not being run unless the
``NotRequired`` is a forward reference
(`#454 <https://github.com/agronholm/typeguard/issues/454>`_)
- Fixed the ``pytest_ignore_collect`` hook in the pytest plugin blocking default pytest
collection ignoring behavior by returning ``None`` instead of ``False``
(PR by @mgorny)

**4.4.0** (2024-10-27)

- Added proper checking for method signatures in protocol checks
(`#465 <https://github.com/agronholm/typeguard/pull/465>`_)
- Fixed basic support for intersection protocols
(`#490 <https://github.com/agronholm/typeguard/pull/490>`_; PR by @antonagestam)
- Fixed protocol checks running against the class of an instance and not the instance
itself (this produced wrong results for non-method member checks)

**4.3.0** (2024-05-27)

Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
requires-python = ">= 3.9"
dependencies = [
Expand Down Expand Up @@ -99,7 +100,7 @@ strict = true
pretty = true

[tool.tox]
env_list = ["py39", "py310", "py311", "py312", "py313"]
env_list = ["py39", "py310", "py311", "py312", "py313", "py314"]
skip_missing_interpreters = true

[tool.tox.env_run_base]
Expand Down
9 changes: 7 additions & 2 deletions src/typeguard/_checkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ def check_typevar(
) -> None:
if origin_type.__bound__ is not None:
annotation = (
Type[origin_type.__bound__] if subclass_check else origin_type.__bound__
type[origin_type.__bound__] if subclass_check else origin_type.__bound__
)
check_type_internal(value, annotation, memo)
elif origin_type.__constraints__:
Expand Down Expand Up @@ -648,7 +648,12 @@ def check_io(


def check_signature_compatible(subject: type, protocol: type, attrname: str) -> None:
subject_sig = inspect.signature(getattr(subject, attrname))
subject_attr = getattr(subject, attrname)
try:
subject_sig = inspect.signature(subject_attr)
except ValueError:
return # this can happen with builtins where the signature cannot be retrieved

protocol_sig = inspect.signature(getattr(protocol, attrname))
protocol_type: typing.Literal["instance", "class", "static"] = "instance"
subject_type: typing.Literal["instance", "class", "static"] = "instance"
Expand Down
5 changes: 4 additions & 1 deletion src/typeguard/_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,10 @@ def instrument(f: T_CallableOrType) -> FunctionType | str:
new_function.__module__ = f.__module__
new_function.__name__ = f.__name__
new_function.__qualname__ = f.__qualname__
new_function.__annotations__ = f.__annotations__
if sys.version_info >= (3, 14):
new_function.__annotate__ = f.__annotate__
else:
new_function.__annotations__ = f.__annotations__
new_function.__doc__ = f.__doc__
new_function.__defaults__ = f.__defaults__
new_function.__kwdefaults__ = f.__kwdefaults__
Expand Down
21 changes: 17 additions & 4 deletions src/typeguard/_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1073,8 +1073,9 @@ def visit_Assign(self, node: Assign) -> Any:

path.insert(0, exp.id)
name = prefix + ".".join(path)
annotation = self._memo.variable_annotations.get(exp.id)
if annotation:
if len(path) == 1 and (
annotation := self._memo.variable_annotations.get(exp.id)
):
annotations_.append((Constant(name), annotation))
check_required = True
else:
Expand Down Expand Up @@ -1137,8 +1138,20 @@ def visit_NamedExpr(self, node: NamedExpr) -> Any:
func_name,
[
node.value,
Constant(node.target.id),
annotation,
List(
[
List(
[
Tuple(
[Constant(node.target.id), annotation],
ctx=Load(),
)
],
ctx=Load(),
)
],
ctx=Load(),
),
self._memo.get_memo_name(),
],
[],
Expand Down
16 changes: 14 additions & 2 deletions src/typeguard/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@
if TYPE_CHECKING:
from ._memo import TypeCheckMemo

if sys.version_info >= (3, 13):
if sys.version_info >= (3, 14):
from typing import get_args, get_origin

def evaluate_forwardref(forwardref: ForwardRef, memo: TypeCheckMemo) -> Any:
return forwardref.evaluate(
globals=memo.globals, locals=memo.locals, type_params=()
)

elif sys.version_info >= (3, 13):
from typing import get_args, get_origin

def evaluate_forwardref(forwardref: ForwardRef, memo: TypeCheckMemo) -> Any:
Expand Down Expand Up @@ -85,7 +93,11 @@ def get_type_name(type_: Any) -> str:

name += f"[{formatted_args}]"

module = getattr(type_, "__module__", None)
# For ForwardRefs, use the module stored on the object if available
if hasattr(type_, "__forward_module__"):
module = type_.__forward_module__
else:
module = getattr(type_, "__module__", None)
if module and module not in (None, "typing", "typing_extensions", "builtins"):
name = module + "." + name

Expand Down
10 changes: 10 additions & 0 deletions tests/deferredannos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from typeguard import typechecked


@typechecked
def uses_forwardref(x: NotYetDefined) -> NotYetDefined: # noqa: F821
return x


class NotYetDefined:
pass
8 changes: 8 additions & 0 deletions tests/dummymodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ class Metaclass(type):

@typechecked
class DummyClass(metaclass=Metaclass):
bar: str
baz: int

def type_checked_method(self, x: int, y: int) -> int:
return x * y

Expand Down Expand Up @@ -270,6 +273,11 @@ def unpacking_assign_star_no_annotation(value: Any) -> Tuple[int, List[bytes], s
return x, y, z


@typechecked
def attribute_assign_unpacking(obj: DummyClass) -> None:
obj.bar, obj.baz = "foo", 123123


@typechecked(forward_ref_policy=ForwardRefPolicy.ERROR)
def override_forward_ref_policy(value: "NonexistentType") -> None: # noqa: F821
pass
Expand Down
21 changes: 9 additions & 12 deletions tests/mypy/test_type_annotations.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import json
import os
import platform
import re
import subprocess
from typing import Dict, List

import pytest

POSITIVE_FILE = "positive.py"
NEGATIVE_FILE = "negative.py"
LINE_PATTERN = NEGATIVE_FILE + ":([0-9]+):"

pytestmark = [
pytest.mark.skipif(
Expand All @@ -18,8 +16,8 @@
]


def get_mypy_cmd(filename: str) -> List[str]:
return ["mypy", "--strict", filename]
def get_mypy_cmd(filename: str) -> list[str]:
return ["mypy", "-O", "json", "--strict", filename]


def get_negative_mypy_output() -> str:
Expand All @@ -34,7 +32,7 @@ def get_negative_mypy_output() -> str:
return output


def get_expected_errors() -> Dict[int, str]:
def get_expected_errors() -> dict[int, str]:
"""
Extract the expected errors from comments in the negative examples file.
"""
Expand All @@ -46,25 +44,23 @@ def get_expected_errors() -> Dict[int, str]:
for idx, line in enumerate(lines):
line = line.rstrip()
if "# error" in line:
expected[idx + 1] = line[line.index("# error") + 2 :]
expected[idx + 1] = line[line.index("# error") + 9 :]

# Sanity check. Should update if negative.py changes.
assert len(expected) == 9
return expected


def get_mypy_errors() -> Dict[int, str]:
def get_mypy_errors() -> dict[int, str]:
"""
Extract the errors from running mypy on the negative examples file.
"""
mypy_output = get_negative_mypy_output()

got = {}
for line in mypy_output.splitlines():
m = re.match(LINE_PATTERN, line)
if m is None:
continue
got[int(m.group(1))] = line[len(m.group(0)) + 1 :]
error = json.loads(line)
got[error["line"]] = f"{error['message']} [{error['code']}]"

return got

Expand Down Expand Up @@ -109,5 +105,6 @@ def test_negative() -> None:
]
for idx, expected, got in mismatches:
print(f"Line {idx}", f"Expected: {expected}", f"Got: {got}", sep="\n\t")

if mismatches:
raise RuntimeError("Error messages changed")
13 changes: 13 additions & 0 deletions tests/test_checkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import sys
import types
from contextlib import nullcontext
from datetime import timedelta
from functools import partial
from io import BytesIO, StringIO
from pathlib import Path
Expand Down Expand Up @@ -1383,6 +1384,18 @@ def meth(self) -> None:
f"be a class method but it's an instance method"
)

def test_builtin_signature_check(self) -> None:
class MyProtocol(Protocol):
def attr(self) -> None:
pass

class Foo:
attr = timedelta

# Foo.attr is incompatible but timedelta has not inspectable signature so the
# check is skipped
check_type(Foo(), MyProtocol)


class TestRecursiveType:
def test_valid(self):
Expand Down
Loading
Loading