Skip to content

Commit 2b039e0

Browse files
committed
fix #86
1 parent 26159bd commit 2b039e0

File tree

10 files changed

+467
-138
lines changed

10 files changed

+467
-138
lines changed

doc/source/changelog.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
Change Log
33
==========
44

5+
v2.3.0 (2025-03-29)
6+
===================
7+
8+
* Implemented `Need a way to distinguish symmetric properties from first class aliases. <https://github.com/bckohan/enum-properties/issues/86>`_
9+
510
v2.2.5 (2025-03-22)
611
===================
712

doc/source/howto.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,18 @@ enumeration for the following reasons:**
282282
.. literalinclude:: ../../tests/examples/howto_dataclass_integration.py
283283

284284

285+
.. _howto_members_and_aliases:
286+
Get members and aliases
287+
-----------------------
288+
289+
Symmetric properties are added to the :attr:`~enum.EnumType.__members__` attribute,
290+
and alias members do not appear in :attr:`~enum.Enum._member_names_`. To get a list
291+
of first class members and aliases use
292+
:attr:`~enum_properties.EnumProperties.__first_class_members__`. This class member may
293+
also be overridden if you wish to customize this behavior for users.
294+
295+
.. literalinclude:: ../../tests/examples/howto_members_and_aliases.py
296+
285297
.. _howto_hash_equivalency:
286298

287299
Define hash equivalent enums

doc/source/reference.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ Module
1414
:undoc-members:
1515
:show-inheritance:
1616
:private-members:
17+
:special-members: __first_class_members__

justfile

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,28 @@ check-docs-links: _link_check
130130
check-docs:
131131
@just run doc8 --ignore-path ./doc/build --max-line-length 100 -q ./doc
132132

133+
# fetch the intersphinx references for the given package
134+
[script]
135+
fetch-refs LIB: install-docs
136+
import os
137+
from pathlib import Path
138+
import logging as _logging
139+
import sys
140+
import runpy
141+
from sphinx.ext.intersphinx import inspect_main
142+
_logging.basicConfig()
143+
144+
libs = runpy.run_path(Path(os.getcwd()) / "doc/source/conf.py").get("intersphinx_mapping")
145+
url = libs.get("{{ LIB }}", None)
146+
if not url:
147+
sys.exit(f"Unrecognized {{ LIB }}, must be one of: {', '.join(libs.keys())}")
148+
if url[1] is None:
149+
url = f"{url[0].rstrip('/')}/objects.inv"
150+
else:
151+
url = url[1]
152+
153+
raise SystemExit(inspect_main([url]))
154+
133155
# lint the code
134156
check-lint:
135157
@just run ruff check --select I

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "enum-properties"
7-
version = "2.2.5"
7+
version = "2.3.0"
88
description = "Add properties and method specializations to Python enumeration values with a simple declarative syntax."
99
requires-python = ">=3.8,<4.0"
1010
authors = [

src/enum_properties/__init__.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from dataclasses import dataclass
2020
from functools import cached_property
2121

22-
VERSION = (2, 2, 5)
22+
VERSION = (2, 3, 0)
2323

2424
__title__ = "Enum Properties"
2525
__version__ = ".".join(str(i) for i in VERSION)
@@ -230,6 +230,38 @@ class SymmetricMixin(with_typehint("EnumProperties")): # type: ignore
230230
property will be a case sensitive symmetric property.
231231
"""
232232

233+
_ep_symmetric_map_: t.Dict[t.Any, enum.Enum]
234+
"""
235+
The case sensitive mapping of symmetric values to enumeration values.
236+
"""
237+
238+
_ep_isymmetric_map_: t.Dict[str, enum.Enum]
239+
"""
240+
The case insensitive mapping of symmetric values to enumeration values.
241+
"""
242+
243+
_ep_coerce_types_: t.List[t.Type[t.Any]]
244+
"""
245+
On instantiation, if _missing_ is invoked a coercion attempt will be made to each
246+
of these types before failure.
247+
"""
248+
249+
_num_sym_props_: int
250+
"""
251+
The number of symmetric properties on this enumeration.
252+
"""
253+
254+
_properties_: t.List[_Prop]
255+
"""
256+
List of properties defined on the enumeration class.
257+
"""
258+
259+
__first_class_members__: t.List[str]
260+
"""
261+
The list of first class members - this includes all members and aliases. May be
262+
overridden.
263+
"""
264+
233265
def __eq__(self, value: t.Any) -> bool:
234266
"""Symmetric equality - try to coerce value before failure"""
235267
if isinstance(value, self.__class__):
@@ -346,6 +378,7 @@ class MyEnum(SymmetricMixin, enum.Enum, metaclass=EnumPropertiesMeta):
346378
# members reserved for use by EnumProperties
347379
RESERVED = [
348380
"_properties_",
381+
"_num_sym_props_",
349382
"_ep_coerce_types_",
350383
"_ep_symmetric_map_",
351384
"_ep_isymmetric_map_",
@@ -354,7 +387,9 @@ class MyEnum(SymmetricMixin, enum.Enum, metaclass=EnumPropertiesMeta):
354387
_ep_symmetric_map_: t.Dict[t.Any, enum.Enum]
355388
_ep_isymmetric_map_: t.Dict[str, enum.Enum]
356389
_ep_coerce_types_: t.List[t.Type[t.Any]]
390+
_num_sym_props_: int
357391
_properties_: t.List[_Prop]
392+
__first_class_members__: t.List[str]
358393

359394
@classmethod
360395
def __prepare__(mcs, cls, bases, **kwargs):
@@ -396,6 +431,7 @@ class _PropertyEnumDict(class_dict.__class__): # type: ignore[name-defined]
396431
_ids_: t.Dict[int, str] = {}
397432
_member_names: t.Union[t.List[str], t.Dict[str, t.Any]]
398433
_create_properties_: bool = False
434+
__first_class_members__: t.List[str] = []
399435

400436
class AnnotationPropertyRecorder(dict):
401437
class_dict: "_PropertyEnumDict"
@@ -479,6 +515,7 @@ def __setitem__(self, key, value):
479515
# todo remove below when minimum python >= 3.13
480516
not isinstance(value, type)
481517
):
518+
self.__first_class_members__.append(key)
482519
try:
483520
num_vals = len(value) - len(self._ep_properties_)
484521
if num_vals < 1 or len(self._ep_properties_) != len(
@@ -553,10 +590,16 @@ def __new__(mcs, classname, bases, classdict, **kwargs):
553590
**kwargs,
554591
)
555592
cls._ep_coerce_types_ = []
593+
cls._num_sym_props_ = 0
556594
cls._ep_symmetric_map_ = cls._member_map_
557595
cls._ep_isymmetric_map_ = {}
558596
cls._properties_ = list(classdict._ep_properties_.keys())
559597

598+
# we allow users to override this
599+
cls.__first_class_members__ = classdict.get(
600+
"__first_class_members__", classdict.__first_class_members__
601+
)
602+
560603
if classdict._specialized_:
561604
for val in cls: # type: ignore[var-annotated]
562605
val = t.cast(enum.Enum, val)
@@ -598,11 +641,13 @@ def add_coerce_type(typ: t.Type[t.Any]):
598641
setattr(member, prop, values[idx])
599642

600643
# we reverse to maintain precedence order for symmetric lookups
644+
cls._num_sym_props_ = 0
601645
member_values = t.cast(
602646
t.List[enum.Enum],
603647
list(cls._value2member_map_.values() or cls.__members__.values()),
604648
)
605649
for prop in reversed([prop for prop in cls._properties_ if prop.symmetric]):
650+
cls._num_sym_props_ += 1
606651
prop = t.cast(_SProp, prop)
607652
for idx, val2 in enumerate(reversed(classdict._ep_properties_[prop])):
608653
enum_cls = member_values[len(member_values) - 1 - idx]
@@ -615,6 +660,7 @@ def add_coerce_type(typ: t.Type[t.Any]):
615660
add_coerce_type(type(val2))
616661

617662
# add builtin symmetries
663+
cls._num_sym_props_ += len(getattr(cls, "_symmetric_builtins_", []))
618664
for sym_builtin in reversed(getattr(cls, "_symmetric_builtins_", [])):
619665
# allow simple strings for the default case
620666
if isinstance(sym_builtin, str):

tests/annotations/test_aliases.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import sys
2+
from enum import auto
3+
from unittest import TestCase
4+
from typing_extensions import Annotated
5+
6+
from enum_properties import (
7+
EnumProperties,
8+
FlagProperties,
9+
IntEnumProperties,
10+
IntFlagProperties,
11+
Symmetric,
12+
specialize,
13+
symmetric,
14+
)
15+
16+
17+
class TestAliases(TestCase):
18+
def test_enum_alias(self):
19+
class EnumWithAliases(EnumProperties):
20+
label: Annotated[str, Symmetric(case_fold=True)]
21+
22+
A = 1, "a"
23+
B = 2, "b"
24+
C = 3, "c"
25+
X = C, "x"
26+
Y = B, "y"
27+
Z = A, "z"
28+
29+
self.assertEqual(
30+
EnumWithAliases.__first_class_members__, ["A", "B", "C", "X", "Y", "Z"]
31+
)
32+
33+
class EnumWithAliasesComplex(EnumProperties):
34+
label: Annotated[str, Symmetric(case_fold=True)]
35+
36+
A = 1, "a"
37+
B = 2, "b"
38+
C = 3, "c"
39+
X = C, "x"
40+
Y = B, "y"
41+
Z = A, "z"
42+
43+
@symmetric(case_fold=True)
44+
def x3(self) -> str:
45+
return self.label * 3
46+
47+
@property
48+
def prop(self) -> str:
49+
return self.label * 5
50+
51+
def method(self) -> str:
52+
return self.label * 7
53+
54+
@specialize(A)
55+
def method(self) -> str:
56+
return self.label * 8
57+
58+
class Nested:
59+
pass
60+
61+
self.assertEqual(
62+
EnumWithAliasesComplex.__first_class_members__,
63+
["A", "B", "C", "X", "Y", "Z"],
64+
)
65+
66+
class EnumWithAliasesOverride1(EnumProperties):
67+
label: Annotated[str, Symmetric(case_fold=True)]
68+
69+
A = 1, "a"
70+
B = 2, "b"
71+
C = 3, "c"
72+
X = C, "x"
73+
Y = B, "y"
74+
Z = A, "z"
75+
76+
__first_class_members__ = ["A", "B", "C", "X", "Y"]
77+
78+
self.assertEqual(
79+
EnumWithAliasesOverride1.__first_class_members__, ["A", "B", "C", "X", "Y"]
80+
)
81+
82+
class EnumWithAliasesOverride2(EnumProperties):
83+
label: Annotated[str, Symmetric(case_fold=True)]
84+
85+
__first_class_members__ = ["A", "B", "C", "X"]
86+
87+
A = 1, "a"
88+
B = 2, "b"
89+
C = 3, "c"
90+
X = C, "x"
91+
Y = B, "y"
92+
Z = A, "z"
93+
94+
self.assertEqual(
95+
EnumWithAliasesOverride2.__first_class_members__, ["A", "B", "C", "X"]
96+
)
97+
98+
def test_flag_alias(self):
99+
class FlagWithAliases(FlagProperties):
100+
label: Annotated[str, Symmetric(case_fold=True)]
101+
102+
A = 1 << 0, "a"
103+
B = 1 << 1, "b"
104+
C = 1 << 2, "c"
105+
X = C, "x"
106+
Y = B, "y"
107+
Z = A, "z"
108+
109+
AB = A | B, "ab"
110+
AC = A | C, "ac"
111+
BC = B | C, "bc"
112+
ABC = A | B | C, "abc"
113+
114+
self.assertEqual(
115+
FlagWithAliases.__first_class_members__,
116+
["A", "B", "C", "X", "Y", "Z", "AB", "AC", "BC", "ABC"],
117+
)
118+
119+
class FlagWithAliasesComplex(FlagProperties):
120+
label: Annotated[str, Symmetric(case_fold=True)]
121+
122+
A = 1 << 0, "a"
123+
B = 1 << 1, "b"
124+
C = 1 << 2, "c"
125+
X = C, "x"
126+
Y = B, "y"
127+
Z = A, "z"
128+
129+
AB = A | B, "ab"
130+
AC = A | C, "ac"
131+
BC = B | C, "bc"
132+
ABC = A | B | C, "abc"
133+
134+
@symmetric(case_fold=True)
135+
def x3(self) -> str:
136+
return self.label * 3
137+
138+
@property
139+
def prop(self) -> str:
140+
return self.label * 5
141+
142+
def method(self) -> str:
143+
return self.label * 7
144+
145+
@specialize(A)
146+
def method(self) -> str:
147+
return self.label * 8
148+
149+
class Nested:
150+
pass
151+
152+
self.assertEqual(
153+
FlagWithAliasesComplex.__first_class_members__,
154+
["A", "B", "C", "X", "Y", "Z", "AB", "AC", "BC", "ABC"],
155+
)
156+
157+
class FlagWithAliasesOverride1(FlagProperties):
158+
label: Annotated[str, Symmetric(case_fold=True)]
159+
160+
__first_class_members__ = ["A", "B", "C", "X", "Y", "ABC"]
161+
162+
A = 1 << 0, "a"
163+
B = 1 << 1, "b"
164+
C = 1 << 2, "c"
165+
X = C, "x"
166+
Y = B, "y"
167+
Z = A, "z"
168+
169+
AB = A | B, "ab"
170+
AC = A | C, "ac"
171+
BC = B | C, "bc"
172+
ABC = A | B | C, "abc"
173+
174+
self.assertEqual(
175+
FlagWithAliasesOverride1.__first_class_members__,
176+
["A", "B", "C", "X", "Y", "ABC"],
177+
)
178+
179+
class FlagWithAliasesOverride2(FlagProperties):
180+
label: Annotated[str, Symmetric(case_fold=True)]
181+
182+
A = 1 << 0, "a"
183+
B = 1 << 1, "b"
184+
C = 1 << 2, "c"
185+
X = C, "x"
186+
Y = B, "y"
187+
Z = A, "z"
188+
189+
AB = A | B, "ab"
190+
AC = A | C, "ac"
191+
BC = B | C, "bc"
192+
ABC = A | B | C, "abc"
193+
194+
__first_class_members__ = ["A", "B", "C", "X", "Y", "ABC"]
195+
196+
self.assertEqual(
197+
FlagWithAliasesOverride2.__first_class_members__,
198+
["A", "B", "C", "X", "Y", "ABC"],
199+
)

0 commit comments

Comments
 (0)