The recipe provides an easy-to-use class for a group of "constants". It provides helper functions for getting the constant values just right. It also exposes the mechanism it uses to bind the contents of an iterable into the namespace of another object.
For this binding and for the grouped constants, this recipe makes it easy to dynamically generate the mapped values you want to expose.
The next step is to make the values aware of the context in which they are bound and to strengthen their association with the group they are in. And that is one of the recipes I'm working on next!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | """constants module
"""
import itertools
# some useful stepper functions for bind()
def step_lowercase(iterable):
"""A stepper function for lower-cased values."""
for name in iterable:
yield name, name.lower()
def step_echo(iterable):
for name in iterable:
yield name, name
def step_index_factory(start=0, step=1):
def step_index(iterable):
counter = itertools.count(start, step)
for name in iterable:
yield name, next(counter)
return step_index
def step_binary_factory(start=0, step=1):
def step_binary(iterable):
"""A stepper function for bitwise or-able values."""
counter = itertools.count(start, step)
for name in iterable:
yield name, 2**next(counter)
return step_binary
#######################
def build_mapping(iterable, stepper=None):
"""A generator for mapping the iterable to stepped values.
iterable - the keys for the mapping. It is also used by the
stepper to generate the mapped values.
stepper - the callable returning an iterator of the mapped
values.
If a mapping is passed for the iterable, it is returned directly
and the stepper is not used. Otherwise, the stepper will be passed
the iterable to generate the mapping.
The stepper function should not change the iterable. Neither
should it produce keys other than those from the iterable. A
default stepper from step_count_factory() will be used if one is
not passed.
"""
try:
for key in iterable:
yield key, iterable[key]
except TypeError:
if stepper is None:
stepper = step_index_factory(step=1)
for key, value in stepper(iterable):
yield key, value
def bind(obj, iterable, stepper=None):
"""Bind the iterable's values to the object.
obj - where to bind the names.
iterable - used to name the attributes and drive the mapper.
stepper - the step function that generates the mapped values.
Use this function to bind attribute names to any object. It is
used by the Constants class to that effect. If the iterable is a
mapping, it is used directly for the name/value pairs. Otherwise
the attribute values are built by the stepper. See the
build_mapping() function for more information.
"""
for key, value in build_mapping(iterable, stepper):
setattr(obj, key, value)
def bind_mapping(mapping, iterable, stepper=None):
"""Update the mapping with the generated values.
This function is analogous to bind(), but updates a mapping rather
than setting an object's attributes.
"""
for key, value in build_mapping(iterable, stepper):
mapping[key] = value
#######################
class Constants:
"""A simple namespace built around the passed iterable.
The bind() function is used to build the namespace. See it for
more explanation of how the attributes are built and bound.
The new attribute names are found in self.names and the
corresponding values in self.values. A reverse mapping from values
to names is found at self.reversed.
"""
def __init__(self, iterable, stepper=None):
self._original = tuple(self.__dict__)
self.names = tuple(iterable)
bind(self, iterable, stepper)
def __add__(self, obj):
result = self.__class__.__new__(self.__class__)
for name in self.names:
setattr(result, name, getattr(self, name))
for name in obj.names:
setattr(result, name, getattr(obj, name))
return result
def __iadd__(self, obj):
for name in obj.names:
setattr(self, name, getattr(obj, name))
def __contains__(self, obj):
return obj in self.values
@property
def values(self):
"""The generated attribute values."""
return tuple(val for name, val in self.__dict__.items()
if name not in self._original)
@property
def reversed(self):
if not hasattr(self, "_reversed"):
self._reversed = {}
for name in self.names:
value = getattr(self, name)
if value not in self._reversed:
self._reversed[value] = []
self._reversed[value].append(name)
return self._reversed
def get_reverse_lookup(self, value):
return self.reversed[value]
|
Example
class Car:
DOORS = Constants([
"DRIVER_FRONT",
"PASSENGER_FRONT",
])
GEARS = Constants({
"REVERSE": "r",
"NEUTRAL": "n",
"FIRST": "1",
"SECOND": "2",
"THIRD": "3",
})
STATUS = Constants([
"RUNNING",
# not running
"EMPTY",
"PARKED",
"STALLED",
# running
"IDLING",
"DRIVING",
], step_binary_factory(start=256))
STATUS.IDLING = STATUS.IDLING | STATUS.RUNNING
STATUS.DRIVING = STATUS.DRIVING | STATUS.RUNNING
def __init__(self, status, gear):
self.status = status
self.gear = gear
def start(self):
if self.status & self.STATUS.RUNNING:
return
if self.gear != self.GEARS.FIRST:
raise Exception("Must be in first gear!")
def shift(self, gear):
if gear not in self.GEARS:
raise Exception("Car does not have that gear: %s" % gear)
self.gear = gear
class Automatic(Car):
GEARS = Car.GEARS + Constants({
"PARK": "p",
"DRIVE": "d",
})
def start(self):
if self.status & self.STATUS.RUNNING:
return
if self.gear != self.GEARS.PARK:
raise Exception("Must be in park!")