mypy icon indicating copy to clipboard operation
mypy copied to clipboard

Inconsistent errors with custom enum base auto items from installed library

Open felix-hilden opened this issue 2 years ago • 3 comments

Hi, I've defined a custom enum base to copy the names of the enum items as values when using auto(). Mypy curiously throws an assignment error about incompatible types when using the items as e.g. default arguments in a function. However, this only happens if the custom base is imported from another library (in site packages, using another local package is fine).

Here's some example code:

from enum import Enum, auto
# from lib import CopyNameEnum

class CopyNameEnum(Enum):
    def _generate_next_value_(name, start, count, last_values):
        return name

class Items(str, CopyNameEnum):
    A = auto()
    B = auto()

def process(item: Items = Items.A):
    pass

The above is fine and produces no errors. However, when using the import instead, we get

Incompatible default for argument "item" (default has type "auto", argument has type "Items") [assignment]

I would assume that this is a bug, but if you have any other thoughts or debugging suggestions, let me know! I found another issue about the same private method in Enum in #7591, but the issue doesn't seem to be exactly related.

Environment

  • mypy 1.3.0
  • Python 3.9.16

felix-hilden avatar Jun 06 '23 13:06 felix-hilden

I believe _generate_next_value_ should be decorated with @staticmethod. But that crashes in python 3.8 and 3.9 . Not sure if that can be fixed on the type-definition side (see https://github.com/python/typeshed/issues/10428)

Avasam avatar Aug 14 '23 16:08 Avasam

I have a very similar case. We define a custom StrEnum class in a library A. If we reference it in library B (with B depends on A), mypy complains about the typing of any enum using this StrEnum as its parent.

Here are the definitions in library A

#library_a.enums
from enum import Enum
from typing import List, cast

# We originally use a custom `BaseEnum` with a custom metaclass, 
# but using a simplified standard `enum.Enum` class also leads to typing errors. 
class StrEnum(str, Enum):
    def __str__(self) -> str:
        return self.value

    @classmethod
    def values(cls) -> List[str]:
        return [cast(str, c.value) for c in cls]

Here are the subclass definitions and use in library B

# library_b.main
from library_a.enums import StrEnum

class MyEnum(StrEnum):
    A = "a"
    B = "b"


def dummy_func(e: MyEnum) -> None:
    pass

# Raises Error
dummy_func(MyEnum.A)

# error: Argument 1 to "dummy_func" has incompatible type "str"; expected "MyEnum"  [arg-type]
#    dummy_func(MyEnum.A)

This type check failure is not raised if the StrEnum is defined in library B

Environment

  • mypy 1.12.0
  • Python 3.11.5

rpmcginty avatar Oct 16 '24 20:10 rpmcginty

I have expanded my tests and it seems that any Enum classes (subclassing any builtin enum class) defined in an installed / dependent library will trip up mypy

Here are definitions in library_a.enums

from enum import Enum, StrEnum


class DepStrEnumBuiltin(StrEnum):
    def extra_method(self) -> str:
        return f"extra method called on {self}"


class DepStrEnumCustom(str, Enum):
    def extra_method(self) -> str:
        return f"extra method called on {self}"


class DepEnum(Enum):
    def extra_method(self) -> str:
        return f"extra method called on {self}"

Then in library_b.enums

from enum import Enum, StrEnum


class LocalStrEnumCustom(str, Enum):
    def extra_method(self) -> str:
        return f"extra method called on {self}"


class LocalStrEnumBuiltin(StrEnum):
    def extra_method(self) -> str:
        return f"extra method called on {self}"

then in library_b.main

from enum import Enum, StrEnum
from typing import reveal_type
from library_a.enums import DepEnum, DepStrEnumCustom, DepStrEnumBuiltin
from library_b.enums import LocalStrEnumCustom, LocalStrEnumBuiltin


class MyDepEnum(DepEnum):
    A = "a"
    B = 1
    C = False


class MyDepStrEnumCustom(DepStrEnumCustom):
    A = "a"


class MyDepStrEnumBuiltin(DepStrEnumBuiltin):
    A = "a"


class MyLocalStrEnumCustom(LocalStrEnumCustom):
    A = "a"
    

class MyLocalStrEnumBuiltin(LocalStrEnumBuiltin):
    A = "a"


# -----------
# functions
# -----------

def func_dep_enum(e: MyDepEnum) -> None:
    pass


def func_dep_str_enum_custom(e: MyDepStrEnumCustom) -> None:
    pass


def func_dep_str_enum_builtin(e: MyDepStrEnumBuiltin) -> None:
    pass


def func_local_str_enum_custom(e: MyLocalStrEnumCustom) -> None:
    pass


def func_local_str_enum_builtin(e: LocalStrEnumBuiltin) -> None:
    pass


# -----------
# usage
# -----------

MyDepEnum.A.extra_method()
MyDepStrEnumCustom.A.extra_method()
MyDepStrEnumBuiltin.A.extra_method()
MyLocalStrEnumCustom.A.extra_method()
MyLocalStrEnumBuiltin.A.extra_method()

func_dep_enum(MyDepEnum.A)
func_dep_str_enum_custom(MyDepStrEnumCustom.A)
func_dep_str_enum_builtin(MyDepStrEnumBuiltin.A)
func_local_str_enum_custom(MyLocalStrEnumCustom.A)
func_local_str_enum_builtin(MyLocalStrEnumBuiltin.A)

reveal_type(MyDepEnum.A)
reveal_type(MyDepEnum.B)
reveal_type(MyDepEnum.C)
reveal_type(MyDepStrEnumCustom.A)
reveal_type(MyDepStrEnumBuiltin.A)
reveal_type(MyLocalStrEnumCustom.A)
reveal_type(MyLocalStrEnumBuiltin.A)

From these files the following mypy errors are raised:

> $ mypy ./src/library_b/main.py
src/library_b/main.py:57: error: "str" has no attribute "extra_method"  [attr-defined]
    MyDepEnum.A.extra_method()
    ^~~~~~~~~~~~~~~~~~~~~~~~
src/library_b/main.py:58: error: "str" has no attribute "extra_method"  [attr-defined]
    MyDepStrEnumCustom.A.extra_method()
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/library_b/main.py:59: error: "str" has no attribute "extra_method"  [attr-defined]
    MyDepStrEnumBuiltin.A.extra_method()
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/library_b/main.py:63: error: Argument 1 to "func_dep_enum" has incompatible type "str"; expected "MyDepEnum"  [arg-type]
    func_dep_enum(MyDepEnum.A)
                  ^~~~~~~~~~~
src/library_b/main.py:64: error: Argument 1 to "func_dep_str_enum_custom" has incompatible type "str"; expected "MyDepStrEnumCustom"  [arg-type]
    func_dep_str_enum_custom(MyDepStrEnumCustom.A)
                             ^~~~~~~~~~~~~~~~~~~~
src/library_b/main.py:65: error: Argument 1 to "func_dep_str_enum_builtin" has incompatible type "str"; expected "MyDepStrEnumBuiltin"  [arg-type]
    func_dep_str_enum_builtin(MyDepStrEnumBuiltin.A)
                              ^~~~~~~~~~~~~~~~~~~~~
src/gcs_core/utils/deleteme.py:70: note: Revealed type is "builtins.str"
src/gcs_core/utils/deleteme.py:71: note: Revealed type is "builtins.int"
src/gcs_core/utils/deleteme.py:72: note: Revealed type is "builtins.bool"
src/gcs_core/utils/deleteme.py:73: note: Revealed type is "builtins.str"
src/gcs_core/utils/deleteme.py:74: note: Revealed type is "builtins.str"
src/gcs_core/utils/deleteme.py:75: note: Revealed type is "Literal[gcs_core.utils.deleteme.MyLocalStrEnumCustom.A]?"
src/gcs_core/utils/deleteme.py:76: note: Revealed type is "Literal[gcs_core.utils.deleteme.MyLocalStrEnumBuiltin.A]?"
Found 6 errors in 1 file (checked 1 source file)

Observations

  • mypy cannot identify any enum parent classes as being enums if they are defined in an installed library
  • reveal_type shows that the types of enum values in a class inheriting from installed package (MyDep*) are inferred as the plain type of the value on the right side of =
  • These enums work as expected at runtime.

rpmcginty avatar Oct 17 '24 20:10 rpmcginty