enumerific 1.0.1__py3-none-any.whl → 1.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
enumerific/extensible.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import typing
4
+ import collections
4
5
 
5
6
  from enumerific.logging import logger
6
7
 
@@ -8,6 +9,7 @@ from enumerific.exceptions import (
8
9
  EnumerationError,
9
10
  EnumerationOptionError,
10
11
  EnumerationSubclassingError,
12
+ EnumerationExtensibilityError,
11
13
  EnumerationNonUniqueError,
12
14
  )
13
15
 
@@ -17,7 +19,71 @@ from types import MappingProxyType
17
19
  logger = logger.getChild(__name__)
18
20
 
19
21
 
20
- class auto(int):
22
+ class anno(collections.abc.Mapping):
23
+ """The annotations class supports adding annotations to an Enumeration option."""
24
+
25
+ _value: object = None
26
+ _annotations: dict[str, object] = None
27
+
28
+ def __init__(self, value: object, **annotations: dict[str, object]):
29
+ self._value: object = value
30
+
31
+ for key, value in annotations.items():
32
+ if not isinstance(key, str):
33
+ raise TypeError("All annotation values must have string keys!")
34
+
35
+ self._annotations: dict[str, object] = annotations
36
+
37
+ def __len__(self) -> int:
38
+ return len(self._annotations)
39
+
40
+ def __iter__(self) -> str:
41
+ for key in self._annotations.keys():
42
+ yield key
43
+
44
+ def __contains__(self, other: object) -> bool:
45
+ return other in self._annotations
46
+
47
+ def __getitem__(self, key: str) -> object | None:
48
+ if key in self._annotations:
49
+ return self._annotations[key]
50
+ else:
51
+ raise KeyError(f"The annotation does not have an '{key}' item!")
52
+
53
+ def __setitem__(self, key: str, value: object):
54
+ raise NotImplementedError
55
+
56
+ def __delitem__(self, key: str, value: object):
57
+ raise NotImplementedError
58
+
59
+ def __getattr__(self, name: str) -> object | None:
60
+ if name.startswith("_"):
61
+ return super().__getattr__(name)
62
+ elif name in self._annotations:
63
+ return self._annotations[name]
64
+ else:
65
+ raise AttributeError(f"The annotation does not have an '{name}' attribute!")
66
+
67
+ def __setattr__(self, name: str, value: object):
68
+ if name.startswith("_"):
69
+ return super().__setattr__(name, value)
70
+ else:
71
+ raise NotImplementedError
72
+
73
+ def __detattr__(self, name: str):
74
+ raise NotImplementedError
75
+
76
+ def get(self, name: str, default: object = None) -> object | None:
77
+ if name in self._annotations:
78
+ return self._annotations[name]
79
+ else:
80
+ return default
81
+
82
+ def unwrap(self) -> object:
83
+ return self._value
84
+
85
+
86
+ class auto(int, anno):
21
87
  """Generate an automatically inrementing integer each time the class is instantiated
22
88
  based on the previously supplied configuration, which allows the start and steps to
23
89
  be configured as well as if the integers should be generated as powers/flags."""
@@ -32,6 +98,7 @@ class auto(int):
32
98
  cls,
33
99
  start: int = None,
34
100
  steps: int = None,
101
+ times: int = None,
35
102
  power: int | bool = None,
36
103
  flags: bool = None,
37
104
  ):
@@ -57,6 +124,15 @@ class auto(int):
57
124
  "The 'steps' argument, if specified, must have a positive integer value!"
58
125
  )
59
126
 
127
+ if times is None:
128
+ times = 0
129
+ elif isinstance(times, int) and times >= 0:
130
+ pass
131
+ else:
132
+ raise TypeError(
133
+ "The 'times' argument, if specified, must have a positive integer value!"
134
+ )
135
+
60
136
  if power is None:
61
137
  power = 0
62
138
  elif isinstance(power, bool):
@@ -90,17 +166,21 @@ class auto(int):
90
166
 
91
167
  cls.steps = steps
92
168
 
169
+ cls.times = times
170
+
93
171
  cls.power = power
94
172
 
95
173
  cls.value = cls.start
96
174
 
97
- def __new__(cls):
175
+ def __new__(cls, **annotations: dict[str, object]):
98
176
  """Create a new integer (int) instance upon each call, incrementing the value as
99
177
  per the configuration defined before this method is called; the configuration
100
178
  can be changed at any time and the next call to this method will generate the
101
179
  next value based on the most recently specified configuration options."""
102
180
 
103
- if cls.power > 0:
181
+ if cls.times > 0:
182
+ value = cls.value * cls.times
183
+ elif cls.power > 0:
104
184
  value = pow(cls.power, (cls.value - 1))
105
185
  else:
106
186
  value = cls.value
@@ -109,52 +189,71 @@ class auto(int):
109
189
 
110
190
  return super().__new__(cls, value)
111
191
 
192
+ def __init__(self, **annotations: dict[str, object]):
193
+ super().__init__(self.value, **annotations)
194
+
112
195
 
113
196
  class EnumerationConfiguration(object):
114
197
  """The EnumerationConfiguration class holds the Enumeration configuration options"""
115
198
 
116
199
  _unique: bool = None
117
- _aliased: bool = False
200
+ _aliased: bool = None
201
+ _backfill: bool = None
118
202
  _overwritable: bool = None
119
203
  _removable: bool = None
120
204
  _subclassable: bool = None
205
+ _extensible: bool = None
121
206
  _raises: bool = None
122
207
  _flags: bool = None
123
208
  _start: int = None
209
+ _steps: int = None
210
+ _times: int = None
124
211
  _typecast: bool = None
125
212
 
126
213
  def __init__(
127
214
  self,
128
215
  unique: bool = None,
129
216
  aliased: bool = None,
217
+ backfill: bool = None,
130
218
  overwritable: bool = None,
131
219
  removable: bool = None,
132
220
  subclassable: bool = None,
221
+ extensible: bool = None,
133
222
  raises: bool = None,
134
223
  flags: bool = None,
135
224
  start: int = None,
225
+ steps: int = None,
226
+ times: int = None,
136
227
  typecast: bool = None,
137
228
  ):
138
229
  self.unique = unique
139
230
  self.aliased = aliased
231
+ self.backfill = backfill
140
232
  self.overwritable = overwritable
141
233
  self.removable = removable
142
234
  self.subclassable = subclassable
235
+ self.extensible = extensible
143
236
  self.raises = raises
144
237
  self.flags = flags
145
238
  self.start = start
239
+ self.steps = steps
240
+ self.times = times
146
241
  self.typecast = typecast
147
242
 
148
243
  def __dir__(self) -> list[str]:
149
244
  return [
150
245
  "unique",
151
246
  "aliased",
247
+ "backfill",
152
248
  "overwritable",
153
249
  "removable",
154
250
  "subclassable",
251
+ "extensible",
155
252
  "raises",
156
253
  "flags",
157
254
  "start",
255
+ "steps",
256
+ "times",
158
257
  "typecast",
159
258
  ]
160
259
 
@@ -220,6 +319,20 @@ class EnumerationConfiguration(object):
220
319
  )
221
320
  self._aliased = aliased
222
321
 
322
+ @property
323
+ def backfill(self) -> bool | None:
324
+ return self._backfill
325
+
326
+ @backfill.setter
327
+ def backfill(self, backfill: bool | None):
328
+ if backfill is None:
329
+ pass
330
+ elif not isinstance(backfill, bool):
331
+ raise TypeError(
332
+ "The 'backfill' argument, if specified, must have a boolean value!"
333
+ )
334
+ self._backfill = backfill
335
+
223
336
  @property
224
337
  def overwritable(self) -> bool | None:
225
338
  return self._overwritable
@@ -262,6 +375,20 @@ class EnumerationConfiguration(object):
262
375
  )
263
376
  self._subclassable = subclassable
264
377
 
378
+ @property
379
+ def extensible(self) -> bool | None:
380
+ return self._extensible
381
+
382
+ @extensible.setter
383
+ def extensible(self, extensible: bool | None):
384
+ if extensible is None:
385
+ pass
386
+ elif not isinstance(extensible, bool):
387
+ raise TypeError(
388
+ "The 'extensible' argument, if specified, must have a boolean value!"
389
+ )
390
+ self._extensible = extensible
391
+
265
392
  @property
266
393
  def raises(self) -> bool | None:
267
394
  return self._raises
@@ -291,7 +418,7 @@ class EnumerationConfiguration(object):
291
418
  self._flags = flags
292
419
 
293
420
  @property
294
- def start(self) -> bool | None:
421
+ def start(self) -> int | None:
295
422
  return self._start
296
423
 
297
424
  @start.setter
@@ -304,6 +431,34 @@ class EnumerationConfiguration(object):
304
431
  )
305
432
  self._start = start
306
433
 
434
+ @property
435
+ def steps(self) -> int | None:
436
+ return self._steps
437
+
438
+ @steps.setter
439
+ def steps(self, steps: int | None):
440
+ if steps is None:
441
+ pass
442
+ elif not (isinstance(steps, int) and steps >= 0):
443
+ raise TypeError(
444
+ "The 'steps' argument, if specified, must have a positive integer value!"
445
+ )
446
+ self._steps = steps
447
+
448
+ @property
449
+ def times(self) -> int | None:
450
+ return self._times
451
+
452
+ @times.setter
453
+ def times(self, times: int | None):
454
+ if times is None:
455
+ pass
456
+ elif not (isinstance(times, int) and times >= 0):
457
+ raise TypeError(
458
+ "The 'times' argument, if specified, must have a positive integer value!"
459
+ )
460
+ self._times = times
461
+
307
462
  @property
308
463
  def typecast(self) -> bool | None:
309
464
  return self._typecast
@@ -345,12 +500,16 @@ class EnumerationMetaClass(type):
345
500
  bases: tuple[type],
346
501
  unique: bool = None,
347
502
  aliased: bool = None,
503
+ backfill: bool = None,
348
504
  overwritable: bool = None,
349
505
  subclassable: bool = None,
506
+ extensible: bool = None,
350
507
  removable: bool = None,
351
508
  raises: bool = None,
352
509
  flags: bool = None,
353
510
  start: int = None,
511
+ steps: int = None,
512
+ times: int = None,
354
513
  typecast: bool = None,
355
514
  **kwargs,
356
515
  ) -> dict:
@@ -361,18 +520,22 @@ class EnumerationMetaClass(type):
361
520
  any other keyword arguments that are included in the class signature call."""
362
521
 
363
522
  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)",
523
+ "[EnumerationMetaClass] %s.__prepare__(name: %s, bases: %s, unique: %s, aliased: %s, backfill: %s, overwritable: %s, subclassable: %s, extensible: %s, removable: %s, raises: %s, flags: %s, start: %s, steps: %s, times: %s, typecast: %s, kwargs: %s)",
365
524
  name,
366
525
  name,
367
526
  bases,
368
527
  unique,
369
528
  aliased,
529
+ backfill,
370
530
  overwritable,
371
531
  subclassable,
532
+ extensible,
372
533
  removable,
373
534
  raises,
374
535
  flags,
375
536
  start,
537
+ steps,
538
+ times,
376
539
  typecast,
377
540
  kwargs,
378
541
  )
@@ -430,9 +593,27 @@ class EnumerationMetaClass(type):
430
593
  "The 'start' argument, if specified, must have a positive integer value!"
431
594
  )
432
595
 
596
+ if steps is None:
597
+ pass
598
+ elif isinstance(steps, int) and steps >= 1:
599
+ pass
600
+ else:
601
+ raise TypeError(
602
+ "The 'steps' argument, if specified, must have a positive integer value!"
603
+ )
604
+
605
+ if times is None:
606
+ pass
607
+ elif isinstance(times, int) and times >= 1:
608
+ pass
609
+ else:
610
+ raise TypeError(
611
+ "The 'times' argument, if specified, must have a positive integer value!"
612
+ )
613
+
433
614
  # Configure the auto() class for subsequent use, resetting the sequence, setting
434
615
  # the new start value, and whether values should be flag values (powers of 2)
435
- auto.configure(start=start, flags=flags)
616
+ auto.configure(start=start, steps=steps, times=times, flags=flags)
436
617
 
437
618
  return dict()
438
619
 
@@ -441,12 +622,16 @@ class EnumerationMetaClass(type):
441
622
  *args,
442
623
  unique: bool = None, # True
443
624
  aliased: bool = None, # False
625
+ backfill: bool = None, # False
444
626
  overwritable: bool = None, # False
445
627
  subclassable: bool = None, # True
628
+ extensible: bool = None, # True
446
629
  removable: bool = None, # False
447
630
  raises: bool = None, # False
448
631
  flags: bool = None, # False
449
632
  start: int = None, # None
633
+ steps: int = None, # None
634
+ times: int = None, # None
450
635
  typecast: bool = None, # True
451
636
  **kwargs,
452
637
  ):
@@ -471,6 +656,13 @@ class EnumerationMetaClass(type):
471
656
  "The 'aliased' argument, if specified, must have a boolean value!"
472
657
  )
473
658
 
659
+ if backfill is None:
660
+ pass
661
+ elif not isinstance(backfill, bool):
662
+ raise TypeError(
663
+ "The 'backfill' argument, if specified, must have a boolean value!"
664
+ )
665
+
474
666
  if overwritable is None:
475
667
  pass
476
668
  elif not isinstance(overwritable, bool):
@@ -485,6 +677,13 @@ class EnumerationMetaClass(type):
485
677
  "The 'subclassable' argument, if specified, must have a boolean value!"
486
678
  )
487
679
 
680
+ if extensible is None:
681
+ pass
682
+ elif not isinstance(extensible, bool):
683
+ raise TypeError(
684
+ "The 'extensible' argument, if specified, must have a boolean value!"
685
+ )
686
+
488
687
  if removable is None:
489
688
  pass
490
689
  elif not isinstance(removable, bool):
@@ -513,6 +712,20 @@ class EnumerationMetaClass(type):
513
712
  "The 'start' argument, if specified, must have a positive integer value!"
514
713
  )
515
714
 
715
+ if steps is None:
716
+ pass
717
+ elif not (isinstance(steps, int) and steps >= 0):
718
+ raise TypeError(
719
+ "The 'steps' argument, if specified, must have a positive integer value!"
720
+ )
721
+
722
+ if times is None:
723
+ pass
724
+ elif not (isinstance(times, int) and times >= 0):
725
+ raise TypeError(
726
+ "The 'times' argument, if specified, must have a positive integer value!"
727
+ )
728
+
516
729
  if typecast is None:
517
730
  pass
518
731
  elif not isinstance(typecast, bool):
@@ -523,12 +736,16 @@ class EnumerationMetaClass(type):
523
736
  configuration = EnumerationConfiguration(
524
737
  unique=unique,
525
738
  aliased=aliased,
739
+ backfill=backfill,
526
740
  overwritable=overwritable,
527
741
  subclassable=subclassable,
742
+ extensible=extensible,
528
743
  removable=removable,
529
744
  raises=raises,
530
745
  flags=flags,
531
746
  start=start,
747
+ steps=steps,
748
+ times=times,
532
749
  typecast=typecast,
533
750
  )
534
751
 
@@ -543,6 +760,7 @@ class EnumerationMetaClass(type):
543
760
  return super().__new__(cls, *args, **kwargs)
544
761
 
545
762
  enumerations: dict[str, object] = {} # Keep track of the enumeration options
763
+ annotations: dict[str, dict] = {} # Keep track of the enumeration annotations
546
764
 
547
765
  names: list[object] = [] # Keep track of the option names to check uniqueness
548
766
  values: list[object] = [] # Keep track of the option values to check uniqueness
@@ -587,12 +805,18 @@ class EnumerationMetaClass(type):
587
805
  logger.debug(
588
806
  " >>> aliased => %s", base_configuration.aliased
589
807
  )
808
+ logger.debug(
809
+ " >>> backfill => %s", base_configuration.backfill
810
+ )
590
811
  logger.debug(
591
812
  " >>> overwritable => %s", base_configuration.overwritable
592
813
  )
593
814
  logger.debug(
594
815
  " >>> subclassable => %s", base_configuration.subclassable
595
816
  )
817
+ logger.debug(
818
+ " >>> extensible => %s", base_configuration.extensible
819
+ )
596
820
  logger.debug(
597
821
  " >>> removable => %s", base_configuration.removable
598
822
  )
@@ -605,13 +829,22 @@ class EnumerationMetaClass(type):
605
829
  logger.debug(
606
830
  " >>> start => %s", base_configuration.start
607
831
  )
832
+ logger.debug(
833
+ " >>> steps => %s", base_configuration.steps
834
+ )
835
+ logger.debug(
836
+ " >>> times => %s", base_configuration.times
837
+ )
608
838
  logger.debug(
609
839
  " >>> typecast => %s", base_configuration.typecast
610
840
  )
611
841
 
612
- if base_configuration.subclassable is False:
842
+ if (
843
+ base_configuration.subclassable is False
844
+ or base_configuration.extensible is False
845
+ ):
613
846
  raise EnumerationSubclassingError(
614
- "The '%s' enumeration class cannot be subclassed when the keyword argument 'subclassable=False' was passed to the class constructor!"
847
+ "The '%s' enumeration class cannot be subclassed when the keyword arguments 'subclassable=False' or 'extensible=False` are passed to the class constructor!"
615
848
  % (base.__name__)
616
849
  )
617
850
 
@@ -626,6 +859,9 @@ class EnumerationMetaClass(type):
626
859
  logger.debug(
627
860
  " >>> (updated) aliased => %s", configuration.aliased
628
861
  )
862
+ logger.debug(
863
+ " >>> (updated) backfill => %s", configuration.backfill
864
+ )
629
865
  logger.debug(
630
866
  " >>> (updated) overwritable => %s",
631
867
  configuration.overwritable,
@@ -634,6 +870,10 @@ class EnumerationMetaClass(type):
634
870
  " >>> (updated) subclassable => %s",
635
871
  configuration.subclassable,
636
872
  )
873
+ logger.debug(
874
+ " >>> (updated) extensible => %s",
875
+ configuration.extensible,
876
+ )
637
877
  logger.debug(
638
878
  " >>> (updated) removable => %s", configuration.removable
639
879
  )
@@ -646,6 +886,12 @@ class EnumerationMetaClass(type):
646
886
  logger.debug(
647
887
  " >>> (updated) start => %s", configuration.start
648
888
  )
889
+ logger.debug(
890
+ " >>> (updated) steps => %s", configuration.steps
891
+ )
892
+ logger.debug(
893
+ " >>> (updated) times => %s", configuration.times
894
+ )
649
895
  logger.debug(
650
896
  " >>> (updated) typecast => %s", configuration.typecast
651
897
  )
@@ -653,7 +899,6 @@ class EnumerationMetaClass(type):
653
899
  # logger.debug(" >>> found base (%s) that is an instance of EnumerationMetaClass and a subclass of Enumeration" % (base))
654
900
 
655
901
  if not (base is Enumeration or Enumeration in base.__bases__):
656
- # enumerations = base._enumerations # reference to the _enumerations dictionary
657
902
  _enumerations = base._enumerations
658
903
 
659
904
  logger.debug(" >>> enumerations => %s" % (base._enumerations))
@@ -675,29 +920,37 @@ class EnumerationMetaClass(type):
675
920
  configuration.defaults(
676
921
  unique=True,
677
922
  aliased=False,
923
+ backfill=False,
678
924
  overwritable=False,
679
925
  subclassable=True,
926
+ extensible=True,
680
927
  removable=False,
681
928
  raises=False,
682
929
  flags=False,
683
930
  start=1,
931
+ steps=1,
932
+ times=None,
684
933
  typecast=True,
685
934
  )
686
935
 
687
936
  logger.debug(" >>> (after defaults) unique => %s", configuration.unique)
688
937
  logger.debug(" >>> (after defaults) aliased => %s", configuration.aliased)
938
+ logger.debug(" >>> (after defaults) backfill => %s", configuration.backfill)
689
939
  logger.debug(
690
940
  " >>> (after defaults) overwritable => %s", configuration.overwritable
691
941
  )
692
942
  logger.debug(
693
943
  " >>> (after defaults) subclassable => %s", configuration.subclassable
694
944
  )
945
+ logger.debug(" >>> (after defaults) extensible => %s", configuration.extensible)
695
946
  logger.debug(
696
947
  " >>> (after defaults) removable => %s", configuration.removable
697
948
  )
698
949
  logger.debug(" >>> (after defaults) raises => %s", configuration.raises)
699
950
  logger.debug(" >>> (after defaults) flags => %s", configuration.flags)
700
951
  logger.debug(" >>> (after defaults) start => %s", configuration.start)
952
+ logger.debug(" >>> (after defaults) steps => %s", configuration.steps)
953
+ logger.debug(" >>> (after defaults) times => %s", configuration.times)
701
954
  logger.debug(" >>> (after defaults) typecast => %s", configuration.typecast)
702
955
 
703
956
  # Iterate over the class attributes, looking for any enumeration options
@@ -709,6 +962,12 @@ class EnumerationMetaClass(type):
709
962
  % (index, attribute, value, type(value))
710
963
  )
711
964
 
965
+ if isinstance(value, auto):
966
+ annotations[attribute] = value
967
+ elif isinstance(value, anno):
968
+ annotations[attribute] = value
969
+ value = value.unwrap() # unwrap the annotated value
970
+
712
971
  if attribute.startswith("_") or attribute in cls._special:
713
972
  continue
714
973
  elif attribute in names:
@@ -730,8 +989,8 @@ class EnumerationMetaClass(type):
730
989
  )
731
990
  else:
732
991
  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)
992
+ "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!"
993
+ % (attribute, value, values)
735
994
  )
736
995
  else:
737
996
  logger.debug(
@@ -763,6 +1022,8 @@ class EnumerationMetaClass(type):
763
1022
  if isinstance(_enumerations, dict):
764
1023
  attributes["base_enumerations"] = _enumerations
765
1024
 
1025
+ attributes["annotations"] = annotations
1026
+
766
1027
  # If the new enumeration class is not subclassing an existing enumeration class
767
1028
  if configuration.typecast is True and (
768
1029
  (baseclass is None) or (baseclass is Enumeration)
@@ -815,31 +1076,26 @@ class EnumerationMetaClass(type):
815
1076
 
816
1077
  logger.debug(" >>> bases => %s", [base for base in bases])
817
1078
 
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
1079
  args: tuple[object] = (name, bases, attributes)
823
1080
 
824
1081
  # Create the new enumeration class instance
825
1082
  instance = super().__new__(cls, *args, **kwargs)
826
1083
 
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
1084
  logger.debug(" >>> baseclass => %s", baseclass)
834
1085
  logger.debug(" >>> instance => %s", instance)
835
1086
 
836
1087
  logger.debug(" >>> unique => %s", configuration.unique)
837
1088
  logger.debug(" >>> aliased => %s", configuration.aliased)
1089
+ logger.debug(" >>> backfill => %s", configuration.backfill)
838
1090
  logger.debug(" >>> overwritable => %s", configuration.overwritable)
839
1091
  logger.debug(" >>> subclassable => %s", configuration.subclassable)
1092
+ logger.debug(" >>> extensible => %s", configuration.extensible)
840
1093
  logger.debug(" >>> removable => %s", configuration.removable)
841
1094
  logger.debug(" >>> raises => %s", configuration.raises)
842
1095
  logger.debug(" >>> flags => %s", configuration.flags)
1096
+ logger.debug(" >>> start => %s", configuration.start)
1097
+ logger.debug(" >>> steps => %s", configuration.steps)
1098
+ logger.debug(" >>> times => %s", configuration.times)
843
1099
  logger.debug(" >>> typecast => %s", configuration.typecast)
844
1100
 
845
1101
  # Store the enumeration class configuration options for future reference
@@ -862,7 +1118,16 @@ class EnumerationMetaClass(type):
862
1118
  )
863
1119
 
864
1120
  if isinstance(base_enumerations := attributes.get("base_enumerations"), dict):
865
- self._enumerations: dict[str, Enumeration] = base_enumerations
1121
+ if (
1122
+ self._configuration.backfill is True
1123
+ and self._configuration.extensible is True
1124
+ ):
1125
+ self._enumerations: dict[str, Enumeration] = base_enumerations
1126
+ else:
1127
+ self._enumerations: dict[str, Enumeration] = {}
1128
+
1129
+ for enumeration_name, enumeration in base_enumerations.items():
1130
+ self._enumerations[enumeration_name] = enumeration
866
1131
  else:
867
1132
  self._enumerations: dict[str, Enumeration] = {}
868
1133
 
@@ -876,6 +1141,8 @@ class EnumerationMetaClass(type):
876
1141
  logger.debug("+" * 100)
877
1142
 
878
1143
  if isinstance(enumerations := attributes.get("enumerations"), dict):
1144
+ annotations: dict[str, anno] = attributes.get("annotations") or {}
1145
+
879
1146
  for attribute, value in enumerations.items():
880
1147
  if attribute in self._enumerations:
881
1148
  continue
@@ -913,6 +1180,7 @@ class EnumerationMetaClass(type):
913
1180
  enumeration=self,
914
1181
  name=attribute,
915
1182
  value=value,
1183
+ annotations=annotations.get(attribute),
916
1184
  )
917
1185
 
918
1186
  logger.debug(
@@ -935,24 +1203,32 @@ class EnumerationMetaClass(type):
935
1203
  elif self._enumerations and name in self._enumerations:
936
1204
  return self._enumerations[name]
937
1205
  else:
1206
+ # EnumerationOptionError subclasses AttributeError so we adhere to convention
938
1207
  raise EnumerationOptionError(
939
- "The '%s' enumeration class, has no '%s' enumeration option!"
1208
+ "The '%s' enumeration class, has no '%s' enumeration option nor annotation property!"
940
1209
  % (self.__name__, name)
941
1210
  )
942
1211
 
943
1212
  def __dir__(self) -> list[str]:
944
- members: list[str] = []
945
-
946
- for name, enumeration in self._enumerations.items():
947
- members.append(name)
1213
+ members: set[str] = set()
948
1214
 
949
1215
  for member in object.__dir__(self):
950
1216
  if member.startswith("_") or member in self._special:
951
- members.append(member)
1217
+ members.add(member)
1218
+
1219
+ for name, enumeration in self._enumerations.items():
1220
+ members.add(name)
952
1221
 
953
- return members
1222
+ for member in vars(self):
1223
+ members.add(member)
1224
+
1225
+ return list(members)
954
1226
 
955
1227
  def __contains__(self, other: Enumeration | object) -> bool:
1228
+ logger.debug(
1229
+ "%s(%s).__contains__(other: %s)", self.__class__.__name__, self, other
1230
+ )
1231
+
956
1232
  contains: bool = False
957
1233
 
958
1234
  for name, enumeration in self._enumerations.items():
@@ -1132,6 +1408,12 @@ class EnumerationMetaClass(type):
1132
1408
  value,
1133
1409
  )
1134
1410
 
1411
+ if self.configuration.extensible is False:
1412
+ raise EnumerationExtensibilityError(
1413
+ "The '%s' enumeration class has been configured to prevent extensibility, so cannot be extended with new options either through registration or subclassing, so the '%s' option cannot be registered!"
1414
+ % (self.__name__, name)
1415
+ )
1416
+
1135
1417
  if self.configuration.overwritable is False and name in self._enumerations:
1136
1418
  raise EnumerationNonUniqueError(
1137
1419
  "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!"
@@ -1175,11 +1457,12 @@ class EnumerationMetaClass(type):
1175
1457
  self,
1176
1458
  value: Enumeration | object = None,
1177
1459
  name: str = None,
1460
+ caselessly: bool = False,
1178
1461
  ) -> Enumeration | None:
1179
1462
  """The 'reconcile' method can be used to reconcile Enumeration type, enumeration
1180
1463
  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."""
1464
+ match is found the Enumeration type instance will be returned otherwise None will
1465
+ be returned, unless the class is configured to raise an error for mismatches."""
1183
1466
 
1184
1467
  if name is None and value is None:
1185
1468
  raise ValueError(
@@ -1197,14 +1480,20 @@ class EnumerationMetaClass(type):
1197
1480
  reconciled: Enumeration = None
1198
1481
 
1199
1482
  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):
1483
+ if isinstance(value, Enumeration):
1204
1484
  if enumeration is value:
1205
1485
  reconciled = enumeration
1206
1486
  break
1207
- elif isinstance(value, str) and enumeration.name == value:
1487
+ elif isinstance(name, str) and (
1488
+ (enumeration.name == name)
1489
+ or (caselessly and (enumeration.name.casefold() == name.casefold()))
1490
+ ):
1491
+ reconciled = enumeration
1492
+ break
1493
+ elif isinstance(value, str) and (
1494
+ (enumeration.name == value)
1495
+ or (caselessly and (enumeration.name.casefold() == value.casefold()))
1496
+ ):
1208
1497
  reconciled = enumeration
1209
1498
  break
1210
1499
  elif enumeration.value == value:
@@ -1229,6 +1518,14 @@ class EnumerationMetaClass(type):
1229
1518
  )
1230
1519
  )
1231
1520
 
1521
+ # When an enumeration option is reconciled, it may be defined in another class
1522
+ # but have been accessed through a subclass; in order for attribute lookups to
1523
+ # work within the subclass, we need to provide the current lookup context to the
1524
+ # reconciled enumeration option, so that any attribute access on this object can
1525
+ # perform their lookup in the correct part of the class hierarchy
1526
+ if isinstance(reconciled, Enumeration):
1527
+ reconciled._context = self
1528
+
1232
1529
  return reconciled
1233
1530
 
1234
1531
  def validate(self, value: Enumeration | object = None, name: str = None) -> bool:
@@ -1238,13 +1535,20 @@ class EnumerationMetaClass(type):
1238
1535
 
1239
1536
  return not self.reconcile(value=value, name=name) is None
1240
1537
 
1538
+ def options(self) -> MappingProxyType[str, Enumeration]:
1539
+ """The 'options' method returns a read-only mapping proxy of the options."""
1540
+
1541
+ return MappingProxyType(self._enumerations)
1542
+
1241
1543
 
1242
1544
  class Enumeration(metaclass=EnumerationMetaClass):
1243
1545
  """The Enumeration class is the subclass of all enumerations and their subtypes."""
1244
1546
 
1245
1547
  _metaclass: EnumerationMetaClass = None
1548
+ _context: EnumerationMetaClass = None
1246
1549
  _enumeration: Enumeration = None
1247
1550
  _enumerations: dict[str, Enumeration] = None
1551
+ _annotations: anno = None
1248
1552
  _name: str = None
1249
1553
  _value: object = None
1250
1554
  _aliased: Enumeration = None
@@ -1257,6 +1561,7 @@ class Enumeration(metaclass=EnumerationMetaClass):
1257
1561
  name: str = None,
1258
1562
  value: object = None,
1259
1563
  aliased: Enumeration = None,
1564
+ annotations: anno = None,
1260
1565
  **kwargs,
1261
1566
  ) -> Enumeration | None:
1262
1567
  # Supports reconciling enumeration options via their name/value via __new__ call
@@ -1264,12 +1569,14 @@ class Enumeration(metaclass=EnumerationMetaClass):
1264
1569
  value = args[0]
1265
1570
 
1266
1571
  logger.debug(
1267
- "[Enumeration] %s.__new__(args: %s, enumeration: %s, name: %s, value: %s, kwargs: %s)",
1572
+ "[Enumeration] %s.__new__(args: %s, enumeration: %s, name: %s, value: %s, aliased: %s, annotations: %s, kwargs: %s)",
1268
1573
  cls.__name__,
1269
1574
  args,
1270
1575
  enumeration,
1271
1576
  name,
1272
1577
  value,
1578
+ aliased,
1579
+ annotations,
1273
1580
  kwargs,
1274
1581
  )
1275
1582
 
@@ -1298,15 +1605,18 @@ class Enumeration(metaclass=EnumerationMetaClass):
1298
1605
  name: str = None,
1299
1606
  value: object = None,
1300
1607
  aliased: Enumeration = None,
1608
+ annotations: anno = None,
1301
1609
  **kwargs,
1302
1610
  ) -> None:
1303
1611
  logger.debug(
1304
- "[Enumeration] %s.__init__(args: %s, enumeration: %s, name: %s, value: %s, kwargs: %s)",
1612
+ "[Enumeration] %s.__init__(args: %s, enumeration: %s, name: %s, value: %s, aliased: %s, annotations: %s, kwargs: %s)",
1305
1613
  self.__class__.__name__,
1306
1614
  args,
1307
1615
  enumeration,
1308
1616
  name,
1309
1617
  value,
1618
+ aliased,
1619
+ annotations,
1310
1620
  kwargs,
1311
1621
  )
1312
1622
 
@@ -1340,6 +1650,15 @@ class Enumeration(metaclass=EnumerationMetaClass):
1340
1650
  "The 'aliased' argument, if specified, must reference an Enumeration class instance!"
1341
1651
  )
1342
1652
 
1653
+ if annotations is None:
1654
+ pass
1655
+ elif isinstance(annotations, anno):
1656
+ self._annotations = annotations
1657
+ else:
1658
+ raise TypeError(
1659
+ "The 'annotations' argument, if specified, must reference an anno class instance!"
1660
+ )
1661
+
1343
1662
  # NOTE: This method is only called if the instance is called via instance(..) syntax
1344
1663
  def __call__(self, *args, **kwargs) -> Enumeration | None:
1345
1664
  logger.debug(
@@ -1361,32 +1680,51 @@ class Enumeration(metaclass=EnumerationMetaClass):
1361
1680
  return id(self)
1362
1681
 
1363
1682
  def __eq__(self, other: Enumeration | object) -> bool:
1364
- logger.debug("%s.__eq__(other: %s)" % (self.__class__.__name__, other))
1683
+ logger.debug("%s(%s).__eq__(other: %s)", self.__class__.__name__, self, other)
1365
1684
 
1366
1685
  equals: bool = False
1367
1686
 
1368
1687
  if isinstance(other, Enumeration):
1369
- if self is other:
1370
- return True
1688
+ equals = self is other
1689
+ elif self.name == other:
1690
+ equals = True
1691
+ elif self.value == other:
1692
+ equals = True
1371
1693
 
1372
- for attribute, enumeration in self._enumerations.items():
1373
- logger.info(
1374
- "%s.__eq__(other: %s) enumeration => %s"
1375
- % (self.__class__.__name__, other, enumeration)
1694
+ return equals
1695
+
1696
+ def __getattr__(self, name) -> object:
1697
+ """The '__getattr__' method provides support for accessing attribute values that
1698
+ have been assigned to the current enumeration option. If a matching attribute can
1699
+ be found, its value will be returned, otherwise an exception will be raised."""
1700
+
1701
+ logger.debug("%s.__getattr__(name: %s)", self.__class__.__name__, name)
1702
+
1703
+ if name.startswith("_") or name in self.__class__._special or name in dir(self):
1704
+ return object.__getattribute__(self, name)
1705
+ elif self._enumerations and name in self._enumerations:
1706
+ return self._enumerations[name]
1707
+ elif self._annotations and name in self._annotations:
1708
+ return self._annotations[name]
1709
+ elif self._context and name in dir(self._context):
1710
+ return object.__getattribute__(self._context, name)
1711
+ else:
1712
+ # EnumerationOptionError subclasses AttributeError so we adhere to convention
1713
+ raise EnumerationOptionError(
1714
+ "The '%s' enumeration class, has no '%s' enumeration option nor annotation property!"
1715
+ % (self.__class__.__name__, name)
1376
1716
  )
1377
1717
 
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
1718
+ def get(self, name: str, default: object = None) -> object | None:
1719
+ """The 'get' method provides support for accessing annotation values that may
1720
+ have been assigned to the current enumeration option. If a matching annotation
1721
+ can be found, its value will be returned, otherwise the default value will be
1722
+ returned, which defaults to None, but may be specified as any value."""
1388
1723
 
1389
- return equals
1724
+ if name in self._annotations:
1725
+ return self._annotations[name]
1726
+ else:
1727
+ return default
1390
1728
 
1391
1729
  @property
1392
1730
  def enumeration(self) -> Enumeration:
@@ -1407,21 +1745,43 @@ class Enumeration(metaclass=EnumerationMetaClass):
1407
1745
  @property
1408
1746
  def aliased(self) -> bool:
1409
1747
  logger.debug(
1410
- "%s.aliased() >>> id(Colors._enumerations) => %s (%s)",
1748
+ "%s.aliased() >>> id(%s) => %s (%s)",
1411
1749
  self.__class__.__name__,
1750
+ self,
1412
1751
  id(self._enumerations),
1413
1752
  type(self._enumerations),
1414
1753
  )
1415
1754
 
1416
1755
  for name, enumeration in self._enumerations.items():
1417
- logger.info(" >>> checking for alias: %s => %s", name, enumeration)
1756
+ logger.debug(" >>> checking for alias: %s => %s", name, enumeration)
1418
1757
 
1419
1758
  if isinstance(enumeration, Enumeration):
1420
- if name != enumeration.name:
1759
+ if self is enumeration and enumeration.name != name:
1421
1760
  return True
1422
1761
 
1423
1762
  return False
1424
1763
 
1764
+ @property
1765
+ def aliases(self) -> list[Enumeration]:
1766
+ logger.debug(
1767
+ "%s.aliases() >>> id(%s) => %s (%s)",
1768
+ self.__class__.__name__,
1769
+ self,
1770
+ id(self._enumerations),
1771
+ type(self._enumerations),
1772
+ )
1773
+
1774
+ aliases: list[Enumeration] = []
1775
+
1776
+ for name, enumeration in self._enumerations.items():
1777
+ logger.debug(" >>> checking for alias: %s => %s", name, enumeration)
1778
+
1779
+ if isinstance(enumeration, Enumeration):
1780
+ if self is enumeration and enumeration.name != name:
1781
+ aliases.append(enumeration)
1782
+
1783
+ return aliases
1784
+
1425
1785
 
1426
1786
  class EnumerationType(Enumeration, typecast=False):
1427
1787
  """The EnumerationType class represents the type of value held by an enumeration."""
@@ -1462,6 +1822,15 @@ class EnumerationInteger(int, Enumeration):
1462
1822
  def __repr__(self) -> str:
1463
1823
  return Enumeration.__repr__(self)
1464
1824
 
1825
+ def __hash__(self) -> id:
1826
+ return super().__hash__()
1827
+
1828
+ def __eq__(self, other: object) -> bool:
1829
+ if isinstance(other, (int, self.__class__)):
1830
+ return super().__eq__(other)
1831
+ else:
1832
+ return Enumeration.__eq__(self, other)
1833
+
1465
1834
 
1466
1835
  class EnumerationFloat(float, Enumeration):
1467
1836
  """An Enumeration subclass where all values are float values."""
@@ -1505,6 +1874,15 @@ class EnumerationComplex(complex, Enumeration):
1505
1874
  def __repr__(self) -> str:
1506
1875
  return Enumeration.__repr__(self)
1507
1876
 
1877
+ def __hash__(self) -> id:
1878
+ return super().__hash__()
1879
+
1880
+ def __eq__(self, other: object) -> bool:
1881
+ if isinstance(other, (float, self.__class__)):
1882
+ return super().__eq__(other)
1883
+ else:
1884
+ return Enumeration.__eq__(self, other)
1885
+
1508
1886
 
1509
1887
  class EnumerationString(str, Enumeration):
1510
1888
  """An Enumeration subclass where all values are string values."""
@@ -1528,6 +1906,15 @@ class EnumerationString(str, Enumeration):
1528
1906
  def __repr__(self) -> str:
1529
1907
  return Enumeration.__repr__(self)
1530
1908
 
1909
+ def __hash__(self) -> id:
1910
+ return super().__hash__()
1911
+
1912
+ def __eq__(self, other: object) -> bool:
1913
+ if isinstance(other, (str, self.__class__)):
1914
+ return super().__eq__(other)
1915
+ else:
1916
+ return Enumeration.__eq__(self, other)
1917
+
1531
1918
 
1532
1919
  class EnumerationBytes(bytes, Enumeration):
1533
1920
  """An Enumeration subclass where all values are bytes values."""
@@ -1548,6 +1935,15 @@ class EnumerationBytes(bytes, Enumeration):
1548
1935
  def __repr__(self) -> str:
1549
1936
  return Enumeration.__repr__(self)
1550
1937
 
1938
+ def __hash__(self) -> id:
1939
+ return super().__hash__()
1940
+
1941
+ def __eq__(self, other: object) -> bool:
1942
+ if isinstance(other, (bytes, self.__class__)):
1943
+ return super().__eq__(other)
1944
+ else:
1945
+ return Enumeration.__eq__(self, other)
1946
+
1551
1947
 
1552
1948
  class EnumerationTuple(tuple, Enumeration):
1553
1949
  """An Enumeration subclass where all values are tuple values."""
@@ -1568,6 +1964,15 @@ class EnumerationTuple(tuple, Enumeration):
1568
1964
  def __repr__(self) -> str:
1569
1965
  return Enumeration.__repr__(self)
1570
1966
 
1967
+ def __hash__(self) -> id:
1968
+ return super().__hash__()
1969
+
1970
+ def __eq__(self, other: object) -> bool:
1971
+ if isinstance(other, (tuple, self.__class__)):
1972
+ return super().__eq__(other)
1973
+ else:
1974
+ return Enumeration.__eq__(self, other)
1975
+
1571
1976
 
1572
1977
  class EnumerationSet(set, Enumeration):
1573
1978
  """An Enumeration subclass where all values are set values."""
@@ -1588,6 +1993,15 @@ class EnumerationSet(set, Enumeration):
1588
1993
  def __repr__(self) -> str:
1589
1994
  return Enumeration.__repr__(self)
1590
1995
 
1996
+ def __hash__(self) -> id:
1997
+ return super().__hash__()
1998
+
1999
+ def __eq__(self, other: object) -> bool:
2000
+ if isinstance(other, (set, self.__class__)):
2001
+ return super().__eq__(other)
2002
+ else:
2003
+ return Enumeration.__eq__(self, other)
2004
+
1591
2005
 
1592
2006
  class EnumerationList(list, Enumeration):
1593
2007
  """An Enumeration subclass where all values are list values."""
@@ -1608,6 +2022,15 @@ class EnumerationList(list, Enumeration):
1608
2022
  def __repr__(self) -> str:
1609
2023
  return Enumeration.__repr__(self)
1610
2024
 
2025
+ def __hash__(self) -> id:
2026
+ return super().__hash__()
2027
+
2028
+ def __eq__(self, other: object) -> bool:
2029
+ if isinstance(other, (list, self.__class__)):
2030
+ return super().__eq__(other)
2031
+ else:
2032
+ return Enumeration.__eq__(self, other)
2033
+
1611
2034
 
1612
2035
  class EnumerationDictionary(dict, Enumeration):
1613
2036
  """An Enumeration subclass where all values are dictionary values."""
@@ -1631,6 +2054,15 @@ class EnumerationDictionary(dict, Enumeration):
1631
2054
  def __repr__(self) -> str:
1632
2055
  return Enumeration.__repr__(self)
1633
2056
 
2057
+ def __hash__(self) -> id:
2058
+ return super().__hash__()
2059
+
2060
+ def __eq__(self, other: object) -> bool:
2061
+ if isinstance(other, (dict, self.__class__)):
2062
+ return super().__eq__(other)
2063
+ else:
2064
+ return Enumeration.__eq__(self, other)
2065
+
1634
2066
 
1635
2067
  class EnumerationFlag(int, Enumeration):
1636
2068
  """An Enumeration subclass where all values are integer values to the power of 2."""
@@ -1772,6 +2204,15 @@ class EnumerationFlag(int, Enumeration):
1772
2204
  def __repr__(self) -> str:
1773
2205
  return Enumeration.__repr__(self)
1774
2206
 
2207
+ def __hash__(self) -> id:
2208
+ return super().__hash__()
2209
+
2210
+ def __eq__(self, other: object) -> bool:
2211
+ if isinstance(other, (int, self.__class__)):
2212
+ return super().__eq__(other)
2213
+ else:
2214
+ return Enumeration.__eq__(self, other)
2215
+
1775
2216
  def __or__(self, other: EnumerationFlag): # called for: "a | b" (bitwise or)
1776
2217
  """Support performing a bitwise or between the current EnumerationFlag
1777
2218
  instance's bitmask and the 'other' provided EnumerationFlag's bitmask;