microsoft / pyright

Static Type Checker for Python
Other
13.12k stars 1.4k forks source link

reval_type(some_function.__call__) should match reveal_type(some_function) #8804

Closed dimaqq closed 3 weeks ago

dimaqq commented 3 weeks ago

Describe the bug I'm trying to use reveal_type to check random callables.

reveal_type(some_object_with__call__) returns the class name, and I need the Callable[...] signature.

I can use reveal_type(some_object.__call__) on callable objects.

For pure functions it's the inverse.

I can use reveal_type(pure_func) and I cannot use reveal_type(pure_func.__call__), even they behave equivalently at runtime.

Code or Screenshots If possible, provide a minimal, self-contained code sample (surrounded by triple back ticks) to demonstrate the issue. The code should define or import all referenced symbols.

from typing import reveal_type
def foo(): ...

reveal_type(foo)
reveal_type(foo.__call__)

The result:

> pyright foo.py
/.../foo.py
  /.../foo.py:4:13 - information: Type of "foo" is "() -> None"
  /.../foo.py:5:13 - information: Type of "foo.__call__" is "Any"
  /.../foo.py:5:17 - error: Cannot access attribute "__call__" for class "function"
    Attribute "__call__" is unknown (reportFunctionMemberAccess)
1 error, 0 warnings, 2 informations

Tested with pyright v1.1.370 and v1.1.377

Suggestion

I wonder if it would be in line with Python and the typing PEP that defines reveal_type, if revealed type for the callable and callable.call would be the same, at least in cases where these are same at run time.

erictraut commented 3 weeks ago

Pyright is working as intended here, so I don't consider this a bug.

The reveal_type function is specified here in the typing spec. The actual value of the string that a type checker emits here is type-checker specific. It is not recommended that you use this string for test purposes. The assert_type function was added for this purpose. It accepts a type expression as a second argument, and all type checkers should interpret type expressions in the same way, according to the typing spec.

dimaqq commented 3 weeks ago

Granted, assert_type is useful in some cases. But seemingly not in others.

For example, how can I assert that a specific thing can be called in a specific way, when it's not necessarily a function?

assert_type(str.lower, Callable[[str], str]) fails with "assert_type" mismatch: expected "(str) -> str" but received "(self: str) -> str" (reportAssertTypeFailure) and there's no way to inject self: into the Callable signature is there?

For the time being, I'm using a dummy variable as a work-around:

dummy1: Callable[[str], str] = str.lower
dummy2: Callable[[str], str] = some_callable_object
dummy3: Callable[[str], str] = some_callable_module.__call__

I can use functions directly; I can use simple callable objects directly as well; But not a module that extends ModuleType and implements __call__, for that I have to slap __call__ at the end.

Is this the way, or there perhaps a better way?

erictraut commented 3 weeks ago

assert_type tests for type equivalency. If you want to test for assignability, you can assign the value to a variable with a declared type, as you've done above with dummy1, etc.

For more complex cases that involve structural types, you can define a protocol class in your tests. The protocol should define the attributes and methods that must be present in the assigned object.