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-checking generic of enum.Meta with a custom metaclass fails with code that runs, and passes with code that doesn't run. #11970

Open
mvaled opened this issue Jan 11, 2022 · 4 comments
Labels
bug mypy got something wrong topic-enum

Comments

@mvaled
Copy link

mvaled commented Jan 11, 2022

Bug Report

Trying to type-check generic types of a bound enum.Enum (with a custom metaclass) only catches type errors when the actual code fails to run; but doesn't detect the issues when the code runs.

To Reproduce

The code in this gist properly detects the type error while calling Carrier(Foo, Bar.item1):

import enum
from dataclasses import dataclass
from typing import TypeVar, Generic, Type


M = TypeVar('M', bound="MyEnum")

class Meta(enum.EnumMeta):
    pass

class MyEnum(metaclass=Meta):
    ...

class Foo(enum.Enum, MyEnum):
    item1 = "1"
    item2 = "2"


class Bar(enum.Enum, MyEnum):
    item1 = "1"
    item2 = "2"

@dataclass
class Carrier(Generic[M]):
    meta: Type[M]
    instance: M


Carrier(Foo, Foo.item1)
Carrier(Foo, Bar.item1)  # should fail

However that code fails to run with

Traceback (most recent call last):
  File "/home/manu/src/tmp/enums.py", line 14, in <module>
    class Foo(enum.Enum, MyEnum):
  File "/usr/lib/python3.10/enum.py", line 173, in __prepare__
    member_type, first_enum = metacls._get_mixins_(cls, bases)
  File "/usr/lib/python3.10/enum.py", line 619, in _get_mixins_
    raise TypeError("new enumerations should be created as "
TypeError: new enumerations should be created as `EnumName([mixin_type, ...] [data_type,] enum_type)`

In order for the code to run we have to swap the positions of enum.Enum and MyEnum in both Foo and Bar (see the second gist); but then mypy fails to catch the type-error.

Your Environment

  • Mypy version used: master and 0.931
  • Mypy command-line flags: none
  • Mypy configuration options from mypy.ini (and other config files): none
  • Python version used: 3.9
  • Operating system and version: Linux pavla 5.15.12-1-MANJARO #1 SMP PREEMPT Wed Dec 29 18:08:07 UTC 2021 x86_64 GNU/Linux

Previous discussion in gitter: https://gitter.im/python/typing?at=61dab43cf5a3947800f73b71

@mvaled mvaled added the bug mypy got something wrong label Jan 11, 2022
@erictraut
Copy link

I don't think the sample above (or the one in your gist) should generate a type error. There's a valid solution for TypeVar M for both Carrier calls at the bottom of the sample. In the first of the two calls, the M is Foo. In the second M is Foo | Bar or object.

If you want the second call to Carrier to produce an error, you would need to define M as a constrained TypeVar rather than a bound TypeVar.

M = TypeVar("M", "Foo", "Bar")

@mvaled
Copy link
Author

mvaled commented Jan 12, 2022

That's probably correct. My first intuition was to look for something like Instance[T] (for some T bounded to a meta-class). This is the (pseudo) code I was asking about in the gitter thread:

import enum
from dataclasses import dataclass
from typing import TypeVar, Generic, Instance


M = TypeVar('M', bound='Meta')

class Meta(enum.EnumMeta):
    pass

class Foo(enum.Enum, metaclass=Meta):
    item1 = "1"
    item2 = "2"


class Bar(enum.Enum, metaclass=Meta):
    item1 = "1"
    item2 = "2"

@dataclass
class Carrier(Generic[M]):
    meta: M
    instance: Instance[M] 


Carrier(Foo, Foo.item1)
Carrier(Foo, Bar.item1)  # should fail

The difference here is that M is bound to the metaclass Meta but then I would like to refer to some instance of M.

@erictraut
Copy link

Yeah, there's no such thing as Instance in the type system today. (It's generally not needed because you can specify Type[M] to specify that you're talking about the instantiable class rather than an instance of that class.) And as you probably know, bound works only parent/child class relationships, not with metaclasses.

Does my suggestion to use a constrained TypeVar meet your needs?

@mvaled
Copy link
Author

mvaled commented Jan 12, 2022

The actual code is a little bit more complicated; there are methods in the common MyEnum base with a signature similar to:

@classmethod
def convert(cls: Type[M], ...) -> M:
    ...

And have M being constrained to the subclasses doesn't work. I will have to look at it a little more closely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong topic-enum
Projects
None yet
Development

No branches or pull requests

3 participants