Skip to content

Commit

Permalink
Fixed check_callable() not counting positional args with defaults pro…
Browse files Browse the repository at this point in the history
…perly

Fixes #400.
  • Loading branch information
agronholm committed Sep 10, 2023
1 parent b10b9b2 commit 2a58441
Show file tree
Hide file tree
Showing 3 changed files with 33 additions and 23 deletions.
6 changes: 6 additions & 0 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ Version history
This library adheres to
`Semantic Versioning 2.0 <https://semver.org/#semantic-versioning-200>`_.

**UNRELEASED**

- Fixed ``Callable`` erroneously rejecting a callable that has the requested amount of
positional arguments but they have defaults
(`#400 <https://github.com/agronholm/typeguard/issues/400>`_)

**4.1.4** (2023-09-10)

- Fixed ``AttributeError`` where the transformer removed elements from a PEP 604 union
Expand Down
40 changes: 19 additions & 21 deletions src/typeguard/_checkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,31 +172,29 @@ def check_callable(
f'{", ".join(unfulfilled_kwonlyargs)}'
)

num_mandatory_args = len(
[
param.name
for param in signature.parameters.values()
if param.kind
in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD)
and param.default is Parameter.empty
]
)
has_varargs = any(
param
for param in signature.parameters.values()
if param.kind == Parameter.VAR_POSITIONAL
)

if num_mandatory_args > len(argument_types):
num_positional_args = num_mandatory_pos_args = 0
has_varargs = False
for param in signature.parameters.values():
if param.kind in (
Parameter.POSITIONAL_ONLY,
Parameter.POSITIONAL_OR_KEYWORD,
):
num_positional_args += 1
if param.default is Parameter.empty:
num_mandatory_pos_args += 1
elif param.kind == Parameter.VAR_POSITIONAL:
has_varargs = True

if num_mandatory_pos_args > len(argument_types):
raise TypeCheckError(
f"has too many arguments in its declaration; expected "
f"{len(argument_types)} but {num_mandatory_args} argument(s) "
f"declared"
f"has too many mandatory positional arguments in its declaration; "
f"expected {len(argument_types)} but {num_mandatory_pos_args} "
f"mandatory positional argument(s) declared"
)
elif not has_varargs and num_mandatory_args < len(argument_types):
elif not has_varargs and num_positional_args < len(argument_types):
raise TypeCheckError(
f"has too few arguments in its declaration; expected "
f"{len(argument_types)} but {num_mandatory_args} argument(s) "
f"{len(argument_types)} but {num_positional_args} argument(s) "
f"declared"
)

Expand Down
10 changes: 8 additions & 2 deletions tests/test_checkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ def some_callable(x: int, y: str, z: float) -> int:
pytest.raises(
TypeCheckError, check_type, some_callable, Callable[[int, str], int]
).match(
r"has too many arguments in its declaration; expected 2 but 3 "
r"argument\(s\) declared"
r"has too many mandatory positional arguments in its declaration; expected "
r"2 but 3 mandatory positional argument\(s\) declared"
)

def test_mandatory_kwonlyargs(self):
Expand Down Expand Up @@ -264,6 +264,12 @@ def test_concatenate(self):
"""Test that ``Concatenate`` in the arglist is ignored."""
check_type([].append, Callable[Concatenate[object, P], Any])

def test_positional_only_arg_with_default(self):
def some_callable(x: int = 1, /) -> None:
pass

check_type(some_callable, Callable[[int], Any])


class TestLiteral:
def test_literal_union(self):
Expand Down

0 comments on commit 2a58441

Please sign in to comment.