enumerific 1.0.0__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- enumerific/__init__.py +21 -85
- enumerific/exceptions.py +18 -0
- enumerific/extensible.py +1912 -0
- enumerific/logging.py +5 -0
- enumerific/standard.py +85 -0
- enumerific/version.txt +1 -1
- {enumerific-1.0.0.dist-info → enumerific-1.0.1.dist-info}/METADATA +74 -44
- enumerific-1.0.1.dist-info/RECORD +12 -0
- {enumerific-1.0.0.dist-info → enumerific-1.0.1.dist-info}/WHEEL +1 -1
- enumerific-1.0.0.dist-info/RECORD +0 -8
- {enumerific-1.0.0.dist-info → enumerific-1.0.1.dist-info/licenses}/LICENSE.md +0 -0
- {enumerific-1.0.0.dist-info → enumerific-1.0.1.dist-info}/top_level.txt +0 -0
- {enumerific-1.0.0.dist-info → enumerific-1.0.1.dist-info}/zip-safe +0 -0
enumerific/extensible.py
ADDED
|
@@ -0,0 +1,1912 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from enumerific.logging import logger
|
|
6
|
+
|
|
7
|
+
from enumerific.exceptions import (
|
|
8
|
+
EnumerationError,
|
|
9
|
+
EnumerationOptionError,
|
|
10
|
+
EnumerationSubclassingError,
|
|
11
|
+
EnumerationNonUniqueError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from types import MappingProxyType
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
logger = logger.getChild(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class auto(int):
|
|
21
|
+
"""Generate an automatically inrementing integer each time the class is instantiated
|
|
22
|
+
based on the previously supplied configuration, which allows the start and steps to
|
|
23
|
+
be configured as well as if the integers should be generated as powers/flags."""
|
|
24
|
+
|
|
25
|
+
start: int = 0
|
|
26
|
+
steps: int = 1
|
|
27
|
+
power: int = 0
|
|
28
|
+
value: int = 0
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def configure(
|
|
32
|
+
cls,
|
|
33
|
+
start: int = None,
|
|
34
|
+
steps: int = None,
|
|
35
|
+
power: int | bool = None,
|
|
36
|
+
flags: bool = None,
|
|
37
|
+
):
|
|
38
|
+
"""Provide support for configuring the auto class with its start, steps, and
|
|
39
|
+
power options, which once set will be used by all subsequent calls to the class'
|
|
40
|
+
auto.__new__() method as called during each class instantiation."""
|
|
41
|
+
|
|
42
|
+
if start is None:
|
|
43
|
+
start = 1
|
|
44
|
+
elif isinstance(start, int) and start >= 0:
|
|
45
|
+
pass
|
|
46
|
+
else:
|
|
47
|
+
raise TypeError(
|
|
48
|
+
"The 'start' argument, if specified, must have a positive integer value!"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if steps is None:
|
|
52
|
+
steps = 1
|
|
53
|
+
elif isinstance(steps, int) and steps >= 0:
|
|
54
|
+
pass
|
|
55
|
+
else:
|
|
56
|
+
raise TypeError(
|
|
57
|
+
"The 'steps' argument, if specified, must have a positive integer value!"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if power is None:
|
|
61
|
+
power = 0
|
|
62
|
+
elif isinstance(power, bool):
|
|
63
|
+
power = 2 if power is True else 0
|
|
64
|
+
elif isinstance(power, int) and power >= 0:
|
|
65
|
+
pass
|
|
66
|
+
else:
|
|
67
|
+
raise TypeError(
|
|
68
|
+
"The 'power' argument, if specified, must have a positive integer or boolean value!"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if flags is None:
|
|
72
|
+
pass
|
|
73
|
+
elif isinstance(flags, bool):
|
|
74
|
+
if flags is True:
|
|
75
|
+
power = 2
|
|
76
|
+
elif flags is False:
|
|
77
|
+
power = 0
|
|
78
|
+
else:
|
|
79
|
+
raise TypeError(
|
|
80
|
+
"The 'flags' argument, if specified, must have a boolean value!"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if flags is True or power == 2:
|
|
84
|
+
if not (start > 0 and (start & (start - 1) == 0)):
|
|
85
|
+
raise ValueError(
|
|
86
|
+
"If 'flags' is 'True' or 'power' is '2', the 'start' argument must have a value that is a power of two!"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
cls.start = start
|
|
90
|
+
|
|
91
|
+
cls.steps = steps
|
|
92
|
+
|
|
93
|
+
cls.power = power
|
|
94
|
+
|
|
95
|
+
cls.value = cls.start
|
|
96
|
+
|
|
97
|
+
def __new__(cls):
|
|
98
|
+
"""Create a new integer (int) instance upon each call, incrementing the value as
|
|
99
|
+
per the configuration defined before this method is called; the configuration
|
|
100
|
+
can be changed at any time and the next call to this method will generate the
|
|
101
|
+
next value based on the most recently specified configuration options."""
|
|
102
|
+
|
|
103
|
+
if cls.power > 0:
|
|
104
|
+
value = pow(cls.power, (cls.value - 1))
|
|
105
|
+
else:
|
|
106
|
+
value = cls.value
|
|
107
|
+
|
|
108
|
+
cls.value += cls.steps
|
|
109
|
+
|
|
110
|
+
return super().__new__(cls, value)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class EnumerationConfiguration(object):
|
|
114
|
+
"""The EnumerationConfiguration class holds the Enumeration configuration options"""
|
|
115
|
+
|
|
116
|
+
_unique: bool = None
|
|
117
|
+
_aliased: bool = False
|
|
118
|
+
_overwritable: bool = None
|
|
119
|
+
_removable: bool = None
|
|
120
|
+
_subclassable: bool = None
|
|
121
|
+
_raises: bool = None
|
|
122
|
+
_flags: bool = None
|
|
123
|
+
_start: int = None
|
|
124
|
+
_typecast: bool = None
|
|
125
|
+
|
|
126
|
+
def __init__(
|
|
127
|
+
self,
|
|
128
|
+
unique: bool = None,
|
|
129
|
+
aliased: bool = None,
|
|
130
|
+
overwritable: bool = None,
|
|
131
|
+
removable: bool = None,
|
|
132
|
+
subclassable: bool = None,
|
|
133
|
+
raises: bool = None,
|
|
134
|
+
flags: bool = None,
|
|
135
|
+
start: int = None,
|
|
136
|
+
typecast: bool = None,
|
|
137
|
+
):
|
|
138
|
+
self.unique = unique
|
|
139
|
+
self.aliased = aliased
|
|
140
|
+
self.overwritable = overwritable
|
|
141
|
+
self.removable = removable
|
|
142
|
+
self.subclassable = subclassable
|
|
143
|
+
self.raises = raises
|
|
144
|
+
self.flags = flags
|
|
145
|
+
self.start = start
|
|
146
|
+
self.typecast = typecast
|
|
147
|
+
|
|
148
|
+
def __dir__(self) -> list[str]:
|
|
149
|
+
return [
|
|
150
|
+
"unique",
|
|
151
|
+
"aliased",
|
|
152
|
+
"overwritable",
|
|
153
|
+
"removable",
|
|
154
|
+
"subclassable",
|
|
155
|
+
"raises",
|
|
156
|
+
"flags",
|
|
157
|
+
"start",
|
|
158
|
+
"typecast",
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
def update(
|
|
162
|
+
self,
|
|
163
|
+
configuration: EnumerationConfiguration,
|
|
164
|
+
nullify: bool = False,
|
|
165
|
+
) -> EnumerationConfiguration:
|
|
166
|
+
"""Support updating of an existing EnumerationConfiguration class instance from
|
|
167
|
+
another EnumerationConfiguration class instance by copying all the options."""
|
|
168
|
+
|
|
169
|
+
if not isinstance(configuration, EnumerationConfiguration):
|
|
170
|
+
raise TypeError(
|
|
171
|
+
"The 'configuration' argument must have an EnumerationConfiguration class instance value!"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if not isinstance(nullify, bool):
|
|
175
|
+
raise TypeError(
|
|
176
|
+
"The 'nullify' argument, if specified, must have a boolean value!"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
for name, value in configuration.options.items():
|
|
180
|
+
if isinstance(value, bool) or nullify is True:
|
|
181
|
+
setattr(self, name, value)
|
|
182
|
+
|
|
183
|
+
return self
|
|
184
|
+
|
|
185
|
+
def copy(self) -> EnumerationConfiguration:
|
|
186
|
+
return EnumerationConfiguration(**self.options)
|
|
187
|
+
|
|
188
|
+
def defaults(self, **options: dict[str, bool]) -> EnumerationConfiguration:
|
|
189
|
+
for name, value in options.items():
|
|
190
|
+
if getattr(self, name, None) is None:
|
|
191
|
+
setattr(self, name, value)
|
|
192
|
+
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def unique(self) -> bool | None:
|
|
197
|
+
return self._unique
|
|
198
|
+
|
|
199
|
+
@unique.setter
|
|
200
|
+
def unique(self, unique: bool | None):
|
|
201
|
+
if unique is None:
|
|
202
|
+
pass
|
|
203
|
+
elif not isinstance(unique, bool):
|
|
204
|
+
raise TypeError(
|
|
205
|
+
"The 'unique' argument, if specified, must have a boolean value!"
|
|
206
|
+
)
|
|
207
|
+
self._unique = unique
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def aliased(self) -> bool | None:
|
|
211
|
+
return self._aliased
|
|
212
|
+
|
|
213
|
+
@aliased.setter
|
|
214
|
+
def aliased(self, aliased: bool | None):
|
|
215
|
+
if aliased is None:
|
|
216
|
+
pass
|
|
217
|
+
elif not isinstance(aliased, bool):
|
|
218
|
+
raise TypeError(
|
|
219
|
+
"The 'aliased' argument, if specified, must have a boolean value!"
|
|
220
|
+
)
|
|
221
|
+
self._aliased = aliased
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def overwritable(self) -> bool | None:
|
|
225
|
+
return self._overwritable
|
|
226
|
+
|
|
227
|
+
@overwritable.setter
|
|
228
|
+
def overwritable(self, overwritable: bool | None):
|
|
229
|
+
if overwritable is None:
|
|
230
|
+
pass
|
|
231
|
+
elif not isinstance(overwritable, bool):
|
|
232
|
+
raise TypeError(
|
|
233
|
+
"The 'overwritable' argument, if specified, must have a boolean value!"
|
|
234
|
+
)
|
|
235
|
+
self._overwritable = overwritable
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def removable(self) -> bool | None:
|
|
239
|
+
return self._removable
|
|
240
|
+
|
|
241
|
+
@removable.setter
|
|
242
|
+
def removable(self, removable: bool | None):
|
|
243
|
+
if removable is None:
|
|
244
|
+
pass
|
|
245
|
+
elif not isinstance(removable, bool):
|
|
246
|
+
raise TypeError(
|
|
247
|
+
"The 'removable' argument, if specified, must have a boolean value!"
|
|
248
|
+
)
|
|
249
|
+
self._removable = removable
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def subclassable(self) -> bool | None:
|
|
253
|
+
return self._subclassable
|
|
254
|
+
|
|
255
|
+
@subclassable.setter
|
|
256
|
+
def subclassable(self, subclassable: bool | None):
|
|
257
|
+
if subclassable is None:
|
|
258
|
+
pass
|
|
259
|
+
elif not isinstance(subclassable, bool):
|
|
260
|
+
raise TypeError(
|
|
261
|
+
"The 'subclassable' argument, if specified, must have a boolean value!"
|
|
262
|
+
)
|
|
263
|
+
self._subclassable = subclassable
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def raises(self) -> bool | None:
|
|
267
|
+
return self._raises
|
|
268
|
+
|
|
269
|
+
@raises.setter
|
|
270
|
+
def raises(self, raises: bool | None):
|
|
271
|
+
if raises is None:
|
|
272
|
+
pass
|
|
273
|
+
elif not isinstance(raises, bool):
|
|
274
|
+
raise TypeError(
|
|
275
|
+
"The 'raises' argument, if specified, must have a boolean value!"
|
|
276
|
+
)
|
|
277
|
+
self._raises = raises
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def flags(self) -> bool | None:
|
|
281
|
+
return self._flags
|
|
282
|
+
|
|
283
|
+
@flags.setter
|
|
284
|
+
def flags(self, flags: bool | None):
|
|
285
|
+
if flags is None:
|
|
286
|
+
pass
|
|
287
|
+
elif not isinstance(flags, bool):
|
|
288
|
+
raise TypeError(
|
|
289
|
+
"The 'flags' argument, if specified, must have a boolean value!"
|
|
290
|
+
)
|
|
291
|
+
self._flags = flags
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def start(self) -> bool | None:
|
|
295
|
+
return self._start
|
|
296
|
+
|
|
297
|
+
@start.setter
|
|
298
|
+
def start(self, start: int | None):
|
|
299
|
+
if start is None:
|
|
300
|
+
pass
|
|
301
|
+
elif not (isinstance(start, int) and start >= 0):
|
|
302
|
+
raise TypeError(
|
|
303
|
+
"The 'start' argument, if specified, must have a positive integer value!"
|
|
304
|
+
)
|
|
305
|
+
self._start = start
|
|
306
|
+
|
|
307
|
+
@property
|
|
308
|
+
def typecast(self) -> bool | None:
|
|
309
|
+
return self._typecast
|
|
310
|
+
|
|
311
|
+
@typecast.setter
|
|
312
|
+
def typecast(self, typecast: bool | None):
|
|
313
|
+
if typecast is None:
|
|
314
|
+
pass
|
|
315
|
+
elif not isinstance(typecast, bool):
|
|
316
|
+
raise TypeError(
|
|
317
|
+
"The 'typecast' argument, if specified, must have a boolean value!"
|
|
318
|
+
)
|
|
319
|
+
self._typecast = typecast
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def options(self) -> dict[str, bool]:
|
|
323
|
+
properties: dict[str, bool] = {}
|
|
324
|
+
|
|
325
|
+
for name in dir(self):
|
|
326
|
+
properties[name] = getattr(self, name)
|
|
327
|
+
|
|
328
|
+
return properties
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class EnumerationMetaClass(type):
|
|
332
|
+
"""EnumerationMetaClass is the metaclass for the Enumerific extensible enumerations
|
|
333
|
+
base class, Enumeration, which can be used to create enumerations and extensible
|
|
334
|
+
enumerations that can often be used in place of standard library enumerations where
|
|
335
|
+
the additional functionality and flexibility to subclass and register or unregister
|
|
336
|
+
options on existing enumerations are beneficial or required for a given use case."""
|
|
337
|
+
|
|
338
|
+
_special: list[str] = ["mro", "__options__"]
|
|
339
|
+
_instance: Enumeration = None
|
|
340
|
+
_configuration: EnumerationConfiguration = None
|
|
341
|
+
_enumerations: dict[str, Enumeration] = None
|
|
342
|
+
|
|
343
|
+
def __prepare__(
|
|
344
|
+
name: str,
|
|
345
|
+
bases: tuple[type],
|
|
346
|
+
unique: bool = None,
|
|
347
|
+
aliased: bool = None,
|
|
348
|
+
overwritable: bool = None,
|
|
349
|
+
subclassable: bool = None,
|
|
350
|
+
removable: bool = None,
|
|
351
|
+
raises: bool = None,
|
|
352
|
+
flags: bool = None,
|
|
353
|
+
start: int = None,
|
|
354
|
+
typecast: bool = None,
|
|
355
|
+
**kwargs,
|
|
356
|
+
) -> dict:
|
|
357
|
+
"""The __prepare__ method is called when the class signature has been parsed but
|
|
358
|
+
before the class body, allowing us to configure futher class state before the
|
|
359
|
+
class body is parsed. The return value must be a dictionary or dictionary-like
|
|
360
|
+
value that will hold the class' __dict__ values. We are also able to intercept
|
|
361
|
+
any other keyword arguments that are included in the class signature call."""
|
|
362
|
+
|
|
363
|
+
logger.debug(
|
|
364
|
+
"[EnumerationMetaClass] %s.__prepare__(name: %s, bases: %s, unique: %s, aliased: %s, overwritable: %s, subclassable: %s, removable: %s, raises: %s, flags: %s, start: %s, typecast: %s, kwargs: %s)",
|
|
365
|
+
name,
|
|
366
|
+
name,
|
|
367
|
+
bases,
|
|
368
|
+
unique,
|
|
369
|
+
aliased,
|
|
370
|
+
overwritable,
|
|
371
|
+
subclassable,
|
|
372
|
+
removable,
|
|
373
|
+
raises,
|
|
374
|
+
flags,
|
|
375
|
+
start,
|
|
376
|
+
typecast,
|
|
377
|
+
kwargs,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Check if the class has been marked with 'flags=True' or if the base class
|
|
381
|
+
# is EnumerationFlag, for the purpose of configuring the auto() class correctly
|
|
382
|
+
if flags is None:
|
|
383
|
+
flags = False
|
|
384
|
+
|
|
385
|
+
# Some calls to EnumerationMetaClass.__prepare__ occur before EnumerationFlag
|
|
386
|
+
# has been parsed and created, so we cannot hardcode a reference to it below
|
|
387
|
+
if isinstance(_EnumerationFlag := globals().get("EnumerationFlag"), type):
|
|
388
|
+
for base in bases:
|
|
389
|
+
if issubclass(base, _EnumerationFlag):
|
|
390
|
+
flags = True
|
|
391
|
+
break
|
|
392
|
+
elif isinstance(flags, bool):
|
|
393
|
+
pass
|
|
394
|
+
else:
|
|
395
|
+
raise TypeError(
|
|
396
|
+
"The 'flags' argument, if specified, must have a boolean value!"
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# If an existing enumeration class is being subclassed, determine the maximum
|
|
400
|
+
# value assigned to its options, if those options have integer values; this is
|
|
401
|
+
# useful for enumeration classes that inherit from or automatically typecast to
|
|
402
|
+
# EnumerationInteger or EnumerationFlag, combined with the use of auto() so that
|
|
403
|
+
# if there is the need to subclass one of these classes to extend the available
|
|
404
|
+
# options, that the next available option value assigned via auto() will use the
|
|
405
|
+
# expected value, rather than restarting at the default start value
|
|
406
|
+
if start is None:
|
|
407
|
+
for base in bases:
|
|
408
|
+
if issubclass(base, Enumeration):
|
|
409
|
+
_maximum_value: int = None
|
|
410
|
+
|
|
411
|
+
if _enumerations := base.enumerations:
|
|
412
|
+
for _enumeration in _enumerations.values():
|
|
413
|
+
if isinstance(_enumeration, Enumeration):
|
|
414
|
+
if isinstance(_enumeration.value, int):
|
|
415
|
+
if _maximum_value is None:
|
|
416
|
+
_maximum_value = _enumeration.value
|
|
417
|
+
elif _enumeration.value > _maximum_value:
|
|
418
|
+
_maximum_value = _enumeration.value
|
|
419
|
+
|
|
420
|
+
if isinstance(_maximum_value, int):
|
|
421
|
+
# Take the maximum value and increment by 1 for the next value
|
|
422
|
+
if flags is True:
|
|
423
|
+
start = _maximum_value
|
|
424
|
+
else:
|
|
425
|
+
start = _maximum_value + 1
|
|
426
|
+
elif isinstance(start, int) and start >= 0:
|
|
427
|
+
pass
|
|
428
|
+
else:
|
|
429
|
+
raise TypeError(
|
|
430
|
+
"The 'start' argument, if specified, must have a positive integer value!"
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Configure the auto() class for subsequent use, resetting the sequence, setting
|
|
434
|
+
# the new start value, and whether values should be flag values (powers of 2)
|
|
435
|
+
auto.configure(start=start, flags=flags)
|
|
436
|
+
|
|
437
|
+
return dict()
|
|
438
|
+
|
|
439
|
+
def __new__(
|
|
440
|
+
cls,
|
|
441
|
+
*args,
|
|
442
|
+
unique: bool = None, # True
|
|
443
|
+
aliased: bool = None, # False
|
|
444
|
+
overwritable: bool = None, # False
|
|
445
|
+
subclassable: bool = None, # True
|
|
446
|
+
removable: bool = None, # False
|
|
447
|
+
raises: bool = None, # False
|
|
448
|
+
flags: bool = None, # False
|
|
449
|
+
start: int = None, # None
|
|
450
|
+
typecast: bool = None, # True
|
|
451
|
+
**kwargs,
|
|
452
|
+
):
|
|
453
|
+
logger.debug(
|
|
454
|
+
"[EnumerationMetaClass] %s.__new__(args: %s, kwargs: %s)",
|
|
455
|
+
cls.__name__,
|
|
456
|
+
args,
|
|
457
|
+
kwargs,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
if unique is None:
|
|
461
|
+
pass
|
|
462
|
+
elif not isinstance(unique, bool):
|
|
463
|
+
raise TypeError(
|
|
464
|
+
"The 'unique' argument, if specified, must have a boolean value!"
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
if aliased is None:
|
|
468
|
+
pass
|
|
469
|
+
elif not isinstance(aliased, bool):
|
|
470
|
+
raise TypeError(
|
|
471
|
+
"The 'aliased' argument, if specified, must have a boolean value!"
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
if overwritable is None:
|
|
475
|
+
pass
|
|
476
|
+
elif not isinstance(overwritable, bool):
|
|
477
|
+
raise TypeError(
|
|
478
|
+
"The 'overwritable' argument, if specified, must have a boolean value!"
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
if subclassable is None:
|
|
482
|
+
pass
|
|
483
|
+
elif not isinstance(subclassable, bool):
|
|
484
|
+
raise TypeError(
|
|
485
|
+
"The 'subclassable' argument, if specified, must have a boolean value!"
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
if removable is None:
|
|
489
|
+
pass
|
|
490
|
+
elif not isinstance(removable, bool):
|
|
491
|
+
raise TypeError(
|
|
492
|
+
"The 'removable' argument, if specified, must have a boolean value!"
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
if raises is None:
|
|
496
|
+
pass
|
|
497
|
+
elif not isinstance(raises, bool):
|
|
498
|
+
raise TypeError(
|
|
499
|
+
"The 'raises' argument, if specified, must have a boolean value!"
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
if flags is None:
|
|
503
|
+
pass
|
|
504
|
+
elif not isinstance(flags, bool):
|
|
505
|
+
raise TypeError(
|
|
506
|
+
"The 'flags' argument, if specified, must have a boolean value!"
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
if start is None:
|
|
510
|
+
pass
|
|
511
|
+
elif not (isinstance(start, int) and start >= 0):
|
|
512
|
+
raise TypeError(
|
|
513
|
+
"The 'start' argument, if specified, must have a positive integer value!"
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
if typecast is None:
|
|
517
|
+
pass
|
|
518
|
+
elif not isinstance(typecast, bool):
|
|
519
|
+
raise TypeError(
|
|
520
|
+
"The 'typecast' argument, if specified, must have a boolean value!"
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
configuration = EnumerationConfiguration(
|
|
524
|
+
unique=unique,
|
|
525
|
+
aliased=aliased,
|
|
526
|
+
overwritable=overwritable,
|
|
527
|
+
subclassable=subclassable,
|
|
528
|
+
removable=removable,
|
|
529
|
+
raises=raises,
|
|
530
|
+
flags=flags,
|
|
531
|
+
start=start,
|
|
532
|
+
typecast=typecast,
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
(name, bases, attributes) = args # Unpack the arguments passed to the metaclass
|
|
536
|
+
|
|
537
|
+
logger.debug(" >>> name => %s", name)
|
|
538
|
+
logger.debug(" >>> bases => %s", [base for base in bases])
|
|
539
|
+
logger.debug(" >>> attributes => %s", attributes)
|
|
540
|
+
logger.debug(" >>> configuration => %s", configuration)
|
|
541
|
+
|
|
542
|
+
if not bases:
|
|
543
|
+
return super().__new__(cls, *args, **kwargs)
|
|
544
|
+
|
|
545
|
+
enumerations: dict[str, object] = {} # Keep track of the enumeration options
|
|
546
|
+
|
|
547
|
+
names: list[object] = [] # Keep track of the option names to check uniqueness
|
|
548
|
+
values: list[object] = [] # Keep track of the option values to check uniqueness
|
|
549
|
+
|
|
550
|
+
# By default new Enumeration subclasses will be based on the Enumeration class
|
|
551
|
+
baseclass: Enumeration = None
|
|
552
|
+
|
|
553
|
+
_enumerations: dict[str, object] = None
|
|
554
|
+
|
|
555
|
+
# Attempt to inherit enumeration options if an existing populated Enumeration
|
|
556
|
+
# subclass is being subclassed; this is only performed for subclasses of
|
|
557
|
+
# subclasses of Enumeration, not for direct subclasses, such as the specialized
|
|
558
|
+
# Enumeration subclasses like EnumerationInteger which don't have any options
|
|
559
|
+
for base in bases:
|
|
560
|
+
logger.debug(" >>> analysing => %s", base)
|
|
561
|
+
logger.debug(
|
|
562
|
+
" >>> isinstance (meta) => %s", isinstance(base, EnumerationMetaClass)
|
|
563
|
+
)
|
|
564
|
+
logger.debug(" >>> issubclass (main) => %s", issubclass(base, Enumeration))
|
|
565
|
+
|
|
566
|
+
if isinstance(base, EnumerationMetaClass) or issubclass(base, Enumeration):
|
|
567
|
+
logger.debug(" >>> base (type) => %s (%s)", base, type(base))
|
|
568
|
+
|
|
569
|
+
if issubclass(base, Enumeration):
|
|
570
|
+
# Prevent an Enumeration class subclass from being created with two or more Enumeration base classes
|
|
571
|
+
if not baseclass is None:
|
|
572
|
+
raise TypeError(
|
|
573
|
+
"Subclassing an Enumeration from multiple Enumeration superclasses (bases) is not supported; enusure that only one of the base classes is an Enumeration class or one of its subclasses!"
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
baseclass = base
|
|
577
|
+
|
|
578
|
+
logger.debug(" >>> baseclass => %s", baseclass)
|
|
579
|
+
|
|
580
|
+
if isinstance(
|
|
581
|
+
base_configuration := base.configuration,
|
|
582
|
+
EnumerationConfiguration,
|
|
583
|
+
):
|
|
584
|
+
logger.debug(
|
|
585
|
+
" >>> unique => %s", base_configuration.unique
|
|
586
|
+
)
|
|
587
|
+
logger.debug(
|
|
588
|
+
" >>> aliased => %s", base_configuration.aliased
|
|
589
|
+
)
|
|
590
|
+
logger.debug(
|
|
591
|
+
" >>> overwritable => %s", base_configuration.overwritable
|
|
592
|
+
)
|
|
593
|
+
logger.debug(
|
|
594
|
+
" >>> subclassable => %s", base_configuration.subclassable
|
|
595
|
+
)
|
|
596
|
+
logger.debug(
|
|
597
|
+
" >>> removable => %s", base_configuration.removable
|
|
598
|
+
)
|
|
599
|
+
logger.debug(
|
|
600
|
+
" >>> raises => %s", base_configuration.raises
|
|
601
|
+
)
|
|
602
|
+
logger.debug(
|
|
603
|
+
" >>> flags => %s", base_configuration.flags
|
|
604
|
+
)
|
|
605
|
+
logger.debug(
|
|
606
|
+
" >>> start => %s", base_configuration.start
|
|
607
|
+
)
|
|
608
|
+
logger.debug(
|
|
609
|
+
" >>> typecast => %s", base_configuration.typecast
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
if base_configuration.subclassable is False:
|
|
613
|
+
raise EnumerationSubclassingError(
|
|
614
|
+
"The '%s' enumeration class cannot be subclassed when the keyword argument 'subclassable=False' was passed to the class constructor!"
|
|
615
|
+
% (base.__name__)
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
# Copy the base class constructor options and update them with our local configuration
|
|
619
|
+
configuration = base_configuration.copy().update(
|
|
620
|
+
configuration, nullify=False
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
logger.debug(
|
|
624
|
+
" >>> (updated) unique => %s", configuration.unique
|
|
625
|
+
)
|
|
626
|
+
logger.debug(
|
|
627
|
+
" >>> (updated) aliased => %s", configuration.aliased
|
|
628
|
+
)
|
|
629
|
+
logger.debug(
|
|
630
|
+
" >>> (updated) overwritable => %s",
|
|
631
|
+
configuration.overwritable,
|
|
632
|
+
)
|
|
633
|
+
logger.debug(
|
|
634
|
+
" >>> (updated) subclassable => %s",
|
|
635
|
+
configuration.subclassable,
|
|
636
|
+
)
|
|
637
|
+
logger.debug(
|
|
638
|
+
" >>> (updated) removable => %s", configuration.removable
|
|
639
|
+
)
|
|
640
|
+
logger.debug(
|
|
641
|
+
" >>> (updated) raises => %s", configuration.raises
|
|
642
|
+
)
|
|
643
|
+
logger.debug(
|
|
644
|
+
" >>> (updated) flags => %s", configuration.flags
|
|
645
|
+
)
|
|
646
|
+
logger.debug(
|
|
647
|
+
" >>> (updated) start => %s", configuration.start
|
|
648
|
+
)
|
|
649
|
+
logger.debug(
|
|
650
|
+
" >>> (updated) typecast => %s", configuration.typecast
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
# logger.debug(" >>> found base (%s) that is an instance of EnumerationMetaClass and a subclass of Enumeration" % (base))
|
|
654
|
+
|
|
655
|
+
if not (base is Enumeration or Enumeration in base.__bases__):
|
|
656
|
+
# enumerations = base._enumerations # reference to the _enumerations dictionary
|
|
657
|
+
_enumerations = base._enumerations
|
|
658
|
+
|
|
659
|
+
logger.debug(" >>> enumerations => %s" % (base._enumerations))
|
|
660
|
+
|
|
661
|
+
for attribute, enumeration in base._enumerations.items():
|
|
662
|
+
logger.debug(
|
|
663
|
+
" >>> found enumeration: %s => %s"
|
|
664
|
+
% (attribute, enumeration)
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
enumerations[attribute] = enumeration
|
|
668
|
+
|
|
669
|
+
names.append(enumeration.name)
|
|
670
|
+
|
|
671
|
+
values.append(enumeration.value)
|
|
672
|
+
|
|
673
|
+
# Set sensible defaults for any configuration options that have not yet been set
|
|
674
|
+
# these defaults are only applied for options that have not yet been set
|
|
675
|
+
configuration.defaults(
|
|
676
|
+
unique=True,
|
|
677
|
+
aliased=False,
|
|
678
|
+
overwritable=False,
|
|
679
|
+
subclassable=True,
|
|
680
|
+
removable=False,
|
|
681
|
+
raises=False,
|
|
682
|
+
flags=False,
|
|
683
|
+
start=1,
|
|
684
|
+
typecast=True,
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
logger.debug(" >>> (after defaults) unique => %s", configuration.unique)
|
|
688
|
+
logger.debug(" >>> (after defaults) aliased => %s", configuration.aliased)
|
|
689
|
+
logger.debug(
|
|
690
|
+
" >>> (after defaults) overwritable => %s", configuration.overwritable
|
|
691
|
+
)
|
|
692
|
+
logger.debug(
|
|
693
|
+
" >>> (after defaults) subclassable => %s", configuration.subclassable
|
|
694
|
+
)
|
|
695
|
+
logger.debug(
|
|
696
|
+
" >>> (after defaults) removable => %s", configuration.removable
|
|
697
|
+
)
|
|
698
|
+
logger.debug(" >>> (after defaults) raises => %s", configuration.raises)
|
|
699
|
+
logger.debug(" >>> (after defaults) flags => %s", configuration.flags)
|
|
700
|
+
logger.debug(" >>> (after defaults) start => %s", configuration.start)
|
|
701
|
+
logger.debug(" >>> (after defaults) typecast => %s", configuration.typecast)
|
|
702
|
+
|
|
703
|
+
# Iterate over the class attributes, looking for any enumeration options
|
|
704
|
+
for index, (attribute, value) in enumerate(
|
|
705
|
+
attributes.items(), start=configuration.start
|
|
706
|
+
):
|
|
707
|
+
logger.debug(
|
|
708
|
+
" >>> [%d] attribute => %s, value => %s (%s)"
|
|
709
|
+
% (index, attribute, value, type(value))
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
if attribute.startswith("_") or attribute in cls._special:
|
|
713
|
+
continue
|
|
714
|
+
elif attribute in names:
|
|
715
|
+
raise EnumerationNonUniqueError(
|
|
716
|
+
"The enumeration option, '%s', has a name that duplicates the name of an existing enumeration option, however all enumeration options must have unique names; please ensure all option names are unique!"
|
|
717
|
+
% (attribute)
|
|
718
|
+
)
|
|
719
|
+
elif callable(value) and not isinstance(value, type):
|
|
720
|
+
continue
|
|
721
|
+
elif isinstance(value, classmethod):
|
|
722
|
+
continue
|
|
723
|
+
elif isinstance(value, property):
|
|
724
|
+
continue
|
|
725
|
+
elif configuration.unique is True and value in values:
|
|
726
|
+
if configuration.aliased is True:
|
|
727
|
+
logger.debug(
|
|
728
|
+
" >>> attribute (alias) => %s, value => %s (%s)"
|
|
729
|
+
% (attribute, value, type(value))
|
|
730
|
+
)
|
|
731
|
+
else:
|
|
732
|
+
raise EnumerationNonUniqueError(
|
|
733
|
+
"The enumeration option, '%s', has a non-unique value, %r, however, unless either the keyword argument 'unique=False' or 'aliased=True' are passed during class construction, all enumeration options must have unique values!"
|
|
734
|
+
% (attribute, value)
|
|
735
|
+
)
|
|
736
|
+
else:
|
|
737
|
+
logger.debug(
|
|
738
|
+
" >>> attribute (option) => %s, value => %s (%s)"
|
|
739
|
+
% (attribute, value, type(value))
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
enumerations[attribute] = value
|
|
743
|
+
|
|
744
|
+
names.append(attribute)
|
|
745
|
+
|
|
746
|
+
if not value in values:
|
|
747
|
+
values.append(value)
|
|
748
|
+
|
|
749
|
+
# If an attribute was found to be an enumeration option, remove it from the list
|
|
750
|
+
# of class attributes so during class creation it does not become an attribute:
|
|
751
|
+
for attribute in enumerations.keys():
|
|
752
|
+
if attribute in attributes:
|
|
753
|
+
del attributes[attribute]
|
|
754
|
+
|
|
755
|
+
logger.debug(
|
|
756
|
+
"[EnumerationMetaClass] %s.__new__() >>> enumerations => %s",
|
|
757
|
+
name,
|
|
758
|
+
list(enumerations.keys()),
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
attributes["enumerations"] = enumerations
|
|
762
|
+
|
|
763
|
+
if isinstance(_enumerations, dict):
|
|
764
|
+
attributes["base_enumerations"] = _enumerations
|
|
765
|
+
|
|
766
|
+
# If the new enumeration class is not subclassing an existing enumeration class
|
|
767
|
+
if configuration.typecast is True and (
|
|
768
|
+
(baseclass is None) or (baseclass is Enumeration)
|
|
769
|
+
):
|
|
770
|
+
baseclass = Enumeration
|
|
771
|
+
|
|
772
|
+
# Determine the type(s) of the provided enumeration option values
|
|
773
|
+
types: set[type] = set([type(value) for value in enumerations.values()])
|
|
774
|
+
|
|
775
|
+
logger.debug(" >>> types => %s" % (types))
|
|
776
|
+
|
|
777
|
+
# If the enumeration option values have a single data type, use the relevant
|
|
778
|
+
# typed Enumeration superclass as the base for the new enumeration class
|
|
779
|
+
if len(types) == 1 and isinstance(typed := types.pop(), type):
|
|
780
|
+
if typed is str:
|
|
781
|
+
baseclass = EnumerationString
|
|
782
|
+
elif typed is int:
|
|
783
|
+
baseclass = EnumerationInteger
|
|
784
|
+
elif typed is auto:
|
|
785
|
+
baseclass = EnumerationInteger
|
|
786
|
+
elif typed is float:
|
|
787
|
+
baseclass = EnumerationFloat
|
|
788
|
+
elif typed is complex:
|
|
789
|
+
baseclass = EnumerationComplex
|
|
790
|
+
elif typed is bytes:
|
|
791
|
+
baseclass = EnumerationBytes
|
|
792
|
+
elif typed is tuple:
|
|
793
|
+
baseclass = EnumerationTuple
|
|
794
|
+
elif typed is set:
|
|
795
|
+
baseclass = EnumerationSet
|
|
796
|
+
elif typed is list:
|
|
797
|
+
baseclass = EnumerationList
|
|
798
|
+
elif typed is dict:
|
|
799
|
+
baseclass = EnumerationDictionary
|
|
800
|
+
elif baseclass is None:
|
|
801
|
+
baseclass = Enumeration
|
|
802
|
+
|
|
803
|
+
if flags is True:
|
|
804
|
+
baseclass = EnumerationFlag
|
|
805
|
+
|
|
806
|
+
logger.debug(" >>> baseclass => %s", baseclass)
|
|
807
|
+
logger.debug(" >>> new enum name => %s", name)
|
|
808
|
+
logger.debug(" >>> bases => %s", [base for base in bases])
|
|
809
|
+
logger.debug(" >>> attributes => %s", attributes)
|
|
810
|
+
logger.debug(" " + ">" * 100)
|
|
811
|
+
|
|
812
|
+
bases: tuple[type] = tuple(
|
|
813
|
+
[base for base in bases if not issubclass(base, Enumeration)] + [baseclass]
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
logger.debug(" >>> bases => %s", [base for base in bases])
|
|
817
|
+
|
|
818
|
+
# if "EnumerationInteger" in globals():
|
|
819
|
+
# if EnumerationInteger in bases and EnumerationFlag in bases:
|
|
820
|
+
# bases = tuple([base for base in bases if not EnumerationInteger])
|
|
821
|
+
|
|
822
|
+
args: tuple[object] = (name, bases, attributes)
|
|
823
|
+
|
|
824
|
+
# Create the new enumeration class instance
|
|
825
|
+
instance = super().__new__(cls, *args, **kwargs)
|
|
826
|
+
|
|
827
|
+
# logger.debug(
|
|
828
|
+
# " >>> metaclass => %s (base: %s, type: %s, bases: %s)\n"
|
|
829
|
+
# % (enumclass, instance, type(instance), instance.__bases__)
|
|
830
|
+
# )
|
|
831
|
+
# logger.debug(" >>> metaclass => %s (base: %s, type: %s, bases: %s)\n" % (enumclass, instance, type(instance), instance.__bases__))
|
|
832
|
+
|
|
833
|
+
logger.debug(" >>> baseclass => %s", baseclass)
|
|
834
|
+
logger.debug(" >>> instance => %s", instance)
|
|
835
|
+
|
|
836
|
+
logger.debug(" >>> unique => %s", configuration.unique)
|
|
837
|
+
logger.debug(" >>> aliased => %s", configuration.aliased)
|
|
838
|
+
logger.debug(" >>> overwritable => %s", configuration.overwritable)
|
|
839
|
+
logger.debug(" >>> subclassable => %s", configuration.subclassable)
|
|
840
|
+
logger.debug(" >>> removable => %s", configuration.removable)
|
|
841
|
+
logger.debug(" >>> raises => %s", configuration.raises)
|
|
842
|
+
logger.debug(" >>> flags => %s", configuration.flags)
|
|
843
|
+
logger.debug(" >>> typecast => %s", configuration.typecast)
|
|
844
|
+
|
|
845
|
+
# Store the enumeration class configuration options for future reference
|
|
846
|
+
instance._configuration = configuration
|
|
847
|
+
|
|
848
|
+
return instance
|
|
849
|
+
|
|
850
|
+
def __init__(self, *args, **kwargs):
|
|
851
|
+
super().__init__(*args, **kwargs)
|
|
852
|
+
|
|
853
|
+
(name, bases, attributes) = args
|
|
854
|
+
|
|
855
|
+
logger.debug(
|
|
856
|
+
"[EnumerationMetaClass] %s.__init__(args: %s, kwargs: %s) => name => %s => bases => %s",
|
|
857
|
+
self.__name__,
|
|
858
|
+
args,
|
|
859
|
+
kwargs,
|
|
860
|
+
name,
|
|
861
|
+
bases,
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
if isinstance(base_enumerations := attributes.get("base_enumerations"), dict):
|
|
865
|
+
self._enumerations: dict[str, Enumeration] = base_enumerations
|
|
866
|
+
else:
|
|
867
|
+
self._enumerations: dict[str, Enumeration] = {}
|
|
868
|
+
|
|
869
|
+
logger.debug(
|
|
870
|
+
" >>> id(%s._enumerations) => %s => %s",
|
|
871
|
+
name,
|
|
872
|
+
id(self._enumerations),
|
|
873
|
+
self._enumerations,
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
logger.debug("+" * 100)
|
|
877
|
+
|
|
878
|
+
if isinstance(enumerations := attributes.get("enumerations"), dict):
|
|
879
|
+
for attribute, value in enumerations.items():
|
|
880
|
+
if attribute in self._enumerations:
|
|
881
|
+
continue
|
|
882
|
+
|
|
883
|
+
if isinstance(value, Enumeration):
|
|
884
|
+
self._enumerations[attribute] = enum = value
|
|
885
|
+
else:
|
|
886
|
+
existing: Enumeration = None
|
|
887
|
+
|
|
888
|
+
if self.configuration.aliased is True:
|
|
889
|
+
logger.debug(
|
|
890
|
+
" >>> aliased is enabled, looking for alias for: %s<%s>",
|
|
891
|
+
attribute,
|
|
892
|
+
value,
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
for enumeration in self._enumerations.values():
|
|
896
|
+
logger.debug(" >>>> checking: %s", enumeration)
|
|
897
|
+
|
|
898
|
+
if enumeration.value == value:
|
|
899
|
+
existing = enumeration
|
|
900
|
+
logger.debug(
|
|
901
|
+
" >>>> matched: %s (%s)",
|
|
902
|
+
enumeration,
|
|
903
|
+
type(existing),
|
|
904
|
+
)
|
|
905
|
+
break
|
|
906
|
+
else:
|
|
907
|
+
logger.debug(" >>>> no match found")
|
|
908
|
+
|
|
909
|
+
if isinstance(existing, Enumeration):
|
|
910
|
+
self._enumerations[attribute] = enum = existing
|
|
911
|
+
else:
|
|
912
|
+
self._enumerations[attribute] = enum = self(
|
|
913
|
+
enumeration=self,
|
|
914
|
+
name=attribute,
|
|
915
|
+
value=value,
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
logger.debug(
|
|
919
|
+
" => %s => %s => %s (%s)" % (attribute, value, enum, type(enum))
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
logger.debug(
|
|
923
|
+
" => self._enumerations(%s) keys => %s",
|
|
924
|
+
id(self._enumeration),
|
|
925
|
+
[key for key in self._enumerations],
|
|
926
|
+
)
|
|
927
|
+
|
|
928
|
+
logger.debug("+" * 100)
|
|
929
|
+
|
|
930
|
+
def __getattr__(self, name) -> object:
|
|
931
|
+
# logger.debug("%s.__getattr__(name: %s)", self.__class__.__name__, name)
|
|
932
|
+
|
|
933
|
+
if name.startswith("_") or name in self._special:
|
|
934
|
+
return object.__getattribute__(self, name)
|
|
935
|
+
elif self._enumerations and name in self._enumerations:
|
|
936
|
+
return self._enumerations[name]
|
|
937
|
+
else:
|
|
938
|
+
raise EnumerationOptionError(
|
|
939
|
+
"The '%s' enumeration class, has no '%s' enumeration option!"
|
|
940
|
+
% (self.__name__, name)
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
def __dir__(self) -> list[str]:
|
|
944
|
+
members: list[str] = []
|
|
945
|
+
|
|
946
|
+
for name, enumeration in self._enumerations.items():
|
|
947
|
+
members.append(name)
|
|
948
|
+
|
|
949
|
+
for member in object.__dir__(self):
|
|
950
|
+
if member.startswith("_") or member in self._special:
|
|
951
|
+
members.append(member)
|
|
952
|
+
|
|
953
|
+
return members
|
|
954
|
+
|
|
955
|
+
def __contains__(self, other: Enumeration | object) -> bool:
|
|
956
|
+
contains: bool = False
|
|
957
|
+
|
|
958
|
+
for name, enumeration in self._enumerations.items():
|
|
959
|
+
if isinstance(other, Enumeration):
|
|
960
|
+
if enumeration is other:
|
|
961
|
+
contains = True
|
|
962
|
+
break
|
|
963
|
+
elif enumeration.value == other:
|
|
964
|
+
contains = True
|
|
965
|
+
break
|
|
966
|
+
elif isinstance(other, str):
|
|
967
|
+
if name == other:
|
|
968
|
+
contains = True
|
|
969
|
+
break
|
|
970
|
+
|
|
971
|
+
return contains
|
|
972
|
+
|
|
973
|
+
def __getitem__(self, name: str) -> Enumeration | None:
|
|
974
|
+
item: Enumeration = None
|
|
975
|
+
|
|
976
|
+
for attribute, enumeration in self._enumerations.items():
|
|
977
|
+
if enumeration.name == name:
|
|
978
|
+
item = enumeration
|
|
979
|
+
break
|
|
980
|
+
else:
|
|
981
|
+
raise EnumerationOptionError(
|
|
982
|
+
"The '%s' enumeration class, has no '%s' enumeration option!"
|
|
983
|
+
% (self.__name__, name)
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
return item
|
|
987
|
+
|
|
988
|
+
def __len__(self) -> int:
|
|
989
|
+
"""The '__len__' method returns the number of options held by the enumeration."""
|
|
990
|
+
|
|
991
|
+
return len(self._enumerations)
|
|
992
|
+
|
|
993
|
+
def __iter__(self) -> typing.Generator[Enumeration, None, None]:
|
|
994
|
+
"""The '__iter__' method yields each of the enumeration options one-by-one."""
|
|
995
|
+
|
|
996
|
+
for enumeration in self._enumerations.values():
|
|
997
|
+
yield enumeration
|
|
998
|
+
|
|
999
|
+
def __reversed__(self) -> typing.Generator[Enumeration, None, None]:
|
|
1000
|
+
"""The '__reversed__' method yields each of the enumeration options one-by-one
|
|
1001
|
+
in reverse order when compared to the '__iter__' method."""
|
|
1002
|
+
|
|
1003
|
+
for enumeration in reversed(self._enumerations.values()):
|
|
1004
|
+
yield enumeration
|
|
1005
|
+
|
|
1006
|
+
@property
|
|
1007
|
+
def __options__(self) -> MappingProxyType[str, Enumeration]:
|
|
1008
|
+
"""The '__options__' property returns a read-only mapping proxy of the options."""
|
|
1009
|
+
|
|
1010
|
+
return MappingProxyType(self._enumerations)
|
|
1011
|
+
|
|
1012
|
+
@property
|
|
1013
|
+
def __members__(self) -> MappingProxyType[str, Enumeration]:
|
|
1014
|
+
"""The '__members__' property returns a read-only mapping proxy of the options,
|
|
1015
|
+
and is provided for backwards compatibility with the built-in 'enum' package."""
|
|
1016
|
+
|
|
1017
|
+
return MappingProxyType(self._enumerations)
|
|
1018
|
+
|
|
1019
|
+
@property
|
|
1020
|
+
def __aliases__(self) -> MappingProxyType[str, Enumeration]:
|
|
1021
|
+
"""The '__aliases__' property returns a read-only mapping proxy of the option
|
|
1022
|
+
names that are aliases for other options."""
|
|
1023
|
+
|
|
1024
|
+
return MappingProxyType(
|
|
1025
|
+
{
|
|
1026
|
+
name: option
|
|
1027
|
+
for name, option in self.__options__.items()
|
|
1028
|
+
if option.name != name
|
|
1029
|
+
}
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
@property
|
|
1033
|
+
def configuration(self) -> EnumerationConfiguration:
|
|
1034
|
+
return self._configuration
|
|
1035
|
+
|
|
1036
|
+
@property
|
|
1037
|
+
def enumerations(self) -> MappingProxyType[str, Enumeration]:
|
|
1038
|
+
logger.debug(
|
|
1039
|
+
"[EnumerationMetaClass] %s.enumerations() => %s",
|
|
1040
|
+
self.__class__.__name__,
|
|
1041
|
+
self._enumerations,
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
return MappingProxyType(self._enumerations)
|
|
1045
|
+
|
|
1046
|
+
@property
|
|
1047
|
+
def typed(self) -> EnumerationType:
|
|
1048
|
+
types: set[EnumerationType | None] = set()
|
|
1049
|
+
|
|
1050
|
+
for name, enumeration in self._enumerations.items():
|
|
1051
|
+
if typed := EnumerationType.reconcile(type(enumeration.value)):
|
|
1052
|
+
types.add(typed)
|
|
1053
|
+
else:
|
|
1054
|
+
types.add(None)
|
|
1055
|
+
|
|
1056
|
+
logger.debug(
|
|
1057
|
+
"%s.typed() %s => %s -> %s [%d]",
|
|
1058
|
+
self.__class__.__name__,
|
|
1059
|
+
name,
|
|
1060
|
+
enumeration,
|
|
1061
|
+
typed,
|
|
1062
|
+
len(types),
|
|
1063
|
+
)
|
|
1064
|
+
|
|
1065
|
+
return types.pop() if len(types) == 1 else EnumerationType.MIXED
|
|
1066
|
+
|
|
1067
|
+
def names(self) -> list[str]:
|
|
1068
|
+
"""The 'names' method returns a list of the enumeration option names."""
|
|
1069
|
+
|
|
1070
|
+
logger.debug("%s(%s).names()", self.__class__.__name__, type(self))
|
|
1071
|
+
|
|
1072
|
+
return [name for name in self._enumerations]
|
|
1073
|
+
|
|
1074
|
+
def keys(self) -> list[str]:
|
|
1075
|
+
"""The 'keys' method is an alias of 'names' method; the both return the same."""
|
|
1076
|
+
|
|
1077
|
+
logger.debug("%s(%s).keys()", self.__class__.__name__, type(self))
|
|
1078
|
+
|
|
1079
|
+
return self.names()
|
|
1080
|
+
|
|
1081
|
+
def values(self) -> list[Enumeration]:
|
|
1082
|
+
"""The 'values' method returns a list of enumeration option values."""
|
|
1083
|
+
|
|
1084
|
+
logger.debug("%s(%s).values()", self.__class__.__name__, type(self))
|
|
1085
|
+
|
|
1086
|
+
return [enumeration.value for enumeration in self._enumerations.values()]
|
|
1087
|
+
|
|
1088
|
+
def items(self) -> list[tuple[str, Enumeration]]:
|
|
1089
|
+
"""The 'items' method returns a list of tuples of enumeration option names and values."""
|
|
1090
|
+
|
|
1091
|
+
logger.debug("%s(%s).items()" % (self.__class__.__name__, type(self)))
|
|
1092
|
+
|
|
1093
|
+
return [
|
|
1094
|
+
(name, enumeration.value)
|
|
1095
|
+
for name, enumeration in self._enumerations.items()
|
|
1096
|
+
]
|
|
1097
|
+
|
|
1098
|
+
@property
|
|
1099
|
+
def name(self) -> str:
|
|
1100
|
+
"""The 'name' property returns the class name of the enumeration class that was
|
|
1101
|
+
created by this metaclass."""
|
|
1102
|
+
|
|
1103
|
+
return self._instance.__name__
|
|
1104
|
+
|
|
1105
|
+
def register(self, name: str, value: object) -> Enumeration:
|
|
1106
|
+
"""The 'register' method supports registering additional enumeration options for
|
|
1107
|
+
an existing enumeration class. The method accepts the name of the enumeration
|
|
1108
|
+
option and its corresponding value; these are then mapped into a new enumeration
|
|
1109
|
+
class instance and added to the list of available enumerations.
|
|
1110
|
+
|
|
1111
|
+
If the specified name is the same as an enumeration option that has already been
|
|
1112
|
+
registered, either when the enumeration class was created or later through other
|
|
1113
|
+
calls to the 'register' method then an exception will be raised unless the class
|
|
1114
|
+
was constructed using the 'overwritable=True' argument which allows for existing
|
|
1115
|
+
enumeration options to be replaced by a new option stored with the same name. It
|
|
1116
|
+
should also be noted that when an enumeration option is replaced that it will
|
|
1117
|
+
have a new identity, as the class holding the option is replaced, so comparisons
|
|
1118
|
+
using 'is' will not compare between the old and the new, but access will remain
|
|
1119
|
+
the same using the <class-name>.<enumeration-option-name> access pattern and so
|
|
1120
|
+
comparisons made after the replacement when both instances are the same will be
|
|
1121
|
+
treat as equal when using the 'is' operator.
|
|
1122
|
+
|
|
1123
|
+
One should be cautious using the 'overwritable' argument as depending on where
|
|
1124
|
+
and how the replacement of an existing enumeration option with a new replacement
|
|
1125
|
+
is used, it could cause unexpected results elsewhere in the program. As such the
|
|
1126
|
+
overwriting of existing options is prevented by default."""
|
|
1127
|
+
|
|
1128
|
+
logger.debug(
|
|
1129
|
+
"[EnumerationMetaClass] %s.register(name: %s, value: %s)",
|
|
1130
|
+
self.__name__,
|
|
1131
|
+
name,
|
|
1132
|
+
value,
|
|
1133
|
+
)
|
|
1134
|
+
|
|
1135
|
+
if self.configuration.overwritable is False and name in self._enumerations:
|
|
1136
|
+
raise EnumerationNonUniqueError(
|
|
1137
|
+
"The '%s' enumeration class already has an option named '%s', so a new option with the same name cannot be created unless the 'overwritable=True' argument is passed during class construction!"
|
|
1138
|
+
% (self.__name__, name)
|
|
1139
|
+
)
|
|
1140
|
+
|
|
1141
|
+
self._enumerations[name] = enumeration = self(
|
|
1142
|
+
enumeration=self,
|
|
1143
|
+
name=name,
|
|
1144
|
+
value=value,
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
return enumeration
|
|
1148
|
+
|
|
1149
|
+
def unregister(self, name: str):
|
|
1150
|
+
"""The 'unregister' method supports unregistering existing enumeration options
|
|
1151
|
+
from an enumeration class, if the 'removable=True' argument was specified when
|
|
1152
|
+
the enumeration class was created.
|
|
1153
|
+
|
|
1154
|
+
Removal of existing enumeration options should be used cautiously and only for
|
|
1155
|
+
enumeration options that will not be referenced or used during the remainder of
|
|
1156
|
+
a program's runtime, otherwise references to removed enumerations could result
|
|
1157
|
+
in EnumerationError exceptions being raised."""
|
|
1158
|
+
|
|
1159
|
+
logger.debug(
|
|
1160
|
+
"[EnumerationMetaClass] %s.unregister(name: %s)",
|
|
1161
|
+
self.__class__.__name__,
|
|
1162
|
+
name,
|
|
1163
|
+
)
|
|
1164
|
+
|
|
1165
|
+
if self.configuration.removable is False:
|
|
1166
|
+
raise EnumerationError(
|
|
1167
|
+
"The '%s' enumeration class by default does not support unregistering options, unless the 'removable=True' argument is passed during class construction!"
|
|
1168
|
+
% (self.__name__)
|
|
1169
|
+
)
|
|
1170
|
+
|
|
1171
|
+
if name in self._enumerations:
|
|
1172
|
+
del self._enumerations[name]
|
|
1173
|
+
|
|
1174
|
+
def reconcile(
|
|
1175
|
+
self,
|
|
1176
|
+
value: Enumeration | object = None,
|
|
1177
|
+
name: str = None,
|
|
1178
|
+
) -> Enumeration | None:
|
|
1179
|
+
"""The 'reconcile' method can be used to reconcile Enumeration type, enumeration
|
|
1180
|
+
values, or enumeration names to their matching Enumeration type instances. If a
|
|
1181
|
+
match is found the Enumeration type instance will be returned otherwise None
|
|
1182
|
+
will be returned."""
|
|
1183
|
+
|
|
1184
|
+
if name is None and value is None:
|
|
1185
|
+
raise ValueError(
|
|
1186
|
+
"Either the 'value' or 'name' argument must be specified when calling the 'reconcile' function!"
|
|
1187
|
+
)
|
|
1188
|
+
|
|
1189
|
+
if not value is None and not isinstance(value, (Enumeration, object)):
|
|
1190
|
+
raise TypeError(
|
|
1191
|
+
"The 'value' argument must reference an Enumeration type or have an enumeration value!"
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
if not name is None and not isinstance(name, str):
|
|
1195
|
+
raise TypeError("The 'name' argument must have a string value!")
|
|
1196
|
+
|
|
1197
|
+
reconciled: Enumeration = None
|
|
1198
|
+
|
|
1199
|
+
for attribute, enumeration in self._enumerations.items():
|
|
1200
|
+
if isinstance(name, str) and enumeration.name == name:
|
|
1201
|
+
reconciled = enumeration
|
|
1202
|
+
break
|
|
1203
|
+
elif isinstance(value, Enumeration):
|
|
1204
|
+
if enumeration is value:
|
|
1205
|
+
reconciled = enumeration
|
|
1206
|
+
break
|
|
1207
|
+
elif isinstance(value, str) and enumeration.name == value:
|
|
1208
|
+
reconciled = enumeration
|
|
1209
|
+
break
|
|
1210
|
+
elif enumeration.value == value:
|
|
1211
|
+
reconciled = enumeration
|
|
1212
|
+
break
|
|
1213
|
+
|
|
1214
|
+
if reconciled is None and self.configuration.raises is True:
|
|
1215
|
+
if not name is None:
|
|
1216
|
+
raise EnumerationOptionError(
|
|
1217
|
+
"Unable to reconcile %s option with name: %s!"
|
|
1218
|
+
% (
|
|
1219
|
+
self.__class__.__name__,
|
|
1220
|
+
name,
|
|
1221
|
+
)
|
|
1222
|
+
)
|
|
1223
|
+
elif not value is None:
|
|
1224
|
+
raise EnumerationOptionError(
|
|
1225
|
+
"Unable to reconcile %s option with value: %s!"
|
|
1226
|
+
% (
|
|
1227
|
+
self.__class__.__name__,
|
|
1228
|
+
value,
|
|
1229
|
+
)
|
|
1230
|
+
)
|
|
1231
|
+
|
|
1232
|
+
return reconciled
|
|
1233
|
+
|
|
1234
|
+
def validate(self, value: Enumeration | object = None, name: str = None) -> bool:
|
|
1235
|
+
"""The 'validate' method can be used to verify if the Enumeration class contains
|
|
1236
|
+
the specified enumeration or enumeration value. The method returns True if a
|
|
1237
|
+
match is found for the enumeration value or name, otherwise it returns False."""
|
|
1238
|
+
|
|
1239
|
+
return not self.reconcile(value=value, name=name) is None
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
class Enumeration(metaclass=EnumerationMetaClass):
|
|
1243
|
+
"""The Enumeration class is the subclass of all enumerations and their subtypes."""
|
|
1244
|
+
|
|
1245
|
+
_metaclass: EnumerationMetaClass = None
|
|
1246
|
+
_enumeration: Enumeration = None
|
|
1247
|
+
_enumerations: dict[str, Enumeration] = None
|
|
1248
|
+
_name: str = None
|
|
1249
|
+
_value: object = None
|
|
1250
|
+
_aliased: Enumeration = None
|
|
1251
|
+
|
|
1252
|
+
# NOTE: This method is only called if the class is instantiated via class(..) syntax
|
|
1253
|
+
def __new__(
|
|
1254
|
+
cls,
|
|
1255
|
+
*args,
|
|
1256
|
+
enumeration: Enumeration = None,
|
|
1257
|
+
name: str = None,
|
|
1258
|
+
value: object = None,
|
|
1259
|
+
aliased: Enumeration = None,
|
|
1260
|
+
**kwargs,
|
|
1261
|
+
) -> Enumeration | None:
|
|
1262
|
+
# Supports reconciling enumeration options via their name/value via __new__ call
|
|
1263
|
+
if value is None and len(args) >= 1:
|
|
1264
|
+
value = args[0]
|
|
1265
|
+
|
|
1266
|
+
logger.debug(
|
|
1267
|
+
"[Enumeration] %s.__new__(args: %s, enumeration: %s, name: %s, value: %s, kwargs: %s)",
|
|
1268
|
+
cls.__name__,
|
|
1269
|
+
args,
|
|
1270
|
+
enumeration,
|
|
1271
|
+
name,
|
|
1272
|
+
value,
|
|
1273
|
+
kwargs,
|
|
1274
|
+
)
|
|
1275
|
+
|
|
1276
|
+
if enumeration is None and name is None and value is None:
|
|
1277
|
+
raise NotImplementedError
|
|
1278
|
+
elif enumeration is None and ((name is not None) or (value is not None)):
|
|
1279
|
+
if isinstance(
|
|
1280
|
+
reconciled := cls.reconcile(value=value, name=name), Enumeration
|
|
1281
|
+
):
|
|
1282
|
+
return reconciled
|
|
1283
|
+
else:
|
|
1284
|
+
logger.debug(
|
|
1285
|
+
"Unable to reconcile enumeration option <Enumeration(name=%s, value=%s)>",
|
|
1286
|
+
name,
|
|
1287
|
+
value,
|
|
1288
|
+
)
|
|
1289
|
+
return None
|
|
1290
|
+
else:
|
|
1291
|
+
return super().__new__(cls)
|
|
1292
|
+
|
|
1293
|
+
# NOTE: This method is only called if the class is instantiated via class(..) syntax
|
|
1294
|
+
def __init__(
|
|
1295
|
+
self,
|
|
1296
|
+
*args,
|
|
1297
|
+
enumeration: Enumeration = None,
|
|
1298
|
+
name: str = None,
|
|
1299
|
+
value: object = None,
|
|
1300
|
+
aliased: Enumeration = None,
|
|
1301
|
+
**kwargs,
|
|
1302
|
+
) -> None:
|
|
1303
|
+
logger.debug(
|
|
1304
|
+
"[Enumeration] %s.__init__(args: %s, enumeration: %s, name: %s, value: %s, kwargs: %s)",
|
|
1305
|
+
self.__class__.__name__,
|
|
1306
|
+
args,
|
|
1307
|
+
enumeration,
|
|
1308
|
+
name,
|
|
1309
|
+
value,
|
|
1310
|
+
kwargs,
|
|
1311
|
+
)
|
|
1312
|
+
|
|
1313
|
+
if enumeration is None:
|
|
1314
|
+
pass
|
|
1315
|
+
elif issubclass(enumeration, Enumeration):
|
|
1316
|
+
self._enumeration = enumeration
|
|
1317
|
+
|
|
1318
|
+
if name is None:
|
|
1319
|
+
pass
|
|
1320
|
+
elif isinstance(name, str):
|
|
1321
|
+
self._name = name
|
|
1322
|
+
else:
|
|
1323
|
+
raise TypeError("The 'name' argument must have a string value!")
|
|
1324
|
+
|
|
1325
|
+
if value is None:
|
|
1326
|
+
pass
|
|
1327
|
+
else:
|
|
1328
|
+
if isinstance(value, Enumeration):
|
|
1329
|
+
raise TypeError(
|
|
1330
|
+
"The 'value' argument cannot be assigned to another Enumeration!"
|
|
1331
|
+
)
|
|
1332
|
+
self._value = value
|
|
1333
|
+
|
|
1334
|
+
if aliased is None:
|
|
1335
|
+
pass
|
|
1336
|
+
elif isinstance(aliased, Enumeration):
|
|
1337
|
+
self._aliased = aliased
|
|
1338
|
+
else:
|
|
1339
|
+
raise TypeError(
|
|
1340
|
+
"The 'aliased' argument, if specified, must reference an Enumeration class instance!"
|
|
1341
|
+
)
|
|
1342
|
+
|
|
1343
|
+
# NOTE: This method is only called if the instance is called via instance(..) syntax
|
|
1344
|
+
def __call__(self, *args, **kwargs) -> Enumeration | None:
|
|
1345
|
+
logger.debug(
|
|
1346
|
+
"%s.__call__(args: %s, kwargs: %s)",
|
|
1347
|
+
self.__class__.__name__,
|
|
1348
|
+
args,
|
|
1349
|
+
kwargs,
|
|
1350
|
+
)
|
|
1351
|
+
|
|
1352
|
+
return self.reconcile(*args, **kwargs)
|
|
1353
|
+
|
|
1354
|
+
def __str__(self) -> str:
|
|
1355
|
+
return f"{self.__class__.__name__}.{self._name}"
|
|
1356
|
+
|
|
1357
|
+
def __repr__(self) -> str:
|
|
1358
|
+
return f"<{self.__class__.__name__}.{self._name}: {self._value}>"
|
|
1359
|
+
|
|
1360
|
+
def __hash__(self) -> int:
|
|
1361
|
+
return id(self)
|
|
1362
|
+
|
|
1363
|
+
def __eq__(self, other: Enumeration | object) -> bool:
|
|
1364
|
+
logger.debug("%s.__eq__(other: %s)" % (self.__class__.__name__, other))
|
|
1365
|
+
|
|
1366
|
+
equals: bool = False
|
|
1367
|
+
|
|
1368
|
+
if isinstance(other, Enumeration):
|
|
1369
|
+
if self is other:
|
|
1370
|
+
return True
|
|
1371
|
+
|
|
1372
|
+
for attribute, enumeration in self._enumerations.items():
|
|
1373
|
+
logger.info(
|
|
1374
|
+
"%s.__eq__(other: %s) enumeration => %s"
|
|
1375
|
+
% (self.__class__.__name__, other, enumeration)
|
|
1376
|
+
)
|
|
1377
|
+
|
|
1378
|
+
if isinstance(other, Enumeration):
|
|
1379
|
+
if enumeration is other:
|
|
1380
|
+
equals = True
|
|
1381
|
+
break
|
|
1382
|
+
elif enumeration.value == other:
|
|
1383
|
+
equals = True
|
|
1384
|
+
break
|
|
1385
|
+
elif enumeration.name == other:
|
|
1386
|
+
equals = True
|
|
1387
|
+
break
|
|
1388
|
+
|
|
1389
|
+
return equals
|
|
1390
|
+
|
|
1391
|
+
@property
|
|
1392
|
+
def enumeration(self) -> Enumeration:
|
|
1393
|
+
return self._enumeration
|
|
1394
|
+
|
|
1395
|
+
# @property
|
|
1396
|
+
# def enumerations(self) -> MappingProxyType[str, Enumeration]:
|
|
1397
|
+
# return self._enumeration._enumerations
|
|
1398
|
+
|
|
1399
|
+
@property
|
|
1400
|
+
def name(self) -> str:
|
|
1401
|
+
return self._name
|
|
1402
|
+
|
|
1403
|
+
@property
|
|
1404
|
+
def value(self) -> object:
|
|
1405
|
+
return self._value
|
|
1406
|
+
|
|
1407
|
+
@property
|
|
1408
|
+
def aliased(self) -> bool:
|
|
1409
|
+
logger.debug(
|
|
1410
|
+
"%s.aliased() >>> id(Colors._enumerations) => %s (%s)",
|
|
1411
|
+
self.__class__.__name__,
|
|
1412
|
+
id(self._enumerations),
|
|
1413
|
+
type(self._enumerations),
|
|
1414
|
+
)
|
|
1415
|
+
|
|
1416
|
+
for name, enumeration in self._enumerations.items():
|
|
1417
|
+
logger.info(" >>> checking for alias: %s => %s", name, enumeration)
|
|
1418
|
+
|
|
1419
|
+
if isinstance(enumeration, Enumeration):
|
|
1420
|
+
if name != enumeration.name:
|
|
1421
|
+
return True
|
|
1422
|
+
|
|
1423
|
+
return False
|
|
1424
|
+
|
|
1425
|
+
|
|
1426
|
+
class EnumerationType(Enumeration, typecast=False):
|
|
1427
|
+
"""The EnumerationType class represents the type of value held by an enumeration."""
|
|
1428
|
+
|
|
1429
|
+
MIXED = None
|
|
1430
|
+
INTEGER = int
|
|
1431
|
+
FLOAT = float
|
|
1432
|
+
COMPLEX = complex
|
|
1433
|
+
STRING = str
|
|
1434
|
+
BYTES = bytes
|
|
1435
|
+
# BOOLEAN = bool
|
|
1436
|
+
OBJECT = object
|
|
1437
|
+
TUPLE = tuple
|
|
1438
|
+
SET = set
|
|
1439
|
+
LIST = list
|
|
1440
|
+
DICTIONARY = dict
|
|
1441
|
+
|
|
1442
|
+
|
|
1443
|
+
class EnumerationInteger(int, Enumeration):
|
|
1444
|
+
"""An Enumeration subclass where all values are integer values."""
|
|
1445
|
+
|
|
1446
|
+
def __new__(cls, *args, **kwargs):
|
|
1447
|
+
logger.debug(
|
|
1448
|
+
"EnumerationInteger.__new__(cls: %s, args: %s, kwargs: %s)",
|
|
1449
|
+
cls,
|
|
1450
|
+
args,
|
|
1451
|
+
kwargs,
|
|
1452
|
+
)
|
|
1453
|
+
|
|
1454
|
+
if not isinstance(value := kwargs.get("value"), int):
|
|
1455
|
+
raise TypeError("The provided value must be an integer!")
|
|
1456
|
+
|
|
1457
|
+
return super().__new__(cls, value)
|
|
1458
|
+
|
|
1459
|
+
def __str__(self) -> str:
|
|
1460
|
+
return Enumeration.__str__(self)
|
|
1461
|
+
|
|
1462
|
+
def __repr__(self) -> str:
|
|
1463
|
+
return Enumeration.__repr__(self)
|
|
1464
|
+
|
|
1465
|
+
|
|
1466
|
+
class EnumerationFloat(float, Enumeration):
|
|
1467
|
+
"""An Enumeration subclass where all values are float values."""
|
|
1468
|
+
|
|
1469
|
+
def __new__(cls, *args, **kwargs):
|
|
1470
|
+
logger.debug(
|
|
1471
|
+
"EnumerationFloat.__new__(cls: %s, args: %s, kwargs: %s)", cls, args, kwargs
|
|
1472
|
+
)
|
|
1473
|
+
|
|
1474
|
+
if not isinstance(value := kwargs.get("value"), float):
|
|
1475
|
+
raise TypeError("The provided value must be a float!")
|
|
1476
|
+
|
|
1477
|
+
return super().__new__(cls, value)
|
|
1478
|
+
|
|
1479
|
+
def __str__(self) -> str:
|
|
1480
|
+
return Enumeration.__str__(self)
|
|
1481
|
+
|
|
1482
|
+
def __repr__(self) -> str:
|
|
1483
|
+
return Enumeration.__repr__(self)
|
|
1484
|
+
|
|
1485
|
+
|
|
1486
|
+
class EnumerationComplex(complex, Enumeration):
|
|
1487
|
+
"""An Enumeration subclass where all values are complex values."""
|
|
1488
|
+
|
|
1489
|
+
def __new__(cls, *args, **kwargs):
|
|
1490
|
+
logger.debug(
|
|
1491
|
+
"EnumerationComplex.__new__(cls: %s, args: %s, kwargs: %s)",
|
|
1492
|
+
cls,
|
|
1493
|
+
args,
|
|
1494
|
+
kwargs,
|
|
1495
|
+
)
|
|
1496
|
+
|
|
1497
|
+
if not isinstance(value := kwargs.get("value"), complex):
|
|
1498
|
+
raise TypeError("The provided value must be a complex!")
|
|
1499
|
+
|
|
1500
|
+
return super().__new__(cls, value)
|
|
1501
|
+
|
|
1502
|
+
def __str__(self) -> str:
|
|
1503
|
+
return Enumeration.__str__(self)
|
|
1504
|
+
|
|
1505
|
+
def __repr__(self) -> str:
|
|
1506
|
+
return Enumeration.__repr__(self)
|
|
1507
|
+
|
|
1508
|
+
|
|
1509
|
+
class EnumerationString(str, Enumeration):
|
|
1510
|
+
"""An Enumeration subclass where all values are string values."""
|
|
1511
|
+
|
|
1512
|
+
def __new__(cls, *args, **kwargs):
|
|
1513
|
+
logger.debug(
|
|
1514
|
+
"EnumerationString.__new__(cls: %s, args: %s, kwargs: %s)",
|
|
1515
|
+
cls,
|
|
1516
|
+
args,
|
|
1517
|
+
kwargs,
|
|
1518
|
+
)
|
|
1519
|
+
|
|
1520
|
+
if not isinstance(value := kwargs.get("value"), str):
|
|
1521
|
+
raise TypeError("The provided value must be a string!")
|
|
1522
|
+
|
|
1523
|
+
return super().__new__(cls, value)
|
|
1524
|
+
|
|
1525
|
+
def __str__(self) -> str:
|
|
1526
|
+
return Enumeration.__str__(self)
|
|
1527
|
+
|
|
1528
|
+
def __repr__(self) -> str:
|
|
1529
|
+
return Enumeration.__repr__(self)
|
|
1530
|
+
|
|
1531
|
+
|
|
1532
|
+
class EnumerationBytes(bytes, Enumeration):
|
|
1533
|
+
"""An Enumeration subclass where all values are bytes values."""
|
|
1534
|
+
|
|
1535
|
+
def __new__(cls, *args, **kwargs):
|
|
1536
|
+
logger.debug(
|
|
1537
|
+
"EnumerationBytes.__new__(cls: %s, args: %s, kwargs: %s)", cls, args, kwargs
|
|
1538
|
+
)
|
|
1539
|
+
|
|
1540
|
+
if not isinstance(value := kwargs.get("value"), bytes):
|
|
1541
|
+
raise TypeError("The provided value must be a bytes!")
|
|
1542
|
+
|
|
1543
|
+
return super().__new__(cls, value)
|
|
1544
|
+
|
|
1545
|
+
def __str__(self) -> str:
|
|
1546
|
+
return Enumeration.__str__(self)
|
|
1547
|
+
|
|
1548
|
+
def __repr__(self) -> str:
|
|
1549
|
+
return Enumeration.__repr__(self)
|
|
1550
|
+
|
|
1551
|
+
|
|
1552
|
+
class EnumerationTuple(tuple, Enumeration):
|
|
1553
|
+
"""An Enumeration subclass where all values are tuple values."""
|
|
1554
|
+
|
|
1555
|
+
def __new__(cls, *args, **kwargs):
|
|
1556
|
+
logger.debug(
|
|
1557
|
+
"EnumerationTuple.__new__(cls: %s, args: %s, kwargs: %s)", cls, args, kwargs
|
|
1558
|
+
)
|
|
1559
|
+
|
|
1560
|
+
if not isinstance(value := kwargs.get("value"), tuple):
|
|
1561
|
+
raise TypeError("The provided value must be a tuple!")
|
|
1562
|
+
|
|
1563
|
+
return super().__new__(cls, value)
|
|
1564
|
+
|
|
1565
|
+
def __str__(self) -> str:
|
|
1566
|
+
return Enumeration.__str__(self)
|
|
1567
|
+
|
|
1568
|
+
def __repr__(self) -> str:
|
|
1569
|
+
return Enumeration.__repr__(self)
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
class EnumerationSet(set, Enumeration):
|
|
1573
|
+
"""An Enumeration subclass where all values are set values."""
|
|
1574
|
+
|
|
1575
|
+
def __new__(cls, *args, **kwargs):
|
|
1576
|
+
logger.debug(
|
|
1577
|
+
"EnumerationSet.__new__(cls: %s, args: %s, kwargs: %s)", cls, args, kwargs
|
|
1578
|
+
)
|
|
1579
|
+
|
|
1580
|
+
if not isinstance(value := kwargs.get("value"), set):
|
|
1581
|
+
raise TypeError("The provided value must be a set!")
|
|
1582
|
+
|
|
1583
|
+
return super().__new__(cls, value)
|
|
1584
|
+
|
|
1585
|
+
def __str__(self) -> str:
|
|
1586
|
+
return Enumeration.__str__(self)
|
|
1587
|
+
|
|
1588
|
+
def __repr__(self) -> str:
|
|
1589
|
+
return Enumeration.__repr__(self)
|
|
1590
|
+
|
|
1591
|
+
|
|
1592
|
+
class EnumerationList(list, Enumeration):
|
|
1593
|
+
"""An Enumeration subclass where all values are list values."""
|
|
1594
|
+
|
|
1595
|
+
def __new__(cls, *args, **kwargs):
|
|
1596
|
+
logger.debug(
|
|
1597
|
+
"EnumerationList.__new__(cls: %s, args: %s, kwargs: %s)", cls, args, kwargs
|
|
1598
|
+
)
|
|
1599
|
+
|
|
1600
|
+
if not isinstance(value := kwargs.get("value"), list):
|
|
1601
|
+
raise TypeError("The provided value must be a list!")
|
|
1602
|
+
|
|
1603
|
+
return super().__new__(cls, value)
|
|
1604
|
+
|
|
1605
|
+
def __str__(self) -> str:
|
|
1606
|
+
return Enumeration.__str__(self)
|
|
1607
|
+
|
|
1608
|
+
def __repr__(self) -> str:
|
|
1609
|
+
return Enumeration.__repr__(self)
|
|
1610
|
+
|
|
1611
|
+
|
|
1612
|
+
class EnumerationDictionary(dict, Enumeration):
|
|
1613
|
+
"""An Enumeration subclass where all values are dictionary values."""
|
|
1614
|
+
|
|
1615
|
+
def __new__(cls, *args, **kwargs):
|
|
1616
|
+
logger.debug(
|
|
1617
|
+
"EnumerationDictionary.__new__(cls: %s, args: %s, kwargs: %s)",
|
|
1618
|
+
cls,
|
|
1619
|
+
args,
|
|
1620
|
+
kwargs,
|
|
1621
|
+
)
|
|
1622
|
+
|
|
1623
|
+
if not isinstance(value := kwargs.get("value"), dict):
|
|
1624
|
+
raise TypeError("The provided value must be a dictionary!")
|
|
1625
|
+
|
|
1626
|
+
return super().__new__(cls, value)
|
|
1627
|
+
|
|
1628
|
+
def __str__(self) -> str:
|
|
1629
|
+
return Enumeration.__str__(self)
|
|
1630
|
+
|
|
1631
|
+
def __repr__(self) -> str:
|
|
1632
|
+
return Enumeration.__repr__(self)
|
|
1633
|
+
|
|
1634
|
+
|
|
1635
|
+
class EnumerationFlag(int, Enumeration):
|
|
1636
|
+
"""An Enumeration subclass where all values are integer values to the power of 2."""
|
|
1637
|
+
|
|
1638
|
+
_flags: set[EnumerationFlag] = None
|
|
1639
|
+
|
|
1640
|
+
def __new__(
|
|
1641
|
+
cls,
|
|
1642
|
+
*args,
|
|
1643
|
+
flags: list[EnumerationFlag] = None,
|
|
1644
|
+
name: str = None,
|
|
1645
|
+
value: object = None,
|
|
1646
|
+
unique: bool = True,
|
|
1647
|
+
**kwargs,
|
|
1648
|
+
):
|
|
1649
|
+
logger.debug(
|
|
1650
|
+
"%s.__new__(cls: %s, args: %s, flags: %s, name: %s, value: %s, unique: %s, kwargs: %s)",
|
|
1651
|
+
cls.__name__,
|
|
1652
|
+
cls,
|
|
1653
|
+
args,
|
|
1654
|
+
flags,
|
|
1655
|
+
name,
|
|
1656
|
+
value,
|
|
1657
|
+
unique,
|
|
1658
|
+
kwargs,
|
|
1659
|
+
)
|
|
1660
|
+
|
|
1661
|
+
if flags is None:
|
|
1662
|
+
if isinstance(name, str) and isinstance(value, int):
|
|
1663
|
+
if isinstance(reconciled := cls.reconcile(name=name, value=value), cls):
|
|
1664
|
+
return reconciled
|
|
1665
|
+
elif not isinstance(flags, list):
|
|
1666
|
+
raise TypeError(
|
|
1667
|
+
"The 'flags' argument must reference a list of '%s' instances!"
|
|
1668
|
+
% (cls.__name__)
|
|
1669
|
+
)
|
|
1670
|
+
else:
|
|
1671
|
+
for index, flag in enumerate(flags):
|
|
1672
|
+
if not isinstance(flag, cls):
|
|
1673
|
+
raise TypeError(
|
|
1674
|
+
"The 'flags' argument must reference a list of '%s' instances; the item at index %d is not a '%s' instance!"
|
|
1675
|
+
% (cls.__name__, index, cls.__name__)
|
|
1676
|
+
)
|
|
1677
|
+
|
|
1678
|
+
if name is None:
|
|
1679
|
+
name = "|".join([flag.name for flag in flags])
|
|
1680
|
+
|
|
1681
|
+
if value is None:
|
|
1682
|
+
value: int = 0
|
|
1683
|
+
|
|
1684
|
+
for flag in flags:
|
|
1685
|
+
# Use the bitwise 'or' operation to combine the flag bit masks
|
|
1686
|
+
value = value | flag.value
|
|
1687
|
+
|
|
1688
|
+
if value is None:
|
|
1689
|
+
if flags is None:
|
|
1690
|
+
raise ValueError(
|
|
1691
|
+
"The 'flags' argument must be provided if the 'value' argument is not!"
|
|
1692
|
+
)
|
|
1693
|
+
elif isinstance(value, int):
|
|
1694
|
+
if value == 0 or (value > 0 and (value & (value - 1)) == 0):
|
|
1695
|
+
pass
|
|
1696
|
+
elif flags is None:
|
|
1697
|
+
raise ValueError(
|
|
1698
|
+
"The 'value' argument, %r, is invalid; it must be have a positive integer value that is a power of two!"
|
|
1699
|
+
% (value)
|
|
1700
|
+
)
|
|
1701
|
+
else:
|
|
1702
|
+
raise TypeError(
|
|
1703
|
+
"The 'value' argument, if specified, must have a positive integer value!"
|
|
1704
|
+
)
|
|
1705
|
+
|
|
1706
|
+
if not unique is True:
|
|
1707
|
+
raise ValueError(
|
|
1708
|
+
"The 'unique' argument, if specified, must have a boolean 'True' value for all subclasses of the '%s' class!"
|
|
1709
|
+
% (cls.__name__)
|
|
1710
|
+
)
|
|
1711
|
+
|
|
1712
|
+
return super().__new__(cls, value)
|
|
1713
|
+
|
|
1714
|
+
def __init__(
|
|
1715
|
+
self,
|
|
1716
|
+
*args,
|
|
1717
|
+
flags: list[EnumerationFlag] = None,
|
|
1718
|
+
name: str = None,
|
|
1719
|
+
value: object = None,
|
|
1720
|
+
unique: bool = True,
|
|
1721
|
+
**kwargs,
|
|
1722
|
+
):
|
|
1723
|
+
logger.debug(
|
|
1724
|
+
"%s.__init__(self: %s, args: %s, flags: %s, name: %s, value: %s, unique: %s, kwargs: %s)",
|
|
1725
|
+
self.__class__.__name__,
|
|
1726
|
+
self,
|
|
1727
|
+
args,
|
|
1728
|
+
flags,
|
|
1729
|
+
name,
|
|
1730
|
+
value,
|
|
1731
|
+
unique,
|
|
1732
|
+
kwargs,
|
|
1733
|
+
)
|
|
1734
|
+
|
|
1735
|
+
if flags is None:
|
|
1736
|
+
pass
|
|
1737
|
+
elif not isinstance(flags, list):
|
|
1738
|
+
raise TypeError(
|
|
1739
|
+
"The 'flags' argument must reference a list of '%s' instances!"
|
|
1740
|
+
% (cls.__name__)
|
|
1741
|
+
)
|
|
1742
|
+
else:
|
|
1743
|
+
for index, flag in enumerate(flags):
|
|
1744
|
+
if not isinstance(flag, self.__class__):
|
|
1745
|
+
raise TypeError(
|
|
1746
|
+
"The 'flags' argument must reference a list of '%s' instances; the item at index %d is not a '%s' instance!"
|
|
1747
|
+
% (self.__class__.__name__, index, self.__class__.__name__)
|
|
1748
|
+
)
|
|
1749
|
+
|
|
1750
|
+
if name is None:
|
|
1751
|
+
name = "|".join([flag.name for flag in flags])
|
|
1752
|
+
|
|
1753
|
+
if value is None:
|
|
1754
|
+
value = 0
|
|
1755
|
+
|
|
1756
|
+
for flag in flags:
|
|
1757
|
+
value = (
|
|
1758
|
+
value | flag.value
|
|
1759
|
+
) # use the bitwise 'or' operation to combine the values
|
|
1760
|
+
|
|
1761
|
+
super().__init__(
|
|
1762
|
+
*args,
|
|
1763
|
+
name=name,
|
|
1764
|
+
value=value,
|
|
1765
|
+
unique=unique,
|
|
1766
|
+
**kwargs,
|
|
1767
|
+
)
|
|
1768
|
+
|
|
1769
|
+
def __str__(self) -> str:
|
|
1770
|
+
return Enumeration.__str__(self)
|
|
1771
|
+
|
|
1772
|
+
def __repr__(self) -> str:
|
|
1773
|
+
return Enumeration.__repr__(self)
|
|
1774
|
+
|
|
1775
|
+
def __or__(self, other: EnumerationFlag): # called for: "a | b" (bitwise or)
|
|
1776
|
+
"""Support performing a bitwise or between the current EnumerationFlag
|
|
1777
|
+
instance's bitmask and the 'other' provided EnumerationFlag's bitmask;
|
|
1778
|
+
the return value from the operation is a new EnumerationFlag instance
|
|
1779
|
+
that represents the appropriately combined EnumerationFlags bitmasks."""
|
|
1780
|
+
|
|
1781
|
+
logger.debug(
|
|
1782
|
+
"%s.__or__(self: %s, other: %s)", self.__class__.__name__, self, other
|
|
1783
|
+
)
|
|
1784
|
+
|
|
1785
|
+
if not isinstance(other, self.__class__):
|
|
1786
|
+
raise TypeError(
|
|
1787
|
+
"The 'other' argument must be an instance of the '%s' class!"
|
|
1788
|
+
% (self.__class__.__name__)
|
|
1789
|
+
)
|
|
1790
|
+
|
|
1791
|
+
flags = self.flags()
|
|
1792
|
+
|
|
1793
|
+
if not other in flags:
|
|
1794
|
+
flags.append(other)
|
|
1795
|
+
|
|
1796
|
+
logger.debug(" >>> flags => %s", flags)
|
|
1797
|
+
|
|
1798
|
+
return self.__class__(
|
|
1799
|
+
enumeration=self.__class__,
|
|
1800
|
+
flags=sorted(flags),
|
|
1801
|
+
)
|
|
1802
|
+
|
|
1803
|
+
def __xor__(self, other: EnumerationFlag): # called for: "a ^ b" (bitwise xor)
|
|
1804
|
+
"""Support performing a bitwise xor between the current EnumerationFlag
|
|
1805
|
+
instance's bitmask and the 'other' provided EnumerationFlag's bitmask;
|
|
1806
|
+
the return value from the operation is a new EnumerationFlag instance
|
|
1807
|
+
that represents the appropriately combined EnumerationFlags bitmasks."""
|
|
1808
|
+
|
|
1809
|
+
logger.debug(
|
|
1810
|
+
"%s.__xor__(self: %s, other: %s)", self.__class__.__name__, self, other
|
|
1811
|
+
)
|
|
1812
|
+
|
|
1813
|
+
if not isinstance(other, self.__class__):
|
|
1814
|
+
raise TypeError(
|
|
1815
|
+
"The 'other' argument must be an instance of the '%s' class!"
|
|
1816
|
+
% (self.__class__.__name__)
|
|
1817
|
+
)
|
|
1818
|
+
|
|
1819
|
+
flags = self.flags()
|
|
1820
|
+
|
|
1821
|
+
if other in flags:
|
|
1822
|
+
flags.remove(other)
|
|
1823
|
+
|
|
1824
|
+
logger.debug(" >>> flags => %s", flags)
|
|
1825
|
+
|
|
1826
|
+
return self.__class__(
|
|
1827
|
+
enumeration=self.__class__,
|
|
1828
|
+
flags=sorted(flags),
|
|
1829
|
+
)
|
|
1830
|
+
|
|
1831
|
+
def __and__(self, other: EnumerationFlag): # called for: "a & b" (bitwise add)
|
|
1832
|
+
"""Support performing a bitwise and between the current EnumerationFlag
|
|
1833
|
+
instance's bitmask and the 'other' provided EnumerationFlag's bitmask;
|
|
1834
|
+
if the bitwise and finds an overlap, the return value from the operation
|
|
1835
|
+
is the 'other' provided EnumerationFlag. Otherwise the return value will
|
|
1836
|
+
be an 'empty' instance of the EnumerationFlag that doesn't match any."""
|
|
1837
|
+
|
|
1838
|
+
logger.debug(
|
|
1839
|
+
"%s.__and__(self: %s, other: %s)", self.__class__.__name__, self, other
|
|
1840
|
+
)
|
|
1841
|
+
|
|
1842
|
+
if not isinstance(other, self.__class__):
|
|
1843
|
+
raise TypeError(
|
|
1844
|
+
"The 'other' argument must be an instance of the '%s' class!"
|
|
1845
|
+
% (self.__class__.__name__)
|
|
1846
|
+
)
|
|
1847
|
+
|
|
1848
|
+
flags = self.flags()
|
|
1849
|
+
|
|
1850
|
+
if other in flags:
|
|
1851
|
+
return other
|
|
1852
|
+
else:
|
|
1853
|
+
# TODO: Return a singleton instance of the 'NONE' option; this may already
|
|
1854
|
+
# happen based on the superclass' behaviour but need to confirm this
|
|
1855
|
+
return self.__class__(
|
|
1856
|
+
enumeration=self.__class__,
|
|
1857
|
+
name="NONE",
|
|
1858
|
+
value=0,
|
|
1859
|
+
)
|
|
1860
|
+
|
|
1861
|
+
def __invert__(self): # called for: "~a" (bitwise inversion)
|
|
1862
|
+
"""Support inverting the current EnumerationFlag instance's bitmask."""
|
|
1863
|
+
|
|
1864
|
+
logger.debug("%s.__invert__(self: %s)", self.__class__.__name__, self)
|
|
1865
|
+
|
|
1866
|
+
# Obtain a list of flags that is exclusive of the current flag
|
|
1867
|
+
flags = self.flags(exclusive=True)
|
|
1868
|
+
|
|
1869
|
+
logger.debug(" >>> flags => %s", flags)
|
|
1870
|
+
|
|
1871
|
+
return self.__class__(
|
|
1872
|
+
enumeration=self.__class__,
|
|
1873
|
+
flags=sorted(flags),
|
|
1874
|
+
)
|
|
1875
|
+
|
|
1876
|
+
def __contains__(self, other: EnumerationFlag) -> bool: # called for: "a in b"
|
|
1877
|
+
"""Support determining if the current EnumerationFlag instance's bitmask
|
|
1878
|
+
overlaps with the 'other' provided EnumerationFlag instance's bitmask."""
|
|
1879
|
+
|
|
1880
|
+
if not isinstance(other, self.__class__):
|
|
1881
|
+
raise TypeError(
|
|
1882
|
+
"The 'other' argument must be an instance of the '%s' class!"
|
|
1883
|
+
% (self.__class__.__name__)
|
|
1884
|
+
)
|
|
1885
|
+
|
|
1886
|
+
return (self.value & other.value) == other.value
|
|
1887
|
+
|
|
1888
|
+
def flags(self, exclusive: bool = False) -> list[EnumerationFlag]:
|
|
1889
|
+
"""Return a list of EnumerationFlag instances matching the current
|
|
1890
|
+
EnumerationFlag's bitmask. By default the method will return all the
|
|
1891
|
+
flags which match the current bitmask, or when the 'exclusive' argument
|
|
1892
|
+
is set to 'True', the method will return all the flags which do not
|
|
1893
|
+
match the current EnumerationFlag's bitmask (an inversion)."""
|
|
1894
|
+
|
|
1895
|
+
if not isinstance(exclusive, bool):
|
|
1896
|
+
raise TypeError("The 'exclusive' argument must have a boolean value!")
|
|
1897
|
+
|
|
1898
|
+
flags: list[EnumerationFlag] = []
|
|
1899
|
+
|
|
1900
|
+
for name, enumeration in self.enumeration._enumerations.items():
|
|
1901
|
+
logger.debug(
|
|
1902
|
+
"%s.flags() name => %s, enumeration => %s (%s)",
|
|
1903
|
+
self.__class__.__name__,
|
|
1904
|
+
name,
|
|
1905
|
+
enumeration,
|
|
1906
|
+
type(enumeration),
|
|
1907
|
+
)
|
|
1908
|
+
|
|
1909
|
+
if ((self.value & enumeration.value) == enumeration.value) is not exclusive:
|
|
1910
|
+
flags.append(enumeration)
|
|
1911
|
+
|
|
1912
|
+
return flags
|