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.
@@ -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