Skip to content

Commit fc9837f

Browse files
committed
Apply changes from python/cpython#17669
Run builtin python tests directly from python test module Refactor patch applying
1 parent 7ffea32 commit fc9837f

File tree

9 files changed

+552
-9161
lines changed

9 files changed

+552
-9161
lines changed

README.md

+25-19
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,37 @@
11
# fastenum
22
#### Patch for builtin `enum` module to achieve best performance
33

4-
### TL;DR
5-
- up to 6x faster members access
6-
- up to 10x faster members `name`/`value` access
7-
- up to 2x faster values positive check
8-
- up to 3x faster values negative check
9-
- up to 3x faster iteration
4+
This patch is based on
5+
[python/cpython#17669](https://github.com/python/cpython/pull/17669) and [python/cpython#16483](https://github.com/python/cpython/pull/16483)
106

7+
### Why?
8+
- ~100x faster new `Flags` and `IntFlags` creation
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
1114

1215

13-
#### To enable patch, just import fastenum once:
14-
###### **Make sure you import it before importing anything else, otherwise things could (will) break**
16+
### Wow it's fast! How I do use it?
17+
To enable patch, just import fastenum once:
1518
```python
1619
import fastenum
17-
assert fastenum.enabled
1820

19-
from enum import Enum
20-
assert Enum.__is_fast__()
21+
assert fastenum.enabled
2122
```
23+
After you imported fastenum package, all your Enums are patched and fast!
24+
25+
You don't need to re-apply patch across different modules: once it's patched it'll work everywhere.
2226

23-
#### What's changed?
24-
- `EnumMeta.__getattr__` is removed
25-
- `DynamicClassAttribute` is removed in favor of instance attributes
26-
- `name`/`value` are ordinal attributes and put in `__slots__` when possible
27-
- `_missing_` type check is removed, as it was re-e-e-ally slowing things down _(kinda breaking change, but who cares?)_
28-
- `_order_` was removed as since python 3.6 dicts preserve order
29-
- some other minor improvements
3027

31-
I feel that this actually needs to be in stdlib, but patching all this stuff was actually easier for me that opening issue in python bug tracker
28+
### What's changed?
29+
All changes are backwards compatible, so you should be ok with any of your existing code.
30+
But of course, always test first!
31+
- Optimized `Enum.__new__`
32+
- Remove `EnumMeta.__getattr__`
33+
- Store `Enum.name` and `.value` in members `__dict__` for faster access
34+
- Replace `Enum._member_names_` with `._unique_member_map_` for faster lookups and iteration (old arg still remains)
35+
- Replace `_EmumMeta._member_names` and `._last_values` with `.members` mapping (old args still remain)
36+
- Add support for direct setting and getting class attrs on `DynamicClassAttribute` without need to use slow `__getattr__`
37+
- Various minor improvements

fastenum/__init__.py

+9-19
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from enum import Enum
1+
import enum
22

33
from .parcher import _get_all_subclasses, Patch, PatchMeta
4-
from .patches import EnumMetaPatch, EnumPatch
4+
from .patches import EnumMetaPatch, EnumPatch, DynamicClassAttributePatch, EnumDictPatch
55

66
__all__ = (
77
'disable',
@@ -11,46 +11,36 @@
1111

1212
enabled = False
1313

14+
orig_decompose = enum._decompose
15+
1416

1517
def enable():
1618
"""
1719
Patches enum for best performance
1820
"""
1921
global enabled
2022
if enabled:
21-
raise RuntimeError('Builtin enum is already patched')
23+
raise RuntimeError('Nothing to enable: patch is already applied')
2224

2325
patch: PatchMeta
2426
for patch in Patch.__subclasses__():
2527
patch.enable()
26-
27-
# setting missing attributes to enum types and members that were created before patch
28-
for enum_cls in _get_all_subclasses(Enum):
29-
unique_members = set(enum_cls._member_names_)
30-
type.__setattr__(
31-
enum_cls,
32-
'_unique_members_',
33-
{k: v for k, v in enum_cls._member_map_.items() if k in unique_members}
34-
)
35-
# e._value2member_map_ can have extra members so prefer it,
36-
# but it also can be empty if values are unhashable
37-
for member in (enum_cls._value2member_map_ or enum_cls._member_map_).values():
38-
object.__setattr__(member, 'name', member._name_)
39-
object.__setattr__(member, 'value', member._value_)
28+
enum._decompose = patches._decompose
4029
enabled = True
4130

4231

4332
def disable():
4433
"""
45-
Opposite of enable()
34+
Restores enum to its origin state
4635
"""
4736
global enabled
4837
if not enabled:
49-
raise RuntimeError('Builtin enum was not patched yet')
38+
raise RuntimeError('Nothing to disable: patch was not applied')
5039

5140
patch: PatchMeta
5241
for patch in Patch.__subclasses__():
5342
patch.disable()
43+
enum._decompose = orig_decompose
5444
enabled = False
5545

5646

fastenum/parcher.py

+25-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import MutableMapping, Any, Set, Type
1+
import gc
2+
from typing import MutableMapping, Any, Set, Type, Callable
23

34

45
class _Missing:
@@ -47,16 +48,20 @@ def set_attr(t, name, value):
4748
class PatchMeta(type):
4849
__enabled__: bool
4950

51+
__run_on_class__: Callable[[Type[Any]], None]
52+
__run_on_instance__: Callable[[Any], None]
53+
5054
def __prepare__(cls, *args, **kwargs):
5155
return type.__prepare__(*args, **kwargs)
5256

53-
def __new__(mcs, name, bases, namespace, target=None, delete=None, update=None):
57+
def __new__(mcs, name, bases, namespace, target=None, delete=None, update=None, keep=None):
5458
target = target or namespace.pop('__target__', None)
5559
if target is None:
56-
return type.__new__(mcs, name, bases, {})
60+
return type.__new__(mcs, name, bases, namespace)
5761

5862
to_delete = delete or namespace.pop('__to_delete__', set())
5963
to_update = update or namespace.pop('__to_update__', set())
64+
to_keep = keep or namespace.pop('__to_keep__', set())
6065

6166
patched_attrs = {
6267
attr: namespace[attr]
@@ -68,12 +73,12 @@ def __new__(mcs, name, bases, namespace, target=None, delete=None, update=None):
6873
for attr in to_update | to_delete
6974
if attr in target.__dict__
7075
}
71-
cls = type.__new__(mcs, name, bases, {})
76+
cls = type.__new__(mcs, name, bases, namespace)
7277

7378
cls.__target__ = target
7479
cls.__to_update__ = patched_attrs
7580
cls.__original_attrs__ = original_attrs
76-
cls.__extra__ = (to_update | to_delete) ^ original_attrs.keys()
81+
cls.__extra__ = (to_update | to_delete) ^ (original_attrs.keys() | to_keep)
7782
cls.__to_delete__ = to_delete
7883
cls.__redefined_on_subclasses__ = {}
7984
cls.__enabled__ = False
@@ -83,6 +88,16 @@ def enable(cls):
8388
target = cls.__target__
8489
subclasses = _get_all_subclasses(target)
8590

91+
if cls.__run_on_class__:
92+
cls.__run_on_class__(target)
93+
for sub_cls in subclasses:
94+
cls.__run_on_class__(sub_cls)
95+
96+
if cls.__run_on_instance__:
97+
for obj in gc.get_objects():
98+
if isinstance(obj, target):
99+
cls.__run_on_instance__(obj)
100+
86101
for attr in cls.__to_delete__:
87102
old_value = del_attr(target, attr)
88103
if old_value is MISSING:
@@ -118,7 +133,12 @@ def disable(cls):
118133

119134

120135
class Patch(metaclass=PatchMeta):
136+
"""Class to declare attributes to patch other classes"""
137+
121138
__target__: Type[Any]
122139
__to_update__: Set[str]
123140
__to_delete__: Set[str]
124141
__enabled__: bool
142+
143+
__run_on_class__: Callable[[Type[Any]], None] = None
144+
__run_on_instance__: Callable[[Any], None] = None

0 commit comments

Comments
 (0)