Open Nnonexistent opened 1 year ago
In case it helps, I've just encountered the same issue in a slightly different situation:
from enum import StrEnum
class Choices(StrEnum):
A = "a"
def my_func(obj: Choices) -> None:
assert isinstance(obj, Choices)
for element in list(Choices):
my_func(element)
Which gives:
error: Argument 1 to "my_func" has incompatible type "str"; expected "Choices" [arg-type]
In my case I'm not assigning list(Choices)
to anything, but the elements are inferred as str
and not Choices
.
Are there any plans to work on this? We've had to silence this error many times in our codebase :( I can confirm it still happens on Python 3.12 with Mypy 1.8.0.
I was fiddling around with this issue and I found a, somewhat reasonable, workaround where you don't have to cast
or silence the error (@kikones34).
from typing import overload, Self
from enum import StrEnum as _StrEnum
class StrEnum(_StrEnum):
# mimic str.__new__'s overloads
@overload
def __new__(cls, object: object = ...) -> Self: ...
@overload
def __new__(cls, object: object, encoding: str = ..., errors: str = ...) -> Self: ...
def __new__(cls, *values):
# when we import enum, _StrEnum.__new__ gets moved to _new_member_ when
# the "final" _StrEnum class is created via EnumType
return _StrEnum._new_member_(cls, *values)
class Choices(StrEnum):
LOREM = "lorem"
IPSUM = "ipsum"
# this is still ok
def ok_func() -> list[Choices]:
return list(Choices)
# and this no longer produces an error
def error_func() -> list[Choices]:
var = list(Choices)
return var
https://mypy-play.net/?mypy=latest&python=3.11&gist=3e91cdf18f5a07b47d6619cb43940b6c
This also seems to cover the issue @Apakottur found: https://mypy-play.net/?mypy=latest&python=3.11&gist=f32efd891ff8b25fa333c937093682e1
You can then use it like so:
# file path: project_root/fixes/enum.py
from typing import overload, Self
from enum import StrEnum as _StrEnum
__all__ = ['StrEnum', ]
class StrEnum(_StrEnum):
@overload
def __new__(cls, object: object = ...) -> Self: ...
@overload
def __new__(cls, object: object, encoding: str = ..., errors: str = ...) -> Self: ...
def __new__(cls, *values):
return _StrEnum._new_member_(cls, *values)
# file path: project_root/main.py
from fixes.enum import StrEnum
class Choices(StrEnum):
LOREM = "lorem"
IPSUM = "ipsum"
if __name__ == '__main__':
print(Choices.__members__)
# outputs:
# {'LOREM': <Choices.LOREM: 'lorem'>, 'IPSUM': <Choices.IPSUM: 'ipsum'>}
I'm not sure how mypy
works, but I think this issue could be resolved if StrEnum
in enum.pyi
, in the typeshed:
# current enum.pyi
class StrEnum(str, ReprEnum):
def __new__(cls, value: str) -> Self: ...
_value_: str
@_magic_enum_attr
def value(self) -> str: ...
@staticmethod
def _generate_next_value_(name: str, start: int, count: int, last_values: list[str]) -> str: ...
Was updated to account for str.__new__
's overloads:
# proposed enum.pyi
class StrEnum(str, ReprEnum):
@overload
def __new__(cls, object: object = ...) -> Self: ...
@overload
def __new__(cls, object: ReadableBuffer, encoding: str = ..., errors: str = ...) -> Self: ...
_value_: str
@_magic_enum_attr
def value(self) -> str: ...
@staticmethod
def _generate_next_value_(name: str, start: int, count: int, last_values: list[str]) -> str: ...
I understand that the documentation for StrEnum.__new__
states "values must already be of type str
", but if you look at the actual code you'll notice that StrEnum.__new__
is accounting for all of the current parameters to str.__new__
:
def __new__(cls, *values):
"values must already be of type `str`"
if len(values) > 3:
raise TypeError('too many arguments for str(): %r' % (values, ))
if len(values) == 1:
# it must be a string
if not isinstance(values[0], str):
raise TypeError('%r is not a string' % (values[0], ))
if len(values) >= 2:
# check that encoding argument is a string
if not isinstance(values[1], str):
raise TypeError('encoding must be a string, not %r' % (values[1], ))
if len(values) == 3:
# check that errors argument is a string
if not isinstance(values[2], str):
raise TypeError('errors must be a string, not %r' % (values[2]))
value = str(*values)
member = str.__new__(cls, value)
member._value_ = value
return member
Updating the enum.pyi
typeshed file with the proposed updates doesn't seem to fix anything. Hopefully the workaround described above will help someone until the underlying issue gets resolved.
@jhenly your fix works, thanks! Although I really don't like having to import a custom StrEnum, we'll probably just keep silencing the errors for now.
I don't think this has anything to do with assigning StrEnum
to a list. Contrary to the documentation, StrEnum
just doesn't work (although on a closer reading I notice that the example is specific to Enum
): https://mypy.readthedocs.io/en/stable/literal_types.html#enums
from enum import (
StrEnum,
Enum,
)
from typing import reveal_type
class TestEnum(Enum):
ONE = 'one'
class TestStrEnum(StrEnum):
ONE = 'one'
reveal_type(TestEnum.ONE)
reveal_type(TestStrEnum.ONE)
def test_enum(param: TestEnum):
print(param)
def test_str_enum(param: TestStrEnum):
print(param)
test_enum(TestEnum.ONE)
test_str_enum(TestStrEnum.ONE)
The output is:
mypy test_enum.py
test_enum.py:24: error: Argument 1 to "test_str_enum" has incompatible type "str"; expected "TestStrEnum" [arg-type]
Python 3.11.7 (main, Jan 18 2024, 12:21:46) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from test_enum import TestEnum
Runtime type is 'TestEnum'
Runtime type is 'TestStrEnum'
TestEnum.ONE
one
@jarmstrong-atlassian how are you performing the test? I get the following output with Python 3.11.1 and Mypy 1.9.0:
enum_test.py:14: note: Revealed type is "Literal[enum_test.TestEnum.ONE]?"
enum_test.py:15: note: Revealed type is "Literal[enum_test.TestStrEnum.ONE]?"
Success: no issues found in 1 source file
@kikones34 My bad. I was running mypy enum_test.py
, but this was still in the directory with my current mypy.ini
which was still setting python_version = 3.10
. Removing that line, I get the expected result you posted. Sorry for the confusion.
Another weird case + a workaround that appeases mypy:
from collections.abc import Iterable
from enum import StrEnum
def takes_enum(e: StrEnum) -> StrEnum:
return e
def takes_enum_type(type_: type[StrEnum]) -> StrEnum:
return next(iter(type_)) # error: Incompatible return value type (got "str", expected "StrEnum") [return-value]
def takes_enum_type_with_trick(type_: type[StrEnum]) -> StrEnum:
return next(iter(mypy_type_hint_trick(type_))) # no error!
def mypy_type_hint_trick(type_: type[StrEnum]) -> Iterable[StrEnum]:
return type_
Bug Report
On python 3.11 using StrEnum, if converting given enum to a list and assigning to a variable, mypy will wrongly assumes that given variable will be
list[str]
, notlist[StrEnum]
.To Reproduce
https://mypy-play.net/?mypy=latest&python=3.11&gist=e73a15c8902092236567da4a4567f372
Expected Behavior
Mypy should assume
list[Choices]
type forvar
.Actual Behavior
Incompatible return value type (got "List[str]", expected "List[Choices]") [return-value]
Your Environment
mypy.ini
(and other config files): none