enum-properties
enum-properties copied to clipboard
Support type hinting for properties and specialize()
The typing in this package is extremely dynamic so its unclear if the current state of static type checking in python is worth the trouble - but once it is it should be added.
I came here with much excitement, wondering about this very thing. It appears that the dataclass+Enum approach, while being more verbose, does reflect type hints properly — and only because the static type checkers Mypy and Pyright have specialcased Enum processing.
dataclass+Enum approach does address some of the challenges enum-properties does - but there are some key differences - namely that the value of the enumeration is now a dataclass instance which will not work for many use cases without additional adaptation - especially when enums need to be stored in a database.
Explained here: https://enum-properties.readthedocs.io/en/latest/usage.html#id2
I may revisit this soon, its been some time and type hinting has only gotten better. One option might be to dynamically construct a dataclass and inherit from it.
I found a way to solve the primary value problem for dataclass enums. It's a little hacky, but it works:
import typing as t, typing_extensions as te,
import dataclasses
import enum
from reprlib import recursive_repr
_T = t.TypeVar('_T')
class SelfProperty:
def __get__(self, obj: t.Optional[_T], _cls: t.Type[_T]) -> _T:
if obj is None: raise AttributeError("Flag for @dataclass to recognise no default value")
return obj
def __set__(self, _obj: t.Any, _value: t.Any) -> None:
# Do nothing. This method will get exactly one call from the dataclass-generated
# __init__. Future attempts to set the attr will be blocked in a frozen dataclass.
return
@dataclasses.dataclass(eq=False, frozen=True) # eq=False is important!
class MyDataClass(str): # <— Insert database type here (str, int)
if t.TYPE_CHECKING:
# Mypy bug: https://github.com/python/mypy/issues/16538
self: t.Union[SelfProperty, t.Any] # Replace `Any` with the base class's type
else:
# When the Mypy bug is fixed, `self` will get the type of SelfProperty.__set__.value
self: SelfProperty = SelfProperty()
def __new__(cls, self: t.Any, *_args: t.Any, **_kwargs: t.Any) -> te.Self:
# Send
return super().__new__(cls, self) # type: ignore[call-arg]
@recursive_repr()
def __repr__(self) -> str:
"""Provide a dataclass-like repr that doesn't recurse into self."""
self_repr = super().__repr__() # Invoke __repr__ on the data type
fields_repr = ', '.join(
[
f'{field.name}={getattr(self, field.name)!r}'
for field in dataclasses.fields(self)[1:]
if field.repr
]
)
return f'{self.__class__.__qualname__}({self_repr}, {fields_repr})'
# Add other fields here or in a subclass
description: str
optional: t.Optional[str] = None
my_data = MyDataClass('base-data', 'descriptive-data', 'optional-data')
class MyEnum(MyDataclass, enum.Enum):
ONE = 'base-data', 'descriptive-data'
del MyEnum.__str__ # Required if not using ReprEnum (3.11+ only)
del MyEnum.__format__ # Same
It gets a bit more verbose when the support code is moved into a base class but the data type is specified in a subclass, and needs additional testing for pickling and dataclass-generated eq/hash/compare.
Prior to Python 3.11's ReprEnum, subclassing as MyEnum(MyDataClass, Enum) will cause Enum to insert it's own __str__, obliterating access to the core value. That needs a manual del MyEnum.__str__ right after the class.
To be fair, Enum-Properties is vastly superior to this hack, especially with symmetric properties. This is just an interim coping mechanism to get type hints to work.
Its possible to just add type hints for the properties directly to the enum class. This will be added to the documentation. Version 2.0 will also support specifying the properties in the type hints, like dataclasses.
import typing as t
from enum_properties import EnumProperties, s
from enum import auto
class Color(EnumProperties, s('rgb'), s('hex', case_fold=True)):
rgb: t.Tuple[int, int, int]
hex: str
RED = auto(), (1, 0, 0), 'ff0000'
GREEN = auto(), (0, 1, 0), '00ff00'
BLUE = auto(), (0, 0, 1), '0000ff'