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

Deriving builtins from ABCs implies incorrect and problematic metaclass structure #1595

Closed
elazarg opened this issue Sep 8, 2017 · 7 comments
Labels
stubs: false negative Type checkers do not report an error, but should

Comments

@elazarg
Copy link
Contributor

elazarg commented Sep 8, 2017

typeshed claims that str derives from Sequence and therefore indirectly from collections_abc.Sized whose metaclass is declared to be ABCMeta. This is done as a type-checkable alternative to Sequence.register(str) which handles isinstance and issubclass calls.

However derivation and register are not indistinguishable, since the metaclass structure is different. issubclass(type(str), ABCMeta) should be False, otherwise the definition class A(str, Enum): pass would fail at runtime due to inconsistent metaclass structure:

>>> from collections import Sized
>>> from enum import Enum
>>> class A(Sized, Enum): pass
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

The error is that type(Enum) is EnumMeta and type(Sized) is ABCMeta and they are unrelated.

This bug has at least the following implications:

  • It obviously implies that a: ABCMeta = str typechecks.
  • It makes it harder to warn about inconsistent metaclass structure without false positives. This is why mypy only catches specific cases.
  • As exemplified above, class A(str, Enum) appears to have inconsistent metaclass structure, even though this is the recommended way of making an enum of strings. It might also be the case with some other builtins.
@elazarg
Copy link
Contributor Author

elazarg commented Sep 8, 2017

Possible solution, as discussed with @ilevkivskyi and @JukkaL in gitter: use ABCMeta.register as a decorator:

@Sized.register
class str: ...

A caveat is that this does not compile with generic type, i.e. @Iterable[str].register is a syntax error; @JukkaL suggested using @Iterable.register and requiring inference of the type parameter. (I have proposed in python-ideas adding abc.register function and a __registered__ field, which may solve the syntactic problem in the future)

@gvanrossum
Copy link
Member

gvanrossum commented Sep 8, 2017 via email

@ilevkivskyi
Copy link
Member

Maybe if Sequence were a Protocol this would be less of a problem?

I also think this is the right way forward. Although it will be better to first implement more advanced join for protocols, otherwise ['abc', ['ab', 'bc']] will be inferred as List[object] instead of List[Sequence[str]], one can always override this with an explicit annotation. In this particular example List[object] is probably OK. So as Jukka said on gitter, it is a bad idea to indiscriminately remove explicit protocol bases everywhere, but in some particular cases this makes sense.

elazarg added a commit to elazarg/typeshed that referenced this issue Sep 10, 2017
Avoid inconsistent metaclass structure for enum mixins due to python#1595 - e.g. mypy/python#2824
elazarg added a commit to elazarg/typeshed that referenced this issue Sep 10, 2017
Avoid inconsistent metaclass structure for enum mixins due to python#1595 - e.g. mypy/python#2824
JelleZijlstra pushed a commit that referenced this issue Sep 11, 2017
Avoid inconsistent metaclass structure for enum mixins due to #1595 - e.g. mypy/#2824
@Zac-HD
Copy link
Contributor

Zac-HD commented Oct 15, 2017

I have a similar problem in HypothesisWorks/hypothesis#858 - sampling from an Enum works, but Mypy complains about assert len(an_enum), because it is not Sized. Obviously this does work at runtime. A minimal reproducer is:

import enum
print(len(enum.Enum('A', 'a b c')))  # prints "3"
#  error: Argument 1 to "len" has incompatible type "Enum"; expected "Sized"

I think it belongs here, but happy to move to another issue or open on if that's more useful.

@elazarg
Copy link
Contributor Author

elazarg commented Oct 15, 2017

@Zac-HD this seems to be a mypy-specific problem, since it works if you define the enum using the class syntax. Note that the type in the error message is incorrect - it should be a Type[Enum] or something similar.

@ilevkivskyi
Copy link
Member

Yes, mypy has special treatment of enum definitions, so it doesn't detect it in this particular "in-place" definition.

@srittau srittau added stubs: false positive Type checkers report false errors size-medium stubs: false negative Type checkers do not report an error, but should and removed stubs: false positive Type checkers report false errors labels Oct 28, 2018
@hauntsaninja
Copy link
Collaborator

This will require type system features to fix, so five years on, is better discussed at https://github.com/python/typing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stubs: false negative Type checkers do not report an error, but should
Projects
None yet
Development

No branches or pull requests

6 participants