agronholm / typeguard

Run-time type checker for Python
Other
1.5k stars 112 forks source link

`Callable` with `Optional` arg fails to type-check: "too many mandatory positional arguments" #442

Closed ryan-williams closed 5 months ago

ryan-williams commented 5 months ago

Things to check first

Typeguard version

4.1.5

Python version

3.11.8

What happened?

Passing a Callable with Optional args to another function, then calling it, results in a TypeCheckError:

TypeCheckError: argument "fn" (function) has too many mandatory positional arguments in its declaration; expected 0 but 1 mandatory positional argument(s) declared

See test_callables.py:

from typing import Callable, Optional

# ✅ type-checks fine
def _fn0(fn: Callable[[int], bool], s: int) -> bool:
    return fn(s)

def fn0(s: int) -> bool:
    return True

# ❌ TypeCheckError: argument "fn" (function) has too many mandatory positional arguments in its declaration; expected 0 but 1 mandatory positional argument(s) declared
def _fn1(fn: Callable[[Optional[int]], bool], s: Optional[int]) -> bool:
    return fn(s)

def fn1(s: Optional[int]) -> bool:
    return True

def test_fn0():
    """Passing a Callable (``fn0``) works, as long as its arguments are not ``Optional``."""
    _fn0(fn0, 123)  # ✅ OK

def test_fn1():
    """Functions that take optional arguments are not type-checked properly"""
    _fn1(fn1, 123)  # ❌ TypeCheckError: argument "fn" (function) has too many mandatory positional arguments in its declaration; expected 0 but 1 mandatory positional argument(s) declared

How can we reproduce the bug?

ryan-williams/typeguard-issues has a working repro; here's the error in Github Actions.

agronholm commented 5 months ago

This seems to be a bug in the instrumentation (AST rewriting) code. I'm still trying to pinpoint where it happens.

agronholm commented 5 months ago

Found the problem (here): the check for Optional was missing the additional condition that its slice needs to have been erased for the entire node to be removed.