Merge lp://staging/~gary/lazr.enum/devel into lp://staging/~leonardr/lazr.enum/trunk
- devel
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Francis J. Lacoste |
Approved revision: | 26 |
Merge reported by: | Robert Collins |
Merged at revision: | not available |
Proposed branch: | lp://staging/~gary/lazr.enum/devel |
Merge into: | lp://staging/~leonardr/lazr.enum/trunk |
Diff against target: | None lines |
To merge this branch: | bzr merge lp://staging/~gary/lazr.enum/devel |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Francis J. Lacoste (community) | Approve | ||
LAZR Developers | Pending | ||
Review via email:
|
Commit message
Description of the change
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gary Poster (gary) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francis J. Lacoste (flacoste) wrote : | # |
Hi Gary,
This is good. I have a couple of small fixes:
- You have a bunch of files missing a newline at the end:
src/lazr/
src/lazr/
src/lazr/
- There is a comment that needs fixing in
> 656 +# The enumerated_
> 657 +# actual class. There should only ever be one EnumeratedType or
> 658 +# DBEnumerateType with a particular name. This serves two purposes:
> 659 +# * a way to get any enumerated type by its name (used in tales.py)
> 660 +# * a way to iterate over the DBEnumeratedTypes in order to confirm the
> 661 +# values actually stored in the database.
> 662 +enumerated_
The reference to tales.py isn't pertinennt anymore.
You probably want to keep a __version__ in __init__.py ?
Thanks
- 27. By Gary Poster
-
address review: new lines, comment clean-up
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gary Poster (gary) wrote : | # |
Cool, thanks.
I addressed the first two (r27). We discussed the __version__: it is in setup.py because __init__.py imports something and setup.py was trying to use __init__.py for the __version__ so you couldn't build the package.
Gary
- 28. By Gary Poster
-
update top-level README with one-liner description
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Tim Penhey (thumper) wrote : | # |
Is this branch still valid or shall we reject it?
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gary Poster (gary) wrote : | # |
On Apr 1, 2009, at 3:08 PM, Tim Penhey wrote:
> Is this branch still valid or shall we reject it?
Sorry, just saw this. The branch has been merged into the trunk we
care about. It does not need to be merged into leonardr's branch.
I'll try to reject/retract it. Thanks.
Gary
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gary Poster (gary) wrote : | # |
I wrote:
> I'll try to reject/retract it. Thanks.
I can see how to delete it, but that deletes the comments too, which are in fact pertinent since they affected the merge into the actual branch.
Should I just delete it, do you suppose, or do you have a better idea, Tim?
Thanks
Gary
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francis J. Lacoste (flacoste) wrote : | # |
On Tuesday 21 April 2009, Gary Poster wrote:
> I wrote:
> > I'll try to reject/retract it. Thanks.
>
> I can see how to delete it, but that deletes the comments too, which are in
> fact pertinent since they affected the merge into the actual branch.
>
> Should I just delete it, do you suppose, or do you have a better idea, Tim?
>
I think you can just use 'Edit' to set the status of the branch to 'merged'.
The merge proposal could be set to an appropriate status too (Rejected
probably).
--
Francis J. Lacoste
<email address hidden>
Preview Diff
1 | === modified file 'buildout.cfg' |
2 | --- buildout.cfg 2009-03-04 14:22:39 +0000 |
3 | +++ buildout.cfg 2009-03-05 05:27:41 +0000 |
4 | @@ -2,6 +2,7 @@ |
5 | parts = |
6 | interpreter |
7 | test |
8 | + test_proxy |
9 | tags |
10 | unzip = true |
11 | |
12 | @@ -10,7 +11,12 @@ |
13 | [test] |
14 | recipe = zc.recipe.testrunner |
15 | eggs = lazr.enum |
16 | -defaults = '--tests-pattern ^tests --exit-with-status'.split() |
17 | +defaults = '--tests-pattern ^test_documentation --exit-with-status'.split() |
18 | + |
19 | +[test_proxy] |
20 | +recipe = zc.recipe.testrunner |
21 | +eggs = lazr.enum[proxy] |
22 | +defaults = '--tests-pattern ^test_proxy --exit-with-status'.split() |
23 | |
24 | [interpreter] |
25 | recipe = zc.recipe.egg |
26 | |
27 | === modified file 'setup.py' |
28 | --- setup.py 2009-03-04 14:22:39 +0000 |
29 | +++ setup.py 2009-03-05 05:27:41 +0000 |
30 | @@ -34,7 +34,7 @@ |
31 | |
32 | |
33 | sys.path.insert(0, 'src') |
34 | -from lazr.enum import __version__ |
35 | +__version__ = '0.1' |
36 | |
37 | setup( |
38 | name='lazr.enum', |
39 | @@ -54,7 +54,11 @@ |
40 | install_requires=[ |
41 | 'setuptools', |
42 | 'zope.interface', |
43 | + 'zope.schema', |
44 | ], |
45 | + extras_require={ |
46 | + 'proxy': ['zope.security'] |
47 | + }, |
48 | url='https://launchpad.net/lazr.enum', |
49 | classifiers=[ |
50 | "Development Status :: 5 - Production/Stable", |
51 | |
52 | === modified file 'src/lazr/enum/README.txt' |
53 | --- src/lazr/enum/README.txt 2009-03-04 14:22:39 +0000 |
54 | +++ src/lazr/enum/README.txt 2009-03-04 23:07:36 +0000 |
55 | @@ -14,9 +14,366 @@ |
56 | You should have received a copy of the GNU Lesser General Public License |
57 | along with lazr.enum. If not, see <http://www.gnu.org/licenses/>. |
58 | |
59 | -LAZR enum |
60 | -*********** |
61 | - |
62 | -This is a pure template for new lazr namespace packages. Whenever you want to |
63 | -make a new lazr subpackage, just branch this code and change all occurrences |
64 | -of 'enum' with whatever your subpackage's name is. |
65 | +Enumerated Types |
66 | +**************** |
67 | + |
68 | +Enumerated types are used primarily in two distinct places in the Launchpad |
69 | +code: selector types; and database types. |
70 | + |
71 | +Simple enumerated types do not have values, whereas database enumerated |
72 | +types are a mapping from an integer value to something meaningful in the |
73 | +code. |
74 | + |
75 | + >>> from lazr.enum import ( |
76 | + ... EnumeratedType, DBEnumeratedType, Item, DBItem, use_template) |
77 | + |
78 | +The `enum` values of EnumeratedTypes are instances of Item. |
79 | + |
80 | + >>> class Fruit(EnumeratedType): |
81 | + ... "A choice of fruit." |
82 | + ... APPLE = Item('Apple') |
83 | + ... PEAR = Item('Pear') |
84 | + ... ORANGE = Item('Orange') |
85 | + |
86 | +=================== |
87 | +IVocabulary support |
88 | +=================== |
89 | + |
90 | +Enumerated types support IVocabularyTokenized. |
91 | + |
92 | + >>> from zope.interface.verify import verifyObject |
93 | + >>> from zope.schema.interfaces import ( |
94 | + ... ITitledTokenizedTerm, IVocabularyTokenized) |
95 | + >>> verifyObject(IVocabularyTokenized, Fruit) |
96 | + True |
97 | + |
98 | +The items themselves do not support any interface. Items returned |
99 | +by the methods for vocabularies return wrapped items that support |
100 | +the ITitledTokenizedTerm interface. |
101 | + |
102 | +The token used to identify terms in the vocabulary is the name of the |
103 | +Item variable. |
104 | + |
105 | + >>> item = Fruit.getTermByToken('APPLE') |
106 | + >>> type(item) |
107 | + <class 'lazr.enum...TokenizedItem'> |
108 | + >>> verifyObject(ITitledTokenizedTerm, item) |
109 | + True |
110 | + |
111 | +TokenizedItems have three attributes (in order to support |
112 | +ITitledTokenizedTerm): |
113 | + |
114 | + >>> item.value |
115 | + <Item Fruit.APPLE, Apple> |
116 | + >>> item.token |
117 | + 'APPLE' |
118 | + >>> item.title |
119 | + 'Apple' |
120 | + |
121 | +The length of an EnumeratedType returns the number of items it has. |
122 | + |
123 | + >>> print len(Fruit) |
124 | + 3 |
125 | + |
126 | +=========================== |
127 | +The EnumeratedType registry |
128 | +=========================== |
129 | + |
130 | +All enumerated types that are created are added to the |
131 | +enumerated_type_registry. |
132 | + |
133 | + >>> from lazr.enum import enumerated_type_registry |
134 | + |
135 | +The enumerated_type_registry maps the name of the enumerated type to the type |
136 | +itself. |
137 | + |
138 | + >>> 'Fruit' in enumerated_type_registry |
139 | + True |
140 | + >>> enumerated_type_registry['Fruit'] |
141 | + <EnumeratedType 'Fruit'> |
142 | + |
143 | +You cannot redefine an existing enumerated type, nor create another enumerated |
144 | +type with the same name as an existing type. |
145 | + |
146 | + >>> class BranchType(EnumeratedType): |
147 | + ... BAR = Item('Bar') |
148 | + ... |
149 | + >>> BranchType.name = 'AltBranchType' |
150 | + >>> class BranchType(EnumeratedType): |
151 | + ... FOO = Item('Foo') |
152 | + Traceback (most recent call last): |
153 | + ... |
154 | + TypeError: An enumerated type already exists with the name BranchType |
155 | + (...AltBranchType). |
156 | + |
157 | +====================== |
158 | +Enumerated Type basics |
159 | +====================== |
160 | + |
161 | +An EnumeratedType has a name and a description. The name is the same as the |
162 | +class name, and the description is the docstring for the class. |
163 | + |
164 | + >>> print Fruit.name |
165 | + Fruit |
166 | + >>> print Fruit.description |
167 | + A choice of fruit. |
168 | + |
169 | +If you do not specify an explicit sort_order for the items of the |
170 | +EnumeratedType one is created for you. This is tuple of the tokens. |
171 | + |
172 | + >>> print Fruit.sort_order |
173 | + ('APPLE', 'PEAR', 'ORANGE') |
174 | + |
175 | +The items of an enumerated type can be iterated over. However the type that |
176 | +is returned by the iteration is the TokenizedItem, not the item itself. |
177 | + |
178 | + >>> for item in Fruit: |
179 | + ... print item.token, item.title |
180 | + APPLE Apple |
181 | + PEAR Pear |
182 | + ORANGE Orange |
183 | + |
184 | +Items in an enumerator support comparison and equality checks. Comparison |
185 | +is based on the sort order of the items. |
186 | + |
187 | + >>> apple = Fruit.APPLE |
188 | + >>> pear = Fruit.PEAR |
189 | + >>> orange = Fruit.ORANGE |
190 | + >>> apple < pear |
191 | + True |
192 | + >>> apple == pear |
193 | + False |
194 | + >>> apple == apple |
195 | + True |
196 | + >>> apple != pear |
197 | + True |
198 | + >>> apple > pear |
199 | + False |
200 | + >>> pear < orange |
201 | + True |
202 | + >>> apple < orange |
203 | + True |
204 | + |
205 | +The string representation of an Item is the title, and the representation |
206 | +also shows the enumeration that the Item is from. |
207 | + |
208 | + >>> print apple |
209 | + Apple |
210 | + >>> print repr(apple) |
211 | + <Item Fruit.APPLE, Apple> |
212 | + |
213 | +The `items` attribute of an enumerated type is not a list, but a class that |
214 | +provides iteration over the items, and access to the Item attributes through |
215 | +either the name of the Item, or the database value if there is one. |
216 | + |
217 | +The primary use of this is to provide a backwards compatable accessor for items, |
218 | +but it also provides a suitable alternative to getattr |
219 | + |
220 | + >>> name = 'APPLE' |
221 | + >>> Fruit.items[name] |
222 | + <Item Fruit.APPLE, Apple> |
223 | + >>> getattr(Fruit, name) |
224 | + <Item Fruit.APPLE, Apple> |
225 | + |
226 | +========================= |
227 | +Database Enumerated Types |
228 | +========================= |
229 | + |
230 | +A very common use of enumerated types are to give semantic meaning to integer |
231 | +values stored in database columns. EnumeratedType Items themselves don't have |
232 | +any integer values. |
233 | + |
234 | +The DBEnumeratedType provides the semantic framework for a type that is used to |
235 | +map integer values to python enumerated values. |
236 | + |
237 | + >>> # Remove the existing reference to BranchType from the registry |
238 | + >>> del enumerated_type_registry['BranchType'] |
239 | + >>> class BranchType(DBEnumeratedType): |
240 | + ... HOSTED = DBItem(1, """ |
241 | + ... Hosted |
242 | + ... |
243 | + ... Hosted braches use the supermirror as the main repository |
244 | + ... for the branch.""") |
245 | + ... |
246 | + ... MIRRORED = DBItem(2, """ |
247 | + ... Mirrored |
248 | + ... |
249 | + ... Mirrored branches are "pulled" from a remote location.""") |
250 | + ... |
251 | + ... IMPORTED = DBItem(3, """ |
252 | + ... Imported |
253 | + ... |
254 | + ... Imported branches are natively maintained in CVS or SVN""") |
255 | + |
256 | +Note carefully that the value of a DBItem is the integer represenation. But the |
257 | +value of the TokenizedItem is the DBItem itself. |
258 | + |
259 | + >>> hosted = BranchType.HOSTED |
260 | + >>> hosted.value |
261 | + 1 |
262 | + >>> hosted == BranchType.HOSTED |
263 | + True |
264 | + >>> tokenized_item = BranchType.getTermByToken('HOSTED') |
265 | + >>> tokenized_item.value |
266 | + <DBItem BranchType.HOSTED, (1) Hosted> |
267 | + |
268 | +DBEnumeratedTypes also support IVocabularyTokenized |
269 | + |
270 | + >>> verifyObject(IVocabularyTokenized, BranchType) |
271 | + True |
272 | + |
273 | +The items attribute of DBEnumeratedTypes provide a mapping from the database |
274 | +values to the DBItems. |
275 | + |
276 | + >>> BranchType.items[3] |
277 | + <DBItem BranchType.IMPORTED, (3) Imported> |
278 | + |
279 | +Items in a DBEnumeratedType class must be of type DBItem. |
280 | + |
281 | + >>> class BadItemType(DBEnumeratedType): |
282 | + ... TESTING = Item("Testing") |
283 | + Traceback (most recent call last): |
284 | + ... |
285 | + TypeError: Items must be of the appropriate type for the DBEnumeratedType, |
286 | + __builtin__.BadItemType.TESTING |
287 | + |
288 | +You are not able to define a DBEnumeratedType that has two different |
289 | +DBItems that map to the same numeric value. |
290 | + |
291 | + >>> class TwoMapping(DBEnumeratedType): |
292 | + ... FIRST = DBItem(42, 'First') |
293 | + ... SECOND = DBItem(42, 'Second') |
294 | + Traceback (most recent call last): |
295 | + ... |
296 | + TypeError: Two DBItems with the same value 42 (FIRST, SECOND) |
297 | + |
298 | +========================= |
299 | +Overriding the sort order |
300 | +========================= |
301 | + |
302 | +By default the sort order of the items in an enumerated type is defined by the |
303 | +order in which the Items are declared. This my be overridden by specifying |
304 | +a sort_order attribute in the class. |
305 | + |
306 | +If a sort_order is specified, it has to specify every item in the enumeration. |
307 | + |
308 | + >>> class AnimalClassification(EnumeratedType): |
309 | + ... sort_order = "REPTILE", "INSECT", "MAMMAL" |
310 | + ... INSECT = Item("Insect") |
311 | + ... MAMMAL = Item("Mammal") |
312 | + ... FISH = Item("Fish") |
313 | + ... REPTILE = Item("Reptile") |
314 | + Traceback (most recent call last): |
315 | + ... |
316 | + TypeError: sort_order for EnumeratedType must contain all and only Item instances ... |
317 | + |
318 | +The sort_order may also appear anywhere in the definition of the class, although |
319 | +convention has it that it appears first, before the Item instances. |
320 | + |
321 | + >>> class AnimalClassification(EnumeratedType): |
322 | + ... sort_order = "REPTILE", "FISH", "INSECT", "MAMMAL" |
323 | + ... INSECT = Item("Insect") |
324 | + ... MAMMAL = Item("Mammal") |
325 | + ... FISH = Item("Fish") |
326 | + ... REPTILE = Item("Reptile") |
327 | + |
328 | +The items attribute of the enumerated type is ordered based on the sort_order. |
329 | +The items attribute is also used to control iteration using __iter__. |
330 | + |
331 | + >>> for item in AnimalClassification.items: |
332 | + ... print item.title |
333 | + Reptile |
334 | + Fish |
335 | + Insect |
336 | + Mammal |
337 | + |
338 | +The sort order also drives the comparison operations. |
339 | + |
340 | + >>> reptile, fish, insect, mammal = AnimalClassification.items |
341 | + >>> reptile < fish < insect < mammal |
342 | + True |
343 | + |
344 | +========================== |
345 | +Extending enumerated types |
346 | +========================== |
347 | + |
348 | +The simplest way to extend a class is to derive from it. |
349 | + |
350 | + >>> class AnimalClassificationExtended(AnimalClassification): |
351 | + ... INVERTEBRATE = Item("Invertebrate") |
352 | + |
353 | + >>> for item in AnimalClassificationExtended: |
354 | + ... print item.title |
355 | + Reptile |
356 | + Fish |
357 | + Insect |
358 | + Mammal |
359 | + Invertebrate |
360 | + |
361 | +The use_template function inserts the items from the specified enumerated type |
362 | +into the new enumerated type. The default case is to take all the enumerated |
363 | +items. |
364 | + |
365 | + >>> class UIBranchType(EnumeratedType): |
366 | + ... use_template(BranchType) |
367 | + >>> for item in UIBranchType: |
368 | + ... print item.title |
369 | + Hosted |
370 | + Mirrored |
371 | + Imported |
372 | + |
373 | +You can also specify items to be excluded by referring to the attribute name |
374 | +in the exclude parameter. This can be either a string referring to one name |
375 | +or a tuple or list that refers to multiple attribute names. |
376 | + |
377 | + >>> class UIBranchType2(EnumeratedType): |
378 | + ... use_template(BranchType, exclude='IMPORTED') |
379 | + >>> for item in UIBranchType2: |
380 | + ... print item.title |
381 | + Hosted |
382 | + Mirrored |
383 | + |
384 | +Or limit the items to those specified: |
385 | + |
386 | + >>> class UIBranchType3(EnumeratedType): |
387 | + ... use_template(BranchType, include=('HOSTED', 'MIRRORED')) |
388 | + >>> for item in UIBranchType3: |
389 | + ... print item.title |
390 | + Hosted |
391 | + Mirrored |
392 | + |
393 | +================================================ |
394 | +Getting from an item back to the enumerated type |
395 | +================================================ |
396 | + |
397 | +Each Item in an EnumeratedType has a reference back to the EnumeratedType. |
398 | + |
399 | + >>> print repr(apple) |
400 | + <Item Fruit.APPLE, Apple> |
401 | + >>> print repr(apple.enum) |
402 | + <EnumeratedType 'Fruit'> |
403 | + >>> for item in apple.enum: |
404 | + ... print item.title |
405 | + Apple |
406 | + Pear |
407 | + Orange |
408 | + |
409 | +============ |
410 | +Item.sortkey |
411 | +============ |
412 | + |
413 | +The sortkey attribute of the Items are defined by the sort_order that is |
414 | +defined for the enumerated type. The value is often used as a hidden value |
415 | +in columns in order to ensure appropriate sorting. |
416 | + |
417 | + >>> for item in Fruit.items: |
418 | + ... print item.title, item.sortkey |
419 | + Apple 0 |
420 | + Pear 1 |
421 | + Orange 2 |
422 | + |
423 | + >>> for item in BranchType.items: |
424 | + ... print item.title, item.sortkey |
425 | + Hosted 0 |
426 | + Mirrored 1 |
427 | + Imported 2 |
428 | |
429 | === modified file 'src/lazr/enum/__init__.py' |
430 | --- src/lazr/enum/__init__.py 2009-03-04 14:22:39 +0000 |
431 | +++ src/lazr/enum/__init__.py 2009-03-05 05:27:41 +0000 |
432 | @@ -1,4 +1,4 @@ |
433 | -# Copyright 2009 Canonical Ltd. All rights reserved. |
434 | +# Copyright 2004-2009 Canonical Ltd. All rights reserved. |
435 | # |
436 | # This file is part of lazr.enum |
437 | # |
438 | @@ -15,4 +15,521 @@ |
439 | # You should have received a copy of the GNU Lesser General Public License |
440 | # along with lazr.enum. If not, see <http://www.gnu.org/licenses/>. |
441 | |
442 | -__version__ = '0.1' |
443 | +__metaclass__ = type |
444 | + |
445 | +import itertools |
446 | +import operator |
447 | +import sys |
448 | +import warnings |
449 | + |
450 | +from zope.interface import implements |
451 | +from zope.schema.interfaces import ITitledTokenizedTerm, IVocabularyTokenized |
452 | +try: |
453 | + from zope.proxy import removeAllProxies |
454 | +except ImportError: |
455 | + removeAllProxies = lambda obj: obj # no-op |
456 | + |
457 | +from lazr.enum.interfaces import IEnumeratedType |
458 | + |
459 | +__all__ = [ |
460 | + 'BaseItem', |
461 | + 'DBEnumeratedType', |
462 | + 'DBItem', |
463 | + 'EnumeratedType', |
464 | + 'IEnumeratedType', |
465 | + 'Item', |
466 | + 'TokenizedItem', |
467 | + 'enumerated_type_registry', |
468 | + 'use_template', |
469 | + ] |
470 | + |
471 | +def proxy_isinstance(obj, cls): |
472 | + """Test whether an object is an instance of a type. |
473 | + |
474 | + This works even if the object is proxied by zope.proxy, if that package |
475 | + is available. |
476 | + """ |
477 | + return isinstance(removeAllProxies(obj), cls) |
478 | + |
479 | +def docstring_to_title_descr(string): |
480 | + """When given a classically formatted docstring, returns a tuple |
481 | + (title, description). |
482 | + |
483 | + >>> class Foo: |
484 | + ... ''' |
485 | + ... Title of foo |
486 | + ... |
487 | + ... Description of foo starts here. It may |
488 | + ... spill onto multiple lines. It may also have |
489 | + ... indented examples: |
490 | + ... |
491 | + ... Foo |
492 | + ... Bar |
493 | + ... |
494 | + ... like the above. |
495 | + ... ''' |
496 | + ... |
497 | + >>> title, descr = docstring_to_title_descr(Foo.__doc__) |
498 | + >>> print title |
499 | + Title of foo |
500 | + >>> for num, line in enumerate(descr.splitlines()): |
501 | + ... print "%d.%s" % (num, line) |
502 | + ... |
503 | + 0.Description of foo starts here. It may |
504 | + 1.spill onto multiple lines. It may also have |
505 | + 2.indented examples: |
506 | + 3. |
507 | + 4. Foo |
508 | + 5. Bar |
509 | + 6. |
510 | + 7.like the above. |
511 | + |
512 | + """ |
513 | + lines = string.splitlines() |
514 | + # title is the first non-blank line |
515 | + for num, line in enumerate(lines): |
516 | + line = line.strip() |
517 | + if line: |
518 | + title = line |
519 | + break |
520 | + else: |
521 | + raise ValueError |
522 | + assert not lines[num+1].strip() |
523 | + descrlines = lines[num+2:] |
524 | + descr1 = descrlines[0] |
525 | + indent = len(descr1) - len(descr1.lstrip()) |
526 | + descr = '\n'.join([line[indent:] for line in descrlines]) |
527 | + return title, descr |
528 | + |
529 | + |
530 | +class BaseItem: |
531 | + """Items are the primary elements of the enumerated types. |
532 | + |
533 | + `BaseItem` is the base class for both `Item` and `DBItem`. |
534 | + |
535 | + The enum attribute is a reference to the enumerated type that the |
536 | + Item is a member of. |
537 | + |
538 | + The token attribute is the name assigned to the item. |
539 | + |
540 | + The value is the short text string used to identify the item. |
541 | + """ |
542 | + |
543 | + sortkey = 0 |
544 | + name = None |
545 | + description = None |
546 | + title = None |
547 | + |
548 | + def __init__(self, title, description=None): |
549 | + """Items are the main elements of the EnumeratedType. |
550 | + |
551 | + Where the title is passed in without a description, |
552 | + and the title looks like a docstring (has embedded carriage returns), |
553 | + the title is the first line, and the description is the rest. |
554 | + """ |
555 | + |
556 | + self.sortkey = BaseItem.sortkey |
557 | + BaseItem.sortkey += 1 |
558 | + self.title = title |
559 | + # The enum attribute is set duing the class constructor of the |
560 | + # containing enumerated type. |
561 | + |
562 | + self.description = description |
563 | + |
564 | + if self.description is None: |
565 | + # check value |
566 | + if self.title.find('\n') != -1: |
567 | + self.title, self.description = docstring_to_title_descr( |
568 | + self.title) |
569 | + |
570 | + def __int__(self): |
571 | + raise TypeError("Cannot cast Item to int.") |
572 | + |
573 | + def __cmp__(self, other): |
574 | + if proxy_isinstance(other, BaseItem): |
575 | + return cmp(self.sortkey, other.sortkey) |
576 | + else: |
577 | + raise TypeError( |
578 | + 'Comparisons of Items are only valid with other Items') |
579 | + |
580 | + def __eq__(self, other, stacklevel=2): |
581 | + if isinstance(other, int): |
582 | + warnings.warn('comparison of Item to an int: %r' % self, |
583 | + stacklevel=stacklevel) |
584 | + return False |
585 | + elif proxy_isinstance(other, BaseItem): |
586 | + return (self.name == other.name and |
587 | + self.enum == other.enum) |
588 | + else: |
589 | + return False |
590 | + |
591 | + def __ne__(self, other): |
592 | + return not self.__eq__(other, stacklevel=3) |
593 | + |
594 | + def __hash__(self): |
595 | + return hash(self.title) |
596 | + |
597 | + def __str__(self): |
598 | + return str(self.title) |
599 | + |
600 | + |
601 | +class Item(BaseItem): |
602 | + """The `Item` is an element of an `EnumeratedType`.""" |
603 | + @staticmethod |
604 | + def construct(other_item): |
605 | + """Create an Item based on the other_item.""" |
606 | + item = Item(other_item.title, other_item.description) |
607 | + item.sortkey = other_item.sortkey |
608 | + return item |
609 | + |
610 | + def __repr__(self): |
611 | + return "<Item %s.%s, %s>" % ( |
612 | + self.enum.name, self.name, self.title) |
613 | + |
614 | + |
615 | +class DBItem(BaseItem): |
616 | + """The `DBItem` refers to an enumerated item that is used in the database. |
617 | + |
618 | + Database enumerations are stored in the database using integer columns. |
619 | + """ |
620 | + |
621 | + @staticmethod |
622 | + def construct(other_item): |
623 | + """Create an Item based on the other_item.""" |
624 | + item = DBItem( |
625 | + other_item.value, other_item.title, other_item.description) |
626 | + item.sortkey = other_item.sortkey |
627 | + return item |
628 | + |
629 | + def __init__(self, value, title, description=None): |
630 | + BaseItem.__init__(self, title, description) |
631 | + self.value = value |
632 | + |
633 | + def __hash__(self): |
634 | + return self.value |
635 | + |
636 | + def __repr__(self): |
637 | + return "<DBItem %s.%s, (%d) %s>" % ( |
638 | + self.enum.name, self.name, self.value, self.title) |
639 | + |
640 | + # XXX this is probably going away. |
641 | + def __sqlrepr__(self, dbname): |
642 | + return repr(self.value) |
643 | + |
644 | + |
645 | +class TokenizedItem: |
646 | + """Wraps an `Item` or `DBItem` to provide `ITitledTokenizedTerm`.""" |
647 | + |
648 | + implements(ITitledTokenizedTerm) |
649 | + |
650 | + def __init__(self, item): |
651 | + self.value = item |
652 | + self.token = item.name |
653 | + self.title = item.title |
654 | + |
655 | + |
656 | +# The enumerated_type_registry is a mapping of all enumerated types to the |
657 | +# actual class. There should only ever be one EnumeratedType or |
658 | +# DBEnumerateType with a particular name. This serves two purposes: |
659 | +# * a way to get any enumerated type by its name (used in tales.py) |
660 | +# * a way to iterate over the DBEnumeratedTypes in order to confirm the |
661 | +# values actually stored in the database. |
662 | +enumerated_type_registry = {} |
663 | + |
664 | + |
665 | +class EnumItems: |
666 | + """Allow access to Items of an enumerated type using names or db values. |
667 | + |
668 | + Access can be made to the items using the name of the Item. |
669 | + |
670 | + If the enumerated type has DBItems then the mapping includes a mapping of |
671 | + the database integer values to the DBItems. |
672 | + """ |
673 | + def __init__(self, items, mapping): |
674 | + self.items = items |
675 | + self.mapping = mapping |
676 | + def __getitem__(self, key): |
677 | + if key in self.mapping: |
678 | + return self.mapping[key] |
679 | + else: |
680 | + raise KeyError(key) |
681 | + def __iter__(self): |
682 | + return self.items.__iter__() |
683 | + def __len__(self): |
684 | + return len(self.items) |
685 | + |
686 | + |
687 | +class BaseMetaEnum(type): |
688 | + """The metaclass functionality for `EnumeratedType` and `DBEnumeratedType`. |
689 | + |
690 | + This metaclass defines methods that allow the enumerated types to implement |
691 | + the IVocabularyTokenized interface. |
692 | + |
693 | + The metaclass also enforces "correct" definitions of enumerated types by |
694 | + enforcing capitalisation of Item variable names and defining an appropriate |
695 | + ordering. |
696 | + """ |
697 | + implements(IEnumeratedType, IVocabularyTokenized) |
698 | + |
699 | + @classmethod |
700 | + def _enforceSingleInheritance(cls, classname, bases, classdict): |
701 | + """Only one base class is allowed for enumerated types.""" |
702 | + if len(bases) > 1: |
703 | + raise TypeError( |
704 | + 'Multiple inheritance is not allowed with ' |
705 | + '%s, %s.%s' % ( |
706 | + cls.enum_name, classdict['__module__'], classname)) |
707 | + |
708 | + @classmethod |
709 | + def _updateClassDictWithBaseItems(cls, bases, classdict): |
710 | + """Copy each of the items from the base class that hasn't been |
711 | + explicitly defined in the new class.""" |
712 | + if bases: |
713 | + base_class = bases[0] |
714 | + if hasattr(base_class, 'items'): |
715 | + for item in base_class.items: |
716 | + if item.name not in classdict: |
717 | + new_item = cls.item_type.construct(item) |
718 | + classdict[item.name] = new_item |
719 | + |
720 | + @classmethod |
721 | + def _updateClassDictWithTemplateItems(cls, classdict): |
722 | + """If constructed through use_template, we need to construct |
723 | + the appropriate type of items based on our item_type of our class.""" |
724 | + if 'template_items' in classdict: |
725 | + for item in classdict['template_items']: |
726 | + classdict[item.name] = cls.item_type.construct(item) |
727 | + # The template_items key is not wanted or needed in the new type. |
728 | + del classdict['template_items'] |
729 | + |
730 | + @classmethod |
731 | + def _enforceItemClassAndName(cls, items, classname, module_name): |
732 | + """All items must be of the appropriate type for the enumeration type. |
733 | + |
734 | + All item variable names must be capitalised. |
735 | + """ |
736 | + for item_name, item in items: |
737 | + if not isinstance(item, cls.item_type): |
738 | + raise TypeError( |
739 | + 'Items must be of the appropriate type for the ' |
740 | + '%s, %s.%s.%s' % ( |
741 | + cls.enum_name, module_name, classname, item_name)) |
742 | + |
743 | + if item_name.upper() != item_name: |
744 | + raise TypeError( |
745 | + 'Item instance variable names must be capitalised.' |
746 | + ' %s.%s.%s' % (module_name, classname, item_name)) |
747 | + |
748 | + item.name = item_name |
749 | + |
750 | + @classmethod |
751 | + def _generateItemMapping(cls, items): |
752 | + """Each enumerated type has a mapping of the item names to the item |
753 | + instances.""" |
754 | + return dict(items) |
755 | + |
756 | + @classmethod |
757 | + def _enforceSortOrder(cls, classname, classdict, items): |
758 | + """ Override item's default sort order if sort_order is defined. |
759 | + |
760 | + :return: A list of items ordered appropriately. |
761 | + """ |
762 | + items = dict(items) |
763 | + if 'sort_order' in classdict: |
764 | + sort_order = classdict['sort_order'] |
765 | + item_names = sorted(items.keys()) |
766 | + if item_names != sorted(sort_order): |
767 | + raise TypeError( |
768 | + 'sort_order for %s must contain all and ' |
769 | + 'only Item instances %s.%s' % ( |
770 | + cls.enum_name, classdict['__module__'], classname)) |
771 | + else: |
772 | + # Sort the items by the automatically generated |
773 | + # sortkey. |
774 | + sort_order = [ |
775 | + item.name for item in sorted( |
776 | + items.values(), key=operator.attrgetter('sortkey'))] |
777 | + classdict['sort_order'] = tuple(sort_order) |
778 | + # Assign new sortkey values from zero. |
779 | + sorted_items = [] |
780 | + for sort_id, item_name in enumerate(sort_order): |
781 | + item = classdict[item_name] |
782 | + item.sortkey = sort_id |
783 | + sorted_items.append(item) |
784 | + return sorted_items |
785 | + |
786 | + def __new__(cls, classname, bases, classdict): |
787 | + """Called when defining a new class.""" |
788 | + |
789 | + cls._enforceSingleInheritance(classname, bases, classdict) |
790 | + cls._updateClassDictWithBaseItems(bases, classdict) |
791 | + cls._updateClassDictWithTemplateItems(classdict) |
792 | + |
793 | + items = [(key, value) for key, value in classdict.iteritems() |
794 | + if isinstance(value, BaseItem)] |
795 | + |
796 | + cls._enforceItemClassAndName(items, classname, classdict['__module__']) |
797 | + |
798 | + mapping = cls._generateItemMapping(items) |
799 | + sorted_items = cls._enforceSortOrder(classname, classdict, items) |
800 | + |
801 | + classdict['items'] = EnumItems(sorted_items, mapping) |
802 | + classdict['name'] = classname |
803 | + classdict['description'] = classdict.get('__doc__', None) |
804 | + |
805 | + global enumerated_type_registry |
806 | + if classname in enumerated_type_registry: |
807 | + other = enumerated_type_registry[classname] |
808 | + raise TypeError( |
809 | + 'An enumerated type already exists with the name %s (%s.%s).' |
810 | + % (classname, other.__module__, other.name)) |
811 | + |
812 | + instance = type.__new__(cls, classname, bases, classdict) |
813 | + |
814 | + # Add a reference to the enumerated type to each item. |
815 | + for item in instance.items: |
816 | + item.enum = instance |
817 | + |
818 | + # Add the enumerated type to the registry. |
819 | + enumerated_type_registry[classname] = instance |
820 | + |
821 | + return instance |
822 | + |
823 | + def __contains__(self, value): |
824 | + """See `ISource`.""" |
825 | + return value in self.items |
826 | + |
827 | + def __iter__(self): |
828 | + """See `IIterableVocabulary`.""" |
829 | + return itertools.imap(TokenizedItem, self.items) |
830 | + |
831 | + def __len__(self): |
832 | + """See `IIterableVocabulary`.""" |
833 | + return len(self.items) |
834 | + |
835 | + def getTerm(self, value): |
836 | + """See `IBaseVocabulary`.""" |
837 | + if value in self.items: |
838 | + return TokenizedItem(value) |
839 | + raise LookupError(value) |
840 | + |
841 | + def getTermByToken(self, token): |
842 | + """See `IVocabularyTokenized`.""" |
843 | + # The sort_order of the enumerated type lists all the items. |
844 | + if token in self.sort_order: |
845 | + return TokenizedItem(getattr(self, token)) |
846 | + else: |
847 | + # If the token is not specified in the sort order then check |
848 | + # the titles of the items. This is to support the transition |
849 | + # of accessing items by their titles. To continue support |
850 | + # of old URLs et al, this will probably stay for some time. |
851 | + for item in self.items: |
852 | + if item.title == token: |
853 | + return TokenizedItem(item) |
854 | + # The token was not in the sort_order (and hence the name of a |
855 | + # variable), nor was the token the title of one of the items. |
856 | + raise LookupError(token) |
857 | + |
858 | + |
859 | +class MetaEnum(BaseMetaEnum): |
860 | + """The metaclass for `EnumeratedType`.""" |
861 | + item_type = Item |
862 | + enum_name = 'EnumeratedType' |
863 | + |
864 | + def __repr__(self): |
865 | + return "<EnumeratedType '%s'>" % self.name |
866 | + |
867 | + |
868 | +class MetaDBEnum(BaseMetaEnum): |
869 | + """The meta class for `DBEnumeratedType`. |
870 | + |
871 | + Provides a method for getting the item based on the database identifier in |
872 | + addition to all the normal enumerated type methods. |
873 | + """ |
874 | + item_type = DBItem |
875 | + enum_name = 'DBEnumeratedType' |
876 | + |
877 | + @classmethod |
878 | + def _generateItemMapping(cls, items): |
879 | + """DBEnumeratedTypes also map the database value of the DBItem to the |
880 | + item instance.""" |
881 | + mapping = BaseMetaEnum._generateItemMapping(items) |
882 | + for item_name, item in items: |
883 | + # If the value is already in the mapping then we have two |
884 | + # different items attempting to map the same number. |
885 | + if item.value in mapping: |
886 | + # We really want to provide the names in alphabetical order. |
887 | + args = [item.value] + sorted( |
888 | + [item_name, mapping[item.value].name]) |
889 | + raise TypeError( |
890 | + 'Two DBItems with the same value %s (%s, %s)' |
891 | + % tuple(args)) |
892 | + else: |
893 | + mapping[item.value] = item |
894 | + return mapping |
895 | + |
896 | + def __repr__(self): |
897 | + return "<DBEnumeratedType '%s'>" % self.name |
898 | + |
899 | + |
900 | +class EnumeratedType: |
901 | + """An enumeration of items. |
902 | + |
903 | + The items of the enumeration must be instances of the class `Item`. |
904 | + These items are accessible through a class attribute `items`. The ordering |
905 | + of the items attribute is the same order that the items are defined in the |
906 | + class. |
907 | + |
908 | + A `sort_order` attribute can be defined to override the default ordering. |
909 | + The sort_order should contain the names of the all the items in the |
910 | + ordering that is desired. |
911 | + """ |
912 | + __metaclass__ = MetaEnum |
913 | + |
914 | + |
915 | +class DBEnumeratedType: |
916 | + """An enumeration with additional mapping from an integer to `Item`. |
917 | + |
918 | + The items of a class inheriting from DBEnumeratedType must be of type |
919 | + `DBItem`. |
920 | + """ |
921 | + __metaclass__ = MetaDBEnum |
922 | + |
923 | + |
924 | +def use_template(enum_type, include=None, exclude=None): |
925 | + """An alternative way to extend an enumerated type other than inheritance. |
926 | + |
927 | + The parameters include and exclude should either be the name values of the |
928 | + items (the parameter names), or a list or tuple that contains string |
929 | + values. |
930 | + """ |
931 | + frame = sys._getframe(1) |
932 | + locals = frame.f_locals |
933 | + |
934 | + # Try to make sure we were called from a class def. |
935 | + if (locals is frame.f_globals) or ('__module__' not in locals): |
936 | + raise TypeError( |
937 | + "use_template can be used only from a class definition.") |
938 | + |
939 | + # You can specify either includes or excludes, not both. |
940 | + if include and exclude: |
941 | + raise ValueError("You can specify includes or excludes not both.") |
942 | + |
943 | + if include is None: |
944 | + items = enum_type.items |
945 | + else: |
946 | + if isinstance(include, str): |
947 | + include = [include] |
948 | + items = [item for item in enum_type.items if item.name in include] |
949 | + |
950 | + if exclude is None: |
951 | + exclude = [] |
952 | + elif isinstance(exclude, str): |
953 | + exclude = [exclude] |
954 | + |
955 | + template_items = [] |
956 | + for item in items: |
957 | + if item.name not in exclude: |
958 | + template_items.append(item) |
959 | + |
960 | + locals['template_items'] = template_items |
961 | |
962 | === added file 'src/lazr/enum/configure.zcml' |
963 | --- src/lazr/enum/configure.zcml 1970-01-01 00:00:00 +0000 |
964 | +++ src/lazr/enum/configure.zcml 2009-03-05 05:27:41 +0000 |
965 | @@ -0,0 +1,23 @@ |
966 | +<configure |
967 | + xmlns="http://namespaces.zope.org/zope" |
968 | + xmlns:browser="http://namespaces.zope.org/browser" |
969 | + i18n_domain="lazr.enum"> |
970 | + |
971 | + <!-- Enumerated types and items --> |
972 | + <class class="lazr.enum.MetaEnum"> |
973 | + <allow interface="zope.schema.interfaces.IVocabularyTokenized" /> |
974 | + <allow interface="lazr.enum.IEnumeratedType" /> |
975 | + </class> |
976 | + <class class="lazr.enum.Item"> |
977 | + <allow attributes="name title description enum sortkey __sqlrepr__" /> |
978 | + </class> |
979 | + |
980 | + <class class="lazr.enum.MetaDBEnum"> |
981 | + <allow interface="zope.schema.interfaces.IVocabularyTokenized" /> |
982 | + <allow interface="lazr.enum.IEnumeratedType" /> |
983 | + </class> |
984 | + <class class="lazr.enum.DBItem"> |
985 | + <allow attributes="name title value description sortkey enum __sqlrepr__" /> |
986 | + </class> |
987 | + |
988 | +</configure> |
989 | \ No newline at end of file |
990 | |
991 | === removed directory 'src/lazr/enum/docs' |
992 | === removed file 'src/lazr/enum/docs/__init__.py' |
993 | === removed file 'src/lazr/enum/docs/basic.txt' |
994 | --- src/lazr/enum/docs/basic.txt 2009-03-04 14:22:39 +0000 |
995 | +++ src/lazr/enum/docs/basic.txt 1970-01-01 00:00:00 +0000 |
996 | @@ -1,8 +0,0 @@ |
997 | -Importable |
998 | -========== |
999 | - |
1000 | -The lazr.enum package is importable, and has a version number. |
1001 | - |
1002 | - >>> import lazr.enum |
1003 | - >>> print 'VERSION:', lazr.enum.__version__ |
1004 | - VERSION: ... |
1005 | |
1006 | === added file 'src/lazr/enum/interfaces.py' |
1007 | --- src/lazr/enum/interfaces.py 1970-01-01 00:00:00 +0000 |
1008 | +++ src/lazr/enum/interfaces.py 2009-03-04 23:07:36 +0000 |
1009 | @@ -0,0 +1,36 @@ |
1010 | +# Copyright 2004-2009 Canonical Ltd. All rights reserved. |
1011 | +# |
1012 | +# This file is part of lazr.enum |
1013 | +# |
1014 | +# lazr.enum is free software: you can redistribute it and/or modify it |
1015 | +# under the terms of the GNU Lesser General Public License as published by |
1016 | +# the Free Software Foundation, either version 3 of the License, or (at your |
1017 | +# option) any later version. |
1018 | +# |
1019 | +# lazr.enum is distributed in the hope that it will be useful, but WITHOUT |
1020 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
1021 | +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public |
1022 | +# License for more details. |
1023 | +# |
1024 | +# You should have received a copy of the GNU Lesser General Public License |
1025 | +# along with lazr.enum. If not, see <http://www.gnu.org/licenses/>. |
1026 | + |
1027 | +__all__ = [ |
1028 | + 'IEnumeratedType', |
1029 | + ] |
1030 | + |
1031 | +from zope.interface import Attribute, Interface |
1032 | + |
1033 | +class IEnumeratedType(Interface): |
1034 | + """Defines the attributes that EnumeratedTypes have.""" |
1035 | + name = Attribute( |
1036 | + "The name of the EnumeratedType is the same as the name of the class.") |
1037 | + description = Attribute( |
1038 | + "The description is the docstring of the EnumeratedType class.") |
1039 | + sort_order = Attribute( |
1040 | + "A tuple of Item names that is used to determine the ordering of the " |
1041 | + "Items.") |
1042 | + items = Attribute( |
1043 | + "An instance of `EnumItems` which allows access to the enumerated " |
1044 | + "types items by either name of database value if the items are " |
1045 | + "DBItems.") |
1046 | \ No newline at end of file |
1047 | |
1048 | === modified file 'src/lazr/enum/tests/test_documentation.py' |
1049 | --- src/lazr/enum/tests/test_documentation.py 2009-03-03 16:14:37 +0000 |
1050 | +++ src/lazr/enum/tests/test_documentation.py 2009-03-05 05:27:41 +0000 |
1051 | @@ -11,21 +11,20 @@ |
1052 | import doctest |
1053 | import unittest |
1054 | |
1055 | +import lazr.enum |
1056 | + |
1057 | DOCTEST_FLAGS = ( |
1058 | doctest.ELLIPSIS | |
1059 | doctest.NORMALIZE_WHITESPACE | |
1060 | doctest.REPORT_NDIFF) |
1061 | |
1062 | - |
1063 | def test_suite(): |
1064 | - # This breaks the zip-safe flag. |
1065 | - docs_directory = os.path.normpath(os.path.join(__file__, '../../docs')) |
1066 | - doctest_files = ['../docs/%s' % filename |
1067 | - for filename in os.listdir(docs_directory) |
1068 | - if filename.endswith('.txt')] |
1069 | - |
1070 | - kwargs = dict( |
1071 | - module_relative=True, |
1072 | - optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS) |
1073 | + readme = os.path.normpath( |
1074 | + os.path.abspath( |
1075 | + os.path.join(os.path.dirname(__file__), |
1076 | + os.path.pardir, |
1077 | + 'README.txt'))) |
1078 | return unittest.TestSuite(( |
1079 | - doctest.DocFileSuite(*doctest_files, **kwargs))) |
1080 | + doctest.DocFileSuite( |
1081 | + readme, module_relative=False, optionflags=DOCTEST_FLAGS), |
1082 | + doctest.DocTestSuite(lazr.enum))) |
1083 | |
1084 | === added file 'src/lazr/enum/tests/test_proxy.py' |
1085 | --- src/lazr/enum/tests/test_proxy.py 1970-01-01 00:00:00 +0000 |
1086 | +++ src/lazr/enum/tests/test_proxy.py 2009-03-05 05:27:41 +0000 |
1087 | @@ -0,0 +1,44 @@ |
1088 | +# Copyright 2009 Canonical Ltd. All rights reserved. |
1089 | + |
1090 | +"""Test harness.""" |
1091 | + |
1092 | +__all__ = [ |
1093 | + 'test_suite', |
1094 | + ] |
1095 | + |
1096 | +import doctest |
1097 | +import unittest |
1098 | + |
1099 | +def test_proxy_isinstance(): |
1100 | + """ |
1101 | + Proxies such as the zope.security proxy can mask whether an object is an |
1102 | + instance of a class. ``proxy_isinstance`` allows you to ask whether a |
1103 | + proxied object is an instance of another class. |
1104 | + |
1105 | + >>> from lazr.enum import proxy_isinstance |
1106 | + |
1107 | + >>> class C1(object): |
1108 | + ... pass |
1109 | + |
1110 | + >>> c = C1() |
1111 | + >>> proxy_isinstance(c, C1) |
1112 | + True |
1113 | + |
1114 | + >>> from zope.security.checker import ProxyFactory |
1115 | + >>> isinstance(ProxyFactory(c), C1) |
1116 | + False |
1117 | + >>> proxy_isinstance(ProxyFactory(c), C1) |
1118 | + True |
1119 | + |
1120 | + >>> class C2(C1): |
1121 | + ... pass |
1122 | + |
1123 | + >>> c = C2() |
1124 | + >>> proxy_isinstance(ProxyFactory(c), C1) |
1125 | + True |
1126 | + """ |
1127 | + |
1128 | + |
1129 | +def test_suite(): |
1130 | + return unittest.TestSuite(( |
1131 | + doctest.DocTestSuite())) |
1132 | \ No newline at end of file |
Initial checkin of code migrated from Launchpad.