Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type resolution failure when Generic class TypeVar references TypeVar from generic function signature. #7548

Closed
asford opened this issue Mar 23, 2024 · 2 comments
Labels
as designed Not a bug, working as intended bug Something isn't working

Comments

@asford
Copy link

asford commented Mar 23, 2024

Please feel free to edit bug title and/or description to clarify language. 🙏

Types are not properly resolved when a TypeVar from a Generic class is bound to a TypeVar from a generic function signature.
Types are properly resolved when a TypeVar from a generic function signature is bound to a TypeVar from another generic function signature.
Therefor I believe this resolution failure is an error, rather than expected behavior.

This repro case demonstrates the issue in pyright and fails in v1.1.351 and v1.1.355.
wrap_fun binds T to Iterable[A@first] (specified as a TypeVar in the generic signature of wrap_fun),
and then properly resolves A@first to str on call.

generic_wrapped also binds T to Iterable[A@first] (now specified as a TypeVar in the Generic class declaration of Wrap),
but fails to resolve A@first to str on a method invocation.
This is reported as "Literal['foobar']" cannot be assigned to parameter of type "Iterable[A@first]".

This repro case includes a dataclass declaration for concision, but repros without dataclasses.
The repro case demonstrates the resolution failure with properties of the generic , a __call__ on the generic and an operator resolution failure on the generic (presumably due to the type resolution failure surfacing as an operator match failure).

import dataclasses
from typing import (
    Callable,
    Generic,
    Iterable,
    TypeVar,
)

from typing_extensions import assert_type, reveal_type

A = TypeVar("A")
T = TypeVar("T")
R = TypeVar("R")


def first(seq: Iterable[A]) -> A:
    return next(iter(seq))


assert first("foobar") == "f"
reveal_type(first)
assert_type(first("foobar"), str)


def wrap_fun(func: Callable[[T], R]) -> Callable[[T], R]:
    return func


reveal_type(wrap_fun(first))
reveal_type(wrap_fun(first)("foobar"))
assert_type(wrap_fun(first)("foobar"), str)


class ExplicitWrappedFirst:
    def func(self, vals: Iterable[A]) -> A:
        return first(vals)

    def __call__(self, vals: Iterable[A]) -> A:
        return first(vals)

    def __ror__(self, vals: Iterable[A]) -> A:
        return first(vals)


explicit_wrapped = ExplicitWrappedFirst()
assert explicit_wrapped("foobar") == "f"
reveal_type(explicit_wrapped.func)
assert_type(explicit_wrapped.func("foobar"), str)

reveal_type(explicit_wrapped)
reveal_type(explicit_wrapped.__call__)
assert_type(explicit_wrapped("foobar"), str)

reveal_type(explicit_wrapped.__ror__)
assert_type("foobar" | explicit_wrapped, str)


@dataclasses.dataclass
class Wrap(Generic[T, R]):
    func: Callable[[T], R]

    def __call__(self, __value: T) -> R:
        return self.func(__value)

    def __ror__(self, __value: T) -> R:
        return self.func(__value)


generic_wrapped = Wrap(first)

assert generic_wrapped("foobar") == "f"
reveal_type(generic_wrapped)
reveal_type(generic_wrapped.func)
assert_type(generic_wrapped.func("foobar"), str)

reveal_type(generic_wrapped.__call__)
assert_type(generic_wrapped("foobar"), str)

reveal_type(generic_wrapped.__ror__)
assert_type("foobar" | generic_wrapped, str)

Repro log from pyright CLI, which also functions in current Pylance:

$ pyright --version && pyright wrapped_generic_resolution_repro.py
WARNING: there is a new pyright version available (v1.1.351 -> v1.1.355).
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`

pyright 1.1.351
WARNING: there is a new pyright version available (v1.1.351 -> v1.1.355).
Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`

/home/alexford/ab/main/wrapped_generic_resolution_repro.py
  /home/alexford/ab/main/wrapped_generic_resolution_repro.py:21:13 - information: Type of "first" is "(seq: Iterable[A@first]) -> A@first"
  /home/alexford/ab/main/wrapped_generic_resolution_repro.py:29:13 - information: Type of "wrap_fun(first)" is "(Iterable[A@first]) -> A@first"
  /home/alexford/ab/main/wrapped_generic_resolution_repro.py:30:13 - information: Type of "wrap_fun(first)("foobar")" is "str"
  /home/alexford/ab/main/wrapped_generic_resolution_repro.py:47:13 - information: Type of "explicit_wrapped.func" is "(vals: Iterable[A@func]) -> A@func"
  /home/alexford/ab/main/wrapped_generic_resolution_repro.py:50:13 - information: Type of "explicit_wrapped" is "ExplicitWrappedFirst"
  /home/alexford/ab/main/wrapped_generic_resolution_repro.py:51:13 - information: Type of "explicit_wrapped.__call__" is "(vals: Iterable[A@__call__]) -> A@__call__"
  /home/alexford/ab/main/wrapped_generic_resolution_repro.py:54:13 - information: Type of "explicit_wrapped.__ror__" is "(vals: Iterable[A@__ror__]) -> A@__ror__"
  /home/alexford/ab/main/wrapped_generic_resolution_repro.py:71:24 - error: Argument of type "Literal['foobar']" cannot be assigned to parameter "__value" of type "Iterable[A@first]" in function "__call__"
    "Literal['foobar']" is incompatible with "Iterable[A@first]"
      Type parameter "_T_co@Iterable" is covariant, but "str" is not a subtype of "A@first"
        Type "str" cannot be assigned to type "A@first" (reportArgumentType)
  /home/alexford/ab/main/wrapped_generic_resolution_repro.py:72:13 - information: Type of "generic_wrapped" is "Wrap[Iterable[A@first], A@first]"
  /home/alexford/ab/main/wrapped_generic_resolution_repro.py:73:13 - information: Type of "generic_wrapped.func" is "(Iterable[A@first]) -> A@first"
  /home/alexford/ab/main/wrapped_generic_resolution_repro.py:74:13 - error: "assert_type" mismatch: expected "str" but received "A@first" (reportAssertTypeFailure)
  /home/alexford/ab/main/wrapped_generic_resolution_repro.py:74:34 - error: Argument of type "Literal['foobar']" cannot be assigned to parameter of type "Iterable[A@first]"
    "Literal['foobar']" is incompatible with "Iterable[A@first]"
      Type parameter "_T_co@Iterable" is covariant, but "str" is not a subtype of "A@first"
        Type "str" cannot be assigned to type "A@first" (reportArgumentType)
  /home/alexford/ab/main/wrapped_generic_resolution_repro.py:76:13 - information: Type of "generic_wrapped.__call__" is "(__value: Iterable[A@first], /) -> A@first"
  /home/alexford/ab/main/wrapped_generic_resolution_repro.py:77:13 - error: "assert_type" mismatch: expected "str" but received "A@first" (reportAssertTypeFailure)
  /home/alexford/ab/main/wrapped_generic_resolution_repro.py:77:29 - error: Argument of type "Literal['foobar']" cannot be assigned to parameter "__value" of type "Iterable[A@first]" in function "__call__"
    "Literal['foobar']" is incompatible with "Iterable[A@first]"
      Type parameter "_T_co@Iterable" is covariant, but "str" is not a subtype of "A@first"
        Type "str" cannot be assigned to type "A@first" (reportArgumentType)
  /home/alexford/ab/main/wrapped_generic_resolution_repro.py:79:13 - information: Type of "generic_wrapped.__ror__" is "(__value: Iterable[A@first], /) -> A@first"
  /home/alexford/ab/main/wrapped_generic_resolution_repro.py:80:13 - error: Operator "|" not supported for types "Literal['foobar']" and "Wrap[Iterable[A@first], A@first]" (reportOperatorIssue)
  /home/alexford/ab/main/wrapped_generic_resolution_repro.py:80:13 - error: "assert_type" mismatch: expected "str" but received "Unknown" (reportAssertTypeFailure)
7 errors, 0 warnings, 11 informations 
@asford asford added the bug Something isn't working label Mar 23, 2024
@asford asford changed the title Type resolution failure when Generic TypeVar references another TypeVar method signature. Type resolution failure when Generic TypeVar references TypeVar from generic function signature. Mar 23, 2024
@asford asford changed the title Type resolution failure when Generic TypeVar references TypeVar from generic function signature. Type resolution failure when Generic class TypeVar references TypeVar from generic function signature. Mar 23, 2024
@asford
Copy link
Author

asford commented Mar 27, 2024

"maybe* related to #7212

@erictraut
Copy link
Collaborator

The behavior you're seeing is by design, so I don't consider this a bug.

The behavior for wrap_fun is relying on behavior that is currently unspecified in the Python typing spec related to Callable return types. If pyright (or mypy) sees a Callable return type for a function (as in the case of wrap_fun), and a call of that function results in unsolved type variables, even if those type variables come from some other function (such as first in your example), it effectively treats these unsolved type variables as though they are rescoped to the returned Callable. This unspecified and undocumented "hack" allows pyright and mypy to evaluate the expression wrap_fun(first)("foobar") in a manner consistent with what you're seeing in your code sample.

I'd like to see this behavior formally specified in the typing spec at some point, but we're a long way from that. Currently, there's nothing about TypeVar constraint solving in the typing spec, and we have many more fundamental concepts that need to be documented before we get to something like this.

In the case of Wrap, there is no simple Callable as a return type. In this case, it's not clear what should happen to the unsolved type variables that result when evaluating Wrap(first). Pyright evaluates this as Wrap[Iterable[A@first], A@first]. But the type variable A@first is now out of scope, so it can no longer be solved. Unlike the Callable example, it has not been (and really cannot be) rescoped. Mypy generates an error when evaluating Wrap(first) ("Need type annotation for 'generic_wrapped' [var-annotated]"), and it evaluates the expression's type as Wrap[Iterable[Any], Any].

I'm going to close this because pyright is working as designed here. I don't think your code sample will type check without some changes and clarifications to the type spec.

@erictraut erictraut closed this as not planned Won't fix, can't repro, duplicate, stale Apr 7, 2024
@erictraut erictraut added the as designed Not a bug, working as intended label Apr 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
as designed Not a bug, working as intended bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants