├── tests ├── __init__.py ├── test_fastenum.py └── builtin_test.py ├── benchmark ├── __init__.py ├── discordenum.py ├── main.py ├── qratorenum.py └── README.md ├── pytest.ini ├── Makefile ├── fastenum ├── __init__.py ├── parcher.py └── patches.py ├── pyproject.toml ├── README.md ├── .gitignore └── poetry.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /benchmark/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --ignore=tests/builtin_tests -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | RM := rm -rf 2 | 3 | clean: 4 | find . -name '*.pyc' -exec $(RM) {} + 5 | find . -name '*.pyo' -exec $(RM) {} + 6 | find . -name '*~' -exec $(RM) {} + 7 | find . -name '__pycache__' -exec $(RM) {} + 8 | $(RM) build/ dist/ docs/build/ .tox/ .cache/ .pytest_cache/ *.egg-info 9 | 10 | test: 11 | pytest tests/test_fastenum.py 12 | -------------------------------------------------------------------------------- /fastenum/__init__.py: -------------------------------------------------------------------------------- 1 | from fastenum import patches # just to execute module 2 | from fastenum.parcher import Patch, InstancePatch 3 | 4 | assert patches, "Need to load this module" 5 | 6 | __all__ = ( 7 | 'disable', 8 | 'enable', 9 | 'enabled', 10 | ) 11 | 12 | enabled: bool = False 13 | 14 | 15 | def enable() -> None: 16 | """ 17 | Patches enum for best performance 18 | """ 19 | global enabled 20 | if enabled: 21 | raise RuntimeError('Nothing to enable: patch is already applied') 22 | 23 | 24 | Patch.enable_patches() 25 | InstancePatch.enable_patches() 26 | enabled = True 27 | 28 | 29 | def disable() -> None: 30 | """ 31 | Restores enum to its origin state 32 | """ 33 | global enabled 34 | if not enabled: 35 | raise RuntimeError('Nothing to disable: patch was not applied previously') 36 | 37 | Patch.disable_patches() 38 | InstancePatch.disable_patches() 39 | enabled = False 40 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "f-enum" 3 | packages = [ 4 | {include = "fastenum"} 5 | ] 6 | version = "0.2.0" 7 | description = "Patch for builtin enum module to achieve best performance" 8 | authors = ["Bobronium "] 9 | license = "MIT" 10 | readme = "README.md" 11 | classifiers = [ 12 | 'Intended Audience :: Developers', 13 | 'License :: OSI Approved :: MIT License', 14 | 'Programming Language :: Python :: 3.6', 15 | 'Programming Language :: Python :: 3.7', 16 | 'Programming Language :: Python :: 3.8', 17 | 'Programming Language :: Python :: 3.9', 18 | 'Programming Language :: Python :: 3.10', 19 | 'Programming Language :: Python :: 3', 20 | 'Programming Language :: Python', 21 | 'Topic :: Software Development :: Libraries :: Python Modules' 22 | ] 23 | 24 | [tool.poetry.urls] 25 | "Repository" = "https://github.com/Bobronium/fastenum" 26 | 27 | 28 | [tool.poetry.dependencies] 29 | python = ">=3.6.2<3.11" 30 | 31 | [tool.poetry.dev-dependencies] 32 | pytest = "*" 33 | black = "*" 34 | mypy = "*" 35 | flake8 = "*" 36 | 37 | [build-system] 38 | requires = ["poetry-core>=1.0.0"] 39 | build-backend = "poetry.core.masonry.api" 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fastenum 2 | 3 | ###### Based on [python/cpython#17669](https://github.com/python/cpython/pull/17669) and [python/cpython#16483](https://github.com/python/cpython/pull/16483) 4 | 5 | Patch for stdlib `enum` that makes it *fast*. 6 | 7 | ## How fast? 8 | 9 | - ~10x faster `name`/`value` access 10 | - ~6x faster access to enum members 11 | - ~2x faster values positive check 12 | - ~3x faster values negative check 13 | - ~3x faster iteration 14 | - ~100x faster new `Flags` and `IntFlags` creation for Python 3.8 and below 15 | 16 | ## Wow this is fast! How do I use it? 17 | 18 | First, install it from PyPi using pip 19 | 20 | ```shell 21 | pip install f-enum 22 | ``` 23 | 24 | or using poetry 25 | 26 | ```shell 27 | poetry add f-enum 28 | ``` 29 | 30 | Then enable the patch just by calling `fastenum.enable()` once at the start of your programm: 31 | 32 | ```python 33 | import fastenum 34 | 35 | fastenum.enable() 36 | ``` 37 | 38 | You don't need to re-apply patch across different modules: once it's enabled, it'll work everywhere. 39 | 40 | ## What's changed? 41 | 42 | fastenum is designed to give effortless boost for all enums from stdlib. That means that none of optimizations should break existing code, thus requiring no changes other than installing and activating the library. 43 | 44 | Here are summary of internal changes: 45 | 46 | - Optimized `Enum.__new__` 47 | - Remove `EnumMeta.__getattr__` 48 | - Store `Enum.name` and `.value` in members `__dict__` for faster access 49 | - Replace `Enum._member_names_` with `._unique_member_map_` for faster lookups and iteration (old arg still remains) 50 | - Replace `_EmumMeta._member_names` and `._last_values` with `.members` mapping (old args still remain) 51 | - Add support for direct setting and getting class attrs on `DynamicClassAttribute` without need to use slow `__getattr__` 52 | - Various minor improvements 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea/ 3 | 4 | ### Python template 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # celery beat schedule file 97 | celerybeat-schedule 98 | 99 | # SageMath parsed files 100 | *.sage.py 101 | 102 | # Environments 103 | .env 104 | .venv 105 | env/ 106 | venv/ 107 | ENV/ 108 | env.bak/ 109 | venv.bak/ 110 | 111 | # Spyder project settings 112 | .spyderproject 113 | .spyproject 114 | 115 | # Rope project settings 116 | .ropeproject 117 | 118 | # mkdocs documentation 119 | /site 120 | 121 | # mypy 122 | .mypy_cache/ 123 | .dmypy.json 124 | dmypy.json 125 | 126 | # Pyre type checker 127 | .pyre/ 128 | 129 | -------------------------------------------------------------------------------- /tests/test_fastenum.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import subprocess 3 | import sys 4 | from enum import Enum, EnumMeta, Flag 5 | 6 | import pytest 7 | 8 | import fastenum 9 | from fastenum.patches import EnumPatch, EnumMetaPatch 10 | 11 | assert fastenum.enabled 12 | 13 | 14 | def test_name_value_shadowing(): 15 | class Foo(Enum): 16 | name = 1 17 | value = 2 18 | 19 | assert isinstance(Foo.name, Foo) 20 | assert isinstance(Foo.value, Foo) 21 | assert Foo.name.name == 'name' 22 | assert Foo.name.value == 1 23 | assert Foo.value.name == 'value' 24 | assert Foo.value.value == 2 25 | 26 | 27 | def test_patched_enum(): 28 | assert fastenum.enabled 29 | fastenum.disable() 30 | 31 | assert hasattr(EnumMeta, '__getattr__') 32 | assert 'name' in Enum.__dict__ 33 | assert 'value' in Enum.__dict__ 34 | 35 | assert Enum.__new__ is not EnumPatch.__new__ 36 | assert Enum.__dict__['__new__'] is EnumPatch.__original_attrs__['__new__'] 37 | assert EnumMeta.__new__ is not EnumMetaPatch.__new__ 38 | assert EnumMeta.__dict__['__new__'] is EnumMetaPatch.__original_attrs__['__new__'] 39 | 40 | fastenum.enable() 41 | 42 | assert not hasattr(EnumMeta, '__getattr__') 43 | assert 'name' not in Enum.__dict__ 44 | assert 'value' not in Enum.__dict__ 45 | 46 | assert Enum.__new__ is EnumPatch.__to_update__['__new__'] 47 | assert EnumMeta.__new__ is EnumMetaPatch.__to_update__['__new__'] 48 | 49 | 50 | def test_non_named_members_have_attrs(): 51 | assert fastenum.enabled 52 | fastenum.disable() 53 | 54 | class Foo(Flag): 55 | a = 1 56 | b = 2 57 | 58 | fake_member = Foo._create_pseudo_member_(3) 59 | assert 'name' not in fake_member.__dict__ 60 | assert 'value' not in fake_member.__dict__ 61 | Foo._value2member_map_[fake_member._value_] = fake_member 62 | 63 | fastenum.enable() 64 | assert 'name' in fake_member.__dict__ 65 | assert 'value' in fake_member.__dict__ 66 | 67 | 68 | def test_attrs_set(): 69 | class Foo(Flag): 70 | a = 1 71 | b = 2 72 | 73 | Foo.a._value_ = 42 74 | assert Foo.a.value == 42 75 | 76 | 77 | @pytest.mark.parametrize('python', (sys.executable,)) 78 | def test_builtin_tests(python): 79 | try: 80 | print(f'\nRunning {python} tests', file=sys.stderr) 81 | result = subprocess.run([python, str(Path(__file__).parent / f'builtin_test.py')]) 82 | except FileNotFoundError as e: 83 | return pytest.skip(f'Unable to run test with {python}, {str(e)}') 84 | 85 | assert result.returncode == 0, f'Tests failed in {python}' 86 | -------------------------------------------------------------------------------- /benchmark/discordenum.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2015-2019 Rapptz 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a 9 | copy of this software and associated documentation files (the "Software"), 10 | to deal in the Software without restriction, including without limitation 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 12 | and/or sell copies of the Software, and to permit persons to whom the 13 | Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | DEALINGS IN THE SOFTWARE. 25 | """ 26 | 27 | import types 28 | from collections import namedtuple 29 | 30 | __all__ = ( 31 | 'Enum', 32 | ) 33 | 34 | 35 | def _create_value_cls(name): 36 | cls = namedtuple('_EnumValue_' + name, 'name value') 37 | cls.__repr__ = lambda self: '<%s.%s: %r>' % (name, self.name, self.value) 38 | cls.__str__ = lambda self: '%s.%s' % (name, self.name) 39 | return cls 40 | 41 | 42 | def _is_descriptor(obj): 43 | return hasattr(obj, '__get__') or hasattr(obj, '__set__') or hasattr(obj, '__delete__') 44 | 45 | 46 | class EnumMeta(type): 47 | def __new__(cls, name, bases, attrs): 48 | value_mapping = {} 49 | member_mapping = {} 50 | member_names = [] 51 | 52 | value_cls = _create_value_cls(name) 53 | for key, value in list(attrs.items()): 54 | is_descriptor = _is_descriptor(value) 55 | if key[0] == '_' and not is_descriptor: 56 | continue 57 | 58 | # Special case classmethod to just pass through 59 | if isinstance(value, classmethod): 60 | continue 61 | 62 | if is_descriptor: 63 | setattr(value_cls, key, value) 64 | del attrs[key] 65 | continue 66 | 67 | try: 68 | new_value = value_mapping[value] 69 | except KeyError: 70 | new_value = value_cls(name=key, value=value) 71 | value_mapping[value] = new_value 72 | member_names.append(key) 73 | 74 | member_mapping[key] = new_value 75 | attrs[key] = new_value 76 | 77 | attrs['_enum_value_map_'] = value_mapping 78 | attrs['_enum_member_map_'] = member_mapping 79 | attrs['_enum_member_names_'] = member_names 80 | actual_cls = super().__new__(cls, name, bases, attrs) 81 | value_cls._actual_enum_cls_ = actual_cls 82 | return actual_cls 83 | 84 | def __iter__(cls): 85 | return (cls._enum_member_map_[name] for name in cls._enum_member_names_) 86 | 87 | def __reversed__(cls): 88 | return (cls._enum_member_map_[name] for name in reversed(cls._enum_member_names_)) 89 | 90 | def __len__(cls): 91 | return len(cls._enum_member_names_) 92 | 93 | def __repr__(cls): 94 | return '' % cls.__name__ 95 | 96 | @property 97 | def __members__(cls): 98 | return types.MappingProxyType(cls._enum_member_map_) 99 | 100 | def __call__(cls, value): 101 | try: 102 | return cls._enum_value_map_[value] 103 | except (KeyError, TypeError): 104 | raise ValueError("%r is not a valid %s" % (value, cls.__name__)) 105 | 106 | def __getitem__(cls, key): 107 | return cls._enum_member_map_[key] 108 | 109 | def __setattr__(cls, name, value): 110 | raise TypeError('Enums are immutable.') 111 | 112 | def __delattr__(cls, attr): 113 | raise TypeError('Enums are immutable') 114 | 115 | def __instancecheck__(self, instance): 116 | # isinstance(x, Y) 117 | # -> __instancecheck__(Y, x) 118 | try: 119 | return instance._actual_enum_cls_ is self 120 | except AttributeError: 121 | return False 122 | 123 | 124 | class Enum(metaclass=EnumMeta): 125 | @classmethod 126 | def try_value(cls, value): 127 | try: 128 | return cls._enum_value_map_[value] 129 | except (KeyError, TypeError): 130 | return value 131 | -------------------------------------------------------------------------------- /tests/builtin_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Run builtin python tests with some needed changes after patch enabled, 3 | then run it once again after patch disabled to make sure nothing breaks 4 | """ 5 | 6 | import sys 7 | import unittest 8 | from test import test_enum, test_re, test_inspect, test_dynamicclassattribute 9 | 10 | TEST_MODULES = test_enum, test_re, test_inspect, test_dynamicclassattribute 11 | 12 | expected_help_output_with_docs = """\ 13 | Help on class Color in module %s: 14 | 15 | class Color(enum.Enum) 16 | | Color(value, names=None, *, module=None, qualname=None, type=None, start=1) 17 | |\x20\x20 18 | | An enumeration. 19 | |\x20\x20 20 | | Method resolution order: 21 | | Color 22 | | enum.Enum 23 | | builtins.object 24 | |\x20\x20 25 | | Data and other attributes defined here: 26 | |\x20\x20 27 | | blue = 28 | |\x20\x20 29 | | green = 30 | |\x20\x20 31 | | red = 32 | |\x20\x20 33 | | ---------------------------------------------------------------------- 34 | | Readonly properties inherited from enum.EnumMeta: 35 | |\x20\x20 36 | | __members__ 37 | | Returns a mapping of member name->value. 38 | |\x20\x20\x20\x20\x20\x20 39 | | This mapping lists all enum members, including aliases. Note that this 40 | | is a read-only view of the internal mapping.""" 41 | 42 | 43 | if sys.version_info < (3, 8): 44 | expected_help_output_with_docs = expected_help_output_with_docs.replace( 45 | 'Readonly properties inherited from enum.EnumMeta:', 46 | 'Data descriptors inherited from enum.EnumMeta:' 47 | ) 48 | if sys.version_info < (3, 7): 49 | expected_help_output_with_docs = expected_help_output_with_docs.replace( 50 | '\n | Color(value, names=None, *, module=None, qualname=None, type=None, start=1)\n |\x20\x20', 51 | '' 52 | ) 53 | 54 | 55 | def test_inspect_getmembers(self): 56 | values = dict(( 57 | ('__class__', test_enum.EnumMeta), 58 | ('__doc__', 'An enumeration.'), 59 | ('__members__', self.Color.__members__), 60 | ('__module__', test_enum.__name__), 61 | ('blue', self.Color.blue), 62 | ('green', self.Color.green), 63 | ('red', self.Color.red), 64 | )) 65 | result = dict(test_enum.inspect.getmembers(self.Color)) 66 | self.assertEqual(values.keys(), result.keys()) 67 | failed = False 68 | for k in values.keys(): 69 | if result[k] != values[k]: 70 | print() 71 | print('\n%s\n key: %s\n result: %s\nexpected: %s\n%s\n' % 72 | ('=' * 75, k, result[k], values[k], '=' * 75), sep='') 73 | failed = True 74 | if failed: 75 | self.fail("result does not equal expected, see print above") 76 | 77 | 78 | def test_inspect_classify_class_attrs(self): 79 | # indirectly test __objclass__ 80 | from inspect import Attribute 81 | values = [ 82 | Attribute(name='__class__', kind='data', 83 | defining_class=object, object=test_enum.EnumMeta), 84 | Attribute(name='__doc__', kind='data', 85 | defining_class=self.Color, object='An enumeration.'), 86 | Attribute(name='__members__', kind='property', 87 | defining_class=test_enum.EnumMeta, object=test_enum.EnumMeta.__members__), 88 | Attribute(name='__module__', kind='data', 89 | defining_class=self.Color, object=test_enum.__name__), 90 | Attribute(name='blue', kind='data', 91 | defining_class=self.Color, object=self.Color.blue), 92 | Attribute(name='green', kind='data', 93 | defining_class=self.Color, object=self.Color.green), 94 | Attribute(name='red', kind='data', 95 | defining_class=self.Color, object=self.Color.red), 96 | ] 97 | values.sort(key=lambda item: item.name) 98 | result = list(test_enum.inspect.classify_class_attrs(self.Color)) 99 | result.sort(key=lambda item: item.name) 100 | failed = False 101 | for v, r in zip(values, result): 102 | if r != v: 103 | print('\n%s\n%s\n%s\n%s\n' % ('=' * 75, r, v, '=' * 75), sep='') 104 | failed = True 105 | if failed: 106 | self.fail("result does not equal expected, see print above") 107 | 108 | 109 | class TestSetClassAttr(unittest.TestCase): 110 | def test_set_class_attr(self): 111 | class Foo: 112 | def __init__(self, value): 113 | self._value = value 114 | self._spam = 'spam' 115 | 116 | @test_dynamicclassattribute.DynamicClassAttribute 117 | def value(self): 118 | return self._value 119 | 120 | spam = test_dynamicclassattribute.DynamicClassAttribute( 121 | lambda s: s._spam, 122 | alias='my_shiny_spam' 123 | ) 124 | 125 | self.assertFalse(hasattr(Foo, 'value')) 126 | self.assertFalse(hasattr(Foo, 'name')) 127 | 128 | foo_bar = Foo('bar') 129 | value_desc = Foo.__dict__['value'] 130 | value_desc.set_class_attr(Foo, foo_bar) 131 | self.assertIs(Foo.value, foo_bar) 132 | self.assertEqual(Foo.value.value, 'bar') 133 | 134 | foo_baz = Foo('baz') 135 | Foo.my_shiny_spam = foo_baz 136 | self.assertIs(Foo.spam, foo_baz) 137 | self.assertEqual(Foo.spam.spam, 'spam') 138 | 139 | 140 | def run_tests(): 141 | loader = unittest.TestLoader() 142 | suites = [ 143 | loader.loadTestsFromModule(module) for module in TEST_MODULES 144 | ] 145 | result = unittest.TextTestRunner().run(loader.suiteClass(suites)) 146 | if result.failures or result.errors: 147 | sys.exit(1) 148 | 149 | 150 | orig_test_inspect_getmembers = test_enum.TestStdLib.test_inspect_getmembers 151 | orig_test_inspect_classify_class_attrs = test_enum.TestStdLib.test_inspect_classify_class_attrs 152 | orig_expected_help_output_with_docs = test_enum.expected_help_output_with_docs 153 | 154 | if __name__ == '__main__': 155 | # run_tests() # tests will fail here only if something was wrong before patch 156 | import fastenum 157 | assert fastenum.enabled 158 | 159 | test_enum.TestStdLib.test_inspect_getmembers = test_inspect_getmembers 160 | test_enum.TestStdLib.test_inspect_classify_class_attrs = test_inspect_classify_class_attrs 161 | test_enum.expected_help_output_with_docs = expected_help_output_with_docs 162 | test_dynamicclassattribute.TestSetClassAttr = TestSetClassAttr 163 | 164 | run_tests() 165 | 166 | fastenum.disable() 167 | assert not fastenum.enabled 168 | 169 | test_enum.TestStdLib.test_inspect_getmembers = orig_test_inspect_getmembers 170 | test_enum.TestStdLib.test_inspect_classify_class_attrs = orig_test_inspect_classify_class_attrs 171 | test_enum.expected_help_output_with_docs = orig_expected_help_output_with_docs 172 | del test_dynamicclassattribute.TestSetClassAttr 173 | 174 | run_tests() 175 | -------------------------------------------------------------------------------- /benchmark/main.py: -------------------------------------------------------------------------------- 1 | import math 2 | import sys 3 | import time 4 | from enum import Enum 5 | from timeit import timeit 6 | from typing import Iterable, Dict, List, Tuple, Union, TypeVar 7 | 8 | import discordenum 9 | import qratorenum 10 | 11 | import fastenum 12 | 13 | assert sys # used in some cases, passed in through globals() 14 | 15 | TRY_EXCEPT_BLOCK_TMPL = 'try:\n {expr}\nexcept: pass' 16 | 17 | assert fastenum.enabled 18 | 19 | 20 | def geomean(numbers) -> float: 21 | return math.exp(math.fsum(math.log(x) for x in numbers) / len(numbers)) 22 | 23 | 24 | def calculate_difference(time_elapsed: Dict[type, List[float]]) -> str: 25 | time_elapsed = time_elapsed.copy() 26 | fastest = min(time_elapsed, key=lambda i: sum(time_elapsed[i])) 27 | fastest_time = time_elapsed.pop(fastest) 28 | average_fastest = geomean(fastest_time) 29 | 30 | result = ( 31 | f'\n{fastest.__name__:<14}: {sum(fastest_time):10f} seconds in total, {average_fastest:10f} average (Fastest)' 32 | ) 33 | for type_, elapsed in time_elapsed.items(): 34 | average = geomean(elapsed) 35 | result += ( 36 | f'\n{type_.__name__:<14}: {sum(elapsed):10f} seconds in total, {average:10f} average, ' 37 | f'slower than {fastest.__name__} by x{average / average_fastest:10f} (in average)' 38 | ) 39 | 40 | return result 41 | 42 | 43 | def eval_and_timeit(code, global_ns, number, setup='pass', **local_ns): 44 | result = None 45 | exception = None 46 | try: 47 | result = eval(code, global_ns, local_ns) 48 | except SyntaxError: 49 | try: 50 | exec(code, global_ns, local_ns) 51 | except SyntaxError: 52 | raise 53 | except Exception as e: 54 | exception = e 55 | except Exception as e: 56 | exception = e 57 | 58 | if exception is not None: 59 | code = TRY_EXCEPT_BLOCK_TMPL.format(expr=code) 60 | return code, exception, timeit(code, setup, globals=global_ns, number=number) 61 | return code, result, timeit(code, setup, globals=global_ns, number=number) 62 | 63 | 64 | T = TypeVar('T') 65 | 66 | 67 | def test( 68 | *objects: T, 69 | expressions: Iterable[Union[str, Tuple[str, str]]], 70 | number: int = 1000000, 71 | group_by_objects: bool = False, 72 | format_mapping: Dict[str, str] = None, 73 | pause_interval=0, 74 | **globals_ns 75 | ) -> Dict[T, List[float]]: 76 | """ 77 | :param objects: objects to test1 78 | :param expressions: expressions to test1 on objects 79 | :param number: number of repeats for each expression (passed in timeit) 80 | :param group_by_objects: if True, expressions will be evaluated and tested in order of objects, else of expressions 81 | :param format_mapping: mapping to format str where keys is keys in str and values is attrs of current object 82 | :param pause_interval: interval between tests 83 | :param globals_ns: namespace for test1 84 | 85 | :return: dict with objects as keys and total time elapsed by them as values 86 | 87 | >>> class Foo: 88 | ... BAR = 'baz' 89 | 90 | >>> class Bar: 91 | ... @property 92 | ... def BAR(self): 93 | ... return 'baz' 94 | 95 | >>> test(Foo, Bar, expressions=('{obj}().BAR', '{obj}().BAR = 1}'), Foo=Foo, Bar=Bar) 96 | Testing with 1000000 repeats: 97 | 98 | >>> Foo().BAR # 0.1405952029999753 seconds 99 | 'baz' 100 | 101 | >>> Bar().BAR # 0.27527517399721546 seconds 102 | 'baz' 103 | 104 | >>> Foo().BAR = 1 # 0.20322119499905966 seconds 105 | 106 | 107 | >>> try: Bar().BAR = 1 except: pass # 0.41546584199750214 seconds 108 | AttributeError("can't set attribute",) 109 | """ 110 | print(f'Testing with {number} repeats:\n') 111 | 112 | if group_by_objects: 113 | obj_expressions = ((obj, expression) for obj in objects for expression in expressions) 114 | else: # by expressions 115 | obj_expressions = ((obj, expression) for expression in expressions for obj in objects) 116 | 117 | if format_mapping is None: 118 | format_mapping = {'obj': '__name__'} # expression.format(obj=obj.__name__) 119 | 120 | time_elapsed = {} 121 | for obj, expression in obj_expressions: 122 | if not isinstance(expression, str): 123 | expression, setup = expression 124 | else: 125 | setup = 'pass' 126 | 127 | formatting = {key: getattr(obj, attr) for key, attr in format_mapping.items()} 128 | code = expression.format(**formatting) 129 | setup = setup.format(**formatting) 130 | 131 | time.sleep(pause_interval) 132 | code, result, elapsed = eval_and_timeit(code, setup=setup, number=number, global_ns=globals_ns) 133 | 134 | time_elapsed.setdefault(obj, []).append(elapsed) 135 | 136 | code = code.replace('\n', ' ') 137 | result = repr(result) if result is not None else '' 138 | print(f' >>> {code} # {elapsed} seconds\n {result}\n') 139 | 140 | return time_elapsed 141 | 142 | 143 | fastenum.disable() 144 | 145 | 146 | class BuiltinEnum(str, Enum): 147 | FOO = 'FOO' 148 | BAR = 'BAR' 149 | 150 | 151 | fastenum.enable() 152 | 153 | 154 | class PatchedEnum(str, Enum): 155 | FOO = 'FOO' 156 | BAR = 'BAR' 157 | 158 | 159 | class QratorEnum(str, metaclass=qratorenum.FastEnum): 160 | FOO: 'QratorEnum' = 'FOO' 161 | BAR: 'QratorEnum' = 'BAR' 162 | 163 | 164 | class DiscordEnum(str, discordenum.Enum): 165 | FOO = 'FOO' 166 | BAR = 'BAR' 167 | 168 | 169 | if __name__ == '__main__': 170 | CASES = dict( 171 | ATTR_ACCESS=( 172 | '{obj}.FOO', 173 | "{obj}.FOO.value", 174 | ), 175 | INHERITANCE=( 176 | 'issubclass({obj}, Enum)', 177 | 'isinstance({obj}.FOO, Enum)', 178 | 'isinstance({obj}.FOO, {obj})', 179 | 'isinstance({obj}.FOO, str)', 180 | ), 181 | TRYING_VALUES=( 182 | "{obj}.FOO = 'new'", 183 | "{obj}('unknown')", 184 | "{obj}('FOO')", 185 | '{obj}({obj}.FOO)', 186 | "{obj}['FOO']" 187 | ), 188 | MISC=( 189 | 'sys.getsizeof({obj})', 190 | 'sys.getsizeof({obj}.FOO)', 191 | 'for member in {obj}: pass', 192 | 'dir({obj})', 193 | 'repr({obj})', 194 | ) 195 | ) 196 | 197 | candidates = ( 198 | PatchedEnum, 199 | QratorEnum, 200 | DiscordEnum, 201 | ) 202 | 203 | total_time_elapsed = {} 204 | for name, expr in CASES.items(): 205 | print(f'\n\n{name}:') 206 | 207 | # Fast enum also affects builtin enums speed 208 | fastenum.disable() 209 | time_info = test(BuiltinEnum, expressions=expr, group_by_objects=False, **globals()) 210 | fastenum.enable() 211 | 212 | time_info.update(test(*candidates, expressions=expr, group_by_objects=False, **globals())) 213 | for t, elapsed_ in time_info.items(): 214 | total_time_elapsed.setdefault(t, []).extend(elapsed_) 215 | print((calculate_difference(time_info))) 216 | print(f'\n\nTOTAL TIME:') 217 | print((calculate_difference(total_time_elapsed))) 218 | -------------------------------------------------------------------------------- /fastenum/parcher.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import gc 3 | from typing import MutableMapping, Any, AbstractSet, Type, Callable, Dict, Tuple, Mapping, Optional, Set, cast 4 | 5 | 6 | class _Missing: 7 | def __repr__(self) -> str: 8 | return "< 'MISSING' >" 9 | 10 | 11 | MISSING = _Missing() 12 | 13 | 14 | def get_attr(t: Any, name: str, default: Optional[Any] = MISSING) -> Any: 15 | value = t.__dict__.get(name, default) 16 | if value is not get_attr: 17 | if hasattr(value, '__get__') and type(value) in {staticmethod, classmethod}: 18 | return getattr(t, name, get_attr) 19 | else: 20 | return value 21 | return getattr(t, name, get_attr) 22 | 23 | 24 | def del_attr(t: Any, name: str) -> Any: 25 | old = get_attr(t, name, MISSING) 26 | if old is not MISSING: 27 | delattr(t, name) 28 | return old 29 | 30 | 31 | def set_attr(t: Any, name: str, value: Any) -> Any: 32 | old = get_attr(t, name) 33 | if isinstance(t.__dict__, MutableMapping): 34 | t.__dict__[name] = value 35 | else: 36 | setattr(t, name, value) 37 | return old 38 | 39 | 40 | class PatchMeta(type): 41 | __enabled__: bool 42 | __run_on_class__: classmethod 43 | __run_on_instance__: classmethod 44 | 45 | __target__: Type[Any] 46 | __to_update__: dict[str, Any] 47 | __to_delete__: AbstractSet[str] 48 | __original_attrs__: dict[Any, Any] 49 | __extra__: AbstractSet[str] 50 | __redefined_on_subclasses__: dict[str, Any] 51 | 52 | def __prepare__(cls, *args: Any, **kwargs: Any) -> Mapping[str, Any]: # type: ignore 53 | return type.__prepare__(*args, **kwargs) 54 | 55 | def __new__( 56 | mcs, 57 | name: str, 58 | bases: Tuple[Type[Any], ...], 59 | namespace: Dict[str, Any], 60 | target: Any = None, 61 | delete: AbstractSet[str] = None, 62 | update: AbstractSet[str] = None 63 | ) -> PatchMeta: 64 | target = target or namespace.pop('__target__', None) 65 | if target is None: 66 | return type.__new__(mcs, name, bases, namespace) 67 | 68 | to_delete = cast(set, delete or namespace.pop('__to_delete__', set())) 69 | to_update = cast(set, update or namespace.pop('__to_update__', set())) 70 | 71 | patched_attrs = { 72 | attr: namespace[attr] 73 | for attr in to_update 74 | } 75 | 76 | original_attrs = { 77 | attr: target.__dict__[attr] 78 | for attr in to_update | to_delete 79 | if attr in target.__dict__ 80 | } 81 | cls = type.__new__(mcs, name, bases, namespace) 82 | 83 | cls.__target__ = target 84 | cls.__to_update__ = patched_attrs 85 | cls.__original_attrs__ = original_attrs 86 | cls.__extra__ = (to_update | to_delete) ^ original_attrs.keys() 87 | cls.__to_delete__ = to_delete 88 | cls.__redefined_on_subclasses__ = {} 89 | cls.__enabled__ = False 90 | return cls 91 | 92 | def enable(cls, check: bool = True) -> None: 93 | if check and cls.__enabled__: 94 | raise RuntimeError(f"{cls} is already enabled") 95 | 96 | try: 97 | target = cls.__target__ 98 | except AttributeError: 99 | raise TypeError("This patch doesn't have a target. You should define it on its subclass") 100 | subclasses = cls._get_all_subclasses(target) 101 | 102 | if cls.__run_on_class__: 103 | cls.__run_on_class__(target) 104 | for sub_cls in subclasses: 105 | cls.__run_on_class__(sub_cls) 106 | 107 | if cls.__run_on_instance__: 108 | for obj in gc.get_objects(): 109 | if isinstance(obj, target): 110 | cls.__run_on_instance__(obj) 111 | 112 | for attr in cls.__to_delete__: 113 | old_value = del_attr(target, attr) 114 | if old_value is MISSING: 115 | continue 116 | for sub_cls in subclasses: 117 | if get_attr(sub_cls, attr) is old_value: 118 | del_attr(sub_cls, attr) 119 | cls.__redefined_on_subclasses__.setdefault(attr, set()).add(sub_cls) 120 | 121 | for attr, new_value in cls.__to_update__.items(): 122 | old_value = set_attr(target, attr, new_value) 123 | if old_value is MISSING: 124 | continue 125 | cls.__original_attrs__[attr] = old_value 126 | for sub_cls in subclasses: 127 | if get_attr(sub_cls, attr) is old_value: 128 | set_attr(sub_cls, attr, new_value) 129 | cls.__redefined_on_subclasses__.setdefault(attr, set()).add(sub_cls) 130 | 131 | cls.__enabled__ = True 132 | 133 | def disable(cls, check: bool = True) -> None: 134 | if check and not cls.__enabled__: 135 | raise RuntimeError(f"{cls} is already disabled") 136 | 137 | target = cls.__target__ 138 | for attr, value in cls.__original_attrs__.items(): 139 | set_attr(target, attr, value) 140 | for sub_cls in cls.__redefined_on_subclasses__.get(attr, ()): 141 | set_attr(sub_cls, attr, value) 142 | 143 | for attr in cls.__extra__: 144 | del_attr(target, attr) 145 | 146 | cls.__enabled__ = False 147 | 148 | def enable_patches(cls, check: bool = True) -> None: 149 | """This method is used to apply all defined patches""" 150 | if hasattr(cls, '__target__'): 151 | raise TypeError('To apply one particular patch, use .enable() method.') 152 | 153 | for patch in cls.__subclasses__(): 154 | patch.enable(check) 155 | 156 | def disable_patches(cls, check: bool = True) -> None: 157 | """This method is used to disable all defined patches""" 158 | if hasattr(cls, '__target__'): 159 | raise TypeError('To apply one particular patch, use .enable() method.') 160 | 161 | for patch in cls.__subclasses__(): 162 | patch.disable(check) 163 | 164 | def _get_all_subclasses(cls, target: Type[Any]) -> Set[Type[Any]]: 165 | all_subclasses = set() 166 | for subclass in type.__subclasses__(target): 167 | all_subclasses.add(subclass) 168 | all_subclasses.update(cls._get_all_subclasses(subclass)) 169 | 170 | return all_subclasses 171 | 172 | 173 | class Patch(metaclass=PatchMeta): 174 | """Class to declare attributes to patch other classes""" 175 | __enabled__: bool = False 176 | 177 | __run_on_class__: Callable[[Type[Any]], None] | None = None 178 | __run_on_instance__: Callable[[Any], None] | None = None 179 | 180 | 181 | class InstancePatchMeta(PatchMeta): 182 | def _get_all_subclasses(cls, target: Any) -> Set[Type[Any]]: 183 | return set() 184 | 185 | def new( 186 | cls, target: Any, delete: AbstractSet[str] = None, update: Dict[str, Any] = None 187 | ) -> InstancePatchMeta: 188 | return cast(InstancePatchMeta, cls.__class__.__new__( 189 | mcs=cls.__class__, 190 | name=getattr(target, '__name__', target.__class__.__name__) + 'Patch', 191 | bases=(cls,), 192 | namespace=update or {}, 193 | target=target, 194 | delete=delete, 195 | update=update.keys() if update else None, 196 | )) 197 | 198 | 199 | class InstancePatch(metaclass=InstancePatchMeta): 200 | __run_on_class__ = None 201 | __run_on_instance__ = None 202 | -------------------------------------------------------------------------------- /benchmark/qratorenum.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 | Possible options for classes using this metaclass: 46 | - auto-generated values (see examples.py `MixedEnum` and `LightEnum`) 47 | - subclassing possible until actual enum is not declared 48 | (see examples.py `ExtEnumOne` and `ExtEnumTwo`) 49 | - late init hooking (see examples.py `HookedEnum`) 50 | - enum modifications protection (see examples.py comment after `ExtendedEnum`) 51 | """ 52 | # pylint: disable=bad-mcs-classmethod-argument,protected-access,too-many-locals 53 | # pylint: disable=too-many-branches 54 | def __new__(mcs, name, bases, namespace: Dict[Text, Any]): 55 | attributes: List[Text] = [k for k in namespace.keys() 56 | if (not k.startswith('_') and k.isupper())] 57 | attributes += [k for k, v in namespace.get('__annotations__', {}).items() 58 | if (not k.startswith('_') and k.isupper() and v == name)] 59 | light_val = 0 + int(not bool(namespace.get('_ZERO_VALUED'))) 60 | for attr in attributes: 61 | if attr in namespace: 62 | continue 63 | else: 64 | namespace[attr] = light_val 65 | light_val += 1 66 | 67 | __itemsize__ = 0 68 | for bcls in bases: 69 | if bcls is type: 70 | continue 71 | __itemsize__ = max(__itemsize__, bcls.__itemsize__) 72 | 73 | if not __itemsize__: 74 | __slots__ = set(namespace.get('__slots__', tuple())) | {'name', 'value', 75 | '_value_to_instance_map', 76 | '_base_typed'} 77 | namespace['__slots__'] = tuple(__slots__) 78 | namespace['__new__'] = FastEnum.__new 79 | 80 | if '__init__' not in namespace: 81 | namespace['__init__'] = _resolve_init(bases) or mcs.__init 82 | if '__annotations__' not in namespace: 83 | __annotations__ = dict(name=Text, value=Any) 84 | for k in attributes: 85 | __annotations__[k] = name 86 | namespace['__annotations__'] = __annotations__ 87 | namespace['__dir__'] = partial(FastEnum.__dir, bases=bases, namespace=namespace) 88 | typ = type.__new__(mcs, name, bases, namespace) 89 | if attributes: 90 | typ._value_to_instance_map = {} 91 | for instance_name in attributes: 92 | val = namespace[instance_name] 93 | if not isinstance(val, tuple): 94 | val = (val,) 95 | if val[0] in typ._value_to_instance_map: 96 | inst = typ._value_to_instance_map[val[0]] 97 | else: 98 | inst = typ(*val, name=instance_name) 99 | typ._value_to_instance_map[inst.value] = inst 100 | setattr(typ, instance_name, inst) 101 | 102 | # noinspection PyUnresolvedReferences 103 | typ.__call__ = typ.__new__ = typ.get 104 | del typ.__init__ 105 | typ.__hash__ = mcs.__hash 106 | typ.__eq__ = mcs.__eq 107 | typ.__copy__ = mcs.__copy 108 | typ.__deepcopy__ = mcs.__deepcopy 109 | typ.__reduce__ = mcs.__reduce 110 | if '__str__' not in namespace: 111 | typ.__str__ = mcs.__str 112 | if '__repr__' not in namespace: 113 | typ.__repr__ = mcs.__repr 114 | 115 | if f'_{name}__init_late' in namespace: 116 | fun = namespace[f'_{name}__init_late'] 117 | for instance in typ._value_to_instance_map.values(): 118 | fun(instance) 119 | delattr(typ, f'_{name}__init_late') 120 | 121 | typ.__setattr__ = typ.__delattr__ = mcs.__restrict_modification 122 | typ._finalized = True 123 | return typ 124 | 125 | @staticmethod 126 | def __new(cls, *values, **_): 127 | __new__ = _resolve_new(cls.__bases__) 128 | if __new__: 129 | __new__, typ = __new__ 130 | obj = __new__(cls, *values) 131 | obj._base_typed = typ 132 | return obj 133 | 134 | return object.__new__(cls) 135 | 136 | @staticmethod 137 | def __init(instance, value: Any, name: Text): 138 | base_val_type = getattr(instance, '_base_typed', None) 139 | if base_val_type: 140 | value = base_val_type(value) 141 | instance.value = value 142 | instance.name = name 143 | 144 | # pylint: disable=missing-docstring 145 | @staticmethod 146 | def get(typ, val=None): 147 | # noinspection PyProtectedMember 148 | if not isinstance(typ._value_to_instance_map, dict): 149 | for cls in typ.mro(): 150 | if cls is typ: 151 | continue 152 | if hasattr(cls, '_value_to_instance_map') and isinstance(cls._value_to_instance_map, dict): 153 | return cls._value_to_instance_map[val] 154 | raise ValueError(f'Value {val} is not found in this enum type declaration') 155 | # noinspection PyProtectedMember 156 | member = typ._value_to_instance_map.get(val) 157 | if member is None: 158 | raise ValueError(f'Value {val} is not found in this enum type declaration') 159 | return member 160 | 161 | @staticmethod 162 | def __eq(val, other): 163 | return isinstance(val, type(other)) and (val is other if type(other) is type(val) else val.value == other) 164 | 165 | def __hash(cls): 166 | # noinspection PyUnresolvedReferences 167 | return hash(cls.value) 168 | 169 | @staticmethod 170 | def __restrict_modification(*a, **k): 171 | raise TypeError(f'Enum-like classes strictly prohibit changing any attribute/property' 172 | f' after they are once set') 173 | 174 | def __iter__(cls): 175 | return iter(cls._value_to_instance_map.values()) 176 | 177 | def __setattr__(cls, key, value): 178 | if hasattr(cls, '_finalized'): 179 | cls.__restrict_modification() 180 | super().__setattr__(key, value) 181 | 182 | def __delattr__(cls, item): 183 | if hasattr(cls, '_finalized'): 184 | cls.__restrict_modification() 185 | super().__delattr__(item) 186 | 187 | def __getitem__(cls, item): 188 | return getattr(cls, item) 189 | 190 | def has_value(cls, value): 191 | return value in cls._value_to_instance_map 192 | 193 | # pylint: disable=unused-argument 194 | # noinspection PyUnusedLocal,SpellCheckingInspection 195 | def __deepcopy(cls, memodict=None): 196 | return cls 197 | 198 | def __copy(cls): 199 | return cls 200 | 201 | def __reduce(cls): 202 | typ = type(cls) 203 | # noinspection PyUnresolvedReferences 204 | return typ.get, (typ, cls.value) 205 | 206 | @staticmethod 207 | def __str(clz): 208 | return f'{clz.__class__.__name__}.{clz.name}' 209 | 210 | @staticmethod 211 | def __repr(clz): 212 | return f'<{clz.__class__.__name__}.{clz.name}: {repr(clz.value)}>' 213 | 214 | def __dir__(self) -> Iterable[str]: 215 | return [k for k in super().__dir__() if k not in ('_finalized', '_value_to_instance_map')] 216 | 217 | @staticmethod 218 | def __dir(bases, namespace, *_, **__): 219 | keys = [k for k in namespace.keys() if k in ('__annotations__', '__module__', '__qualname__') 220 | or not k.startswith('_')] 221 | for bcls in bases: 222 | keys.extend(dir(bcls)) 223 | return list(set(keys)) -------------------------------------------------------------------------------- /benchmark/README.md: -------------------------------------------------------------------------------- 1 | ```python 2 | ATTR_ACCESS: 3 | Testing with 1000000 repeats: 4 | 5 | >>> BuiltinEnum.FOO # 0.339034597 seconds 6 | 7 | 8 | >>> BuiltinEnum.FOO.value # 0.9988253439999999 seconds 9 | 'FOO' 10 | 11 | Testing with 1000000 repeats: 12 | 13 | >>> PatchedEnum.FOO # 0.06925175300000008 seconds 14 | 15 | 16 | >>> QratorEnum.FOO # 0.07271770099999997 seconds 17 | 18 | 19 | >>> DiscordEnum.FOO # 0.06592386700000019 seconds 20 | 21 | 22 | >>> PatchedEnum.FOO.value # 0.06934381299999992 seconds 23 | 'FOO' 24 | 25 | >>> QratorEnum.FOO.value # 0.06072996999999991 seconds 26 | 'FOO' 27 | 28 | >>> DiscordEnum.FOO.value # 0.054826239000000054 seconds 29 | 'FOO' 30 | 31 | 32 | DiscordEnum : 0.120750 seconds in total, 0.060120 average (Fastest) 33 | BuiltinEnum : 1.337860 seconds in total, 0.581925 average, slower than DiscordEnum by x 9.679462 (in average) 34 | PatchedEnum : 0.138596 seconds in total, 0.069298 average, slower than DiscordEnum by x 1.152667 (in average) 35 | QratorEnum : 0.133448 seconds in total, 0.066454 average, slower than DiscordEnum by x 1.105366 (in average) 36 | 37 | 38 | INHERITANCE: 39 | Testing with 1000000 repeats: 40 | 41 | >>> issubclass(BuiltinEnum, Enum) # 0.1846578179999998 seconds 42 | True 43 | 44 | >>> isinstance(BuiltinEnum.FOO, Enum) # 0.30081541499999975 seconds 45 | True 46 | 47 | >>> isinstance(BuiltinEnum.FOO, BuiltinEnum) # 0.23010569400000014 seconds 48 | True 49 | 50 | >>> isinstance(BuiltinEnum.FOO, str) # 0.2460515889999999 seconds 51 | True 52 | 53 | Testing with 1000000 repeats: 54 | 55 | >>> issubclass(PatchedEnum, Enum) # 0.1541273440000004 seconds 56 | True 57 | 58 | >>> issubclass(QratorEnum, Enum) # 0.17432254900000022 seconds 59 | False 60 | 61 | >>> issubclass(DiscordEnum, Enum) # 0.15594098200000017 seconds 62 | False 63 | 64 | >>> isinstance(PatchedEnum.FOO, Enum) # 0.23045377 seconds 65 | True 66 | 67 | >>> isinstance(QratorEnum.FOO, Enum) # 0.24137184099999986 seconds 68 | False 69 | 70 | >>> isinstance(DiscordEnum.FOO, Enum) # 0.20914493700000003 seconds 71 | False 72 | 73 | >>> isinstance(PatchedEnum.FOO, PatchedEnum) # 0.10320675700000059 seconds 74 | True 75 | 76 | >>> isinstance(QratorEnum.FOO, QratorEnum) # 0.10040993600000014 seconds 77 | True 78 | 79 | >>> isinstance(DiscordEnum.FOO, DiscordEnum) # 0.34633623700000005 seconds 80 | True 81 | 82 | >>> isinstance(PatchedEnum.FOO, str) # 0.11627604399999925 seconds 83 | True 84 | 85 | >>> isinstance(QratorEnum.FOO, str) # 0.12633884900000059 seconds 86 | True 87 | 88 | >>> isinstance(DiscordEnum.FOO, str) # 0.15043016200000014 seconds 89 | False 90 | 91 | 92 | PatchedEnum : 0.604064 seconds in total, 0.143686 average (Fastest) 93 | BuiltinEnum : 0.961631 seconds in total, 0.236813 average, slower than PatchedEnum by x 1.648124 (in average) 94 | QratorEnum : 0.642443 seconds in total, 0.151998 average, slower than PatchedEnum by x 1.057847 (in average) 95 | DiscordEnum : 0.861852 seconds in total, 0.203030 average, slower than PatchedEnum by x 1.413008 (in average) 96 | 97 | 98 | TRYING_VALUES: 99 | Testing with 1000000 repeats: 100 | 101 | >>> try: BuiltinEnum.FOO = 'new' except: pass # 0.9492342350000005 seconds 102 | AttributeError('Cannot reassign members.') 103 | 104 | >>> try: BuiltinEnum('unknown') except: pass # 5.5065217849999994 seconds 105 | ValueError("'unknown' is not a valid BuiltinEnum") 106 | 107 | >>> BuiltinEnum('FOO') # 0.7844771290000025 seconds 108 | 109 | 110 | >>> BuiltinEnum(BuiltinEnum.FOO) # 0.733109000999999 seconds 111 | 112 | 113 | >>> BuiltinEnum['FOO'] # 0.3065877790000009 seconds 114 | 115 | 116 | Testing with 1000000 repeats: 117 | 118 | >>> try: PatchedEnum.FOO = 'new' except: pass # 0.7640039160000001 seconds 119 | AttributeError('Cannot reassign members.') 120 | 121 | >>> try: QratorEnum.FOO = 'new' except: pass # 0.7405447570000021 seconds 122 | TypeError('Enum-like classes strictly prohibit changing any attribute/property after they are once set') 123 | 124 | >>> try: DiscordEnum.FOO = 'new' except: pass # 0.4569283679999998 seconds 125 | TypeError('Enums are immutable.') 126 | 127 | >>> try: PatchedEnum('unknown') except: pass # 1.9096569710000004 seconds 128 | ValueError("'unknown' is not a valid PatchedEnum") 129 | 130 | >>> try: QratorEnum('unknown') except: pass # 0.8517799580000016 seconds 131 | ValueError('Value unknown is not found in this enum type declaration') 132 | 133 | >>> try: DiscordEnum('unknown') except: pass # 1.1884922160000002 seconds 134 | ValueError("'unknown' is not a valid DiscordEnum") 135 | 136 | >>> PatchedEnum('FOO') # 0.4758100359999986 seconds 137 | 138 | 139 | >>> QratorEnum('FOO') # 0.39784907800000013 seconds 140 | 141 | 142 | >>> DiscordEnum('FOO') # 0.2402779069999994 seconds 143 | 144 | 145 | >>> PatchedEnum(PatchedEnum.FOO) # 0.5433184440000005 seconds 146 | 147 | 148 | >>> QratorEnum(QratorEnum.FOO) # 1.1917125610000028 seconds 149 | 150 | 151 | >>> try: DiscordEnum(DiscordEnum.FOO) except: pass # 1.8536693709999987 seconds 152 | ValueError(" is not a valid DiscordEnum") 153 | 154 | >>> PatchedEnum['FOO'] # 0.18254251400000143 seconds 155 | 156 | 157 | >>> QratorEnum['FOO'] # 0.2101041059999993 seconds 158 | 159 | 160 | >>> DiscordEnum['FOO'] # 0.18098364900000163 seconds 161 | 162 | 163 | 164 | QratorEnum : 3.391990 seconds in total, 0.574964 average (Fastest) 165 | BuiltinEnum : 8.279930 seconds in total, 0.983809 average, slower than QratorEnum by x 1.711080 (in average) 166 | PatchedEnum : 3.875332 seconds in total, 0.585572 average, slower than QratorEnum by x 1.018451 (in average) 167 | DiscordEnum : 3.920352 seconds in total, 0.534867 average, slower than QratorEnum by x 0.930262 (in average) 168 | 169 | 170 | MISC: 171 | Testing with 1000000 repeats: 172 | 173 | >>> sys.getsizeof(BuiltinEnum) # 0.3024344879999994 seconds 174 | 1064 175 | 176 | >>> sys.getsizeof(BuiltinEnum.FOO) # 0.42169187800000074 seconds 177 | 100 178 | 179 | >>> for member in BuiltinEnum: pass # 1.1020697800000008 seconds 180 | 181 | 182 | >>> dir(BuiltinEnum) # 0.7576597479999982 seconds 183 | ['BAR', 'FOO', '__class__', '__doc__', '__members__', '__module__'] 184 | 185 | >>> repr(BuiltinEnum) # 0.6420236060000022 seconds 186 | "" 187 | 188 | Testing with 1000000 repeats: 189 | 190 | >>> sys.getsizeof(PatchedEnum) # 0.28691167299999876 seconds 191 | 1064 192 | 193 | >>> sys.getsizeof(QratorEnum) # 0.28854570699999726 seconds 194 | 896 195 | 196 | >>> sys.getsizeof(DiscordEnum) # 0.28451946799999916 seconds 197 | 1064 198 | 199 | >>> sys.getsizeof(PatchedEnum.FOO) # 0.2607541659999981 seconds 200 | 100 201 | 202 | >>> sys.getsizeof(QratorEnum.FOO) # 0.2656812199999976 seconds 203 | 100 204 | 205 | >>> sys.getsizeof(DiscordEnum.FOO) # 0.2842819829999996 seconds 206 | 56 207 | 208 | >>> for member in PatchedEnum: pass # 0.3214790119999975 seconds 209 | 210 | 211 | >>> for member in QratorEnum: pass # 0.33148565999999846 seconds 212 | 213 | 214 | >>> for member in DiscordEnum: pass # 0.7288776139999982 seconds 215 | 216 | 217 | >>> dir(PatchedEnum) # 0.5787732339999963 seconds 218 | ['BAR', 'FOO', '__class__', '__doc__', '__members__', '__module__'] 219 | 220 | >>> dir(QratorEnum) # 24.630986686999996 seconds 221 | ['BAR', 'FOO', '__add__', '__annotations__', '__call__', '__class__', '__contains__', '__copy__', '__deepcopy__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__module__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', '_base_typed', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'name', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'value', 'zfill'] 222 | 223 | >>> dir(DiscordEnum) # 17.887501173000004 seconds 224 | ['BAR', 'FOO', '__add__', '__class__', '__contains__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__module__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_enum_member_map_', '_enum_member_names_', '_enum_value_map_', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'try_value', 'upper', 'zfill'] 225 | 226 | >>> repr(PatchedEnum) # 0.5370067930000033 seconds 227 | "" 228 | 229 | >>> repr(QratorEnum) # 0.29461049399999695 seconds 230 | "" 231 | 232 | >>> repr(DiscordEnum) # 0.4948041620000083 seconds 233 | "" 234 | 235 | 236 | PatchedEnum : 1.984925 seconds in total, 0.375599 average (Fastest) 237 | BuiltinEnum : 3.225880 seconds in total, 0.584753 average, slower than PatchedEnum by x 1.556855 (in average) 238 | QratorEnum : 25.811310 seconds in total, 0.713106 average, slower than PatchedEnum by x 1.898585 (in average) 239 | DiscordEnum : 19.679984 seconds in total, 0.878011 average, slower than PatchedEnum by x 2.337630 (in average) 240 | 241 | 242 | TOTAL TIME: 243 | 244 | PatchedEnum : 6.602916 seconds in total, 0.274735 average (Fastest) 245 | BuiltinEnum : 13.805300 seconds in total, 0.548496 average, slower than PatchedEnum by x 1.996453 (in average) 246 | QratorEnum : 29.979191 seconds in total, 0.336723 average, slower than PatchedEnum by x 1.225629 (in average) 247 | DiscordEnum : 24.582938 seconds in total, 0.372982 average, slower than PatchedEnum by x 1.357605 (in average) 248 | ``` -------------------------------------------------------------------------------- /fastenum/patches.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from enum import ( # type: ignore 3 | Enum, 4 | EnumMeta, 5 | _EnumDict, 6 | auto, 7 | _auto_null, 8 | _high_bit, 9 | _is_descriptor, 10 | _is_dunder, 11 | _is_sunder, 12 | _make_class_unpicklable, 13 | ) 14 | from types import DynamicClassAttribute 15 | 16 | from fastenum.parcher import Patch, InstancePatch 17 | 18 | 19 | class DynamicClassAttributePatch( 20 | Patch, target=DynamicClassAttribute, update={'__init__', '__get__', '__set_name__', 'set_class_attr'} 21 | ): 22 | def __init__(self, fget=None, fset=None, fdel=None, doc=None, alias=None): 23 | self.fget = fget 24 | self.fset = fset 25 | self.fdel = fdel 26 | 27 | # next two lines make DynamicClassAttribute act the same as property 28 | self.__doc__ = doc or fget.__doc__ 29 | self.overwrite_doc = doc is None 30 | # support for abstract methods 31 | self.__isabstractmethod__ = bool(getattr(fget, '__isabstractmethod__', False)) 32 | # define name for class attributes 33 | self.alias = alias 34 | 35 | def __get__(self, instance, ownerclass): 36 | if instance is None: 37 | if self.__isabstractmethod__: 38 | return self 39 | return getattr(ownerclass, self.alias) 40 | elif self.fget is None: 41 | raise AttributeError("unreadable attribute") 42 | return self.fget(instance) 43 | 44 | def __set_name__(self, ownerclass, alias): 45 | if self.alias is None: 46 | self.alias = f'_cls_attr_{alias}' 47 | 48 | def set_class_attr(self, cls, value): 49 | setattr(cls, self.alias, value) 50 | 51 | @classmethod 52 | def __run_on_instance__(cls, instance): 53 | instance.alias = f'_cls_attr_{instance.fget.__name__}' 54 | 55 | 56 | class EnumPatch( 57 | Patch, 58 | target=Enum, 59 | delete={'name', 'value'}, 60 | update={'__new__', '__setattr__', '__delattr__', '__dir__'}, 61 | ): 62 | def __new__(cls, value): 63 | # all enum instances are actually created during class construction 64 | # without calling this method; this method is called by the metaclass' 65 | # __call__ (i.e. Color(3) ), and by pickle 66 | 67 | # using .__class__ instead of type() as it 2x faster 68 | if value.__class__ is cls: 69 | # For lookups like Color(Color.RED) 70 | return value 71 | # by-value search for a matching enum member 72 | # see if it's in the reverse mapping (for hashable values) 73 | try: 74 | return cls._value2member_map_[value] 75 | except KeyError: 76 | # Not found, no need to do long O(n) search 77 | pass 78 | except TypeError: 79 | # not there, now do long search -- O(n) behavior 80 | for member in cls._unique_member_map_.values(): 81 | if member.value == value: 82 | return member 83 | # still not found -- try _missing_ hook 84 | 85 | # TODO: Maybe remove try/except block and setting __context__ in this case? 86 | try: 87 | result = cls._missing_(value) 88 | except Exception as e: 89 | if cls._missing_ is Enum._missing_: 90 | # assuming Enum._missing_ is always raises exception 91 | # This gives huge boost for standard enum 92 | raise 93 | else: 94 | e.__context__ = ValueError("%r is not a valid %s" % (value, cls.__qualname__)) 95 | raise 96 | 97 | if isinstance(result, cls): 98 | return result 99 | 100 | ve_exc = ValueError("%r is not a valid %s" % (value, cls.__qualname__)) 101 | if result is None: 102 | try: 103 | raise ve_exc 104 | finally: 105 | ve_exc = None 106 | else: 107 | exc = TypeError( 108 | 'error in %s._missing_: returned %r instead of None or a valid member' 109 | % (cls.__name__, result) 110 | ) 111 | exc.__context__ = ve_exc 112 | try: 113 | raise exc 114 | finally: 115 | exc = None 116 | 117 | def __dir__(self): 118 | added_behavior = [ 119 | m 120 | for cls in self.__class__.mro() 121 | for m in cls.__dict__ 122 | if m[0] != '_' and m not in self._member_map_ 123 | ] + [m for m in self.__dict__ if m[0] != '_'] 124 | return ['__class__', '__doc__', '__module__', 'name', 'value'] + added_behavior 125 | 126 | def __setattr__(self, key, value): 127 | if key in {'name', 'value'}: 128 | raise AttributeError("Can't set attribute") 129 | elif key in {'_name_', '_value_'}: 130 | # hook to also set 'value' and 'name' attr 131 | object.__setattr__(self, key[1:-1], value) 132 | object.__setattr__(self, key, value) 133 | 134 | def __delattr__(self, key): 135 | if key in {'name', 'value'}: 136 | raise AttributeError("Can't del attribute") 137 | object.__delattr__(self, key) 138 | 139 | @classmethod 140 | def __run_on_class__(cls, enum_cls: EnumMeta): # type: ignore 141 | cls._set_names(enum_cls) 142 | cls._set_dynamic_class_attrs(enum_cls) 143 | 144 | @classmethod 145 | def __run_on_instance__(cls, member: Enum): # type: ignore 146 | member.__dict__['name'] = member._name_ 147 | member.__dict__['value'] = member._value_ 148 | 149 | @classmethod 150 | def _set_names(cls, enum_cls): 151 | unique_members = set(enum_cls._member_names_) 152 | type.__setattr__( 153 | enum_cls, 154 | '_unique_member_map_', 155 | {k: v for k, v in enum_cls._member_map_.items() if k in unique_members} 156 | ) 157 | 158 | @classmethod 159 | def _set_dynamic_class_attrs(cls, enum_cls): 160 | assert DynamicClassAttributePatch.__enabled__, 'DynamicClassAttr must be patched first' 161 | assert not EnumMetaPatch.__enabled__, 'EnumMetaPatch.__getattr__ method is needed for retrieving class attrs' 162 | 163 | for k, v in enum_cls.__dict__.items(): 164 | if isinstance(v, DynamicClassAttribute): 165 | try: 166 | v.set_class_attr(enum_cls, getattr(enum_cls, k)) 167 | except AttributeError: 168 | pass 169 | 170 | 171 | class EnumDictPatch( 172 | Patch, target=_EnumDict, update={'__init__', '__setitem__', '_member_names', '_last_values'} 173 | ): 174 | 175 | def __init__(self): 176 | dict.__init__(self) 177 | self.members = {} 178 | self._ignore = [] 179 | self._auto_called = False 180 | 181 | def __setitem__(self, key, value): 182 | """Changes anything not dundered or not a descriptor. 183 | 184 | If an enum member name is used twice, an error is raised; duplicate 185 | values are not checked for. 186 | 187 | Single underscore (sunder) names are reserved. 188 | 189 | """ 190 | if _is_sunder(key): 191 | import warnings 192 | warnings.warn( 193 | "private variables, such as %r, will be normal attributes in 3.10" 194 | % (key, ), 195 | DeprecationWarning, 196 | stacklevel=2, 197 | ) 198 | if key not in { 199 | '_order_', '_create_pseudo_member_', 200 | '_generate_next_value_', '_missing_', '_ignore_', 201 | }: 202 | raise ValueError('_names_ are reserved for future Enum use') 203 | if key == '_generate_next_value_': 204 | # check if members already defined as auto() 205 | if self._auto_called: 206 | raise TypeError("_generate_next_value_ must be defined before members") 207 | setattr(self, '_generate_next_value', value) 208 | elif key == '_ignore_': 209 | if isinstance(value, str): 210 | value = value.replace(',', ' ').split() 211 | else: 212 | value = list(value) 213 | self._ignore = value 214 | already = set(value) & self.members.keys() 215 | if already: 216 | raise ValueError('_ignore_ cannot specify already set names: %r' % (already,)) 217 | elif _is_dunder(key): 218 | if key == '__order__': 219 | key = '_order_' 220 | elif key in self.members: 221 | # descriptor overwriting an enum? 222 | raise TypeError('Attempted to reuse key: %r' % key) 223 | elif key in self._ignore: 224 | pass 225 | elif not _is_descriptor(value): 226 | if key in self: 227 | # enum overwriting a descriptor? 228 | raise TypeError('%r already defined as: %r' % (key, self[key])) 229 | if isinstance(value, auto): 230 | if value.value == _auto_null: 231 | value.value = self._generate_next_value(key, 1, len(self.members), list(self.members.values())) 232 | self._auto_called = True 233 | value = value.value 234 | self.members[key] = value 235 | dict.__setitem__(self, key, value) 236 | 237 | @property 238 | def _member_names(self): 239 | return list(self.members) 240 | 241 | @property 242 | def _last_values(self): 243 | return list(self.members.values()) 244 | 245 | 246 | class EnumMetaPatch( 247 | type, Patch, target=EnumMeta, delete={'__getattr__'}, update={'__new__', '__iter__'} 248 | ): 249 | 250 | def __new__(metacls, cls, bases, classdict): 251 | # an Enum class is final once enumeration items have been defined; it 252 | # cannot be mixed with other types (int, float, etc.) if it has an 253 | # inherited __new__ unless a new __new__ is defined (or the resulting 254 | # class will fail). 255 | # 256 | # remove any keys listed in _ignore_ 257 | classdict.setdefault('_ignore_', []).append('_ignore_') 258 | ignore = classdict['_ignore_'] 259 | for key in ignore: 260 | classdict.pop(key, None) 261 | member_type, first_enum = metacls._get_mixins_(cls, bases) 262 | __new__, save_new, use_args = metacls._find_new_(classdict, member_type, 263 | first_enum) 264 | 265 | # save enum items into separate mapping so they don't get baked into 266 | # the new class 267 | enum_members = classdict.members 268 | for name in enum_members: 269 | del classdict[name] 270 | 271 | # adjust the sunders 272 | _order_ = classdict.pop('_order_', None) 273 | 274 | # check for illegal enum names (any others?) 275 | invalid_names = enum_members.keys() & {'mro', ''} 276 | if invalid_names: 277 | raise ValueError('Invalid enum member name: {0}'.format( 278 | ','.join(invalid_names))) 279 | 280 | # create a default docstring if one has not been provided 281 | if '__doc__' not in classdict: 282 | classdict['__doc__'] = 'An enumeration.' 283 | 284 | # create our new Enum type 285 | enum_class = type.__new__(metacls, cls, bases, classdict) 286 | enum_class._member_names_ = [] # names in definition order 287 | enum_class._member_map_ = {} # name->value map 288 | enum_class._unique_member_map_ = {} 289 | enum_class._member_type_ = member_type 290 | 291 | dynamic_attributes = {k: v for c in enum_class.mro() 292 | for k, v in c.__dict__.items() 293 | if isinstance(v, DynamicClassAttribute)} 294 | 295 | # Reverse value->name map for hashable values. 296 | enum_class._value2member_map_ = {} 297 | 298 | # If a custom type is mixed into the Enum, and it does not know how 299 | # to pickle itself, pickle.dumps will succeed but pickle.loads will 300 | # fail. Rather than have the error show up later and possibly far 301 | # from the source, sabotage the pickle protocol for this class so 302 | # that pickle.dumps also fails. 303 | # 304 | # However, if the new class implements its own __reduce_ex__, do not 305 | # sabotage -- it's on them to make sure it works correctly. We use 306 | # __reduce_ex__ instead of any of the others as it is preferred by 307 | # pickle over __reduce__, and it handles all pickle protocols. 308 | if '__reduce_ex__' not in classdict: 309 | if member_type is not object: 310 | methods = {'__getnewargs_ex__', '__getnewargs__', 311 | '__reduce_ex__', '__reduce__'} 312 | if not any(m in member_type.__dict__ for m in methods): 313 | if '__new__' in classdict: 314 | # too late, sabotage 315 | _make_class_unpicklable(enum_class) 316 | else: 317 | # final attempt to verify that pickling would work: 318 | # travel mro until __new__ is found, checking for 319 | # __reduce__ and friends along the way -- if any of them 320 | # are found before/when __new__ is found, pickling should 321 | # work 322 | sabotage = None 323 | for chain in bases: 324 | for base in chain.__mro__: 325 | if base is object: 326 | continue 327 | elif any(m in base.__dict__ for m in methods): 328 | # found one, we're good 329 | sabotage = False 330 | break 331 | elif '__new__' in base.__dict__: 332 | # not good 333 | sabotage = True 334 | break 335 | if sabotage is not None: 336 | break 337 | if sabotage: 338 | _make_class_unpicklable(enum_class) 339 | 340 | # instantiate them, checking for duplicates as we go 341 | # we instantiate first instead of checking for duplicates first in case 342 | # a custom __new__ is doing something funky with the values -- such as 343 | # auto-numbering ;) 344 | for member_name, value in enum_members.items(): 345 | if not isinstance(value, tuple): 346 | args = (value,) 347 | else: 348 | args = value 349 | if member_type is tuple: # special case for tuple enums 350 | args = (args,) # wrap it one more time 351 | if not use_args: 352 | enum_member = __new__(enum_class) 353 | if not hasattr(enum_member, 'value'): 354 | enum_member._value_ = value 355 | else: 356 | enum_member = __new__(enum_class, *args) 357 | if not hasattr(enum_member, 'value'): 358 | if member_type is object: 359 | enum_member._value_ = value 360 | else: 361 | enum_member._value_ = member_type(*args) 362 | 363 | value = enum_member.value 364 | enum_member._name_ = member_name 365 | # setting protected attributes 366 | enum_member.__objclass__ = enum_class 367 | enum_member.__init__(*args) 368 | # If another member with the same value was already defined, the 369 | # new member becomes an alias to the existing one. 370 | for name, canonical_member in enum_class._member_map_.items(): 371 | if canonical_member.value == enum_member.value: 372 | enum_member = canonical_member 373 | break 374 | else: 375 | # Aliases don't appear in member names (only in __members__). 376 | enum_class._unique_member_map_[member_name] = enum_member 377 | enum_class._member_names_.append(member_name) 378 | 379 | dynamic_attr: DynamicClassAttributePatch = dynamic_attributes.get(member_name) 380 | if dynamic_attr is not None: 381 | # Setting attrs respectively to dynamic attribute so access member_name 382 | # through a class will be routed to enum_member 383 | # setattr(enum_class, dynamic_attr.class_attr_name, enum_member) 384 | # name and value dynamic attrs are deleted from EnumMeta and shouldn't fall in this condition at this point 385 | # this is just a way to support any user defined dynamic class attrs 386 | dynamic_attr.set_class_attr(enum_class, enum_member) 387 | else: 388 | setattr(enum_class, member_name, enum_member) 389 | 390 | # now add to _member_map_ 391 | enum_class._member_map_[member_name] = enum_member 392 | try: 393 | # This may fail if value is not hashable. We can't add the value 394 | # to the map, and by-value lookups for this value will be 395 | # linear. 396 | enum_class._value2member_map_[value] = enum_member 397 | except TypeError: 398 | pass 399 | 400 | # double check that repr and friends are not the mixin's or various 401 | # things break (such as pickle) 402 | for name in ('__repr__', '__str__', '__format__', '__reduce_ex__'): 403 | if name in classdict: 404 | continue 405 | class_method = getattr(enum_class, name) 406 | obj_method = getattr(member_type, name, None) 407 | enum_method = getattr(first_enum, name, None) 408 | if obj_method is not None and obj_method is class_method: 409 | setattr(enum_class, name, enum_method) 410 | 411 | # replace any other __new__ with our own (as long as Enum is not None, 412 | # anyway) -- again, this is to support pickle 413 | if Enum is not None: 414 | # if the user defined their own __new__, save it before it gets 415 | # clobbered in case they subclass later 416 | if save_new: 417 | enum_class.__new_member__ = __new__ 418 | enum_class.__new__ = Enum.__new__ 419 | 420 | # py3 support for definition order (helps keep py2/py3 code in sync) 421 | if _order_ is not None: 422 | if isinstance(_order_, str): 423 | _order_ = _order_.replace(',', ' ').split() 424 | if _order_ != list(enum_class._unique_member_map_): 425 | raise TypeError('member order does not match _order_') 426 | 427 | return enum_class 428 | 429 | def __iter__(cls): 430 | return iter(cls._unique_member_map_.values()) 431 | 432 | def __reversed__(cls): 433 | return reversed(list(cls._unique_member_map_.values())) 434 | 435 | 436 | # faster _decompose version from python 3.9 437 | def _decompose(flag, value): 438 | """Extract all members from the value.""" 439 | # _decompose is only called if the value is not named 440 | not_covered = value 441 | negative = value < 0 442 | members = [] 443 | for member in flag: 444 | member_value = member._value_ 445 | if member_value and member_value & value == member_value: 446 | members.append(member) 447 | not_covered &= ~member_value 448 | if not negative: 449 | tmp = not_covered 450 | while tmp: 451 | flag_value = 2 ** _high_bit(tmp) 452 | if flag_value in flag._value2member_map_: 453 | members.append(flag._value2member_map_[flag_value]) 454 | not_covered &= ~flag_value 455 | tmp &= ~flag_value 456 | if not members and value in flag._value2member_map_: 457 | members.append(flag._value2member_map_[value]) 458 | members.sort(key=lambda m: m._value_, reverse=True) 459 | if len(members) > 1 and members[0].value == value: 460 | # we have the breakdown, don't need the value member itself 461 | members.pop(0) 462 | return members, not_covered 463 | 464 | 465 | InstancePatch.new(target=enum, update={'_decompose': _decompose}) 466 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "atomicwrites" 3 | version = "1.4.0" 4 | description = "Atomic file writes." 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 8 | 9 | [[package]] 10 | name = "attrs" 11 | version = "21.4.0" 12 | description = "Classes Without Boilerplate" 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 16 | 17 | [package.extras] 18 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] 19 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 20 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] 21 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] 22 | 23 | [[package]] 24 | name = "black" 25 | version = "22.3.0" 26 | description = "The uncompromising code formatter." 27 | category = "dev" 28 | optional = false 29 | python-versions = ">=3.6.2" 30 | 31 | [package.dependencies] 32 | click = ">=8.0.0" 33 | dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} 34 | mypy-extensions = ">=0.4.3" 35 | pathspec = ">=0.9.0" 36 | platformdirs = ">=2" 37 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 38 | typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} 39 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 40 | 41 | [package.extras] 42 | colorama = ["colorama (>=0.4.3)"] 43 | d = ["aiohttp (>=3.7.4)"] 44 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 45 | uvloop = ["uvloop (>=0.15.2)"] 46 | 47 | [[package]] 48 | name = "click" 49 | version = "8.0.4" 50 | description = "Composable command line interface toolkit" 51 | category = "dev" 52 | optional = false 53 | python-versions = ">=3.6" 54 | 55 | [package.dependencies] 56 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 57 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 58 | 59 | [[package]] 60 | name = "colorama" 61 | version = "0.4.5" 62 | description = "Cross-platform colored terminal text." 63 | category = "dev" 64 | optional = false 65 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 66 | 67 | [[package]] 68 | name = "dataclasses" 69 | version = "0.8" 70 | description = "A backport of the dataclasses module for Python 3.6" 71 | category = "dev" 72 | optional = false 73 | python-versions = ">=3.6, <3.7" 74 | 75 | [[package]] 76 | name = "flake8" 77 | version = "4.0.1" 78 | description = "the modular source code checker: pep8 pyflakes and co" 79 | category = "dev" 80 | optional = false 81 | python-versions = ">=3.6" 82 | 83 | [package.dependencies] 84 | importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} 85 | mccabe = ">=0.6.0,<0.7.0" 86 | pycodestyle = ">=2.8.0,<2.9.0" 87 | pyflakes = ">=2.4.0,<2.5.0" 88 | 89 | [[package]] 90 | name = "importlib-metadata" 91 | version = "4.2.0" 92 | description = "Read metadata from Python packages" 93 | category = "dev" 94 | optional = false 95 | python-versions = ">=3.6" 96 | 97 | [package.dependencies] 98 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 99 | zipp = ">=0.5" 100 | 101 | [package.extras] 102 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 103 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 104 | 105 | [[package]] 106 | name = "iniconfig" 107 | version = "1.1.1" 108 | description = "iniconfig: brain-dead simple config-ini parsing" 109 | category = "dev" 110 | optional = false 111 | python-versions = "*" 112 | 113 | [[package]] 114 | name = "mccabe" 115 | version = "0.6.1" 116 | description = "McCabe checker, plugin for flake8" 117 | category = "dev" 118 | optional = false 119 | python-versions = "*" 120 | 121 | [[package]] 122 | name = "mypy" 123 | version = "0.961" 124 | description = "Optional static typing for Python" 125 | category = "dev" 126 | optional = false 127 | python-versions = ">=3.6" 128 | 129 | [package.dependencies] 130 | mypy-extensions = ">=0.4.3" 131 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 132 | typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} 133 | typing-extensions = ">=3.10" 134 | 135 | [package.extras] 136 | dmypy = ["psutil (>=4.0)"] 137 | python2 = ["typed-ast (>=1.4.0,<2)"] 138 | reports = ["lxml"] 139 | 140 | [[package]] 141 | name = "mypy-extensions" 142 | version = "0.4.3" 143 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 144 | category = "dev" 145 | optional = false 146 | python-versions = "*" 147 | 148 | [[package]] 149 | name = "packaging" 150 | version = "21.3" 151 | description = "Core utilities for Python packages" 152 | category = "dev" 153 | optional = false 154 | python-versions = ">=3.6" 155 | 156 | [package.dependencies] 157 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 158 | 159 | [[package]] 160 | name = "pathspec" 161 | version = "0.9.0" 162 | description = "Utility library for gitignore style pattern matching of file paths." 163 | category = "dev" 164 | optional = false 165 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 166 | 167 | [[package]] 168 | name = "platformdirs" 169 | version = "2.4.0" 170 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 171 | category = "dev" 172 | optional = false 173 | python-versions = ">=3.6" 174 | 175 | [package.extras] 176 | docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] 177 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 178 | 179 | [[package]] 180 | name = "pluggy" 181 | version = "1.0.0" 182 | description = "plugin and hook calling mechanisms for python" 183 | category = "dev" 184 | optional = false 185 | python-versions = ">=3.6" 186 | 187 | [package.dependencies] 188 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 189 | 190 | [package.extras] 191 | dev = ["pre-commit", "tox"] 192 | testing = ["pytest", "pytest-benchmark"] 193 | 194 | [[package]] 195 | name = "py" 196 | version = "1.11.0" 197 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 198 | category = "dev" 199 | optional = false 200 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 201 | 202 | [[package]] 203 | name = "pycodestyle" 204 | version = "2.8.0" 205 | description = "Python style guide checker" 206 | category = "dev" 207 | optional = false 208 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 209 | 210 | [[package]] 211 | name = "pyflakes" 212 | version = "2.4.0" 213 | description = "passive checker of Python programs" 214 | category = "dev" 215 | optional = false 216 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 217 | 218 | [[package]] 219 | name = "pyparsing" 220 | version = "3.0.7" 221 | description = "Python parsing module" 222 | category = "dev" 223 | optional = false 224 | python-versions = ">=3.6" 225 | 226 | [package.extras] 227 | diagrams = ["jinja2", "railroad-diagrams"] 228 | 229 | [[package]] 230 | name = "pytest" 231 | version = "7.0.1" 232 | description = "pytest: simple powerful testing with Python" 233 | category = "dev" 234 | optional = false 235 | python-versions = ">=3.6" 236 | 237 | [package.dependencies] 238 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 239 | attrs = ">=19.2.0" 240 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 241 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 242 | iniconfig = "*" 243 | packaging = "*" 244 | pluggy = ">=0.12,<2.0" 245 | py = ">=1.8.2" 246 | tomli = ">=1.0.0" 247 | 248 | [package.extras] 249 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 250 | 251 | [[package]] 252 | name = "tomli" 253 | version = "1.2.3" 254 | description = "A lil' TOML parser" 255 | category = "dev" 256 | optional = false 257 | python-versions = ">=3.6" 258 | 259 | [[package]] 260 | name = "typed-ast" 261 | version = "1.5.4" 262 | description = "a fork of Python 2 and 3 ast modules with type comment support" 263 | category = "dev" 264 | optional = false 265 | python-versions = ">=3.6" 266 | 267 | [[package]] 268 | name = "typing-extensions" 269 | version = "4.1.1" 270 | description = "Backported and Experimental Type Hints for Python 3.6+" 271 | category = "dev" 272 | optional = false 273 | python-versions = ">=3.6" 274 | 275 | [[package]] 276 | name = "zipp" 277 | version = "3.6.0" 278 | description = "Backport of pathlib-compatible object wrapper for zip files" 279 | category = "dev" 280 | optional = false 281 | python-versions = ">=3.6" 282 | 283 | [package.extras] 284 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 285 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 286 | 287 | [metadata] 288 | lock-version = "1.1" 289 | python-versions = ">=3.6.2<3.10" 290 | content-hash = "f239ee725d10e7999261ab2be808ca8aecf2053474fcd034fdbbbe14ba776016" 291 | 292 | [metadata.files] 293 | atomicwrites = [ 294 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 295 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 296 | ] 297 | attrs = [ 298 | {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, 299 | {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, 300 | ] 301 | black = [ 302 | {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, 303 | {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, 304 | {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, 305 | {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, 306 | {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, 307 | {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, 308 | {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, 309 | {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, 310 | {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, 311 | {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, 312 | {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, 313 | {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, 314 | {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, 315 | {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, 316 | {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, 317 | {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, 318 | {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, 319 | {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, 320 | {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, 321 | {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, 322 | {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, 323 | {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, 324 | {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, 325 | ] 326 | click = [ 327 | {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, 328 | {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, 329 | ] 330 | colorama = [ 331 | {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, 332 | {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, 333 | ] 334 | dataclasses = [ 335 | {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, 336 | {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, 337 | ] 338 | flake8 = [ 339 | {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, 340 | {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, 341 | ] 342 | importlib-metadata = [ 343 | {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, 344 | {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, 345 | ] 346 | iniconfig = [ 347 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 348 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 349 | ] 350 | mccabe = [ 351 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 352 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 353 | ] 354 | mypy = [ 355 | {file = "mypy-0.961-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:697540876638ce349b01b6786bc6094ccdaba88af446a9abb967293ce6eaa2b0"}, 356 | {file = "mypy-0.961-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b117650592e1782819829605a193360a08aa99f1fc23d1d71e1a75a142dc7e15"}, 357 | {file = "mypy-0.961-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bdd5ca340beffb8c44cb9dc26697628d1b88c6bddf5c2f6eb308c46f269bb6f3"}, 358 | {file = "mypy-0.961-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3e09f1f983a71d0672bbc97ae33ee3709d10c779beb613febc36805a6e28bb4e"}, 359 | {file = "mypy-0.961-cp310-cp310-win_amd64.whl", hash = "sha256:e999229b9f3198c0c880d5e269f9f8129c8862451ce53a011326cad38b9ccd24"}, 360 | {file = "mypy-0.961-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b24be97351084b11582fef18d79004b3e4db572219deee0212078f7cf6352723"}, 361 | {file = "mypy-0.961-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f4a21d01fc0ba4e31d82f0fff195682e29f9401a8bdb7173891070eb260aeb3b"}, 362 | {file = "mypy-0.961-cp36-cp36m-win_amd64.whl", hash = "sha256:439c726a3b3da7ca84a0199a8ab444cd8896d95012c4a6c4a0d808e3147abf5d"}, 363 | {file = "mypy-0.961-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5a0b53747f713f490affdceef835d8f0cb7285187a6a44c33821b6d1f46ed813"}, 364 | {file = "mypy-0.961-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0e9f70df36405c25cc530a86eeda1e0867863d9471fe76d1273c783df3d35c2e"}, 365 | {file = "mypy-0.961-cp37-cp37m-win_amd64.whl", hash = "sha256:b88f784e9e35dcaa075519096dc947a388319cb86811b6af621e3523980f1c8a"}, 366 | {file = "mypy-0.961-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d5aaf1edaa7692490f72bdb9fbd941fbf2e201713523bdb3f4038be0af8846c6"}, 367 | {file = "mypy-0.961-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9f5f5a74085d9a81a1f9c78081d60a0040c3efb3f28e5c9912b900adf59a16e6"}, 368 | {file = "mypy-0.961-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f4b794db44168a4fc886e3450201365c9526a522c46ba089b55e1f11c163750d"}, 369 | {file = "mypy-0.961-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:64759a273d590040a592e0f4186539858c948302c653c2eac840c7a3cd29e51b"}, 370 | {file = "mypy-0.961-cp38-cp38-win_amd64.whl", hash = "sha256:63e85a03770ebf403291ec50097954cc5caf2a9205c888ce3a61bd3f82e17569"}, 371 | {file = "mypy-0.961-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f1332964963d4832a94bebc10f13d3279be3ce8f6c64da563d6ee6e2eeda932"}, 372 | {file = "mypy-0.961-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:006be38474216b833eca29ff6b73e143386f352e10e9c2fbe76aa8549e5554f5"}, 373 | {file = "mypy-0.961-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9940e6916ed9371809b35b2154baf1f684acba935cd09928952310fbddaba648"}, 374 | {file = "mypy-0.961-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a5ea0875a049de1b63b972456542f04643daf320d27dc592d7c3d9cd5d9bf950"}, 375 | {file = "mypy-0.961-cp39-cp39-win_amd64.whl", hash = "sha256:1ece702f29270ec6af25db8cf6185c04c02311c6bb21a69f423d40e527b75c56"}, 376 | {file = "mypy-0.961-py3-none-any.whl", hash = "sha256:03c6cc893e7563e7b2949b969e63f02c000b32502a1b4d1314cabe391aa87d66"}, 377 | {file = "mypy-0.961.tar.gz", hash = "sha256:f730d56cb924d371c26b8eaddeea3cc07d78ff51c521c6d04899ac6904b75492"}, 378 | ] 379 | mypy-extensions = [ 380 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 381 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 382 | ] 383 | packaging = [ 384 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 385 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 386 | ] 387 | pathspec = [ 388 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 389 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 390 | ] 391 | platformdirs = [ 392 | {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, 393 | {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, 394 | ] 395 | pluggy = [ 396 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 397 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 398 | ] 399 | py = [ 400 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 401 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 402 | ] 403 | pycodestyle = [ 404 | {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, 405 | {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, 406 | ] 407 | pyflakes = [ 408 | {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, 409 | {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, 410 | ] 411 | pyparsing = [ 412 | {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, 413 | {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, 414 | ] 415 | pytest = [ 416 | {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, 417 | {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"}, 418 | ] 419 | tomli = [ 420 | {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, 421 | {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, 422 | ] 423 | typed-ast = [ 424 | {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, 425 | {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, 426 | {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, 427 | {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, 428 | {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, 429 | {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, 430 | {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, 431 | {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, 432 | {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, 433 | {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, 434 | {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, 435 | {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, 436 | {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, 437 | {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, 438 | {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, 439 | {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, 440 | {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, 441 | {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, 442 | {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, 443 | {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, 444 | {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, 445 | {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, 446 | {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, 447 | {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, 448 | ] 449 | typing-extensions = [ 450 | {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, 451 | {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, 452 | ] 453 | zipp = [ 454 | {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, 455 | {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, 456 | ] 457 | --------------------------------------------------------------------------------