Merge lp:~wallyworld/lazr.enum/json-serialisation-support into lp:lazr.enum

Proposed by Ian Booth
Status: Merged
Merged at revision: 37
Proposed branch: lp:~wallyworld/lazr.enum/json-serialisation-support
Merge into: lp:lazr.enum
Diff against target: 591 lines (+360/-67)
9 files modified
.bzrignore (+2/-0)
_bootstrap/bootstrap.py (+229/-46)
ez_setup.py (+1/-1)
src/lazr/enum/NEWS.txt (+7/-0)
src/lazr/enum/README.txt (+34/-0)
src/lazr/enum/__init__.py (+3/-0)
src/lazr/enum/_enum.py (+29/-19)
src/lazr/enum/_json.py (+54/-0)
src/lazr/enum/version.txt (+1/-1)
To merge this branch: bzr merge lp:~wallyworld/lazr.enum/json-serialisation-support
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+102430@code.launchpad.net

Commit message

Add json serialisation support and drive by fix a couple of minor bugs

Description of the change

== Implementation ==

This branch adds json serialisation support to enumerated types.

A enum instance is serialised as a dict containing:
- the enumerated type name as per the enumerated_type_registry
- the enum instance name

This information is sufficient to allow the dict to be deserialised back to an enum.

I had to upgrade bootstrap.py and ez_setup.py since they were way out of date.
(Ignore these in the diff since they are not ours).

I also fixed a couple of minor bugs as a drive by: bugs 524259 and 526484

== Tests ==

Add new doc tests to README.txt

== Lint ==

I didn't bother with:
  ez_setup.py
  _bootstrap/bootstrap.py
since they are not ours.

Linting changed files:
  .bzrignore
  src/lazr/enum/NEWS.txt
  src/lazr/enum/README.txt
  src/lazr/enum/__init__.py
  src/lazr/enum/_enum.py
  src/lazr/enum/_json.py
  src/lazr/enum/version.txt

./src/lazr/enum/README.txt
     302: Line exceeds 80 characters.
     419: Line exceeds 80 characters.
     420: Line exceeds 80 characters.
./src/lazr/enum/__init__.py
      25: 'from lazr.enum._enum import *' used; unable to detect undefined names
      27: 'from lazr.enum._json import *' used; unable to detect undefined names
      28: redefinition of unused '_all' from line 26
      29: 'from lazr.enum.interfaces import *' used; unable to detect undefined names
./src/lazr/enum/_enum.py
      29: redefinition of unused 'removeAllProxies' from line 27
      29: E261 at least two spaces before inline comment
      44: E261 at least two spaces before inline comment

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2009-03-24 14:12:06 +0000
3+++ .bzrignore 2012-04-18 04:43:17 +0000
4@@ -9,3 +9,5 @@
5 build
6 *.egg
7 dist
8+eggs
9+MANIFEST
10
11=== modified file '_bootstrap/bootstrap.py'
12--- _bootstrap/bootstrap.py 2009-03-24 14:12:06 +0000
13+++ _bootstrap/bootstrap.py 2012-04-18 04:43:17 +0000
14@@ -1,6 +1,6 @@
15 ##############################################################################
16 #
17-# Copyright (c) 2006 Zope Corporation and Contributors.
18+# Copyright (c) 2006 Zope Foundation and Contributors.
19 # All Rights Reserved.
20 #
21 # This software is subject to the provisions of the Zope Public License,
22@@ -16,25 +16,10 @@
23 Simply run this script in a directory containing a buildout.cfg.
24 The script accepts buildout command-line options, so you can
25 use the -c option to specify an alternate configuration file.
26-
27-$Id$
28 """
29
30-import os, shutil, sys, tempfile, urllib2
31-
32-tmpeggs = tempfile.mkdtemp()
33-
34-is_jython = sys.platform.startswith('java')
35-
36-try:
37- import pkg_resources
38-except ImportError:
39- ez = {}
40- exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
41- ).read() in ez
42- ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
43-
44- import pkg_resources
45+import os, shutil, sys, tempfile, textwrap, urllib, urllib2, subprocess
46+from optparse import OptionParser
47
48 if sys.platform == 'win32':
49 def quote(c):
50@@ -43,35 +28,233 @@
51 else:
52 return c
53 else:
54- def quote (c):
55- return c
56-
57-cmd = 'from setuptools.command.easy_install import main; main()'
58-ws = pkg_resources.working_set
59+ quote = str
60+
61+# See zc.buildout.easy_install._has_broken_dash_S for motivation and comments.
62+stdout, stderr = subprocess.Popen(
63+ [sys.executable, '-Sc',
64+ 'try:\n'
65+ ' import ConfigParser\n'
66+ 'except ImportError:\n'
67+ ' print 1\n'
68+ 'else:\n'
69+ ' print 0\n'],
70+ stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
71+has_broken_dash_S = bool(int(stdout.strip()))
72+
73+# In order to be more robust in the face of system Pythons, we want to
74+# run without site-packages loaded. This is somewhat tricky, in
75+# particular because Python 2.6's distutils imports site, so starting
76+# with the -S flag is not sufficient. However, we'll start with that:
77+if not has_broken_dash_S and 'site' in sys.modules:
78+ # We will restart with python -S.
79+ args = sys.argv[:]
80+ args[0:0] = [sys.executable, '-S']
81+ args = map(quote, args)
82+ os.execv(sys.executable, args)
83+# Now we are running with -S. We'll get the clean sys.path, import site
84+# because distutils will do it later, and then reset the path and clean
85+# out any namespace packages from site-packages that might have been
86+# loaded by .pth files.
87+clean_path = sys.path[:]
88+import site
89+sys.path[:] = clean_path
90+for k, v in sys.modules.items():
91+ if k in ('setuptools', 'pkg_resources') or (
92+ hasattr(v, '__path__') and
93+ len(v.__path__)==1 and
94+ not os.path.exists(os.path.join(v.__path__[0],'__init__.py'))):
95+ # This is a namespace package. Remove it.
96+ sys.modules.pop(k)
97+
98+is_jython = sys.platform.startswith('java')
99+
100+setuptools_source = 'http://peak.telecommunity.com/dist/ez_setup.py'
101+distribute_source = 'http://python-distribute.org/distribute_setup.py'
102+
103+# parsing arguments
104+def normalize_to_url(option, opt_str, value, parser):
105+ if value:
106+ if '://' not in value: # It doesn't smell like a URL.
107+ value = 'file://%s' % (
108+ urllib.pathname2url(
109+ os.path.abspath(os.path.expanduser(value))),)
110+ if opt_str == '--download-base' and not value.endswith('/'):
111+ # Download base needs a trailing slash to make the world happy.
112+ value += '/'
113+ else:
114+ value = None
115+ name = opt_str[2:].replace('-', '_')
116+ setattr(parser.values, name, value)
117+
118+usage = '''\
119+[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
120+
121+Bootstraps a buildout-based project.
122+
123+Simply run this script in a directory containing a buildout.cfg, using the
124+Python that you want bin/buildout to use.
125+
126+Note that by using --setup-source and --download-base to point to
127+local resources, you can keep this script from going over the network.
128+'''
129+
130+parser = OptionParser(usage=usage)
131+parser.add_option("-v", "--version", dest="version",
132+ help="use a specific zc.buildout version")
133+parser.add_option("-d", "--distribute",
134+ action="store_true", dest="use_distribute", default=False,
135+ help="Use Distribute rather than Setuptools.")
136+parser.add_option("--setup-source", action="callback", dest="setup_source",
137+ callback=normalize_to_url, nargs=1, type="string",
138+ help=("Specify a URL or file location for the setup file. "
139+ "If you use Setuptools, this will default to " +
140+ setuptools_source + "; if you use Distribute, this "
141+ "will default to " + distribute_source +"."))
142+parser.add_option("--download-base", action="callback", dest="download_base",
143+ callback=normalize_to_url, nargs=1, type="string",
144+ help=("Specify a URL or directory for downloading "
145+ "zc.buildout and either Setuptools or Distribute. "
146+ "Defaults to PyPI."))
147+parser.add_option("--eggs",
148+ help=("Specify a directory for storing eggs. Defaults to "
149+ "a temporary directory that is deleted when the "
150+ "bootstrap script completes."))
151+parser.add_option("-t", "--accept-buildout-test-releases",
152+ dest='accept_buildout_test_releases',
153+ action="store_true", default=False,
154+ help=("Normally, if you do not specify a --version, the "
155+ "bootstrap script and buildout gets the newest "
156+ "*final* versions of zc.buildout and its recipes and "
157+ "extensions for you. If you use this flag, "
158+ "bootstrap and buildout will get the newest releases "
159+ "even if they are alphas or betas."))
160+parser.add_option("-c", None, action="store", dest="config_file",
161+ help=("Specify the path to the buildout configuration "
162+ "file to be used."))
163+
164+options, args = parser.parse_args()
165+
166+# if -c was provided, we push it back into args for buildout's main function
167+if options.config_file is not None:
168+ args += ['-c', options.config_file]
169+
170+if options.eggs:
171+ eggs_dir = os.path.abspath(os.path.expanduser(options.eggs))
172+else:
173+ eggs_dir = tempfile.mkdtemp()
174+
175+if options.setup_source is None:
176+ if options.use_distribute:
177+ options.setup_source = distribute_source
178+ else:
179+ options.setup_source = setuptools_source
180+
181+if options.accept_buildout_test_releases:
182+ args.append('buildout:accept-buildout-test-releases=true')
183+args.append('bootstrap')
184+
185+try:
186+ import pkg_resources
187+ import setuptools # A flag. Sometimes pkg_resources is installed alone.
188+ if not hasattr(pkg_resources, '_distribute'):
189+ raise ImportError
190+except ImportError:
191+ ez_code = urllib2.urlopen(
192+ options.setup_source).read().replace('\r\n', '\n')
193+ ez = {}
194+ exec ez_code in ez
195+ setup_args = dict(to_dir=eggs_dir, download_delay=0)
196+ if options.download_base:
197+ setup_args['download_base'] = options.download_base
198+ if options.use_distribute:
199+ setup_args['no_fake'] = True
200+ ez['use_setuptools'](**setup_args)
201+ if 'pkg_resources' in sys.modules:
202+ reload(sys.modules['pkg_resources'])
203+ import pkg_resources
204+ # This does not (always?) update the default working set. We will
205+ # do it.
206+ for path in sys.path:
207+ if path not in pkg_resources.working_set.entries:
208+ pkg_resources.working_set.add_entry(path)
209+
210+cmd = [quote(sys.executable),
211+ '-c',
212+ quote('from setuptools.command.easy_install import main; main()'),
213+ '-mqNxd',
214+ quote(eggs_dir)]
215+
216+if not has_broken_dash_S:
217+ cmd.insert(1, '-S')
218+
219+find_links = options.download_base
220+if not find_links:
221+ find_links = os.environ.get('bootstrap-testing-find-links')
222+if find_links:
223+ cmd.extend(['-f', quote(find_links)])
224+
225+if options.use_distribute:
226+ setup_requirement = 'distribute'
227+else:
228+ setup_requirement = 'setuptools'
229+ws = pkg_resources.working_set
230+setup_requirement_path = ws.find(
231+ pkg_resources.Requirement.parse(setup_requirement)).location
232+env = dict(
233+ os.environ,
234+ PYTHONPATH=setup_requirement_path)
235+
236+requirement = 'zc.buildout'
237+version = options.version
238+if version is None and not options.accept_buildout_test_releases:
239+ # Figure out the most recent final version of zc.buildout.
240+ import setuptools.package_index
241+ _final_parts = '*final-', '*final'
242+ def _final_version(parsed_version):
243+ for part in parsed_version:
244+ if (part[:1] == '*') and (part not in _final_parts):
245+ return False
246+ return True
247+ index = setuptools.package_index.PackageIndex(
248+ search_path=[setup_requirement_path])
249+ if find_links:
250+ index.add_find_links((find_links,))
251+ req = pkg_resources.Requirement.parse(requirement)
252+ if index.obtain(req) is not None:
253+ best = []
254+ bestv = None
255+ for dist in index[req.project_name]:
256+ distv = dist.parsed_version
257+ if _final_version(distv):
258+ if bestv is None or distv > bestv:
259+ best = [dist]
260+ bestv = distv
261+ elif distv == bestv:
262+ best.append(dist)
263+ if best:
264+ best.sort()
265+ version = best[-1].version
266+if version:
267+ requirement = '=='.join((requirement, version))
268+cmd.append(requirement)
269
270 if is_jython:
271 import subprocess
272-
273- assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd',
274- quote(tmpeggs), 'zc.buildout'],
275- env=dict(os.environ,
276- PYTHONPATH=
277- ws.find(pkg_resources.Requirement.parse('setuptools')).location
278- ),
279- ).wait() == 0
280-
281-else:
282- assert os.spawnle(
283- os.P_WAIT, sys.executable, quote (sys.executable),
284- '-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout',
285- dict(os.environ,
286- PYTHONPATH=
287- ws.find(pkg_resources.Requirement.parse('setuptools')).location
288- ),
289- ) == 0
290-
291-ws.add_entry(tmpeggs)
292-ws.require('zc.buildout')
293+ exitcode = subprocess.Popen(cmd, env=env).wait()
294+else: # Windows prefers this, apparently; otherwise we would prefer subprocess
295+ exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env]))
296+if exitcode != 0:
297+ sys.stdout.flush()
298+ sys.stderr.flush()
299+ print ("An error occurred when trying to install zc.buildout. "
300+ "Look above this message for any errors that "
301+ "were output by easy_install.")
302+ sys.exit(exitcode)
303+
304+ws.add_entry(eggs_dir)
305+ws.require(requirement)
306 import zc.buildout.buildout
307-zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap'])
308-shutil.rmtree(tmpeggs)
309+zc.buildout.buildout.main(args)
310+if not options.eggs: # clean up temporary egg directory
311+ shutil.rmtree(eggs_dir)
312
313=== modified file 'ez_setup.py'
314--- ez_setup.py 2009-03-24 14:12:06 +0000
315+++ ez_setup.py 2012-04-18 04:43:17 +0000
316@@ -14,7 +14,7 @@
317 This file can also be run as a script to install or upgrade setuptools.
318 """
319 import sys
320-DEFAULT_VERSION = "0.6c8"
321+DEFAULT_VERSION = "0.6c11"
322 DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
323
324 md5_data = {
325
326=== modified file 'src/lazr/enum/NEWS.txt'
327--- src/lazr/enum/NEWS.txt 2011-04-20 02:05:19 +0000
328+++ src/lazr/enum/NEWS.txt 2012-04-18 04:43:17 +0000
329@@ -2,6 +2,13 @@
330 NEWS for lazr.enum
331 ==================
332
333+1.1.4 (2012-04-18)
334+==================
335+
336+- Support for serialising enums to/from json (lp:984549)
337+- Items which are not in an enumerator always compare as False (lp:524259)
338+- Fix the licence statement in _enum.py to be LGPLv3 not LGPLv3+ (lp:526484)
339+
340 1.1.3 (2011-04-20)
341 ==================
342
343
344=== modified file 'src/lazr/enum/README.txt'
345--- src/lazr/enum/README.txt 2011-03-01 22:28:23 +0000
346+++ src/lazr/enum/README.txt 2012-04-18 04:43:17 +0000
347@@ -169,6 +169,11 @@
348 >>> apple < orange
349 True
350
351+Items which are not in an enumerator always compare as False.
352+ >>> from lazr.enum import Item
353+ >>> Item('a') == Item('b')
354+ False
355+
356 The string representation of an Item is the title, and the representation
357 also shows the enumeration that the Item is from.
358
359@@ -407,6 +412,35 @@
360 Mirrored 1
361 Imported 2
362
363+============
364+JSON Support
365+============
366+
367+Enumerated types instances can be serialised to/from json. This library provides the
368+necessary encode and decode classes which can be used directly or as part of the
369+lazr.json package where they are registered as default handlers for lazr enums.
370+
371+A enum instance is serialised as a dict containing:
372+- the enumerated type name as per the enumerated_type_registry
373+- the enum instance name
374+
375+ >>> import json
376+ >>> from lazr.enum import EnumJSONEncoder
377+
378+ >>> json_enum = json.dumps(Fruit.APPLE, cls=EnumJSONEncoder)
379+ >>> print json_enum
380+ {"type": "Fruit", "name": "APPLE"}
381+
382+To deserialse, we can specify a json object_hook as follows.
383+This is done transparently when using the lazr.json package.
384+
385+ >>> def fruit_enum_decoder(value_dict):
386+ ... return EnumJSONDecoder.from_dict(Fruit, value_dict)
387+
388+ >>> from lazr.enum import EnumJSONDecoder
389+ >>> json.loads(json_enum, object_hook=fruit_enum_decoder)
390+ <Item Fruit.APPLE, Apple>
391+
392 .. pypi description ends here
393
394 ===============
395
396=== modified file 'src/lazr/enum/__init__.py'
397--- src/lazr/enum/__init__.py 2009-08-31 19:30:41 +0000
398+++ src/lazr/enum/__init__.py 2012-04-18 04:43:17 +0000
399@@ -24,8 +24,11 @@
400 # exported.
401 from lazr.enum._enum import *
402 from lazr.enum._enum import __all__ as _all
403+from lazr.enum._json import *
404+from lazr.enum._json import __all__ as _jall
405 from lazr.enum.interfaces import *
406 from lazr.enum.interfaces import __all__ as _iall
407 __all__ = []
408 __all__.extend(_all)
409 __all__.extend(_iall)
410+__all__.extend(_jall)
411
412=== modified file 'src/lazr/enum/_enum.py'
413--- src/lazr/enum/_enum.py 2011-03-01 22:28:23 +0000
414+++ src/lazr/enum/_enum.py 2012-04-18 04:43:17 +0000
415@@ -4,8 +4,7 @@
416 #
417 # lazr.enum is free software: you can redistribute it and/or modify it
418 # under the terms of the GNU Lesser General Public License as published by
419-# the Free Software Foundation, either version 3 of the License, or (at your
420-# option) any later version.
421+# the Free Software Foundation, version 3 of the License.
422 #
423 # lazr.enum is distributed in the hope that it will be useful, but WITHOUT
424 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
425@@ -17,20 +16,6 @@
426
427 __metaclass__ = type
428
429-import itertools
430-import operator
431-import sys
432-import warnings
433-
434-from zope.interface import implements
435-from zope.schema.interfaces import ITitledTokenizedTerm, IVocabularyTokenized
436-try:
437- from zope.proxy import removeAllProxies
438-except ImportError:
439- removeAllProxies = lambda obj: obj # no-op
440-
441-from lazr.enum.interfaces import IEnumeratedType
442-
443 __all__ = [
444 'BaseItem',
445 'DBEnumeratedType',
446@@ -46,6 +31,21 @@
447 'MetaDBEnum', # needed for configure.zcml
448 ]
449
450+import itertools
451+import operator
452+import sys
453+import warnings
454+
455+from zope.interface import implements
456+from zope.schema.interfaces import ITitledTokenizedTerm, IVocabularyTokenized
457+try:
458+ from zope.proxy import removeAllProxies
459+except ImportError:
460+ removeAllProxies = lambda obj: obj # no-op
461+
462+from lazr.enum.interfaces import IEnumeratedType
463+
464+
465 def proxy_isinstance(obj, cls):
466 """Test whether an object is an instance of a type.
467
468@@ -54,6 +54,7 @@
469 """
470 return isinstance(removeAllProxies(obj), cls)
471
472+
473 def docstring_to_title_descr(string):
474 """When given a classically formatted docstring, returns a tuple
475 (title, description).
476@@ -97,8 +98,8 @@
477 break
478 else:
479 raise ValueError
480- assert not lines[num+1].strip()
481- descrlines = lines[num+2:]
482+ assert not lines[num + 1].strip()
483+ descrlines = lines[num + 2:]
484 descr1 = descrlines[0]
485 indent = len(descr1) - len(descr1.lstrip())
486 descr = '\n'.join([line[indent:] for line in descrlines])
487@@ -163,6 +164,12 @@
488 stacklevel=stacklevel)
489 return False
490 elif proxy_isinstance(other, BaseItem):
491+ if (not getattr(self, 'enum', None)
492+ or not getattr(other, 'enum', None)):
493+ warnings.warn('comparison of Items which are not '
494+ 'enumerated types',
495+ stacklevel=stacklevel)
496+ return False
497 return (self.name == other.name and
498 self.enum == other.enum)
499 else:
500@@ -201,7 +208,7 @@
501 @staticmethod
502 def construct(other_item):
503 """Create an Item based on the other_item."""
504- item = DBItem(other_item.value, other_item.title,
505+ item = DBItem(other_item.value, other_item.title,
506 other_item.description, other_item.url)
507 item.sortkey = other_item.sortkey
508 return item
509@@ -253,13 +260,16 @@
510 def __init__(self, items, mapping):
511 self.items = items
512 self.mapping = mapping
513+
514 def __getitem__(self, key):
515 if key in self.mapping:
516 return self.mapping[key]
517 else:
518 raise KeyError(key)
519+
520 def __iter__(self):
521 return self.items.__iter__()
522+
523 def __len__(self):
524 return len(self.items)
525
526
527=== added file 'src/lazr/enum/_json.py'
528--- src/lazr/enum/_json.py 1970-01-01 00:00:00 +0000
529+++ src/lazr/enum/_json.py 2012-04-18 04:43:17 +0000
530@@ -0,0 +1,54 @@
531+# Copyright 2012 Canonical Ltd. All rights reserved.
532+#
533+# This file is part of lazr.enum
534+#
535+# lazr.enum is free software: you can redistribute it and/or modify it
536+# under the terms of the GNU Lesser General Public License as published by
537+# the Free Software Foundation, either version 3 of the License, or (at your
538+# option) any later version.
539+#
540+# lazr.enum is distributed in the hope that it will be useful, but WITHOUT
541+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
542+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
543+# License for more details.
544+#
545+# You should have received a copy of the GNU Lesser General Public License
546+# along with lazr.enum. If not, see <http://www.gnu.org/licenses/>.
547+
548+__metaclass__ = type
549+
550+__all__ = [
551+ 'EnumJSONDecoder',
552+ 'EnumJSONEncoder',
553+ ]
554+
555+import json
556+from lazr.enum import (
557+ BaseItem,
558+ enumerated_type_registry,
559+ )
560+
561+
562+class EnumJSONEncoder(json.JSONEncoder):
563+ """A JSON encoder that understands enum objects.
564+
565+ Objects are serialized using their type and enum name.
566+ """
567+ def default(self, obj):
568+ if isinstance(obj, BaseItem):
569+ return {
570+ 'type': obj.enum.name,
571+ 'name': obj.name}
572+ return json.JSONEncoder.default(self, obj)
573+
574+
575+class EnumJSONDecoder():
576+ """A decoder can reconstruct enums from a JSON dict.
577+
578+ Objects are reconstructed using their type and enum name.
579+ """
580+ @classmethod
581+ def from_dict(cls, class_, values):
582+ type_name = values['type']
583+ item_name = values['name']
584+ return enumerated_type_registry[type_name].items[item_name]
585
586=== modified file 'src/lazr/enum/version.txt'
587--- src/lazr/enum/version.txt 2011-04-20 02:05:19 +0000
588+++ src/lazr/enum/version.txt 2012-04-18 04:43:17 +0000
589@@ -1,1 +1,1 @@
590-1.1.3
591+1.1.4

Subscribers

People subscribed via source and target branches

to all changes: