bckohan/enum-properties

Add static type hints.

bckohan opened this issue · 4 comments

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.

jace commented

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.

jace commented

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.

jace commented

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.