microsoft / pyright

Static Type Checker for Python
Other
13.37k stars 1.46k forks source link

False positive when decorator uses inner function with ParamSpec and inferred return type #8779

Closed erictraut closed 2 months ago

erictraut commented 2 months ago

Discussed in https://github.com/microsoft/pyright/discussions/8778

Originally posted by **MikuAuahDark** August 17, 2024 Hello, I need help on annotating a call decorator using param spec. It used to work with older version of pyright. However in 1.1.376, it fails type-checking. Pardon me if I did something wrong or this has been discussed before. If this is a bug in pyright, feel free to convert this dicussion into an issue. This is a simplification on what I'm trying to do in a bigger project. So I have call decorator that accepts 1 argument to decorate a function with 2 fixed positional-only arguments, followed by additional arbitrary positional or keyword arguments. This is my attempt: ```py # Filename: test_psec.py import collections.abc from typing import Protocol class CallParam[**P](Protocol): def __call__( self, context: str, userid: int, /, *args: P.args, **kwds: P.kwargs ) -> collections.abc.Awaitable[str]: ... def call_decorator(num_type: int, /): def wrapper0[**P](func: CallParam[P]): async def wrapper1(context: str, userid: int, /, *args: P.args, **kwargs: P.kwargs) -> str: return f"calling with num: {num_type}; " + await func(context, userid, *args, **kwargs) return wrapper1 return wrapper0 @call_decorator(1) async def call_num_type_1(context: str, userid: int, /): return f"{context} {userid} type 1" @call_decorator(2) async def call_num_type_2(context: str, userid: int, /, mul: int): return f"{context} {userid} = {userid * mul} type 2" ``` Then I have another Python file that uses `call_num_*` functions: ```py # Filename: main.py import test_pspec async def main(): result = [ await test_pspec.call_num_type_1("Hello", 12345), await test_pspec.call_num_type_2("World", 2531011, 214013), ] print("\n".join(result)) if __name__ == "__main__": import asyncio asyncio.run(main()) ``` When I run pyright command-line to `main.py` file, I get these errors (actual program run as expected): ``` D:\omitted\project>pyright main.py d:\omitted\project\main.py d:\omitted\project\main.py:6:15 - error: Arguments for ParamSpec "P@wrapper0" are missing (reportCallIssue) d:\omitted\project\main.py:7:60 - error: Expected 2 positional arguments (reportCallIssue) 2 errors, 0 warnings, 0 informations ``` The reason I'm unable to fix this is, if I merge `main.py` and `test_pspec.py` and run `pyright` on `test_pspec.py`, I get no errors. It only errors if they're in separate file. Any pointers on how to do what I need? Or is this is a bug in pyright? Or is it impossible due to limitation in Python typing spec? I appreciate any inputs, thanks.
erictraut commented 2 months ago

This will be fixed in the next release.

This turns out to be a long-standing bug that was exposed by a recent change. It was very subtle and occurred under very specific conditions, some of them relating to the specific order of evaluation which can easily change (e.g. based on whether you're hovering your mouse over a particular identifier immediately after editing a file). Interestingly, these conditions don't appear to occur anywhere in the large corpus of public code bases that we use for smoke testing pyright, which explains why the problem wasn't detected previously.

Thanks for providing the repro steps that allowed me to find and fix the issue.

erictraut commented 2 months ago

This is addressed in pyright 1.1.377.