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