Welcome, guest | Sign In | My Account | Store | Cart

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!

Python, 150 lines
  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!")