Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/cardinal_pythonlib/enumlike.py : 61%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
2# cardinal_pythonlib/enumlike.py
4"""
5===============================================================================
7 Original code copyright (C) 2009-2021 Rudolf Cardinal (rudolf@pobox.com).
9 This file is part of cardinal_pythonlib.
11 Licensed under the Apache License, Version 2.0 (the "License");
12 you may not use this file except in compliance with the License.
13 You may obtain a copy of the License at
15 https://www.apache.org/licenses/LICENSE-2.0
17 Unless required by applicable law or agreed to in writing, software
18 distributed under the License is distributed on an "AS IS" BASIS,
19 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 See the License for the specific language governing permissions and
21 limitations under the License.
23===============================================================================
25**Enum-based classes**
27See https://docs.python.org/3/library/enum.html.
29The good things about enums are:
31- they are immutable
32- they are "class-like", not "instance-like"
33- they can be accessed via attribute (like an object) or item (like a dict):
34- you can add a ``@unique`` decorator to ensure no two have the same value
35- IDEs know about them
37``AttrDict``'s disadvantages are:
39- more typing / DRY
40- IDEs don't know about them
42Plain old objects:
44- not immutable
45- no dictionary access -- though can use ``getattr()``
46- but otherwise simpler than enums
48LowerCaseAutoStringObject:
50- IDEs don't understand their values, so get types wrong
52.. code-block:: python
54 from enum import Enum
56 class Colour(Enum):
57 red = 1
58 green = 2
59 blue = 3
61 Colour.red # <Colour.red: 1>
62 Colour.red.name # 'red'
63 Colour.red.value # 1
64 Colour['red'] # <Colour.red: 1>
66 Colour.red = 4 # AttributeError: Cannot reassign members.
68Then, for fancier things below, note that:
70.. code-block:: none
72 metaclass
73 __prepare__(mcs, name, bases)
74 ... prepares (creates) the class namespace
75 ... use if you don't want the namespace to be a plain dict()
76 ... https://docs.python.org/3/reference/datamodel.html
77 ... returns the (empty) namespace
78 __new__(mcs, name, bases, namespace)
79 ... called with the populated namespace
80 ... makes and returns the class object, cls
82 class
83 __new__(cls)
84 ... controls the creation of a new instance; static classmethod
85 ... makes self
87 __init__(self)
88 ... controls the initialization of an instance
91"""
93import collections
94from collections import OrderedDict
95# noinspection PyProtectedMember
96from enum import EnumMeta, Enum, _EnumDict
97import itertools
98from typing import Any, List, Optional, Tuple, Type
100from cardinal_pythonlib.logs import get_brace_style_log_with_null_handler
101from cardinal_pythonlib.reprfunc import ordered_repr
103log = get_brace_style_log_with_null_handler(__name__)
106# =============================================================================
107# Enum-based classes
108# =============================================================================
110STR_ENUM_FWD_REF = "StrEnum"
111# class name forward reference for type checker:
112# http://mypy.readthedocs.io/en/latest/kinds_of_types.html
113# ... but also: a variable (rather than a string literal) stops PyCharm giving
114# the curious error "PEP 8: no newline at end of file" and pointing to the
115# type hint string literal.
118class StrEnum(Enum):
119 """
120 StrEnum:
122 - makes ``str(myenum.x)`` give ``str(myenum.x.value)``
123 - adds a lookup function (from a string literal)
124 - adds ordering by value
126 """
127 def __str__(self) -> str:
128 return str(self.value)
130 @classmethod
131 def lookup(cls,
132 value: Any,
133 allow_none: bool = False) -> Optional[STR_ENUM_FWD_REF]:
134 for item in cls:
135 if value == item.value:
136 return item
137 if not value and allow_none:
138 return None
139 raise ValueError(
140 f"Value {value!r} not found in enum class {cls.__name__}")
142 def __lt__(self, other: STR_ENUM_FWD_REF) -> bool:
143 return str(self) < str(other)
146# -----------------------------------------------------------------------------
147# AutoStrEnum
148# -----------------------------------------------------------------------------
150class AutoStrEnumMeta(EnumMeta):
151 # noinspection PyInitNewSignature
152 def __new__(mcs, cls, bases, oldclassdict):
153 """
154 Scan through ``oldclassdict`` and convert any value that is a plain
155 tuple into a ``str`` of the name instead.
156 """
157 newclassdict = _EnumDict()
158 for k, v in oldclassdict.items():
159 if v == ():
160 v = k
161 newclassdict[k] = v
162 return super().__new__(mcs, cls, bases, newclassdict)
165class AutoStrEnum(str,
166 StrEnum, # was Enum,
167 metaclass=AutoStrEnumMeta):
168 """
169 Base class for ``name=value`` ``str`` enums.
171 Example:
173 .. code-block:: python
175 class Animal(AutoStrEnum):
176 horse = ()
177 dog = ()
178 whale = ()
180 print(Animal.horse)
181 print(Animal.horse == 'horse')
182 print(Animal.horse.name, Animal.horse.value)
184 See
185 https://stackoverflow.com/questions/32214614/automatically-setting-an-enum-members-value-to-its-name/32215467
186 and then inherit from :class:`StrEnum` rather than :class:`Enum`.
188 """ # noqa
189 pass
192# -----------------------------------------------------------------------------
193# LowerCaseAutoStrEnumMeta
194# -----------------------------------------------------------------------------
196class LowerCaseAutoStrEnumMeta(EnumMeta):
197 # noinspection PyInitNewSignature
198 def __new__(mcs, cls, bases, oldclassdict):
199 """
200 Scan through ``oldclassdict`` and convert any value that is a plain
201 tuple into a ``str`` of the name instead.
202 """
203 newclassdict = _EnumDict()
204 for k, v in oldclassdict.items():
205 if v == ():
206 v = k.lower()
207 if v in newclassdict.keys():
208 raise ValueError(f"Duplicate value caused by key {k}")
209 newclassdict[k] = v
210 return super().__new__(mcs, cls, bases, newclassdict)
213class LowerCaseAutoStrEnum(str, StrEnum, metaclass=LowerCaseAutoStrEnumMeta):
214 """
215 Base class for ``name=value`` ``str`` enums, forcing lower-case values.
217 Example:
219 .. code-block:: python
221 class AnimalLC(LowerCaseAutoStrEnum):
222 Horse = ()
223 Dog = ()
224 Whale = ()
226 print(AnimalLC.Horse)
227 print(AnimalLC.Horse == 'horse')
228 print(AnimalLC.Horse.name, AnimalLC.Horse.value)
230 """
231 pass
234# -----------------------------------------------------------------------------
235# AutoNumberEnum
236# -----------------------------------------------------------------------------
238class AutoNumberEnum(Enum):
239 """
240 As per https://docs.python.org/3/library/enum.html (in which, called
241 AutoNumber).
243 Usage:
245 .. code-block:: python
247 class Color(AutoNumberEnum):
248 red = ()
249 green = ()
250 blue = ()
252 Color.green.value == 2 # True
253 """
254 def __new__(cls):
255 value = len(cls.__members__) + 1 # will be numbered from 1
256 obj = object.__new__(cls)
257 obj._value_ = value
258 return obj
261# -----------------------------------------------------------------------------
262# AutoNumberObject
263# -----------------------------------------------------------------------------
265class AutoNumberObjectMetaClass(type):
266 @classmethod
267 def __prepare__(mcs, name, bases): # mcs: was metaclass
268 """
269 Called when AutoEnum (below) is defined, prior to ``__new__``, with:
271 .. code-block:: python
273 name = 'AutoEnum'
274 bases = ()
275 """
276 # print("__prepare__: name={}, bases={}".format(
277 # repr(name), repr(bases)))
278 return collections.defaultdict(itertools.count().__next__)
280 # noinspection PyInitNewSignature
281 def __new__(mcs, name, bases, classdict): # mcs: was cls
282 """
283 Called when AutoEnum (below) is defined, with:
285 .. code-block:: python
287 name = 'AutoEnum'
288 bases = ()
289 classdict = defaultdict(<method-wrapper '__next__' of itertools.count
290 object at 0x7f7d8fc5f648>,
291 {
292 '__doc__': '... a docstring... ',
293 '__qualname__': 'AutoEnum',
294 '__name__': 0,
295 '__module__': 0
296 }
297 )
298 """ # noqa
299 # print("__new__: name={}, bases={}, classdict={}".format(
300 # repr(name), repr(bases), repr(classdict)))
301 cls = type.__new__(mcs, name, bases, dict(classdict))
302 return cls # cls: was result
305class AutoNumberObject(metaclass=AutoNumberObjectMetaClass):
306 """
307 From comment by Duncan Booth at
308 http://www.acooke.org/cute/Pythonssad0.html, with trivial rename.
310 Usage:
312 .. code-block:: python
314 class MyThing(AutoNumberObject):
315 a
316 b
318 MyThing.a
319 # 1
320 MyThing.b
321 # 2
322 """
323 pass
326# -----------------------------------------------------------------------------
327# LowerCaseAutoStringObject
328# -----------------------------------------------------------------------------
329# RNC. We need a defaultdict that does the job...
330# Or similar. But the defaultdict argument function receives no parameters,
331# so it can't read the key. Therefore:
333class LowerCaseAutoStringObjectMetaClass(type):
334 @classmethod
335 def __prepare__(mcs, name, bases):
336 return collections.defaultdict(int) # start with all values as 0
338 # noinspection PyInitNewSignature
339 def __new__(mcs, name, bases, classdict):
340 for k in classdict.keys():
341 if k.startswith('__'): # e.g. __qualname__, __name__, __module__
342 continue
343 value = k.lower()
344 if value in classdict.values():
345 raise ValueError(f"Duplicate value from key: {k}")
346 classdict[k] = value
347 cls = type.__new__(mcs, name, bases, dict(classdict))
348 return cls
351class LowerCaseAutoStringObject(metaclass=LowerCaseAutoStringObjectMetaClass):
352 """
353 Usage:
355 .. code-block:: python
357 class Wacky(LowerCaseAutoStringObject):
358 Thing # or can use Thing = () to avoid IDE complaints
359 OtherThing = ()
361 Wacky.Thing # 'thing'
362 Wacky.OtherThing # 'otherthing'
363 """
364 pass
367# -----------------------------------------------------------------------------
368# AutoStringObject
369# -----------------------------------------------------------------------------
370# RNC. We need a defaultdict that does the job...
371# Or similar. But the defaultdict argument function receives no parameters,
372# so it can't read the key. Therefore:
374class AutoStringObjectMetaClass(type):
375 @classmethod
376 def __prepare__(mcs, name, bases):
377 return collections.defaultdict(int)
379 # noinspection PyInitNewSignature
380 def __new__(mcs, name, bases, classdict):
381 for k in classdict.keys():
382 if k.startswith('__'): # e.g. __qualname__, __name__, __module__
383 continue
384 classdict[k] = k
385 cls = type.__new__(mcs, name, bases, dict(classdict))
386 return cls
389class AutoStringObject(metaclass=AutoStringObjectMetaClass):
390 """
391 Usage:
393 .. code-block:: python
395 class Fish(AutoStringObject):
396 Thing
397 Blah
399 Fish.Thing # 'Thing'
400 """
401 pass
404# =============================================================================
405# enum: TOO OLD; NAME CLASH; DEPRECATED/REMOVED
406# =============================================================================
408# def enum(**enums: Any) -> Enum:
409# """Enum support, as at https://stackoverflow.com/questions/36932"""
410# return type('Enum', (), enums)
413# =============================================================================
414# AttrDict: DEPRECATED
415# =============================================================================
417class AttrDict(dict):
418 """
419 Dictionary with attribute access; see
420 https://stackoverflow.com/questions/4984647
421 """
422 def __init__(self, *args, **kwargs) -> None:
423 super(AttrDict, self).__init__(*args, **kwargs)
424 self.__dict__ = self
427# =============================================================================
428# OrderedNamespace
429# =============================================================================
430# for attrdict itself: use the attrdict package
432class OrderedNamespace(object):
433 """
434 As per https://stackoverflow.com/questions/455059, modified for
435 ``__init__``.
436 """
437 def __init__(self, *args):
438 super().__setattr__('_odict', OrderedDict(*args))
440 def __getattr__(self, key):
441 odict = super().__getattribute__('_odict')
442 if key in odict:
443 return odict[key]
444 return super().__getattribute__(key)
446 def __setattr__(self, key, val):
447 self._odict[key] = val
449 @property
450 def __dict__(self):
451 return self._odict
453 def __setstate__(self, state): # Support copy.copy
454 super().__setattr__('_odict', OrderedDict())
455 self._odict.update(state)
457 def __eq__(self, other):
458 return self.__dict__ == other.__dict__
460 def __ne__(self, other):
461 return not self.__eq__(other)
463 # Plus more (RNC):
464 def items(self):
465 return self.__dict__.items()
467 def __repr__(self):
468 return ordered_repr(self, self.__dict__.keys())
471# =============================================================================
472# keys_descriptions_from_enum
473# =============================================================================
475def keys_descriptions_from_enum(
476 enum: Type[Enum],
477 sort_keys: bool = False,
478 keys_to_lower: bool = False,
479 keys_to_upper: bool = False,
480 key_to_description: str = ": ",
481 joiner: str = " // ") -> Tuple[List[str], str]:
482 """
483 From an Enum subclass, return (keys, descriptions_as_formatted_string).
484 This is a convenience function used to provide command-line help for
485 options involving a choice of enums from an Enum class.
486 """
487 assert not (keys_to_lower and keys_to_upper)
488 keys = [e.name for e in enum]
489 if keys_to_lower:
490 keys = [k.lower() for k in keys]
491 elif keys_to_upper:
492 keys = [k.upper() for k in keys]
493 if sort_keys:
494 keys.sort()
495 descriptions = [
496 f"{k}{key_to_description}{enum[k].value}"
497 for k in keys
498 ]
499 description_str = joiner.join(descriptions)
500 return keys, description_str
503# =============================================================================
504# EnumLower
505# =============================================================================
507class CaseInsensitiveEnumMeta(EnumMeta):
508 """
509 An Enum that permits lookup by a lower-case version of its keys.
511 https://stackoverflow.com/questions/42658609/how-to-construct-a-case-insensitive-enum
513 Example:
515 .. code-block:: python
517 from enum import Enum
518 from cardinal_pythonlib.enumlike import CaseInsensitiveEnumMeta
520 class TestEnum(Enum, metaclass=CaseInsensitiveEnumMeta):
521 REDAPPLE = 1
522 greenapple = 2
523 PineApple = 3
525 >>> TestEnum["REDAPPLE"]
526 <TestEnum.REDAPPLE: 1>
527 >>> TestEnum["redapple"]
528 <TestEnum.REDAPPLE: 1>
529 >>> TestEnum["greenapple"]
530 <TestEnum.greenapple: 2>
531 >>> TestEnum["greenappLE"]
532 <TestEnum.greenapple: 2>
533 >>> TestEnum["PineApple"]
534 <TestEnum.PineApple: 3>
535 >>> TestEnum["PineApplE"]
536 <TestEnum.PineApple: 3>
538 """ # noqa
539 def __getitem__(self, item: Any) -> Any:
540 if isinstance(item, str):
541 item_lower = item.lower()
542 for member in self:
543 if member.name.lower() == item_lower:
544 return member
545 return super().__getitem__(item)