Source code for named_enum.meta
# -*- coding: utf-8 -*-
"""Module for the rewriting EnumMeta class. It contains 2 classes:
`NamedEnumMeta`, `_NamedEnumDict`."""
# mypy: ignore-errors
import sys as _sys
from collections import namedtuple, OrderedDict
from collections.abc import Sequence
from enum import Enum, EnumMeta, _EnumDict
from functools import partial
from typing import (
Any, ClassVar, Dict, Generator, List, NamedTuple, Optional, Set, Tuple, Union, Type
)
__all__ = ['NamedEnumMeta']
class _NamedEnumDict(_EnumDict):
"""Customizes _EnumDict, such that it allows setting the value for the keyword
'_field_names_' and provides the functions for cleaning itself and
converting the collection type value (except str) to NamedTuple type.
"""
def __setitem__(self, key: str, value: Any) -> None:
"""Makes an exception for the single underscore name '_field_names_'.
Args:
key (str): variable or function names defined in class.
value (Any): values or functions.
"""
if key == '_field_names_':
dict.__setitem__(self, key, value)
else:
super().__setitem__(key, value)
def _clean(self) -> None:
"""Removes all the items, and set the variables '_member_names' and
'_last_values' to empty."""
# for dictionary, that's the only way to _clean it
for name in self._member_names:
del self[name]
# _clean the variables as well
tmp_dict = _EnumDict()
self._member_names = tmp_dict._member_names
self._last_values = tmp_dict._last_values
def _convert(self, tuple_cls: Type[NamedTuple]) -> None:
"""Uses the given tuple class to _convert the items.
Args:
tuple_cls (Type[NamedTuple]): using namedtuple generated tuple class.
"""
if tuple_cls is tuple or not issubclass(tuple_cls, tuple):
raise ValueError("'tuple_cls' must be a customized tuple class "
"using namedtuple generated class instead.")
_member_names = self._member_names
_last_values = []
feature_num = len(getattr(tuple_cls, '_fields'))
if feature_num == 1:
# converting the type of the value in customized tuple
for value in self._last_values:
_last_values.append(tuple_cls(value))
else:
# converting the type of the value in customized tuple
for value in self._last_values:
if not isinstance(value, Sequence) or isinstance(value, str):
err_msg = "Unable to unpack the value '{}' as {} " \
"for the fields.".format(value, tuple_cls.__name__)
raise ValueError(err_msg)
else:
_last_values.append(tuple_cls(*value))
self._clean()
# put the converted items back, the __setitem__ function in _EnumDict
# will fill the variables, like '_member_names' and '_last_values'
for i, name in enumerate(_member_names):
self[name] = _last_values[i]
[docs]class NamedEnumMeta(EnumMeta):
"""Extends the `EnumMeta` class for three purposes:
1. uses the `_NamedEnumDict` as the data type of the `namespace` parameter
for `__new__` function, such that we can use the `namedtuple` as the data
type of the value of each enumeration item.
2. provides extra functions, which is independent of the variable
`_field_names_`, such as `names`, `values`, `as_dict`, `as_list`, `as_set`,
`as_tuple`, `as_ordereddict`, `describe`, `gen`. The aim is extending the
Enum class for complicated use cases in software development.
3. provides functions for each field name defined in class variable
`_field_names_` in the NamedEnum class and its subclasses, for example:
assuming `'key'` is included in `_field_names_`, then the functions for this
field name are: `keys`, `from_key`, `has_key`.
"""
@classmethod
def __prepare__(mcs, cls: str, bases: Tuple) -> _NamedEnumDict:
"""Namespace hook, uses _NamedEnumDict as the type of namespace instead
of _EnumDict.
Args:
cls (str): name of the class to create.
bases (Tuple): parent classes of the class to create.
Returns:
_NamedEnumDict: namespace dictionary.
"""
# create the namespace dict
enum_dict = _NamedEnumDict()
enum_dict._cls_name = cls
# inherit previous flags and _generate_next_value_ function
if _sys.version_info >= (3, 8, 6):
# inherit previous flags and _generate_next_value_ function
member_type, first_enum = mcs._get_mixins_(cls, bases)
else:
member_type, first_enum = mcs._get_mixins_(bases)
if first_enum is not None:
enum_dict['_generate_next_value_'] = getattr(
first_enum, '_generate_next_value_', None)
return enum_dict
def __new__(mcs, name: str, bases: Tuple, namespace: _NamedEnumDict) -> ClassVar:
"""Besides the class creation, this function also intends to create a
named tuple data type depending on the given value of '_field_names_'
variable to be the data type of the value of enumeration iem and add
those extra functions to the class.
Args:
name (str): name of the instance class.
bases (Tuple): base classes, its instance class inherits from.
namespace (_NamedEnumDict): contains the attributes and functions of the
instance class.
Returns:
ClassVar: class object
"""
# if the _field_names_ not defined in the class, get it from its parent
# class; otherwise uses the defined one.
if '_field_names_' not in namespace:
_field_names_ = bases[0].__dict__.get('_field_names_', None)
else:
_field_names_ = namespace['_field_names_']
# if the _field_names_ is not defined, then switch back to the normal
# enum but with extended functions
if _field_names_:
# created the customized tuple class with the defined _field_names_
_tuple_cls = namedtuple("NamedTuple", _field_names_)
if {"name", "value"}.issubset(_tuple_cls._fields):
raise AttributeError("'name' or 'value' cannot be attributes")
# _convert the type of the item in namespace dictionary to the named
# tuple type
namespace._convert(_tuple_cls)
# create the class and define a class variable to hold the
# customized tuple class. It's needed to return the field names.
cls = super().__new__(mcs, name, bases, namespace)
cls._tuple_cls = _tuple_cls
# the function name formats, docstring formats and bases functions
func_factory_mapping = [
("%ss",
"Collective method to return the values of the attribute `%s` "
"from all the enumeration items.",
mcs._field_values),
("from_%s",
"Returns a tuple of the defined enumeration items regarding to "
"the given `field_value` of field `%s`, if `as_tuple` is True; "
"otherwise returns a generator.",
mcs._from_field),
("has_%s",
"Returns a boolean value which indicates if there is at least "
"one enumeration item in which the value of the field `%s` "
"matches the given field_value.",
mcs._has_field)
]
# function creation factory: create functions for each field_name
for field_name in cls._fields():
for name, docstring, mcs_func in func_factory_mapping:
func_name = name % field_name
func_docstring = docstring % field_name
setattr(cls, func_name, partial(mcs_func, cls, field_name))
# override the docstring of the partial function
par_func = getattr(cls, func_name)
par_func.__doc__ = func_docstring
par_func.__name__ = func_name
cls._tuple_cls = _tuple_cls
else:
cls = super().__new__(mcs, name, bases, namespace)
return cls
def __contains__(cls, member: Union[str, Enum]) -> bool:
"""verrides the magic method in Enum class, which doesn't support
member name search from python 3.8.
Args:
member (Union[str, Enum]):
Returns:
bool: if the member is contained in the enumeration.
"""
if isinstance(member, str):
return member in cls._member_map_
return isinstance(member, cls) and member._name_ in cls._member_map_
[docs] def _fields(cls) -> Tuple:
"""Returns the defined field names as a `tuple` for the enumeration
class.
Note:
If the variable `_field_names_` is `None` or empty value, then
returns an empty `tuple`.
Returns:
Tuple: tuple of field names.
Examples:
>>> class TripleEnum(NamedEnum):
... _field_names_ = ("first", "second", "third")
>>> TripleEnum._fields()
('first', 'second', 'third')
>>> class Triangle(TripleEnum):
... EQUILATERAL = (6, 6, 6)
... RIGHT = (3, 4, 5)
>>> Triangle._fields()
('first', 'second', 'third')
"""
if cls._field_names_:
return cls._tuple_cls._fields
return tuple()
@classmethod
def _field_values(mcs, cls: Enum, field_name: str,
as_tuple: Optional[bool] = True) -> Union[Tuple, Generator]:
"""Base function returns a `tuple`/`generator` containing just the
value of the given field_name of all the elements from the cls.
Note:
It's used to generate the particular function with name format
`<field_name>s` for each `field_name`.
Args:
cls (Enum): subclass of NamedEnum class.
field_name (str): attribute's name.
as_tuple (Optional[bool]): returns a tuple of the values if True;
otherwise returns a generator. Default value is True.
Returns:
Union[Tuple, Generator]: corresponding values of the field name in
all enumeration items
"""
g = (getattr(item.value, field_name)
for item in cls.gen(name_value_pair=False))
return tuple(g) if as_tuple else g
@classmethod
def _from_field(mcs, cls: Enum, field_name: str, field_value: Any,
as_tuple: Optional[bool] = True) -> Union[Tuple, Generator]:
"""Base function returns a `tuple` of the defined enumeration items
regarding to the given `field_value` of field with `field_name`, if
`as_tuple` is True; otherwise returns a generator.
Args:
cls (Enum): subclass of NamedEnum class.
field_name (str): attribute's name.
field_value (Any): key to search for.
as_tuple (Optional[bool]): returns a tuple of the values if True;
otherwise returns a generator. Default value is True.
Returns:
Union[Tuple, Generator]: collection of enumeration items matching
the condition.
"""
g = (item for item in cls.gen(name_value_pair=False)
if getattr(item.value, field_name) == field_value)
return tuple(g) if as_tuple else g
@classmethod
def _has_field(mcs, cls: Enum, field_name: str, field_value: Any) -> bool:
"""Base function returns a boolean value which indicates if there is at
least one enumeration item in which the value of the field `field_name`
corresponding value matches the given `field_value`.
Note:
It's used to generate the particular function with name format
`has_<field_name>` for each `field_name`.
Args:
cls (Enum): subclass of NamedEnum class.
field_name (str): attribute's name.
field_value (Any): key to search for
Returns:
bool: True, if has at least one matching; otherwise False.
"""
gen_field_values = mcs._field_values(cls, field_name, as_tuple=False)
return field_value in gen_field_values
[docs] def gen(cls, name_value_pair: Optional[bool] = True) -> Generator:
"""Returns a generator of pairs consisting of each enumeration item's
name and value, if name_value_pair is True; otherwise a generator of the
enumeration items.
Args:
name_value_pair (Optional[bool]): controls the return result. If true,
returns the generator of name-value pair; if False, returns the
generator of the enumeration items. Default value is True.
Returns:
Generator: a generator which iterates all the enumeration items.
Examples:
>>> from types import GeneratorType
>>> class TripleEnum(NamedEnum):
... _field_names_ = ("first", "second", "third")
>>> class Triangle(TripleEnum):
... EQUILATERAL = (6, 6, 6)
... RIGHT = (3, 4, 5)
>>> isinstance(TripleEnum.gen(), GeneratorType)
True
>>> list(TripleEnum.gen())
[]
>>> isinstance(TripleEnum.gen(name_value_pair=False), GeneratorType)
True
>>> list(TripleEnum.gen(name_value_pair=False))
[]
>>> isinstance(Triangle.gen(), GeneratorType)
True
>>> list(Triangle.gen())
[('EQUILATERAL', NamedTuple(first=6, second=6, third=6)), ('RIGHT', NamedTuple(first=3, second=4, third=5))]
>>> isinstance(Triangle.gen(name_value_pair=False), GeneratorType)
True
>>> list(Triangle.gen(name_value_pair=False))
[<Triangle.EQUILATERAL: NamedTuple(first=6, second=6, third=6)>, <Triangle.RIGHT: NamedTuple(first=3, second=4, third=5)>]
"""
if name_value_pair:
return ((name, item.value) for name, item in cls._member_map_.items())
return (item for name, item in cls._member_map_.items())
def _as_data_type(cls, data_type: Union[dict, list, set, tuple, OrderedDict])\
-> Union[Dict, List, Set, Tuple, OrderedDict]:
"""ase function converts the enumeration class to the given data type
value.
Note:
It's used for generating the functions like `as_dict`, `as_tuple`,
`as_set`, `as_list`, `as_ordereddict`.
Args:
data_type (Union[dict, list, set, tuple, OrderedDict]): desired data
type for the output.
Returns:
Union[Dict, List, Set, Tuple, OrderedDict]: converted value
depending on the given data type.
"""
return data_type(cls.gen(name_value_pair=True))
[docs] def as_dict(cls) -> Dict:
"""Converts the enumeration to a `dict`, in which the key is the name
of the enumeration item and value is its value.
Returns:
Dict: a dictionary containing name-value-pairs of the enumeration.
Examples:
>>> class TripleEnum(NamedEnum):
... _field_names_ = ("first", "second", "third")
>>> class Triangle(TripleEnum):
... EQUILATERAL = (6, 6, 6)
... RIGHT = (3, 4, 5)
>>> TripleEnum.as_dict()
{}
>>> Triangle.as_dict()
{'EQUILATERAL': NamedTuple(first=6, second=6, third=6), 'RIGHT': NamedTuple(first=3, second=4, third=5)}
"""
return cls._as_data_type(dict)
[docs] def as_tuple(cls) -> Tuple:
"""Converts the enumerations to a `tuple`, in which each item is a tuple
of the enumeration item's name and value.
Returns:
Tuple: a tuple containing name-value-pairs of the enumeration.
Examples:
>>> class TripleEnum(NamedEnum):
... _field_names_ = ("first", "second", "third")
>>> class Triangle(TripleEnum):
... EQUILATERAL = (6, 6, 6)
... RIGHT = (3, 4, 5)
>>> TripleEnum.as_tuple()
()
>>> Triangle.as_tuple()
(('EQUILATERAL', NamedTuple(first=6, second=6, third=6)), ('RIGHT', NamedTuple(first=3, second=4, third=5)))
"""
return cls._as_data_type(tuple)
[docs] def as_set(cls) -> Set:
"""Converts the enumerations to a `set`, in which each item is a tuple of
the enumeration item's name and value.
Returns:
Set: a set containing name-value-pairs of the enumeration.
Examples:
>>> class TripleEnum(NamedEnum):
... _field_names_ = ("first", "second", "third")
>>> class Triangle(TripleEnum):
... EQUILATERAL = (6, 6, 6)
... RIGHT = (3, 4, 5)
>>> TripleEnum.as_set() == set()
True
>>> isinstance(Triangle.as_set(), set)
True
>>> dict(Triangle.as_set()) == Triangle.as_dict()
True
"""
return cls._as_data_type(set)
[docs] def as_list(cls) -> List:
"""Converts the enumerations to a `list`, in which each item is a tuple of
the enumeration item's name and value.
Returns:
List: a list containing name-value-pairs of the enumeration.
Examples:
>>> class TripleEnum(NamedEnum):
... _field_names_ = ("first", "second", "third")
>>> class Triangle(TripleEnum):
... EQUILATERAL = (6, 6, 6)
... RIGHT = (3, 4, 5)
>>> TripleEnum.as_list()
[]
>>> Triangle.as_list()
[('EQUILATERAL', NamedTuple(first=6, second=6, third=6)), ('RIGHT', NamedTuple(first=3, second=4, third=5))]
"""
return cls._as_data_type(list)
[docs] def as_ordereddict(cls) -> OrderedDict:
"""Converts the enumerations to an `OrderedDict`, in which each item is a
tuple of the enumeration item's name and value.
Returns:
OrderedDict: an OrderedDict containing name-value-pairs of the
enumeration.
Examples:
>>> class TripleEnum(NamedEnum):
... _field_names_ = ("first", "second", "third")
>>> class Triangle(TripleEnum):
... EQUILATERAL = (6, 6, 6)
... RIGHT = (3, 4, 5)
>>> TripleEnum.as_ordereddict()
OrderedDict()
>>> Triangle.as_ordereddict()
OrderedDict([('EQUILATERAL', NamedTuple(first=6, second=6, third=6)), ('RIGHT', NamedTuple(first=3, second=4, third=5))])
"""
return cls._as_data_type(OrderedDict)
def __repr__(cls) -> str:
"""Overrides the __repr__ function from EnumMeta class.
Returns:
str: string represents the class.
"""
return "<named enum %r>" % cls.__name__
[docs] def describe(cls) -> None:
"""Prints in the console a table showing the content of the enumeration.
Examples:
>>> class TripleEnum(NamedEnum):
... _field_names_ = ("first", "second", "third")
>>> class Triangle(TripleEnum):
... EQUILATERAL = (6, 6, 6)
... RIGHT = (3, 4, 5)
>>> TripleEnum.describe()
Class: TripleEnum
Name | First | Second | Third
-----------------------------
<BLANKLINE>
>>> Triangle.describe()
Class: Triangle
Name | First | Second | Third
------------------------------------
EQUILATERAL | 6 | 6 | 6
RIGHT | 3 | 4 | 5
<BLANKLINE>
"""
name = "name"
max_lengths, headers = [], []
fields = cls._fields() if cls._fields() else ("value",)
row_format = ["{:>%d}"] * (len(fields) + 1)
names = [name] + list(cls.names())
max_lengths.append(max(list(map(len, names))))
headers.append(name.capitalize())
for attr_name in fields:
attr_func = "%ss" % attr_name
attr_list = list(map(str, getattr(cls, attr_func)()))
max_lengths.append(max(list(map(len, [attr_name] + attr_list))))
headers.append(attr_name.capitalize())
row_format = " | ".join(row_format) % tuple(max_lengths)
header_line = row_format.format(*headers)
output = "Class: %s\n" % cls.__name__
output += header_line + "\n"
output += "-" * (len(header_line)) + "\n"
if cls._fields():
for name, value in cls.gen():
output += row_format.format(name, *value) + "\n"
else:
for name, value in cls.gen():
output += row_format.format(name, str(value)) + "\n"
print(output)
[docs] def names(cls, as_tuple: Optional[bool] = True) -> Union[Tuple, Generator]:
"""Returns the names of all the enumeration items as a `tuple`, if
parameter `as_tuple` is `True`; otherwise returns a generator.
Args:
as_tuple (bool): returns a tuple if True; otherwise returns a
generator.
Returns:
Union[Tuple, Generator]: names of all the enumeration items inside
the class in a specific form.
Examples:
>>> from types import GeneratorType
>>> class TripleEnum(NamedEnum):
... _field_names_ = ("first", "second", "third")
>>> class Triangle(TripleEnum):
... EQUILATERAL = (6, 6, 6)
... RIGHT = (3, 4, 5)
>>> TripleEnum.names()
()
>>> isinstance(TripleEnum.names(as_tuple=False), GeneratorType)
True
>>> list(TripleEnum.names(as_tuple=False))
[]
>>> Triangle.names()
('EQUILATERAL', 'RIGHT')
>>> isinstance(Triangle.names(as_tuple=False), GeneratorType)
True
>>> list(Triangle.names(as_tuple=False))
['EQUILATERAL', 'RIGHT']
"""
g = (name for name in cls._member_map_.keys())
return tuple(g) if as_tuple else g
[docs] def values(cls, as_tuple: Optional[bool] = True) -> Union[Tuple, Generator]:
"""Returns the values of all the enumeration items as a tuple, if
parameter `as_tuple` is `True`, otherwise returns a generator.
Args:
as_tuple (Optional[bool]): returns a tuple if True; otherwise
returns a generator. Default value is True.
Returns:
Union[Tuple, Generator]: values of all the enumeration items inside
the class in a specific form.
Examples:
>>> from types import GeneratorType
>>> class TripleEnum(NamedEnum):
... _field_names_ = ("first", "second", "third")
>>> class Triangle(TripleEnum):
... EQUILATERAL = (6, 6, 6)
... RIGHT = (3, 4, 5)
>>> TripleEnum.values()
()
>>> isinstance(TripleEnum.values(as_tuple=False), GeneratorType)
True
>>> list(TripleEnum.values(as_tuple=False))
[]
>>> Triangle.values()
(NamedTuple(first=6, second=6, third=6), NamedTuple(first=3, second=4, third=5))
>>> isinstance(Triangle.values(as_tuple=False), GeneratorType)
True
>>> list(Triangle.values(as_tuple=False))
[NamedTuple(first=6, second=6, third=6), NamedTuple(first=3, second=4, third=5)]
"""
g = (item.value for item in cls._member_map_.values())
return tuple(g) if as_tuple else g