Skip to content

Commit e78069f

Browse files
committed
implement lazy loading strategy #97
1 parent ae04f8e commit e78069f

File tree

1 file changed

+80
-34
lines changed

1 file changed

+80
-34
lines changed

src/enum_properties/__init__.py

Lines changed: 80 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@
5757
S = t.TypeVar("S")
5858

5959

60+
_lazy_annotations_: bool = sys.version_info[:2] >= (3, 14)
61+
"""
62+
Annotations are loaded after enum member definitions in python 3.14+
63+
"""
64+
65+
6066
def with_typehint(baseclass: t.Type[T]) -> t.Type[T]:
6167
"""
6268
This is icky but it works - revisit in future.
@@ -435,6 +441,12 @@ class _PropertyEnumDict(class_dict.__class__): # type: ignore[name-defined]
435441
"""
436442

437443
_ep_properties_ = properties
444+
445+
# lazy properties in annotation declaration order
446+
_lazy_properties_: t.List[_Prop] = []
447+
448+
# member -> value tuple
449+
_lazy_property_values_: t.Dict[str, t.Any] = {}
438450
_specialized_: t.Dict[str, t.Dict[str, _Specialized]] = {}
439451
_ids_: t.Dict[int, str] = {}
440452
_member_names: t.Union[t.List[str], t.Dict[str, t.Any]]
@@ -479,7 +491,10 @@ def __setitem__(self, key, value):
479491
else:
480492
self.class_dict["_symmetric_builtins_"] = [prop]
481493
else:
482-
self.class_dict._ep_properties_[prop()] = []
494+
if _lazy_annotations_:
495+
self.class_dict._lazy_properties_.append(prop())
496+
else:
497+
self.class_dict._ep_properties_[prop()] = []
483498
super().__setitem__(key, value)
484499

485500
def __init__(self):
@@ -490,6 +505,32 @@ def __init__(self):
490505
self[item] = value
491506
self._create_properties_ = not self._ep_properties_
492507

508+
def add_member_and_properties(self, key: str, value: t.Any) -> t.Any:
509+
try:
510+
num_vals = len(value) - len(self._ep_properties_)
511+
if num_vals < 1 or len(self._ep_properties_) != len(
512+
value[num_vals:]
513+
):
514+
raise ValueError(
515+
f"{key} must have "
516+
f"{len(self._ep_properties_)} property "
517+
f"values."
518+
)
519+
idx = num_vals
520+
for values in self._ep_properties_.values():
521+
values.append(value[idx])
522+
idx += 1
523+
524+
if num_vals == 1:
525+
return value[0]
526+
else:
527+
return value[0:num_vals]
528+
529+
except TypeError as type_err:
530+
raise ValueError(
531+
f"{key} must have {len(self._ep_properties_)} property values."
532+
) from type_err
533+
493534
def __setitem__(self, key, value):
494535
if isinstance(value, _Specialized):
495536
for en_val in value.ids:
@@ -505,15 +546,18 @@ def __setitem__(self, key, value):
505546
dict.__setitem__(self, key, value)
506547
elif key in EnumPropertiesMeta.RESERVED:
507548
raise ValueError(f"{key} is reserved.")
508-
elif self._ep_properties_:
549+
elif self._ep_properties_ or (
550+
_lazy_annotations_ and isinstance(value, tuple)
551+
):
509552
member_names = getattr(class_dict, "_member_names")
510553
# are we an enum value? - just kick this up to parent class
511554
# logic, this code runs once on load - its fine that it's
512555
# doing a little redundant work and doing it this way
513556
# ensures robust fidelity to Enum behavior.
514557
before = len(member_names)
515558
class_dict[key] = value
516-
self._create_properties_ = False # we're done with annotations
559+
# are we done with annotations?
560+
self._create_properties_ = _lazy_annotations_
517561
remove = False
518562
if (
519563
len(member_names) > before
@@ -524,28 +568,17 @@ def __setitem__(self, key, value):
524568
not isinstance(value, type)
525569
):
526570
self.__first_class_members__.append(key)
527-
try:
528-
num_vals = len(value) - len(self._ep_properties_)
529-
if num_vals < 1 or len(self._ep_properties_) != len(
530-
value[num_vals:]
531-
):
532-
raise ValueError(
533-
f"{key} must have "
534-
f"{len(self._ep_properties_)} property "
535-
f"values."
536-
)
537-
idx = num_vals
538-
for values in self._ep_properties_.values():
539-
values.append(value[idx])
540-
idx += 1
541-
571+
if _lazy_annotations_ and not self._ep_properties_:
572+
self._lazy_property_values_[key] = value
573+
# we set the value of the member to the first value in the
574+
# tuple - this is important to do here because it allows
575+
# members to be used as their value element later on in the
576+
# declaration - think named composite flag values - we may
577+
# have to change this later because we do not know what our
578+
# properties are yet
542579
value = value[0]
543-
544-
except TypeError as type_err:
545-
raise ValueError(
546-
f"{key} must have {len(self._ep_properties_)} "
547-
f"property values."
548-
) from type_err
580+
else:
581+
value = self.add_member_and_properties(key, value)
549582

550583
elif key in member_names:
551584
remove = True # pragma: no cover
@@ -569,10 +602,8 @@ def __setitem__(self, key, value):
569602
before = len(self._member_names)
570603
super().__setitem__(key, value)
571604
if len(self._member_names) > before:
572-
self._create_properties_ = False
605+
self._create_properties_ = _lazy_annotations_
573606

574-
if sys.version_info[:2] >= (3, 14):
575-
return {"_ep_enum_dict_": _PropertyEnumDict()}
576607
return _PropertyEnumDict() # type: ignore[no-untyped-call]
577608

578609
def __new__(mcs, classname, bases, classdict, **kwargs):
@@ -590,24 +621,39 @@ def __new__(mcs, classname, bases, classdict, **kwargs):
590621
incorrectly, or if non-hashable values are provided for a
591622
symmetric property.
592623
"""
593-
if sys.version_info[:2] >= (3, 14):
624+
if _lazy_annotations_ and not classdict._ep_properties_:
625+
"""
626+
In python 3.14+ annotations are loaded after enum members are
627+
defined, so we have to reconcile our properties here.
628+
"""
594629
from annotationlib import (
595630
Format,
596631
call_annotate_function,
597632
get_annotate_from_class_namespace,
598633
)
599634

600-
lazy_dict = classdict.pop("_ep_enum_dict_")
601635
annotate = get_annotate_from_class_namespace(classdict)
602636
if annotate:
603-
lazy_dict["__annotations__"] = {}
637+
classdict["__annotations__"] = {}
604638
for attr, typ in call_annotate_function(
605639
annotate, format=Format.VALUE
606640
).items():
607-
lazy_dict["__annotations__"][attr] = typ # or other formats
608-
for key, value in classdict.items():
609-
lazy_dict[key] = value
610-
classdict = lazy_dict
641+
classdict["__annotations__"][attr] = typ # or other formats
642+
643+
if classdict._lazy_property_values_:
644+
classdict._ep_properties_ = {
645+
prop: [] for prop in classdict._lazy_properties_
646+
}
647+
for en, value in classdict._lazy_property_values_.items():
648+
real_value = classdict.add_member_and_properties(en, value)
649+
if real_value != classdict[en] and isinstance(real_value, tuple):
650+
# if we're here - our assumption that the value was the first
651+
# element of the member tuple was wrong - reset it to the real
652+
# value bypassing EnumDict checks
653+
dict.__setitem__(classdict, en, real_value)
654+
655+
classdict._lazy_properties_.clear()
656+
classdict._lazy_property_values_.clear()
611657

612658
cls = super().__new__(
613659
mcs,

0 commit comments

Comments
 (0)