Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Re-did things the right way. No streams, no classes, just a generator…

… function.
  • Loading branch information...
commit 05b24ab5128b4576b6e26500cba4f81ce68ebba0 1 parent fe34d10
@zacharyvoase zacharyvoase authored
Showing with 105 additions and 140 deletions.
  1. +38 −2 README.rst
  2. +67 −138 jsonpipe.py
View
40 README.rst
@@ -90,6 +90,43 @@ the key in most JSON objects, but any character or string (e.g. ``:``, ``::``,
``~``) will do.
+Python API
+==========
+
+Since jsonpipe is written in Python, you can import it and use it without
+having to spawn another process::
+
+ >>> from jsonpipe import jsonpipe
+ >>> for line in jsonpipe({"a": 1, "b": 2}):
+ ... print line
+ / {}
+ /a 1
+ /b 2
+
+Note that the ``jsonpipe()`` generator function takes a Python object, not a
+JSON string, so the order of dictionary keys may be slightly unpredictable in
+the output. You can use ``simplejson.OrderedDict`` to get a fixed ordering::
+
+ >>> from simplejson import OrderedDict
+ >>> obj = OrderedDict([('a', 1), ('b', 2), ('c', 3)])
+ >>> obj
+ OrderedDict([('a', 1), ('b', 2), ('c', 3)])
+ >>> for line in jsonpipe(obj):
+ ... print line
+ / {}
+ /a 1
+ /b 2
+ /c 3
+
+A more general hint: if you need to parse JSON but maintain ordering for object
+keys, use the ``object_pairs_hook`` option on ``simplejson.load(s)``::
+
+ >>> import simplejson
+ >>> simplejson.loads('{"a": 1, "b": 2, "c": 3}',
+ ... object_pairs_hook=simplejson.OrderedDict)
+ OrderedDict([('a', 1), ('b', 2), ('c', 3)])
+
+
Installation
============
@@ -97,8 +134,7 @@ Installation
pip install jsonpipe
-Note that it probably requires Python v2.5+ for now, although work on
-compatibility with previous versions of Python is in progress.
+Note that it requires Python v2.5 or later (simplejson only supports 2.5+).
(Un)license
View
205 jsonpipe.py
@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
-from __future__ import with_statement
-import contextlib
import os.path as p
import sys
@@ -9,20 +7,27 @@
import simplejson
-__all__ = ['JSONPiper', 'jsonpipe']
-__version__ = '0.0.3'
+__all__ = ['jsonpipe']
+__version__ = '0.0.4'
-class JSONPiper(object):
+def jsonpipe(obj, pathsep='/', path=()):
- u"""
- Class to convert a (parsed) JSON object into a UNIX-friendly text stream.
+ r"""
+ Generate a jsonpipe stream for the provided (parsed) JSON object.
- You need to initialize your piper with a stream; for practical purposes
- I'll be using `sys.stdout`:
+ This generator will yield output as UTF-8-encoded bytestrings line-by-line.
+ These lines will *not* be terminated with line ending characters.
- >>> import sys
- >>> p = JSONPiper(sys.stdout)
+ The provided object can be as complex as you like, but it must consist only
+ of:
+
+ * Dictionaries (or subclasses of `dict`)
+ * Lists or tuples (or subclasses of the built-in types)
+ * Unicode Strings (`unicode`, utf-8 encoded `str`)
+ * Numbers (`int`, `long`, `float`)
+ * Booleans (`True`, `False`)
+ * `None`
Please note that, where applicable, *all* input must use either native
Unicode strings or UTF-8-encoded bytestrings, and all output will be UTF-8
@@ -31,23 +36,25 @@ class JSONPiper(object):
The simplest case is outputting JSON values (strings, numbers, booleans and
nulls):
- >>> p.write(u"Hello, World!")
+ >>> def pipe(obj): # Shim for easier demonstration.
+ ... print '\n'.join(jsonpipe(obj))
+ >>> pipe(u"Hello, World!")
/ "Hello, World!"
- >>> p.write(123)
+ >>> pipe(123)
/ 123
- >>> p.write(0.25)
+ >>> pipe(0.25)
/ 0.25
- >>> p.write(None)
+ >>> pipe(None)
/ null
- >>> p.write(True)
+ >>> pipe(True)
/ true
- >>> p.write(False)
+ >>> pipe(False)
/ false
jsonpipe always uses '/' to represent the top-level object. Dictionaries
are displayed as ``{}``, with each key shown as a sub-path:
- >>> p.write({"a": 1, "b": 2})
+ >>> pipe({"a": 1, "b": 2})
/ {}
/a 1
/b 2
@@ -55,7 +62,7 @@ class JSONPiper(object):
Lists are treated in much the same way, only the integer indices are used
as the keys, and the top-level list object is shown as ``[]``:
- >>> p.write([1, "foo", 2, "bar"])
+ >>> pipe([1, "foo", 2, "bar"])
/ []
/0 1
/1 "foo"
@@ -65,7 +72,7 @@ class JSONPiper(object):
Finally, the practical benefit of using hierarchical paths is that the
syntax supports nesting of arbitrarily complex constructs:
- >>> p.write([{"a": [{"b": {"c": ["foo"]}}]}])
+ >>> pipe([{"a": [{"b": {"c": ["foo"]}}]}])
/ []
/0 {}
/0/a []
@@ -77,9 +84,9 @@ class JSONPiper(object):
Because the sole separator of path components is a ``/`` character by
default, keys containing this character would result in ambiguous output.
Therefore, if you try to write a dictionary with a key containing the path
- separator, `JSONPiper` will raise a :exc:`ValueError`:
+ separator, :func:`jsonpipe` will raise a :exc:`ValueError`:
- >>> p.write({"a/b": 1})
+ >>> pipe({"a/b": 1})
Traceback (most recent call last):
...
ValueError: Path separator '/' present in key 'a/b'
@@ -88,104 +95,43 @@ class JSONPiper(object):
is raised. To mitigate this problem, you can provide a custom path
separator:
- >>> colon_p = JSONPiper(sys.stdout, pathsep=':')
- >>> colon_p.write({"a/b": 1})
+ >>> print '\n'.join(jsonpipe({"a/b": 1}, pathsep=':'))
: {}
:a/b 1
- The path separator should be a bytestring, and it is of course advisable
- that you use something you are almost certain will not be present in your
- dictionary keys.
+ The path separator should be a bytestring, and you are advised to use
+ something you are almost certain will not be present in your dictionary
+ keys.
"""
- def __init__(self, stream, pathsep='/'):
- self.stream = stream
- self.stack = []
- self.pathsep = pathsep
-
- def write(self, obj):
-
- """
- Output the provided (parsed) JSON object to this piper's stream.
-
- The object can be as complex as you like, but it must consist only of:
-
- * Dictionaries (or subclasses of `dict`)
- * Lists or tuples (or subclasses of the built-in types)
- * Unicode Strings (`unicode`, utf-8 encoded `str`)
- * Numbers (`int`, `long`, `float`)
- * Booleans (`True`, `False`)
- * `None`
-
- Consult the :class:`JSONPiper` documentation for a full run-down of the
- output format.
- """
-
- if is_value(obj):
- self._write(simplejson.dumps(obj))
- return
-
- if isinstance(obj, dict):
- self._write('{}')
- iterator = obj.iteritems()
- elif isinstance(obj, (list, tuple)):
- self._write('[]')
- iterator = enumerate(obj)
- else:
- raise TypeError("Unsupported type for jsonpipe output: %r" % type(obj))
-
- for key, value in iterator:
- with self.push(key) as sub_writer:
- sub_writer.write(value)
-
- def _write(self, string):
- """Write a simple string to the output stream at the current path."""
-
- self.stream.write("%s\t%s\n" % (self.path, string))
-
- @contextlib.contextmanager
- def push(self, key):
-
- """
- Context manager to push a key onto the stack and pop it afterwards.
-
- For now, this will modify the piper inside the `with` block and reverse
- the modification afterwards, but in future it may return a new piper
- object, so you should use the bound variable in the body of your `with`
- block for forwards-compatibility.
-
- Example:
-
- >>> import sys
- >>> p = JSONPiper(sys.stdout)
- >>> print p.path
- /
- >>> with p.push('example') as p2:
- ... print p2.path
- /example
- >>> print p.path
- /
- """
-
+ def output(string):
+ return pathsep + pathsep.join(path) + "\t" + string
+
+ if is_value(obj):
+ yield output(simplejson.dumps(obj))
+ raise StopIteration # Stop the generator immediately.
+ elif isinstance(obj, dict):
+ yield output('{}')
+ iterator = obj.iteritems()
+ elif isinstance(obj, (list, tuple)):
+ yield output('[]')
+ iterator = enumerate(obj)
+ else:
+ raise TypeError("Unsupported type for jsonpipe output: %r" %
+ type(obj))
+
+ for key, value in iterator:
+ # Check the key for sanity.
key = to_str(key)
- if self.pathsep in key:
- # In almost any case this is not what the user wants; because
- # joining the strings with a human-readable character is
- # potentially a lossy process we should fail if
+ if pathsep in key:
+ # In almost any case this is not what the user wants; having
+ # the path separator in the key would create ambiguous output
+ # so we should fail loudly and as quickly as possible.
raise ValueError("Path separator %r present in key %r" %
- (self.pathsep, key))
-
- self.stack.append(key)
- try:
- yield self
- finally:
- self.stack.pop()
-
- @property
- def path(self):
- """The current path in the object tree, as a bytestring."""
+ (pathsep, key))
- return self.pathsep + self.pathsep.join(self.stack)
+ for line in jsonpipe(value, pathsep=pathsep, path=path + (key,)):
+ yield line
def to_str(obj):
@@ -222,41 +168,24 @@ def is_value(obj):
return isinstance(obj, (str, unicode, int, long, float, bool, type(None)))
-def jsonpipe(json_obj, stream=None, writer=None):
-
- """
- Pipe the provided JSON object to the output stream (defaulting to stdout).
-
- This is just a shortcut for constructing a :class:`JSONPiper`:
-
- >>> jsonpipe({"a": 1, "b": 2})
- / {}
- /a 1
- /b 2
- """
-
- if stream is None:
- stream = sys.stdout
- if writer is None:
- writer = JSONPiper(stream)
- writer.write(json_obj)
-
-
def _get_tests():
import doctest
- return doctest.DocTestSuite(
- optionflags=(doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE))
+ return doctest.DocTestSuite(optionflags=(doctest.ELLIPSIS |
+ doctest.NORMALIZE_WHITESPACE))
PARSER = argparse.ArgumentParser()
PARSER.add_argument('-s', '--separator', metavar='SEP', default='/',
- help="Set a custom path component separator (default: /)")
+ help="Set a custom path component separator (default: /)")
PARSER.add_argument('-v', '--version', action='version',
- version='jsonpipe v%s' % (__version__,))
+ version='jsonpipe v%s' % (__version__,))
def main():
args = PARSER.parse_args()
+
+ # Load JSON from stdin, preserving the order of object keys.
json_obj = simplejson.load(sys.stdin,
- object_pairs_hook=simplejson.OrderedDict)
- JSONPiper(sys.stdout, pathsep=args.separator).write(json_obj)
+ object_pairs_hook=simplejson.OrderedDict)
+ for line in jsonpipe(json_obj, pathsep=args.separator):
+ print line

0 comments on commit 05b24ab

Please sign in to comment.
Something went wrong with that request. Please try again.