enums.py
Less sophisticated, less restrictive, more magical and funky!
enums.py is a module that implements enhanced enumerations for Python.
Incompatible with standard library!
Below are many examples of using this module.
Prerequisites
Code snippets and examples are using several common imports and types, which are mainly omitted for simplicity:
from typing import TypeVar # for different typing purposes
from enums import ( # library imports used in examples
# enumerations
Enum,
StrEnum,
IntEnum,
# flag boundary
FlagBoundary,
# boundaries
CONFORM,
EJECT,
KEEP,
STRICT,
# flags
Flag,
IntFlag,
# traits
Trait,
Order,
StrFormat,
# auto item
auto,
# unique decorator
unique,
)
T = TypeVar("T") # general (and generic) type variable
E = TypeVar("E", bound=Enum) # enumeration type variable
F = TypeVar("F", bound=Flag) # flag type variable
FB = TypeVar("FB", bound=FlagBoundary) # flag boundary type variable
IF = TypeVar("IF", bound=IntFlag) # integer flag type variable
Creating Enumerations
There are many ways to create enumerations.
This can be done in classical way:
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
Like the standard enum
module, enums.py
has auto
class:
class Color(Enum):
RED = auto()
GREEN = auto()
BLUE = auto()
Enumerations can be created without explicit class
usage:
Color = Enum("Color", ["RED", "GREEN", "BLUE"])
Strings can also be used here:
Color = Enum("Color", "RED GREEN BLUE")
You can also use keyword arguments in order to define members:
Color = Enum("Color", RED=1, GREEN=2, BLUE=3)
Same with auto()
, of course:
Color = Enum("Color", RED=auto(), GREEN=auto(), BLUE=auto())
All code snippets above produce Color
in the end, which has 3 members:
<Color.RED: 1>
<Color.GREEN: 2>
<Color.BLUE: 3>
Using Arguments
Enumeration members that have tuple
values but do not subclass tuple
are interpreted as values passed to __init__
of their class:
class Planet(Enum):
MERCURY = (3.303e+23, 2.4397e6)
VENUS = (4.869e+24, 6.0518e6)
EARTH = (5.976e+24, 6.37814e6)
MARS = (6.421e+23, 3.3972e6)
JUPITER = (1.9e+27, 7.1492e7)
SATURN = (5.688e+26, 6.0268e7)
URANUS = (8.686e+25, 2.5559e7)
NEPTUNE = (1.024e+26, 2.4746e7)
def __init__(self, mass: float, radius: float) -> None:
self.mass = mass # kg
self.radius = radius # m
@property
def surface_gravity(self) -> float:
# universal gravitational constant
G = 6.67300E-11 # m^3 kg^(-1) s^(-2)
return G * self.mass / (self.radius * self.radius)
print(Planet.EARTH.value) # (5.976e+24, 6378140.0)
print(Planet.EARTH.surface_gravity) # 9.802652743337129
Iteration
It is possible to iterate over unique enumeration members:
Color = Enum("Color", RED=1, GREEN=2, BLUE=3)
for color in Color:
print(Color.title)
# Red
# Green
# Blue
Or over all members, including aliases:
Color = Enum("Color", RED=1, GREEN=2, BLUE=3, R=1, G=2, B=3)
for name, color in Color.members.items():
print(name, color.name)
# RED RED
# GREEN GREEN
# BLUE BLUE
# R RED
# G GREEN
# B BLUE
Member Attributes
Enumeration members have several useful attributes:
- name, which represents their actual name;
- value, which contains their value;
- title, which is more human-readable version of their name.
print(Color.BLUE.name) # BLUE
print(Color.BLUE.value) # 3
print(Color.BLUE.title) # Blue
Advanced Access
Enumeration members can be accessed with case insensitive strings:
class Test(Enum):
TEST = 13
test = Test.from_name("test") # <Test.TEST: 13>
Note that if two members have same case insensitive name version, last in wins!
Also keep in mind Enum.from_name
will not work with composite flags!
You can use Flag.from_args
to create composite flag from multiple values or names:
Perm = Flag("Perm", "Z X W R", start=0)
Perm.from_args("r", "w", "x") # <Perm.X|R|W: 7>
Perm.from_args(2, 4) # <Perm.W|R: 6>
There is also Enum.from_value
, which tries to use Enum.from_name
if given value is string,
and otherwise (also if failed), it attempts by-value lookup. This function accepts default
argument, such that Enum.from_value(default)
will be called on fail if default
was given.
Example:
class Perm(Flag):
Z, X, W, R = 0, 1, 2, 4
Perm.from_value(8, default=0) # <Perm.Z: 0>
Perm.from_value("broken", "r") # <Perm.R: 4>
String Enumeration
StrEnum
is a simple type derived from Enum
,
which only affects enum_generate_next_value
by making it use the casefolded version of the member name:
class Relationship(StrEnum):
BLOCKED = auto() # "blocked"
FOLLOWED = auto() # "followed"
FRIEND = auto() # "friend"
Flags
Flag
is a special enumeration that focuses around supporting bit-flags along with operations on them,
such as OR |
, AND &
, XOR ^
and INVERT ~
.
class Perm(Flag):
Z = 0
X = 1
W = 2
R = 4
# <Perm.W|R: 6>
RW = Perm.R | Perm.W
# <Perm.R: 4>
R = (Perm.R | Perm.W) & Perm.R
# <Perm.X|W: 3>
WX = Perm.W ^ Perm.X
# <Perm.Z: 0>
Z = Perm.X ^ Perm.X
# <Perm.X|R: 5>
RX = ~Perm.W
Flag Boundaries
Flags can have different boundaries (of type FlagBoundary
)
that define how unknown bits are handled.
STRICT
Strict boundary is pretty straightforward: an exception is raised on any unknown bits.
class Strict(Flag, boundary=STRICT):
X = auto() # 0b0001
Y = auto() # 0b0010
Z = auto() # 0b0100
strict = Strict(0b1101) # error!
# Traceback (most recent call last):
# <...>
# ValueError: Invalid value 13 in Strict:
# given 0b0 1101
# allowed 0b0 0111
# <...>
CONFORM
Conform boundary is going to remove any invalid bits, leaving only known ones.
class Conform(Flag, boundary=CONFORM):
X = auto() # 0b0001
Y = auto() # 0b0010
Z = auto() # 0b0100
conform = Conform(0b1101) # 0b0101 -> <Conform.X|Z: 5>
EJECT
Eject boundary is going to remove Flag
membership from out-of-range values.
class Eject(Flag, boundary=EJECT):
X = auto() # 0b0001
Y = auto() # 0b0010
Z = auto() # 0b0100
eject = Eject(0b1101) # 13
KEEP
Keep boundary is going to save all invalid bits.
class Keep(Flag, boundary=KEEP):
X = auto() # 0b0001
Y = auto() # 0b0010
Z = auto() # 0b0100
keep = Keep(0b1101) # <Keep.X|Z|0x8: 13>
Type Restriction and Inheritance
Enumeration members can be restricted to have values of the same type:
class OnlyInt(IntEnum):
SOME = 1
OTHER = "2" # will be casted
BROKEN = "broken" # error will be raised on creation
As well as inherit behavior from that type:
class Access(IntFlag):
NONE = 0
SIMPLE = 1
MAIN = 2
FULL = Access.SIMPLE | Access.MAIN
print(FULL + 10) # 13
Because IntEnum
and IntFlag
are subclasses of int
,
they lose their membership when int
operations are used with them.
Method Resolution Order
enums.py
requires the following definiton of new Enum
subclass:
EnumName([trait_type, ...] [data_type] enum_type)
For example:
class Value(Order, Enum):
"""Generic value that supports ordering."""
class FloatValue(float, Value):
"""Float value that inherits Value."""
Here, FloatValue
bases are going to be transformed into:
(Value, float, Order, Enum)
Which allows us to preserve functions defined in enumerations or flags, while still having traits work nicely with overriding them.
Traits
enums.py
implements special traits (aka mixins), which add specific behavior to classes.
Each Trait implements some functionality for enumerations, but does not subclass Enum
.
Therefore they are pretty much useless on their own.
StrFormat
Default __format__
of Enum
will attempt to use __format__
of member data type, if given:
class Foo(IntEnum):
BAR = 42
print(f"{Foo.BAR}") # 42
StrFormat
overwrites that behavior and uses str(member).__format__(format_spec)
instead:
class Foo(StrFormat, IntEnum):
BAR = 42
print(f"{Foo.BAR}") # Foo.BAR
Order
Order
trait implements ordering (==
, !=
, <
, >
, <=
and >=
)
for enumeration members. This function will attempt to find member by value.
Example:
class Grade(Order, Enum):
A = 5
B = 4
C = 3
D = 2
F = 1
print(Grade.A > Grade.C) # True
print(Grade.F <= Grade.D) # True
print(Grade.B == 4) # True
print(Grade.F >= 0) # True
Defining Traits
One can define their own trait for enumerations by deriving from Trait
.
Example:
class StrTitle(Trait):
"""Use title of the member in str() calls."""
def __str__(self) -> str:
return self.title
Using the trait is as simple as expected:
class Color(StrTitle, Enum):
RED = auto()
GREEN = auto()
BLUE = auto()
print(Color.RED) # Red
Unique Enumerations
Enumeration members can have aliases, for example:
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
R, G, B = RED, GREEN, BLUE # aliases
enums.py
has @unique
class decorator, that can be used
to check and identify that enumeration does not have aliases.
That is, the following snippet will error:
@unique
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
R, G, B = RED, GREEN, BLUE # aliases
With the following exception:
ValueError: Duplicates found in <enum 'Color'>: R -> RED, G -> GREEN, B -> BLUE.
Class Keyword Arguments
Enum
class knows several class keyword arguments:
-
auto_on_missing -
bool
-
ignore -
Union[str, Iterable[str]]
-
start -
T
-
boundary -
FB
(used inFlag
)
auto_on_missing
Boolean flag, if set to True
(default is False
), allows to do something like:
class Color(Enum, auto_on_missing=True):
RED # 1
GREEN # 2
BLUE # 3
print(repr(Color.RED)) # <Color.RED: 1>
ignore
Works same as putting enum_ignore
inside the class (default is ()
(empty tuple)):
class Time(Enum, ignore=("time_vars", "day")):
time_vars = vars()
for day in range(366):
time_vars[f"day_{day}"] = day
day = Time.day_365 # <Time.day_365: 365>
start
Just like enum_start
, defines a start value that should be used for enum members (default is None
):
class Perm(Flag, start=0):
Z = auto() # 0
X = auto() # 1
W = auto() # 2
R = auto() # 4
print(repr(Perm.R | Perm.W)) # <Perm.R|W: 6>
boundary
Represents boundaries for flags. See Flag Boundaries section for more information.
Special Names
enums.py
uses special names for managing behavior:
-
enum_missing -
classmethod(cls: Type[E], value: T) -> E
-
enum_ignore -
Union[str, Iterable[str]]
-
enum_generate_next_value -
staticmethod(name: str, start: Optional[T], count: int, member_values: List[T]) -> T
-
enum_auto_on_missing -
bool
-
enum_start -
T
-
_name -
Optional[str]
-
_value -
T
enum_missing
Class method that should be used in order to process values that are not present in the enumeration:
from typing import Union
class Speed(Enum):
SLOW = 1
NORMAL = 2
FAST = 3
@classmethod
def enum_missing(cls, value: Union[float, int]) -> Enum:
if value < 1:
return cls.SLOW
elif value > 3:
return cls.FAST
else:
return cls.NORMAL
speed = Speed(5) # <Speed.FAST: 3>
enum_ignore
Iterable of strings or a string that contains names of class members that should be ignored when creating enumeration members:
class Time(IntEnum):
enum_ignore = ["Time", "second"] # or "Time, second" or "Time second" or "Time,second"
Time = vars()
for second in range(60):
Time[f"s_{second}"] = second
print(repr(Time.s_59)) # <Time.s_59: 59>
print(repr(Time.s_0)) # <Time.s_0: 0>
enum_generate_next_value
Static method that takes member name, start value (default is None, unless specified otherwise), count of unique members already created and list of all member values (including aliases).
This method should output value for the new member:
from typing import List, Optional
class CountEnum(Enum):
@staticmethod
def enum_generate_next_value(
name: str, start: Optional[T], count: int, values: List[T]
) -> T:
"""Return count of unique members + 1."""
return count + 1
class Mark(CountEnum):
F = auto() # 1
D = auto() # 2
C = auto() # 3
B = auto() # 4
A = auto() # 5
enum_auto_on_missing
Boolean that indicates whether auto() should be used to generate values for missing names:
class Color(Enum):
enum_auto_on_missing = True
RED, GREEN, BLUE # 1, 2, 3
enum_start
Variable that indicates what value should be passed as start
to enum_generate_next_value
.
_name
Private attribute, name of the member. Ideally it should never be modified.
_value
Private attribute, value of the member. Again, it is better not to modify it, unless required.
Updating (Mutating) Enumerations
Unlike in standard enum
module, enumerations can be mutated:
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
ALPHA = Color.add_member("ALPHA", 0) # <Color.ALPHA: 0>
Or using Enum.update()
for several members:
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
Color.update(ALPHA=0, BROKEN=-1)
Even Flag
flags operate nicely when mutated:
class P(Flag):
R = 4
W = 2
X = 1
Z = 0
RWX = P.R | P.W | P.X # <P.R|W|X: 7>
P.update(RWX=RWX.value) # RWX is now <P.RWX: 7>
Installing
Python 3.6 or higher is required
To install the library, you can just run the following command:
# Linux / OS X
python3 -m pip install --upgrade enums.py
# Windows
py -3 -m pip install --upgrade enums.py
In order to install the library from source, you can do the following:
$ git clone https://github.com/nekitdev/enums.py
$ cd enums.py
$ python -m pip install --upgrade .
Testing
In order to test the library, you need to have coverage, flake8 and pytest packages.
They can be installed like so:
$ cd enums.py
$ python -m pip install .[test]
Then linting and running tests with coverage:
# lint the source
$ flake8
# run tests and record coverage
$ coverage run -m pytest test_enums.py
Changlelog
- 0.1.0 - Initial release, almost full support of standard enum module;
- 0.1.1 - Make bitwise operations in Flag smarter;
- 0.1.2 - Add IntEnum and IntFlag;
- 0.1.3 - Add Traits and fix bugs;
- 0.1.4 - Add nice dir() implementation for both Enum class and members;
- 0.1.5 - Fix small bugs;
- 0.2.0 - Fix IntEnum to be almost even with standard library, fix bugs and add tests.
- 0.3.0 - Fix MRO resolution and add small enhancements.
- 0.3.1 - Fix small typos and other non-code-related things.
-
0.4.0 - Typing fixes and usage of
ENUM_DEFINED
flag instead of setting toNone
and checks. -
0.5.0 - Preserve important methods, such as
__format__
,__repr__
,__str__
and others. - 0.6.0 - Overall rewrite, implement flag boundaries and improve flags.
Authors
This project is mainly developed by nekitdev.