8000 Issue with constrained type var and Protocol · Issue #18024 · python/mypy · GitHub
[go: up one dir, main page]

Skip to content

Issue with constrained type var and Protocol #18024

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

Closed
Redoubts opened this issue Oct 23, 2024 · 7 comments · Fixed by #18943
Closed

Issue with constrained type var and Protocol #18024

Redoubts opened this issue Oct 23, 2024 · 7 comments · Fixed by #18943
Labels
bug mypy got something wrong

Comments

@Redoubts
Copy link

Bug Report

Seems like a regression from 11.2, consider the following

import gzip, shutil
from pathlib import Path


with open("/dev/null") as src, gzip.open("/dev/null", "wt") as dest:
    reveal_type(src)
    reveal_type(dest)
    shutil.copyfileobj(src, dest)


with Path("/dev/null").open() as src2, gzip.open("/dev/null", "wt") as dest2:
    reveal_type(src2)
    reveal_type(dest2)
    shutil.copyfileobj(src2, dest2)

I see errors even though this should be fine?

% mypy x.py     
x.py:6: note: Revealed type is "io.TextIOWrapper[io._WrappedBuffer]"
x.py:7: note: Revealed type is "typing.TextIO"
x.py:8: error: Cannot infer type argument 1 of "copyfileobj"  [misc]
x.py:12: note: Revealed type is "io.TextIOWrapper[io._WrappedBuffer]"
x.py:13: note: Revealed type is "typing.TextIO"
x.py:14: error: Cannot infer type argument 1 of "copyfileobj"  [misc]
Found 2 errors in 1 file (checked 1 source file)

no errors in 1.11.2:

x.py:6: note: Revealed type is "io.TextIOWrapper"
x.py:7: note: Revealed type is "typing.TextIO"
x.py:12: note: Revealed type is "io.TextIOWrapper"
x.py:13: note: Revealed type is "typing.TextIO"
Success: no issues found in 1 source file

Your Environment

  • Mypy version used: 1.13
  • Mypy command-line flags: mypy
  • Mypy configuration options from mypy.ini (and other config files): none
  • Python version used: Python 3.11.10
@Redoubts Redoubts added the bug mypy got something wrong label Oct 23, 2024
@brianschubert
Copy link
Collaborator

Looks potentially related to python/typeshed#12286 (which landed in v1.12.0 with #17586)

@hauntsaninja
Copy link
Collaborator
hauntsaninja commented Oct 24, 2024

mypy_primer -p test.py --bisect --debug --old 'v1.11.0' confirms it's the sync PR brianschubert mentions

@hauntsaninja hauntsaninja changed the title Erroneous Cannot infer type argument for open & gzip [1.12 regression] Erroneous Cannot infer type argument for open & gzip Oct 26, 2024
@JukkaL
Copy link
Collaborator
JukkaL commented Oct 28, 2024

Here's an attempt to create a simplified, self-contained reproducer:

from typing import AnyStr, Protocol, TypeVar, Generic, overload

class Buffer(Protocol):
    def __buffer__(self, flags: int, /) -> memoryview: ...

class IO(Generic[AnyStr]):
    @overload
    def write(self: IO[bytes], s: Buffer, /) -> int: ...
    @overload
    def write(self, s: AnyStr, /) -> int: ...
    def write(self, s):
        return 0

class TextIO(IO[str]):
    pass

_T_contra = TypeVar("_T_contra", contravariant=True)

class SupportsWrite(Protocol[_T_contra]):
    def write(self, s: _T_contra, /) -> object: ...

def foo(
    f: SupportsWrite[AnyStr],
) -> None: ...

def bar(d: TextIO) -> None:
    foo(d)  # Value of type variable "AnyStr" of "foo" cannot be "Buffer"

Interestingly, this fails on 1.11 as well, and doesn't depend on the changes in #17586.

I looked at this a bit, it seems that constraints inference from protocol generates invalid constraints (infer_constraints_from_protocol_members). The root of the problem seems to be in mypy.subtypes.find_member.

@ilevkivskyi I remember that you mentioned that you've looked into inconsistencies between find_member and analyze_member_access. This may be an example where they deviate.

@ilevkivskyi
Copy link
Member

I remember that you mentioned that you've looked into inconsistencies between find_member and analyze_member_access. This may be an example where they deviate.

Y 8000 es, and it may not be trivial to fix. Ultimately, we need to refactor this, so that everything uses analyze_member_access (e.g. also sub-classing overrides checks, although they are currently closer in semantics/implementation). As you know, this is a big project, and I didn't yet have courage to start it :-)

@JukkaL
Copy link
Collaborator
JukkaL commented Nov 11, 2024

It may still be possible to fix this specific regression by a more ad-hoc fix, by adding some functionality to find_member, even if we don't fully migrate to analyze_member_access until later. However, it may be turn out to be a bit messy, and it will increase code duplication.

@kgutwin
Copy link
kgutwin commented Apr 12, 2025

As of at least version 1.15.0, this issue seems to be resolved...

$ mypy --version
mypy 1.15.0 (compiled: yes)
$ mypy x.py  # from the original issue report
x.py:6: note: Revealed type is "_io.TextIOWrapper[_io._WrappedBuffer]"
x.py:7: note: Revealed type is "_io.TextIOWrapper[_io._WrappedBuffer]"
x.py:12: note: Revealed type is "_io.TextIOWrapper[_io._WrappedBuffer]"
x.py:13: note: Revealed type is "_io.TextIOWrapper[_io._WrappedBuffer]"
Success: no issues found in 1 source file

@hauntsaninja
Copy link
Collaborator

Hm yeah, looks like it got fixed in a more recent typeshed sync: #17971

Jukka's self contained repro still reproduces, so we should keep this issue open

It could also maybe be worth adding a test case to typeshed

@hauntsaninja hauntsaninja changed the title [1.12 regression] Erroneous Cannot infer type argument for open & gzip Issue with constrained type var and Protocol Apr 13, 2025
ilevkivskyi added a commit that referenced this issue May 30, 2025
Fixes #18024
Fixes #18706
Fixes #17734
Fixes #15097
Fixes #14814
Fixes #14806
Fixes #14259
Fixes #13041
Fixes #11993
Fixes #9585
Fixes #9266
Fixes #9202
Fixes #5481

This is a fourth "major" PR toward
#7724. This is one is
watershed/crux of the whole series (but to set correct expectations,
there are almost a dozen smaller follow-up/clean-up PRs in the
pipeline).

The core of the idea is to set current type-checker as part of the
global state. There are however some details:
* There are cases where we call `is_subtype()` before type-checking. For
now, I fall back to old logic in this cases. In follow up PRs we may
switch to using type-checker instances before type checking phase (this
requires some care).
* This increases typeops import cycle by a few modules, but
unfortunately this is inevitable.
* This PR increases potential for infinite recursion in protocols. To
mitigate I add: one legitimate fix for `__call__`, and one temporary
hack for `freshen_all_functions_type_vars` (to reduce performance
impact).
* Finally I change semantics for method access on class objects to match
the one in old `find_member()`. Now we will expand type by instance, so
we have something like this:
  ```python
  class B(Generic[T]):
      def foo(self, x: T) -> T: ...
  class C(B[str]): ...
  reveal_type(C.foo)  # def (self: B[str], x: str) -> str
  ```
FWIW, I am not even 100% sure this is correct, it seems to me we _may_
keep the method generic. But in any case what we do currently is
definitely wrong (we infer a _non-generic_ `def (x: T) -> T`).

---------

Co-authored-by: hauntsaninja <hauntsaninja@gmail.com>
Co-authored-by: Shantanu <12621235+hauntsaninja@users.noreply.github.com>
cdce8p pushed a commit to cdce8p/mypy that referenced this issue May 31, 2025
Fixes python#18024
Fixes python#18706
Fixes python#17734
Fixes python#15097
Fixes python#14814
Fixes python#14806
Fixes python#14259
Fixes python#13041
Fixes python#11993
Fixes python#9585
Fixes python#9266
Fixes python#9202
Fixes python#5481

This is a fourth "major" PR toward
python#7724. This is one is
watershed/crux of the whole series (but to set correct expectations,
there are almost a dozen smaller follow-up/clean-up PRs in the
pipeline).

The core of the idea is to set current type-checker as part of the
global state. There are however some details:
* There are cases where we call `is_subtype()` before type-checking. For
now, I fall back to old logic in this cases. In follow up PRs we may
switch to using type-checker instances before type checking phase (this
requires some care).
* This increases typeops import cycle by a few modules, but
unfortunately this is inevitable.
* This PR increases potential for infinite recursion in protocols. To
mitigate I add: one legitimate fix for `__call__`, and one temporary
hack for `freshen_all_functions_type_vars` (to reduce performance
impact).
* Finally I change semantics for method access on class objects to match
the one in old `find_member()`. Now we will expand type by instance, so
we have something like this:
  ```python
  class B(Generic[T]):
      def foo(self, x: T) -> T: ...
  class C(B[str]): ...
  reveal_type(C.foo)  # def (self: B[str], x: str) -> str
  ```
FWIW, I am not even 100% sure this is correct, it seems to me we _may_
keep the method generic. But in any case what we do currently is
definitely wrong (we infer a _non-generic_ `def (x: T) -> T`).

---------

Co-authored-by: hauntsaninja <hauntsaninja@gmail.com>
Co-authored-by: Shantanu <12621235+hauntsaninja@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants
0