~lazr-developers/lazr.enum/trunk

24.1.1 by Gary Poster
initial checkin of abstracted version
1
# Copyright 2004-2009 Canonical Ltd.  All rights reserved.
1 by Barry Warsaw
Initial package template for lazr packages.
2
#
24 by Leonard Richardson
Initial preparation.
3
# This file is part of lazr.enum
1 by Barry Warsaw
Initial package template for lazr packages.
4
#
24 by Leonard Richardson
Initial preparation.
5
# lazr.enum is free software: you can redistribute it and/or modify it
1 by Barry Warsaw
Initial package template for lazr packages.
6
# under the terms of the GNU Lesser General Public License as published by
7
# the Free Software Foundation, either version 3 of the License, or (at your
8
# option) any later version.
9
#
24 by Leonard Richardson
Initial preparation.
10
# lazr.enum is distributed in the hope that it will be useful, but WITHOUT
1 by Barry Warsaw
Initial package template for lazr packages.
11
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
13
# License for more details.
14
#
15
# You should have received a copy of the GNU Lesser General Public License
24 by Leonard Richardson
Initial preparation.
16
# along with lazr.enum.  If not, see <http://www.gnu.org/licenses/>.
1 by Barry Warsaw
Initial package template for lazr packages.
17
24.1.1 by Gary Poster
initial checkin of abstracted version
18
__metaclass__ = type
19
20
import itertools
21
import operator
22
import sys
23
import warnings
24
25
from zope.interface import implements
26
from zope.schema.interfaces import ITitledTokenizedTerm, IVocabularyTokenized
27
try:
28
    from zope.proxy import removeAllProxies
29
except ImportError:
30
    removeAllProxies = lambda obj: obj # no-op
31
32
from lazr.enum.interfaces import IEnumeratedType
33
34
__all__ = [
35
    'BaseItem',
36
    'DBEnumeratedType',
37
    'DBItem',
38
    'EnumeratedType',
39
    'IEnumeratedType',
40
    'Item',
41
    'TokenizedItem',
42
    'enumerated_type_registry',
43
    'use_template',
25.1.1 by Gary Poster
make license only v3 of LGPL. update to lazr.yourpkg current patterns.
44
    'proxy_isinstance',
25.1.2 by Gary Poster
reexport more items
45
    'MetaEnum', # needed for configure.zcml
46
    'MetaDBEnum', # needed for configure.zcml
24.1.1 by Gary Poster
initial checkin of abstracted version
47
    ]
48
49
def proxy_isinstance(obj, cls):
50
    """Test whether an object is an instance of a type.
51
52
    This works even if the object is proxied by zope.proxy, if that package
53
    is available.
54
    """
55
    return isinstance(removeAllProxies(obj), cls)
56
57
def docstring_to_title_descr(string):
58
    """When given a classically formatted docstring, returns a tuple
59
    (title, description).
60
61
    >>> class Foo:
62
    ...     '''
63
    ...     Title of foo
64
    ...
65
    ...     Description of foo starts here.  It may
66
    ...     spill onto multiple lines.  It may also have
67
    ...     indented examples:
68
    ...
69
    ...       Foo
70
    ...       Bar
71
    ...
72
    ...     like the above.
73
    ...     '''
74
    ...
75
    >>> title, descr = docstring_to_title_descr(Foo.__doc__)
76
    >>> print title
77
    Title of foo
78
    >>> for num, line in enumerate(descr.splitlines()):
79
    ...    print "%d.%s" % (num, line)
80
    ...
81
    0.Description of foo starts here.  It may
82
    1.spill onto multiple lines.  It may also have
83
    2.indented examples:
84
    3.
85
    4.  Foo
86
    5.  Bar
87
    6.
88
    7.like the above.
89
90
    """
91
    lines = string.splitlines()
92
    # title is the first non-blank line
93
    for num, line in enumerate(lines):
94
        line = line.strip()
95
        if line:
96
            title = line
97
            break
98
    else:
99
        raise ValueError
100
    assert not lines[num+1].strip()
101
    descrlines = lines[num+2:]
102
    descr1 = descrlines[0]
103
    indent = len(descr1) - len(descr1.lstrip())
104
    descr = '\n'.join([line[indent:] for line in descrlines])
105
    return title, descr
106
107
108
class BaseItem:
109
    """Items are the primary elements of the enumerated types.
110
111
    `BaseItem` is the base class for both `Item` and `DBItem`.
112
113
    The enum attribute is a reference to the enumerated type that the
114
    Item is a member of.
115
116
    The token attribute is the name assigned to the item.
117
118
    The value is the short text string used to identify the item.
119
    """
120
121
    sortkey = 0
122
    name = None
123
    description = None
124
    title = None
27.1.1 by Barry Warsaw
Support for Item urls.
125
    url = None
24.1.1 by Gary Poster
initial checkin of abstracted version
126
27.1.1 by Barry Warsaw
Support for Item urls.
127
    def __init__(self, title, description=None, url=None):
24.1.1 by Gary Poster
initial checkin of abstracted version
128
        """Items are the main elements of the EnumeratedType.
129
130
        Where the title is passed in without a description,
131
        and the title looks like a docstring (has embedded carriage returns),
132
        the title is the first line, and the description is the rest.
133
        """
134
135
        self.sortkey = BaseItem.sortkey
136
        BaseItem.sortkey += 1
137
        self.title = title
138
        # The enum attribute is set duing the class constructor of the
139
        # containing enumerated type.
140
141
        self.description = description
27.1.1 by Barry Warsaw
Support for Item urls.
142
        self.url = url
24.1.1 by Gary Poster
initial checkin of abstracted version
143
144
        if self.description is None:
145
            # check value
146
            if self.title.find('\n') != -1:
147
                self.title, self.description = docstring_to_title_descr(
148
                    self.title)
149
150
    def __int__(self):
151
        raise TypeError("Cannot cast Item to int.")
152
153
    def __cmp__(self, other):
154
        if proxy_isinstance(other, BaseItem):
155
            return cmp(self.sortkey, other.sortkey)
156
        else:
157
            raise TypeError(
158
                'Comparisons of Items are only valid with other Items')
159
160
    def __eq__(self, other, stacklevel=2):
161
        if isinstance(other, int):
162
            warnings.warn('comparison of Item to an int: %r' % self,
163
                stacklevel=stacklevel)
164
            return False
165
        elif proxy_isinstance(other, BaseItem):
166
            return (self.name == other.name and
167
                    self.enum == other.enum)
168
        else:
169
            return False
170
171
    def __ne__(self, other):
172
        return not self.__eq__(other, stacklevel=3)
173
174
    def __hash__(self):
175
        return hash(self.title)
176
177
    def __str__(self):
178
        return str(self.title)
179
180
181
class Item(BaseItem):
182
    """The `Item` is an element of an `EnumeratedType`."""
183
    @staticmethod
184
    def construct(other_item):
185
        """Create an Item based on the other_item."""
27.1.3 by Barry Warsaw
DBItem override the base class constructor. Make sure DBItem also supports
186
        item = Item(other_item.title, other_item.description, other_item.url)
24.1.1 by Gary Poster
initial checkin of abstracted version
187
        item.sortkey = other_item.sortkey
188
        return item
189
190
    def __repr__(self):
191
        return "<Item %s.%s, %s>" % (
192
            self.enum.name, self.name, self.title)
193
194
195
class DBItem(BaseItem):
196
    """The `DBItem` refers to an enumerated item that is used in the database.
197
198
    Database enumerations are stored in the database using integer columns.
199
    """
200
201
    @staticmethod
202
    def construct(other_item):
203
        """Create an Item based on the other_item."""
27.1.3 by Barry Warsaw
DBItem override the base class constructor. Make sure DBItem also supports
204
        item = DBItem(other_item.value, other_item.title, 
205
                      other_item.description, other_item.url)
24.1.1 by Gary Poster
initial checkin of abstracted version
206
        item.sortkey = other_item.sortkey
207
        return item
208
27.1.3 by Barry Warsaw
DBItem override the base class constructor. Make sure DBItem also supports
209
    def __init__(self, value, title, description=None, url=None):
210
        BaseItem.__init__(self, title, description, url)
24.1.1 by Gary Poster
initial checkin of abstracted version
211
        self.value = value
212
213
    def __hash__(self):
214
        return self.value
215
216
    def __repr__(self):
217
        return "<DBItem %s.%s, (%d) %s>" % (
218
            self.enum.name, self.name, self.value, self.title)
219
24.1.2 by Gary Poster
add configure.zcml; hook up the rest of the tests; comment that __sqlrepr__ may be going away.
220
    # XXX this is probably going away.
24.1.1 by Gary Poster
initial checkin of abstracted version
221
    def __sqlrepr__(self, dbname):
222
        return repr(self.value)
223
224
225
class TokenizedItem:
226
    """Wraps an `Item` or `DBItem` to provide `ITitledTokenizedTerm`."""
227
228
    implements(ITitledTokenizedTerm)
229
230
    def __init__(self, item):
231
        self.value = item
232
        self.token = item.name
233
        self.title = item.title
234
235
236
# The enumerated_type_registry is a mapping of all enumerated types to the
237
# actual class.  There should only ever be one EnumeratedType or
238
# DBEnumerateType with a particular name.  This serves two purposes:
24.1.3 by Gary Poster
address review: new lines, comment clean-up
239
#   * a way to get any enumerated type by its name
24.1.1 by Gary Poster
initial checkin of abstracted version
240
#   * a way to iterate over the DBEnumeratedTypes in order to confirm the
241
#     values actually stored in the database.
242
enumerated_type_registry = {}
243
244
245
class EnumItems:
246
    """Allow access to Items of an enumerated type using names or db values.
247
248
    Access can be made to the items using the name of the Item.
249
250
    If the enumerated type has DBItems then the mapping includes a mapping of
251
    the database integer values to the DBItems.
252
    """
253
    def __init__(self, items, mapping):
254
        self.items = items
255
        self.mapping = mapping
256
    def __getitem__(self, key):
257
        if key in self.mapping:
258
            return self.mapping[key]
259
        else:
260
            raise KeyError(key)
261
    def __iter__(self):
262
        return self.items.__iter__()
263
    def __len__(self):
264
        return len(self.items)
265
266
267
class BaseMetaEnum(type):
268
    """The metaclass functionality for `EnumeratedType` and `DBEnumeratedType`.
269
270
    This metaclass defines methods that allow the enumerated types to implement
271
    the IVocabularyTokenized interface.
272
273
    The metaclass also enforces "correct" definitions of enumerated types by
274
    enforcing capitalisation of Item variable names and defining an appropriate
275
    ordering.
276
    """
277
    implements(IEnumeratedType, IVocabularyTokenized)
278
279
    @classmethod
280
    def _enforceSingleInheritance(cls, classname, bases, classdict):
281
        """Only one base class is allowed for enumerated types."""
282
        if len(bases) > 1:
283
            raise TypeError(
284
                'Multiple inheritance is not allowed with '
285
                '%s, %s.%s' % (
286
                cls.enum_name, classdict['__module__'], classname))
287
288
    @classmethod
289
    def _updateClassDictWithBaseItems(cls, bases, classdict):
290
        """Copy each of the items from the base class that hasn't been
291
        explicitly defined in the new class."""
292
        if bases:
293
            base_class = bases[0]
294
            if hasattr(base_class, 'items'):
295
                for item in base_class.items:
296
                    if item.name not in classdict:
297
                        new_item = cls.item_type.construct(item)
298
                        classdict[item.name] = new_item
299
300
    @classmethod
301
    def _updateClassDictWithTemplateItems(cls, classdict):
302
        """If constructed through use_template, we need to construct
303
        the appropriate type of items based on our item_type of our class."""
304
        if 'template_items' in classdict:
305
            for item in classdict['template_items']:
306
                classdict[item.name] = cls.item_type.construct(item)
307
            # The template_items key is not wanted or needed in the new type.
308
            del classdict['template_items']
309
310
    @classmethod
311
    def _enforceItemClassAndName(cls, items, classname, module_name):
312
        """All items must be of the appropriate type for the enumeration type.
313
314
        All item variable names must be capitalised.
315
        """
316
        for item_name, item in items:
317
            if not isinstance(item, cls.item_type):
318
                raise TypeError(
319
                    'Items must be of the appropriate type for the '
320
                    '%s, %s.%s.%s' % (
321
                    cls.enum_name, module_name, classname, item_name))
322
323
            if item_name.upper() != item_name:
324
                raise TypeError(
325
                    'Item instance variable names must be capitalised.'
326
                    '  %s.%s.%s' % (module_name, classname, item_name))
327
328
            item.name = item_name
329
330
    @classmethod
331
    def _generateItemMapping(cls, items):
332
        """Each enumerated type has a mapping of the item names to the item
333
        instances."""
334
        return dict(items)
335
336
    @classmethod
337
    def _enforceSortOrder(cls, classname, classdict, items):
338
        """ Override item's default sort order if sort_order is defined.
339
340
        :return: A list of items ordered appropriately.
341
        """
342
        items = dict(items)
343
        if 'sort_order' in classdict:
344
            sort_order = classdict['sort_order']
345
            item_names = sorted(items.keys())
346
            if item_names != sorted(sort_order):
347
                raise TypeError(
348
                    'sort_order for %s must contain all and '
349
                    'only Item instances  %s.%s' % (
350
                    cls.enum_name, classdict['__module__'], classname))
351
        else:
352
            # Sort the items by the automatically generated
353
            # sortkey.
354
            sort_order = [
355
                item.name for item in sorted(
356
                items.values(), key=operator.attrgetter('sortkey'))]
357
            classdict['sort_order'] = tuple(sort_order)
358
        # Assign new sortkey values from zero.
359
        sorted_items = []
360
        for sort_id, item_name in enumerate(sort_order):
361
            item = classdict[item_name]
362
            item.sortkey = sort_id
363
            sorted_items.append(item)
364
        return sorted_items
365
366
    def __new__(cls, classname, bases, classdict):
367
        """Called when defining a new class."""
368
369
        cls._enforceSingleInheritance(classname, bases, classdict)
370
        cls._updateClassDictWithBaseItems(bases, classdict)
371
        cls._updateClassDictWithTemplateItems(classdict)
372
373
        items = [(key, value) for key, value in classdict.iteritems()
374
                 if isinstance(value, BaseItem)]
375
376
        cls._enforceItemClassAndName(items, classname, classdict['__module__'])
377
378
        mapping = cls._generateItemMapping(items)
379
        sorted_items = cls._enforceSortOrder(classname, classdict, items)
380
381
        classdict['items'] = EnumItems(sorted_items, mapping)
382
        classdict['name'] = classname
383
        classdict['description'] = classdict.get('__doc__', None)
384
385
        global enumerated_type_registry
386
        if classname in enumerated_type_registry:
387
            other = enumerated_type_registry[classname]
388
            raise TypeError(
389
                'An enumerated type already exists with the name %s (%s.%s).'
390
                % (classname, other.__module__, other.name))
391
392
        instance = type.__new__(cls, classname, bases, classdict)
393
394
        # Add a reference to the enumerated type to each item.
395
        for item in instance.items:
396
            item.enum = instance
397
398
        # Add the enumerated type to the registry.
399
        enumerated_type_registry[classname] = instance
400
401
        return instance
402
403
    def __contains__(self, value):
404
        """See `ISource`."""
405
        return value in self.items
406
407
    def __iter__(self):
408
        """See `IIterableVocabulary`."""
409
        return itertools.imap(TokenizedItem, self.items)
410
411
    def __len__(self):
412
        """See `IIterableVocabulary`."""
413
        return len(self.items)
414
415
    def getTerm(self, value):
416
        """See `IBaseVocabulary`."""
417
        if value in self.items:
418
            return TokenizedItem(value)
419
        raise LookupError(value)
420
421
    def getTermByToken(self, token):
422
        """See `IVocabularyTokenized`."""
423
        # The sort_order of the enumerated type lists all the items.
30.1.1 by Tim Penhey
Make the token checking case insensitive.
424
        upper_token = token.upper()
425
        if upper_token in self.sort_order:
426
            return TokenizedItem(getattr(self, upper_token))
24.1.1 by Gary Poster
initial checkin of abstracted version
427
        else:
428
            # If the token is not specified in the sort order then check
429
            # the titles of the items.  This is to support the transition
430
            # of accessing items by their titles.  To continue support
431
            # of old URLs et al, this will probably stay for some time.
432
            for item in self.items:
433
                if item.title == token:
434
                    return TokenizedItem(item)
435
        # The token was not in the sort_order (and hence the name of a
436
        # variable), nor was the token the title of one of the items.
437
        raise LookupError(token)
438
439
440
class MetaEnum(BaseMetaEnum):
441
    """The metaclass for `EnumeratedType`."""
442
    item_type = Item
443
    enum_name = 'EnumeratedType'
444
445
    def __repr__(self):
446
        return "<EnumeratedType '%s'>" % self.name
447
448
449
class MetaDBEnum(BaseMetaEnum):
450
    """The meta class for `DBEnumeratedType`.
451
452
    Provides a method for getting the item based on the database identifier in
453
    addition to all the normal enumerated type methods.
454
    """
455
    item_type = DBItem
456
    enum_name = 'DBEnumeratedType'
457
458
    @classmethod
459
    def _generateItemMapping(cls, items):
460
        """DBEnumeratedTypes also map the database value of the DBItem to the
461
        item instance."""
462
        mapping = BaseMetaEnum._generateItemMapping(items)
463
        for item_name, item in items:
464
            # If the value is already in the mapping then we have two
465
            # different items attempting to map the same number.
466
            if item.value in mapping:
467
                # We really want to provide the names in alphabetical order.
468
                args = [item.value] + sorted(
469
                    [item_name, mapping[item.value].name])
470
                raise TypeError(
471
                    'Two DBItems with the same value %s (%s, %s)'
472
                    % tuple(args))
473
            else:
474
                mapping[item.value] = item
475
        return mapping
476
477
    def __repr__(self):
478
        return "<DBEnumeratedType '%s'>" % self.name
479
480
481
class EnumeratedType:
482
    """An enumeration of items.
483
484
    The items of the enumeration must be instances of the class `Item`.
485
    These items are accessible through a class attribute `items`.  The ordering
486
    of the items attribute is the same order that the items are defined in the
487
    class.
488
489
    A `sort_order` attribute can be defined to override the default ordering.
490
    The sort_order should contain the names of the all the items in the
491
    ordering that is desired.
492
    """
493
    __metaclass__ = MetaEnum
494
495
496
class DBEnumeratedType:
497
    """An enumeration with additional mapping from an integer to `Item`.
498
499
    The items of a class inheriting from DBEnumeratedType must be of type
500
    `DBItem`.
501
    """
502
    __metaclass__ = MetaDBEnum
503
504
505
def use_template(enum_type, include=None, exclude=None):
506
    """An alternative way to extend an enumerated type other than inheritance.
507
508
    The parameters include and exclude should either be the name values of the
509
    items (the parameter names), or a list or tuple that contains string
510
    values.
511
    """
512
    frame = sys._getframe(1)
513
    locals = frame.f_locals
514
515
    # Try to make sure we were called from a class def.
516
    if (locals is frame.f_globals) or ('__module__' not in locals):
517
        raise TypeError(
518
            "use_template can be used only from a class definition.")
519
520
    # You can specify either includes or excludes, not both.
521
    if include and exclude:
522
        raise ValueError("You can specify includes or excludes not both.")
523
524
    if include is None:
525
        items = enum_type.items
526
    else:
527
        if isinstance(include, str):
528
            include = [include]
529
        items = [item for item in enum_type.items if item.name in include]
530
531
    if exclude is None:
532
        exclude = []
533
    elif isinstance(exclude, str):
534
        exclude = [exclude]
535
536
    template_items = []
537
    for item in items:
538
        if item.name not in exclude:
539
            template_items.append(item)
540
541
    locals['template_items'] = template_items