Skip to content

Fix function is_protocol_implementation for quite similar protocol classes #10308

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

Merged
merged 3 commits into from
Apr 13, 2021

Conversation

tyralla
Copy link
Collaborator

@tyralla tyralla commented Apr 11, 2021

Fixes #9771

from typing import Protocol, TypeVar, Union

T1 = TypeVar("T1", covariant=True)
T2 = TypeVar("T2")

class P1(Protocol[T1]):
    def b(self) -> int: ...
    def a(self, other: "P1[T2]") -> T1: ...

class P2(Protocol[T1]):
    def a(self, other: Union[P1[T2], "P2[T2]"]) -> T1: ...

Method infer_constraints_from_protocol_members of class ConstraintBuilderVisitor raises an assertion for the above code when we pass P2 as instance and P1 as template to it (inline comment: The above is safe since at this point we know that 'instance' is a subtype of (erased) 'template', therefore it defines all protocol members).

So, the actual error happens before. Method visit_instance of class ConstraintBuilderVisitor should never apply the method infer_constraints_from_protocol_members on P1 and P2. It nevertheless does so due to function is_protocol_implementation indicating P2 actually implements P1.

In my naive understanding, the problem seems to lie in the recursive approach underlying function is_protocol_implementation. This function first analyses method a to check if P2 is a subclass of P1. Therefore, it needs to check if P1 is a subclass of P2. This can result in a situation where the "'assuming' structural subtype matrix" is already filled with P1 and P2. is_protocol_implementation then returns True (for avoiding infinite recursion in other cases) without checking any other members, which is incorrect for the given case.

My simple solution is first to check if all members of P1 (the potential supertype) define a subset of P2 (the potential subtype). If this does not hold, we do not need to perform the complex recursive approach and can return False immediately. Maybe not the most general solution, but it works on the additional test testTwoUncomfortablyIncompatibleProtocolsWithoutRunningInIssue9771 (I can't think of a better name...) and seems not to impact any other tests.

tyralla added 2 commits April 10, 2021 16:15
… case that both the potential subtype and the potential supertype are protocols.

The simple check only relies on the subset relationship of the member names.  Eventually, it avoids the more complicated recursive search for potential discrepancies.  I do not know if it positively affects performance, but it fixes python#9771.
@github-actions
Copy link
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

pandas (https://github.com/pandas-dev/pandas.git)
+ pandas/core/frame.py:6045: error: Incompatible types in assignment (expression has type "Iterable[Any]", variable has type "Union[Hashable, Sequence[Hashable], None]")  [assignment]
+ pandas/core/frame.py:6054: error: Unsupported right operand type for in ("Union[Index, Hashable]")  [operator]


black (https://github.com/psf/black.git)
+ src/black/__init__.py:307:43: error: Argument 1 to "__call__" of "_lru_cache_wrapper" has incompatible type "Iterable[str]"; expected "Hashable"

JelleZijlstra added a commit to JelleZijlstra/black that referenced this pull request Apr 11, 2021
@JelleZijlstra
Copy link
Member

I'm not sure how, but this PR actually fixes a more basic bug. This code passes on master:

from typing import Hashable, Iterable

def f(x: Hashable) -> None:
    pass

def g(x: Iterable[str]) -> None:
    f(x)

But with your PR it correctly produces lruc.py:20: error: Argument 1 to "f" has incompatible type "Iterable[str]"; expected "Hashable". The error mypy-primer found in Black above alerted me to this.

JelleZijlstra added a commit to psf/black that referenced this pull request Apr 11, 2021
Copy link
Collaborator

@hauntsaninja hauntsaninja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Jelle's fixed issue is explained by:

  • method = info.get_method(name)
    finds that Iterable does have a __hash__ method in its MRO.
  • The protocol_members attribute doesn't look at object
    for base in self.mro[:-1]: # we skip "object" since everyone implements it

I'm not the most familiar with this code, but your write up makes sense, this passes tests and fixes a crash multiple people have run into, so I'll merge soon if no one else chimes in. Thanks! :-)

@JelleZijlstra
Copy link
Member

I feel like we should add an explicit test for the Hashable/Iterable case to make sure it doesn't regress.

@tyralla
Copy link
Collaborator Author

tyralla commented Apr 12, 2021

I tried to add the following regression test you suggested:

[case testHashable]

from typing import Hashable, Iterable

def f(x: Hashable) -> None:
    pass

def g(x: Iterable[str]) -> None:
    f(x)   # E: Argument 1 to "f" has incompatible type "Iterable[str]"; expected "Hashable"

[typing fixtures/typing-full.pyi]

Additional content of typing-full.pyi:

@runtime_checkable
class Hashable(Protocol, metaclass=ABCMeta):
    @abstractmethod
    def __hash__(self) -> int: pass

However, this test passes both with and without the PR. Am I missing something?

@hauntsaninja
Copy link
Collaborator

hauntsaninja commented Apr 12, 2021

I'm guessing object doesn't have __hash__ in the default builtins test fixture

@tyralla
Copy link
Collaborator Author

tyralla commented Apr 12, 2021

Yes, you are right. Defining a fixture for such an essential feature did not come into my mind.

I added the minimal stub file object_hashable.pyi. Now the test actually fails after uncommenting the additional check that I introduced into is_protocol_implementation.

@github-actions
Copy link
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

pandas (https://github.com/pandas-dev/pandas.git)
+ pandas/core/frame.py:6045: error: Incompatible types in assignment (expression has type "Iterable[Any]", variable has type "Union[Hashable, Sequence[Hashable], None]")  [assignment]
+ pandas/core/frame.py:6054: error: Unsupported right operand type for in ("Union[Index, Hashable]")  [operator]

@JelleZijlstra
Copy link
Member

Thanks for adding the test!

@github-actions
Copy link
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

pandas (https://github.com/pandas-dev/pandas.git)
+ pandas/core/frame.py:6045: error: Incompatible types in assignment (expression has type "Iterable[Any]", variable has type "Union[Hashable, Sequence[Hashable], None]")  [assignment]
+ pandas/core/frame.py:6054: error: Unsupported right operand type for in ("Union[Index, Hashable]")  [operator]

@hauntsaninja hauntsaninja merged commit c0490b4 into python:master Apr 13, 2021
@hauntsaninja
Copy link
Collaborator

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

INTERNAL ERROR when defining "__add__" in two related Protocol classes
3 participants