python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.42k stars 2.82k forks source link

[1.12 regression] Erroneous `Cannot infer type argument` for open & gzip #18024

Open Redoubts opened 1 week ago

Redoubts commented 1 week ago

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

brianschubert commented 1 week ago

Looks potentially related to https://github.com/python/typeshed/pull/12286 (which landed in v1.12.0 with #17586)

hauntsaninja commented 1 week ago

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

JukkaL commented 4 days ago

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.