enum-properties icon indicating copy to clipboard operation
enum-properties copied to clipboard

Support type hinting for properties and specialize()

Open bckohan opened this issue 2 years ago • 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.

bckohan avatar Apr 09 '23 03:04 bckohan

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.

jace avatar Nov 23 '23 09:11 jace

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.

bckohan avatar Nov 26 '23 17:11 bckohan

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 avatar Nov 27 '23 10:11 jace

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.

jace avatar Nov 27 '23 10:11 jace

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'

bckohan avatar Aug 26 '24 06:08 bckohan