├── .gitignore ├── LICENSE ├── README.md ├── examples.py ├── fast_enum ├── __init__.py └── fastenum.py ├── setup.py └── test ├── __init__.py └── test_01_fastenum.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | .idea/ 107 | *.iml 108 | manage.sh 109 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Andrey Semenov, Qrator Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fast_enum 2 | A fast Enum implementation for Python 3 | 4 | The purpose is to replace Python3.4+ standard library Enum. 5 | 6 | The main objective to using standard library's Enum is that it's super slow. 7 | 8 | Features implemented: 9 | - as in stdlib's enums all Enum members are instances of the Enum itself 10 | ```python 11 | type(LightEnum.ONE) 12 | # 13 | ``` 14 | - all enum members have at least `name` and `value` properties; the `name` property 15 | is a string containing the class member's name itself; the `value` property contains the value 16 | assigned 17 | ```python 18 | class ValuesGivenEnum(metaclass=FastEnum): 19 | ONE: 'ValuesGivenEnum' = 1 20 | FOUR: 'ValuesGivenEnum' = 4 21 | ELEVEN: 'ValuesGivenEnum' = 11 22 | 23 | ValuesGivenEnum.FOUR 24 | # 25 | ValuesGivenEnum.FOUR.value 26 | # 4 27 | ValuesGivenEnum.FOUR.name 28 | # 'FOUR' 29 | ``` 30 | - a lightweight form of enum declaration is possible 31 | ```python 32 | class LightEnum(metaclass=FastEnum): 33 | ONE: 'LightEnum' 34 | TWO: 'LightEnum' 35 | THREE: 'LightEnum' 36 | ``` 37 | When this form is used it is strictly required that members are "type-hinted" 38 | as instances of the enum class. Otherwise they will remain just as property/attributes 39 | annotations deep inside the `cls.__annotations__` 40 | 41 | - an enum could be accessed by value 42 | ```python 43 | LightEnum(1) 44 | # 45 | ``` 46 | - or by name 47 | ```python 48 | LightEnum['ONE'] 49 | # 50 | ``` 51 | 52 | - it is possible to mix lightweight declaration and a value-provided one in the same class: 53 | ```python 54 | class MixedEnum(metaclass=FastEnum): 55 | _ZERO_VALUED = 1 56 | AUTO_ZERO: 'MixedEnum' 57 | ONE: 'MixedEnum' = 1 58 | AUTO_ONE: 'MixedEnum' 59 | TWO: 'MixedEnum' = 2 60 | 61 | MixedEnum(1) 62 | # 63 | MixedEnum.AUTO_ZERO 64 | # 65 | MixedEnum.AUTO_ONE 66 | # 67 | ``` 68 | When this form is used, if there are more than one Enum with the same value as a result (`MixedEnum.AUTO_ONE.value` 69 | and `MixedEnum.ONE.value` in this example) all subsequent enums are rendered as just aliases to the first declared 70 | (the order of declaration is: first value-provided enums then lightweight forms so auto-valued will always become 71 | aliases, not vice versa). The auto-valued enums value provider is independent from value-provided ones. 72 | 73 | - as shown in the previous example, a special attribute `_ZERO_VALUED` could be provided in class declaration; 74 | given it's value renders to `True` in boolean context auto-valued enums will start from zero instead of integer 1; 75 | The `_ZERO_VALUED` attribute is erased from the resulting enum type 76 | 77 | - an enum creation can be hooked with 'late-init'. If a special method `def __init_late(self): ...` is provided within 78 | enum class' declaration, it's run for every enum instance created after all of them are created successfully 79 | ```python 80 | class HookedEnum(metaclass=FastEnum): 81 | halved_value: 'HookedEnum' 82 | 83 | __slots__ = ('halved_value',) 84 | 85 | def __init_late(self): 86 | # noinspection PyTypeChecker 87 | self.halved_value: 'HookedEnum' = self.__class__(self.value // 2) 88 | 89 | ZERO: 'HookedEnum' = 0 90 | ONE: 'HookedEnum' = 1 91 | TWO: 'HookedEnum' = 2 92 | THREE: 'HookedEnum' = 3 93 | 94 | HookedEnum.ZERO.halved_value 95 | # 96 | HookedEnum.ONE.halved_value 97 | # 98 | HookedEnum.TWO.halved_value 99 | # 100 | HookedEnum.THREE.halved_value 101 | # 102 | ``` 103 | 104 | - enums are singletons 105 | ```python 106 | from pickle import dumps, loads 107 | o = LightEnum.ONE 108 | r = loads(dumps(o)) 109 | id(o) 110 | # 139649196736456 111 | id(r) 112 | # 139649196736456 113 | o is r 114 | # True 115 | ``` 116 | - enums are hashable 117 | ```python 118 | list(LightEnum) 119 | # [, , ] 120 | set(LightEnum) 121 | # {, , } 122 | ``` 123 | - enums are easily extended if one needs 124 | ```python 125 | class ExtendedEnum(metaclass=FastEnum): 126 | description: Text 127 | __slots__ = ('description',) 128 | 129 | def __init__(self, value, description, name): 130 | self.value = value 131 | self.name = name 132 | self.description = description 133 | 134 | def describe(self): 135 | return self.description 136 | 137 | RED = 'red', 'a color of blood' 138 | GREEN = 'green', 'a color of grass in the spring' 139 | 140 | ExtendedEnum.GREEN 141 | # 142 | str(ExtendedEnum.GREEN) 143 | # 'ExtendedEnum.GREEN' 144 | ExtendedEnum.GREEN.name 145 | # 'GREEN' 146 | ExtendedEnum.GREEN.value 147 | # 'green' 148 | ExtendedEnum.GREEN.description 149 | # 'a color of grass in the spring' 150 | ExtendedEnum.GREEN.describe() 151 | # 'a color of grass in the spring' 152 | ``` 153 | In case an enum has extended set of fields it it must be guaranteed that the `__init__` 154 | method has the `name` argument in the last position. It's because enum instances are 155 | instantiated like `enumtype(*value, name=name)` where `value` is the right side of 156 | assignment in the code `RED = 'red', 'a color of blood'` (in case the right side is not 157 | a tuple-like object it is wrapped into tuple beforehand) 158 | - protected from modifications 159 | ```python 160 | del LightEnum.ONE 161 | #Traceback (most recent call last): 162 | # File "", line 1, in 163 | # File "fastenum.py", line 81, in __delattr__ 164 | # self.__restrict_modification() 165 | # File "fastenum.py", line 69, in __restrict_modification 166 | # raise TypeError(f'Enum-like classes strictly prohibit changing any attribute/property after they are once set') 167 | #TypeError: Enum-like classes strictly prohibit changing any attribute/property after they are once set 168 | del LightEnum.ONE.name 169 | #Traceback (most recent call last): 170 | # File "", line 1, in 171 | # File "fastenum.py", line 69, in __restrict_modification 172 | # raise TypeError(f'Enum-like classes strictly prohibit changing any attribute/property after they are once set') 173 | #TypeError: Enum-like classes strictly prohibit changing any attribute/property after they are once set 174 | ExtendedEnum.GREEN.description = "I've changed my mind, it's a colour of swamps" 175 | #Traceback (most recent call last): 176 | # File "", line 1, in 177 | # File "fastenum.py", line 69, in __restrict_modification 178 | # raise TypeError(f'Enum-like classes strictly prohibit changing any attribute/property after they are once set') 179 | #TypeError: Enum-like classes strictly prohibit changing any attribute/property after they are once set 180 | ``` 181 | - protected from subclassing 182 | ```python 183 | class LightSub(LightEnum): 184 | FOUR: 'LightSub' 185 | 186 | #Traceback (most recent call last): 187 | # File "", line 1, in 188 | # File "fastenum.py", line 34, in __new__ 189 | # typ.__call__ = typ.__new__ = typ.get 190 | # File "fastenum.py", line 76, in __setattr__ 191 | # self.__restrict_modification() 192 | # File "fastenum.py", line 69, in __restrict_modification 193 | # raise TypeError(f'Enum-like classes strictly prohibit changing any attribute/property after they are once set') 194 | #TypeError: Enum-like classes strictly prohibit changing any attribute/property after they are once set 195 | ``` 196 | - but you could declare a class providing no new values (the result will be just an alias): 197 | ```python 198 | class LightAlias(LightEnum): 199 | pass 200 | 201 | LightAlias.ONE 202 | # 203 | ``` 204 | - and extensible in superclasses 205 | ```python 206 | class ExtEnumBase(metaclass=FastEnum): 207 | description: Text 208 | 209 | __slots__ = ('description',) 210 | 211 | def __init__(self, value, description, name): 212 | self.value = value 213 | self.name = name 214 | self.description = description 215 | 216 | 217 | class ExtEnumOne(ExtEnumBase): 218 | ONE = 1, 'First positive integer' 219 | TWO = 2, 'Second positive integer' 220 | 221 | 222 | class ExtEnumTwo(ExtEnumBase): 223 | RED = 'red', 'A sunset' 224 | GREEN = 'green', 'Allows to cross the road' 225 | ``` 226 | - as requested after publication, it's possible to subclass arbitrary classes (look at tests for more) starting from 1.3: 227 | ```python 228 | class IntEnum(int, metaclass=FastEnum): 229 | ONE = 1 230 | TWO = 2 231 | 232 | # >>> IntEnum.ONE == 1 233 | # True 234 | # >>> IntEnum.ONE * 100 235 | # 100 236 | 237 | import sys 238 | sys.exit(IntEnum.TWO) # sets python's interpreter exit code to 2 239 | ``` 240 | 241 | - faster than standard library's one 242 | ``` 243 | In [2]: %timeit ValuesGivenEnum.FOUR 244 | 21.4 ns ± 0.196 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each) 245 | 246 | In [3]: %timeit ValuesGivenEnum.FOUR.name 247 | 30.3 ns ± 0.121 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each) 248 | 249 | In [4]: %timeit ValuesGivenEnum.FOUR.value 250 | 30.4 ns ± 0.166 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each) 251 | 252 | In [5]: %timeit ValuesGivenEnum(4) 253 | 111 ns ± 0.599 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each) 254 | 255 | In [6]: %timeit ValuesGivenEnum['FOUR'] 256 | 84.6 ns ± 0.188 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each) 257 | ``` 258 | compare that to 259 | ```python 260 | # Classic enum 261 | from enum import Enum 262 | class StdEnum(Enum): 263 | ONE = 1 264 | TWO = 2 265 | ``` 266 | ``` 267 | In [7]: %timeit StdEnum.ONE 268 | 69.2 ns ± 0.195 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each) 269 | 270 | In [8]: %timeit StdEnum.ONE.name 271 | 247 ns ± 0.501 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each) 272 | 273 | In [9]: %timeit StdEnum.ONE.value 274 | 249 ns ± 1.43 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each) 275 | 276 | In [10]: %timeit StdEnum(1) 277 | 380 ns ± 3.74 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each) 278 | 279 | In [11]: %timeit StdEnum['ONE'] 280 | 134 ns ± 0.246 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each) 281 | ``` 282 | That is: 283 | - 3 times faster on enum's member access 284 | - ~8.5 times faster on enum's property (`name`, `value`) access 285 | - 3 times faster on enum's access by val (call on enum's class `MyEnum(value)`) 286 | - 1.5 times faster on enum's access by name (dict-like `MyEnum[name]`) 287 | -------------------------------------------------------------------------------- /examples.py: -------------------------------------------------------------------------------- 1 | from typing import Text 2 | 3 | from fast_enum import FastEnum 4 | 5 | 6 | class LightEnum(metaclass=FastEnum): 7 | ONE: 'LightEnum' 8 | TWO: 'LightEnum' 9 | THREE: 'LightEnum' 10 | # >>> LightEnum.ONE 11 | # 12 | # >>> str(LightEnum.ONE) 13 | # 'LightEnum.ONE' 14 | # >>> LightEnum.ONE.name 15 | # 'ONE' 16 | # >>> LightEnum.ONE.value 17 | # 1 18 | # >>> LightEnum(1) 19 | # 20 | # >>> LightEnum['ONE'] 21 | # 22 | # >>> list(LightEnum) 23 | # [, , ] 24 | # >>> set(LightEnum) 25 | # {, , } 26 | # >>> type(LightEnum.ONE) 27 | # 28 | 29 | 30 | class ValuesGivenEnum(metaclass=FastEnum): 31 | ONE: 'ValuesGivenEnum' = 1 32 | FOUR: 'ValuesGivenEnum' = 4 33 | ELEVEN: 'ValuesGivenEnum' = 11 34 | # >>> ValuesGivenEnum.FOUR 35 | # 36 | # >>> ValuesGivenEnum.FOUR.value 37 | # 4 38 | # >>> ValuesGivenEnum.FOUR.name 39 | # 'FOUR' 40 | # >>> ValuesGivenEnum.ELEVEN 41 | # 42 | # >>> list(ValuesGivenEnum) 43 | # [, , ] 44 | 45 | 46 | class ExtendedEnum(metaclass=FastEnum): 47 | description: Text 48 | __slots__ = ('description',) 49 | 50 | def __init__(self, value, description, name): 51 | # noinspection PyDunderSlots,PyUnresolvedReferences 52 | self.value = value 53 | # noinspection PyDunderSlots,PyUnresolvedReferences 54 | self.name = name 55 | self.description = description 56 | 57 | def describe(self): 58 | return self.description 59 | 60 | RED = 'red', 'a color of blood' 61 | GREEN = 'green', 'a color of grass in the spring' 62 | # >>> ExtendedEnum.GREEN 63 | # 64 | # >>> str(ExtendedEnum.GREEN) 65 | # 'ExtendedEnum.GREEN' 66 | # >>> ExtendedEnum.GREEN.name 67 | # 'GREEN' 68 | # >>> ExtendedEnum.GREEN.value 69 | # 'green' 70 | # >>> ExtendedEnum.GREEN.description 71 | # 'a color of grass in the spring' 72 | # >>> ExtendedEnum.GREEN.describe() 73 | # 'a color of grass in the spring' 74 | 75 | # PROTECTION 76 | # >>> ExtendedEnum.GREEN.description = "I've changed my mind, it's a colour of swamps" 77 | # Traceback (most recent call last): 78 | # File "", line 1, in 79 | # File "fastenum.py", line 69, in __restrict_modification 80 | # raise TypeError(f'Enum-like classes strictly prohibit changing any attribute/property after they are once set') 81 | # TypeError: Enum-like classes strictly prohibit changing any attribute/property after they are once set 82 | # >>> class LightAlias(LightEnum): 83 | # ... pass 84 | # ... 85 | # Traceback (most recent call last): 86 | # File "", line 1, in 87 | # File "fastenum.py", line 34, in __new__ 88 | # typ.__call__ = typ.__new__ = typ.get 89 | # File "fastenum.py", line 76, in __setattr__ 90 | # self.__restrict_modification() 91 | # File "fastenum.py", line 69, in __restrict_modification 92 | # raise TypeError(f'Enum-like classes strictly prohibit changing any attribute/property after they are once set') 93 | # TypeError: Enum-like classes strictly prohibit changing any attribute/property after they are once set 94 | 95 | 96 | class ExtEnumBase(metaclass=FastEnum): 97 | description: Text 98 | 99 | __slots__ = ('description',) 100 | 101 | def __init__(self, value, description, name): 102 | # noinspection PyDunderSlots,PyUnresolvedReferences 103 | self.value = value 104 | # noinspection PyDunderSlots,PyUnresolvedReferences 105 | self.name = name 106 | self.description = description 107 | 108 | 109 | class ExtEnumOne(ExtEnumBase): 110 | ONE = 1, 'First positive integer' 111 | TWO = 2, 'Second positive integer' 112 | 113 | 114 | class ExtEnumTwo(ExtEnumBase): 115 | RED = 'red', 'A sunset' 116 | GREEN = 'green', 'Allows to cross the road' 117 | 118 | 119 | class MixedEnum(metaclass=FastEnum): 120 | _ZERO_VALUED = True 121 | AUTO_ZERO: 'MixedEnum' 122 | ONE: 'MixedEnum' = 1 123 | AUTO_ONE: 'MixedEnum' 124 | TWO: 'MixedEnum' = 2 125 | 126 | # >>> MixedEnum(1) 127 | # 128 | # >>> MixedEnum.AUTO_ZERO 129 | # 130 | # >>> MixedEnum.AUTO_ONE 131 | # 132 | 133 | 134 | class HookedEnum(metaclass=FastEnum): 135 | halved_value: 'HookedEnum' 136 | 137 | __slots__ = ('halved_value',) 138 | 139 | def __init_late(self): 140 | # noinspection PyTypeChecker 141 | self.halved_value: 'HookedEnum' = self.__class__(self.value // 2) 142 | 143 | ZERO: 'HookedEnum' = 0 144 | ONE: 'HookedEnum' = 1 145 | TWO: 'HookedEnum' = 2 146 | THREE: 'HookedEnum' = 3 147 | 148 | # >>> HookedEnum.ZERO.halved_value 149 | # 150 | # >>> HookedEnum.ONE.halved_value 151 | # 152 | # >>> HookedEnum.TWO.halved_value 153 | # 154 | # >>> HookedEnum.THREE.halved_value 155 | # 156 | 157 | 158 | class AliasEnum(HookedEnum): 159 | pass 160 | 161 | # >>> AliasEnum.ZERO.halved_value 162 | # 163 | # >>> AliasEnum.ONE.halved_value 164 | # 165 | # >>> AliasEnum.TWO.halved_value 166 | # 167 | # >>> AliasEnum.THREE.halved_value 168 | # 169 | -------------------------------------------------------------------------------- /fast_enum/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring 2 | from .fastenum import FastEnum 3 | 4 | __all__ = ['FastEnum'] 5 | -------------------------------------------------------------------------------- /fast_enum/fastenum.py: -------------------------------------------------------------------------------- 1 | """ 2 | (c) 2019 Andrey Semenov 3 | (c) 2019 Qrator Labs 4 | A fast(er) Enum implementation with a set of features not provided by stdlib's Enum: 5 | - an enum class can be declared in lightweight (auto-valued) mode 6 | - a set of enum classes can have a base class providing common logic; subclassing is restricted 7 | only after an enum is actually generated (a class declared has at least one enum-value), until 8 | then an arbitrary base classes tree could be declared to provide a logic needed 9 | - an auto-valued and value-provided form of enum declarations could be mixed 10 | - for auto-valued enum instances a counter could be 0 or 1 based (configurable within class 11 | declaration) 12 | - an enum class declaration could be after-init hooked to handle arbitrary additional setup for 13 | enum values (at that point the context has all the values created and initialized, additional 14 | properties that depend on other enum values could be calculated and set up) 15 | """ 16 | from functools import partial 17 | from typing import Any, Text, Dict, List, Tuple, Type, Optional, Callable, Iterable 18 | 19 | 20 | # pylint: disable=inconsistent-return-statements 21 | def _resolve_init(bases: Tuple[Type]) -> Optional[Callable]: 22 | for bcls in bases: 23 | for rcls in bcls.mro(): 24 | resolved_init = getattr(rcls, '__init__') 25 | if resolved_init and resolved_init is not object.__init__: 26 | return resolved_init 27 | 28 | 29 | def _resolve_new(bases: Tuple[Type]) -> Optional[Tuple[Callable, Type]]: 30 | for bcls in bases: 31 | new = getattr(bcls, '__new__', None) 32 | if new not in { 33 | None, 34 | None.__new__, 35 | object.__new__, 36 | FastEnum.__new__, 37 | getattr(FastEnum, '_FastEnum__new') 38 | }: 39 | return new, bcls 40 | 41 | 42 | class FastEnum(type): 43 | """ 44 | A metaclass that handles enum-classes creation. 45 | 46 | Possible options for classes using this metaclass: 47 | - auto-generated values (see examples.py `MixedEnum` and `LightEnum`) 48 | - subclassing possible until actual enum is not declared 49 | (see examples.py `ExtEnumOne` and `ExtEnumTwo`) 50 | - late init hooking (see examples.py `HookedEnum`) 51 | - enum modifications protection (see examples.py comment after `ExtendedEnum`) 52 | """ 53 | # pylint: disable=bad-mcs-classmethod-argument,protected-access,too-many-locals 54 | # pylint: disable=too-many-branches 55 | def __new__(mcs, name, bases, namespace: Dict[Text, Any]): 56 | attributes: List[Text] = [k for k in namespace.keys() 57 | if (not k.startswith('_') and k.isupper())] 58 | attributes += [k for k, v in namespace.get('__annotations__', {}).items() 59 | if (not k.startswith('_') and k.isupper() and v == name)] 60 | light_val = 0 + int(not bool(namespace.get('_ZERO_VALUED'))) 61 | for attr in attributes: 62 | if attr in namespace: 63 | continue 64 | else: 65 | namespace[attr] = light_val 66 | light_val += 1 67 | 68 | __itemsize__ = 0 69 | for bcls in bases: 70 | if bcls is type: 71 | continue 72 | __itemsize__ = max(__itemsize__, bcls.__itemsize__) 73 | 74 | if not __itemsize__: 75 | __slots__ = set(namespace.get('__slots__', tuple())) | {'name', 'value', 76 | '_value_to_instance_map', 77 | '_base_typed'} 78 | namespace['__slots__'] = tuple(__slots__) 79 | namespace['__new__'] = FastEnum.__new 80 | 81 | if '__init__' not in namespace: 82 | namespace['__init__'] = _resolve_init(bases) or mcs.__init 83 | if '__annotations__' not in namespace: 84 | __annotations__ = dict(name=Text, value=Any) 85 | for k in attributes: 86 | __annotations__[k] = name 87 | namespace['__annotations__'] = __annotations__ 88 | namespace['__dir__'] = partial(FastEnum.__dir, bases=bases, namespace=namespace) 89 | typ = type.__new__(mcs, name, bases, namespace) 90 | if attributes: 91 | typ._value_to_instance_map = {} 92 | for instance_name in attributes: 93 | val = namespace[instance_name] 94 | if not isinstance(val, tuple): 95 | val = (val,) 96 | if val[0] in typ._value_to_instance_map: 97 | inst = typ._value_to_instance_map[val[0]] 98 | else: 99 | inst = typ(*val, name=instance_name) 100 | typ._value_to_instance_map[inst.value] = inst 101 | setattr(typ, instance_name, inst) 102 | 103 | # noinspection PyUnresolvedReferences 104 | typ.__call__ = typ.__new__ = typ.get 105 | del typ.__init__ 106 | typ.__hash__ = mcs.__hash 107 | typ.__eq__ = mcs.__eq 108 | typ.__copy__ = mcs.__copy 109 | typ.__deepcopy__ = mcs.__deepcopy 110 | typ.__reduce__ = mcs.__reduce 111 | if '__str__' not in namespace: 112 | typ.__str__ = mcs.__str 113 | if '__repr__' not in namespace: 114 | typ.__repr__ = mcs.__repr 115 | 116 | if f'_{name}__init_late' in namespace: 117 | fun = namespace[f'_{name}__init_late'] 118 | for instance in typ._value_to_instance_map.values(): 119 | fun(instance) 120 | delattr(typ, f'_{name}__init_late') 121 | 122 | typ.__setattr__ = typ.__delattr__ = mcs.__restrict_modification 123 | typ._finalized = True 124 | return typ 125 | 126 | @staticmethod 127 | def __new(cls, *values, **_): 128 | __new__ = _resolve_new(cls.__bases__) 129 | if __new__: 130 | __new__, typ = __new__ 131 | obj = __new__(cls, *values) 132 | obj._base_typed = typ 133 | return obj 134 | 135 | return object.__new__(cls) 136 | 137 | @staticmethod 138 | def __init(instance, value: Any, name: Text): 139 | base_val_type = getattr(instance, '_base_typed', None) 140 | if base_val_type: 141 | value = base_val_type(value) 142 | instance.value = value 143 | instance.name = name 144 | 145 | # pylint: disable=missing-docstring 146 | @staticmethod 147 | def get(typ, val=None): 148 | # noinspection PyProtectedMember 149 | if not isinstance(typ._value_to_instance_map, dict): 150 | for cls in typ.mro(): 151 | if cls is typ: 152 | continue 153 | if hasattr(cls, '_value_to_instance_map') and isinstance(cls._value_to_instance_map, dict): 154 | return cls._value_to_instance_map[val] 155 | raise ValueError(f'Value {val} is not found in this enum type declaration') 156 | # noinspection PyProtectedMember 157 | member = typ._value_to_instance_map.get(val) 158 | if member is None: 159 | raise ValueError(f'Value {val} is not found in this enum type declaration') 160 | return member 161 | 162 | @staticmethod 163 | def __eq(val, other): 164 | return isinstance(val, type(other)) and (val is other if type(other) is type(val) else val.value == other) 165 | 166 | def __hash(cls): 167 | # noinspection PyUnresolvedReferences 168 | return hash(cls.value) 169 | 170 | @staticmethod 171 | def __restrict_modification(*a, **k): 172 | raise TypeError(f'Enum-like classes strictly prohibit changing any attribute/property' 173 | f' after they are once set') 174 | 175 | def __iter__(cls): 176 | return iter(cls._value_to_instance_map.values()) 177 | 178 | def __setattr__(cls, key, value): 179 | if hasattr(cls, '_finalized'): 180 | cls.__restrict_modification() 181 | super().__setattr__(key, value) 182 | 183 | def __delattr__(cls, item): 184 | if hasattr(cls, '_finalized'): 185 | cls.__restrict_modification() 186 | super().__delattr__(item) 187 | 188 | def __getitem__(cls, item): 189 | return getattr(cls, item) 190 | 191 | def has_value(cls, value): 192 | return value in cls._value_to_instance_map 193 | 194 | # pylint: disable=unused-argument 195 | # noinspection PyUnusedLocal,SpellCheckingInspection 196 | def __deepcopy(cls, memodict=None): 197 | return cls 198 | 199 | def __copy(cls): 200 | return cls 201 | 202 | def __reduce(cls): 203 | typ = type(cls) 204 | # noinspection PyUnresolvedReferences 205 | return typ.get, (typ, cls.value) 206 | 207 | @staticmethod 208 | def __str(clz): 209 | return f'{clz.__class__.__name__}.{clz.name}' 210 | 211 | @staticmethod 212 | def __repr(clz): 213 | return f'<{clz.__class__.__name__}.{clz.name}: {repr(clz.value)}>' 214 | 215 | def __dir__(self) -> Iterable[str]: 216 | return [k for k in super().__dir__() if k not in ('_finalized', '_value_to_instance_map')] 217 | 218 | @staticmethod 219 | def __dir(bases, namespace, *_, **__): 220 | keys = [k for k in namespace.keys() if k in ('__annotations__', '__module__', '__qualname__') 221 | or not k.startswith('_')] 222 | for bcls in bases: 223 | keys.extend(dir(bcls)) 224 | return list(set(keys)) 225 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open('README.md', 'r', encoding='utf-8') as rfd: 4 | long_description = rfd.read() 5 | 6 | setuptools.setup( 7 | name='fast-enum', 8 | version='1.3.0', 9 | license='MIT', 10 | platforms=['any'], 11 | author='Andrey Semenov', 12 | author_email='gatekeeper.mail@gmail.com', 13 | description='A fast pure-python implementation of Enum', 14 | long_description=long_description, 15 | long_description_content_type='text/markdown', 16 | url='https://github.com/QratorLabs/fastenum', 17 | packages=['fast_enum'], 18 | classifiers=[ 19 | 'License :: OSI Approved :: MIT License', 20 | 'Development Status :: 5 - Production/Stable', 21 | 'Intended Audience :: Developers', 22 | 'Natural Language :: English', 23 | 'Natural Language :: Russian', 24 | 'Programming Language :: Python :: 3 :: Only', 25 | 'Programming Language :: Python :: 3.6', 26 | 'Programming Language :: Python :: 3.7', 27 | 'Programming Language :: Python :: 3.8', 28 | 'Topic :: Software Development :: Libraries :: Python Modules', 29 | ] 30 | ) 31 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/test_01_fastenum.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | from typing import Text 3 | from unittest import TestCase 4 | 5 | from fast_enum import FastEnum 6 | 7 | 8 | class FastEnumTestCase(TestCase): 9 | def test_01_value_provided_enum(self): 10 | """ 11 | Test standard (value-provided) functionality 12 | """ 13 | global StdEnum 14 | class StdEnum(metaclass=FastEnum): 15 | ONE = 1 16 | TWO = 2 17 | THREE = 3 18 | ALIAS_THREE = 3 19 | 20 | self.assertIsInstance(StdEnum.ONE, StdEnum) 21 | self.assertIsInstance(StdEnum.TWO, StdEnum) 22 | self.assertIsInstance(StdEnum.THREE, StdEnum) 23 | self.assertIs(StdEnum.ALIAS_THREE, StdEnum.THREE) 24 | 25 | one = pickle.loads(pickle.dumps(StdEnum.ONE)) 26 | self.assertIs(one, StdEnum.ONE) 27 | 28 | self.assertEqual(StdEnum.ONE.name, 'ONE') 29 | self.assertEqual(StdEnum.ONE.value, 1) 30 | self.assertEqual(StdEnum.TWO.name, 'TWO') 31 | self.assertEqual(StdEnum.TWO.value, 2) 32 | self.assertEqual(StdEnum.THREE.name, 'THREE') 33 | self.assertEqual(StdEnum.THREE.value, 3) 34 | self.assertEqual(StdEnum.ALIAS_THREE.name, 'THREE') 35 | self.assertEqual(StdEnum.ALIAS_THREE.value, 3) 36 | 37 | self.assertIs(StdEnum(1), StdEnum.ONE) 38 | self.assertIs(StdEnum(2), StdEnum.TWO) 39 | self.assertIs(StdEnum(3), StdEnum.THREE) 40 | 41 | self.assertIs(StdEnum['ONE'], StdEnum.ONE) 42 | self.assertIs(StdEnum['TWO'], StdEnum.TWO) 43 | self.assertIs(StdEnum['THREE'], StdEnum.THREE) 44 | self.assertIs(StdEnum['ALIAS_THREE'], StdEnum.THREE) 45 | 46 | self.assertListEqual(list(StdEnum), [StdEnum.ONE, StdEnum.TWO, StdEnum.THREE]) 47 | 48 | self.assertSetEqual(set(StdEnum), {StdEnum.ONE, StdEnum.TWO, StdEnum.THREE}) 49 | 50 | def test_02_lightweight(self): 51 | """ 52 | Test lightweight (auto-valued) form 53 | """ 54 | class LightEnumOne(metaclass=FastEnum): 55 | ONE: 'LightEnumOne' 56 | TWO: 'LightEnumOne' 57 | 58 | class LightEnumZero(metaclass=FastEnum): 59 | _ZERO_VALUED = True 60 | ZERO: 'LightEnumZero' 61 | ONE: 'LightEnumZero' 62 | 63 | self.assertEqual(LightEnumOne.ONE.value, 1) 64 | self.assertEqual(LightEnumOne.TWO.value, 2) 65 | 66 | self.assertEqual(LightEnumZero.ZERO.value, 0) 67 | self.assertEqual(LightEnumZero.ONE.value, 1) 68 | 69 | def test_03_mixed(self): 70 | """ 71 | Test a mixed (auto-valued and value-provided) forms mixed in the same class 72 | """ 73 | class MixedEnum(metaclass=FastEnum): 74 | _ZERO_VALUED = True 75 | 76 | ONE = 1 77 | AUTO_ZERO: 'MixedEnum' 78 | TWO = 2 79 | AUTO_ONE: 'MixedEnum' 80 | 81 | self.assertEqual(MixedEnum.ONE.value, 1) 82 | self.assertEqual(MixedEnum.TWO.value, 2) 83 | self.assertEqual(MixedEnum.AUTO_ZERO.value, 0) 84 | self.assertEqual(MixedEnum.AUTO_ONE.value, 1) 85 | self.assertIs(MixedEnum.AUTO_ONE, MixedEnum.ONE) 86 | self.assertIs(MixedEnum['AUTO_ONE'], MixedEnum.ONE) 87 | 88 | def test_04_baseclass(self): 89 | """ 90 | Test functionality of subclassed enums 91 | """ 92 | class EnumBase(metaclass=FastEnum): 93 | __slots__ = ('desc',) 94 | desc: 'Text' 95 | def __init__(self, value, desc, name): 96 | self.value = value 97 | self.name = name 98 | self.desc = desc 99 | 100 | class SubEnumOrder(EnumBase): 101 | ONE = 1, 'First' 102 | TWO = 2, 'Second' 103 | 104 | class SubEnumCount(EnumBase): 105 | ONE = 1, 'One' 106 | TWO = 2, 'Two' 107 | 108 | self.assertEqual(SubEnumOrder.ONE.desc, 'First') 109 | self.assertEqual(SubEnumOrder.TWO.desc, 'Second') 110 | 111 | self.assertEqual(SubEnumCount.ONE.desc, 'One') 112 | self.assertEqual(SubEnumCount.TWO.desc, 'Two') 113 | 114 | def test_05_late_init(self): 115 | """ 116 | Test late-init hooking 117 | """ 118 | class HookedEnum(metaclass=FastEnum): 119 | halved_value: 'HookedEnum' 120 | __slots__ = ('halved_value',) 121 | def __init_late(self): 122 | self.halved_value: 'HookedEnum' = self.__class__(self.value // 2) 123 | 124 | ZERO: 'HookedEnum' = 0 125 | ONE: 'HookedEnum' = 1 126 | TWO: 'HookedEnum' = 2 127 | THREE: 'HookedEnum' = 3 128 | 129 | self.assertIs(HookedEnum.ZERO.halved_value, HookedEnum.ZERO) 130 | self.assertIs(HookedEnum.ONE.halved_value, HookedEnum.ZERO) 131 | self.assertIs(HookedEnum.TWO.halved_value, HookedEnum.ONE) 132 | self.assertIs(HookedEnum.THREE.halved_value, HookedEnum.ONE) 133 | 134 | def test_06_restrict_subclassing(self): 135 | """ 136 | Test if subclassing is restricted 137 | """ 138 | with self.assertRaises(TypeError): 139 | class SuperEnum(metaclass=FastEnum): 140 | ONE: 'SuperEnum' 141 | TWO: 'SuperEnum' 142 | 143 | class SubEnum(SuperEnum): 144 | FOUR = 4 145 | 146 | def test_07_restrict_modifications(self): 147 | """ 148 | Test if enum is protected from any modification 149 | """ 150 | class RestrictEnum(metaclass=FastEnum): 151 | ONE = 1 152 | TWO = 2 153 | 154 | with self.assertRaises(TypeError): 155 | setattr(RestrictEnum.ONE, 'value', 5) 156 | with self.assertRaises(TypeError): 157 | delattr(RestrictEnum.TWO, 'name') 158 | 159 | def test_08_base_types(self): 160 | class IntEnum(int, metaclass=FastEnum): 161 | ZERO: 'IntEnum' = 0 162 | ONE: 'IntEnum' 163 | TWO: 'IntEnum' 164 | 165 | self.assertEqual(IntEnum.ZERO, 0) 166 | 167 | class StrEnum(str, metaclass=FastEnum): 168 | _ZERO_VALUED = True 169 | 170 | STR_ZERO: 'StrEnum' 171 | STR_ONE: 'StrEnum' 172 | 173 | self.assertEqual(StrEnum.STR_ZERO, '0') 174 | self.assertEqual('0', StrEnum.STR_ZERO) 175 | self.assertEqual(StrEnum.STR_ONE, '1') 176 | self.assertEqual('1', StrEnum.STR_ONE) 177 | 178 | class FloatEnum(float, metaclass=FastEnum): 179 | ONE: 'FloatEnum' 180 | TWO: 'FloatEnum' 181 | 182 | self.assertEqual(str(FloatEnum.TWO * 10), '20.0') 183 | --------------------------------------------------------------------------------