Skip to content

Commit

Permalink
Add dump in addition to dumps.
Browse files Browse the repository at this point in the history
This bumps the version to 2.0.1.
  • Loading branch information
Anteru committed May 11, 2018
1 parent 2115b7a commit 6966f50
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 18 deletions.
12 changes: 11 additions & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,21 @@ As an extension, SJSON allows for raw string literals.
Usage
-----

The library provides two methods, `dumps` and `loads`. `dumps` encodes an object as SJSON, and `loads` decodes a string into a Python dictionary.
The library provides four methods, similar to the Python JSON module. These are:

* `dump`: Encode an object as SJSON and write to a stream.
* `dumps`: Encode an object as SJSON and return a string.
* `load`: Decode a SJSON encoded object from a stream.
* `loads`: Decode a SJSON encoded object from a string.

Changelog
---------

### 2.0.1

* Add `dump` in addition to `dumps` for consistency with the Python JSON module.
* Additional PEP8 conformance tweaks.

### 2.0.0

* The library is now PEP8 compliant. This should *not* affect most users of this library, the only user-visible change is that `ParseException.GetLocation` has been renamed to `get_location`. The core functions have not been renamed and are API compatible.
Expand Down
74 changes: 57 additions & 17 deletions sjson/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
"""Module to parse SJSON files."""

# coding=utf8
# @author: Matthäus G. Chajdas
# @license: 3-clause BSD

__version__ = '2.0.0'

import collections.abc
import collections
import numbers
import string
import io

__version__ = '2.0.1'


class MemoryInputStream:
"""Input stream wrapper for reading directly from memory."""
def __init__(self, s):
Expand All @@ -27,21 +27,21 @@ def read(self, count=1):
end_index = self._current_index + count
if end_index > self._length:
_raise_end_of_file_exception(self)
result = self._stream[self._current_index : end_index]
result = self._stream[self._current_index:end_index]
self._current_index = end_index
return result

def peek(self, count=1, allow_end_of_file=False):
"""peek ``count`` bytes from the stream. If ``allow_end_of_file`` is ``True``,
no error will be raised if the end of the stream is reached while trying
to peek."""
"""peek ``count`` bytes from the stream. If ``allow_end_of_file`` is
``True``, no error will be raised if the end of the stream is reached
while trying to peek."""
end_index = self._current_index + count
if end_index > self._length:
if allow_end_of_file:
return None
_raise_end_of_file_exception(self)

return self._stream[self._current_index : end_index]
return self._stream[self._current_index:end_index]

def skip(self, count=1):
"""skip ``count`` bytes."""
Expand All @@ -50,7 +50,7 @@ def skip(self, count=1):
def get_location(self):
"""Get the current location in the stream."""
loc = collections.namedtuple('Location', ['line', 'column'])
bytes_read = self._stream[0 : self._current_index]
bytes_read = self._stream[:self._current_index]
line = 1
column = 1
for byte in bytes_read:
Expand All @@ -62,6 +62,7 @@ def get_location(self):
column += 1
return loc(line, column)


class ByteBufferInputStream:
"""Input stream wrapper for reading directly from an I/O object."""
def __init__(self, stream):
Expand All @@ -86,9 +87,9 @@ def read(self, count=1):
return result

def peek(self, count=1, allow_end_of_file=False):
"""peek ``count`` bytes from the stream. If ``allow_end_of_file`` is ``True``,
no error will be raised if the end of the stream is reached while trying
to peek."""
"""peek ``count`` bytes from the stream. If ``allow_end_of_file`` is
``True``, no error will be raised if the end of the stream is reached
while trying to peek."""
result = self._stream.peek(count)
if not result and not allow_end_of_file:
_raise_end_of_file_exception(self)
Expand All @@ -106,6 +107,7 @@ def get_location(self):
loc = collections.namedtuple('Location', ['line', 'column'])
return loc(self._line, self._column)


class ParseException(RuntimeError):
"""Parse exception."""
def __init__(self, msg, location):
Expand All @@ -122,24 +124,31 @@ def __str__(self):
self._location.line,
self._location.column)


def _raise_end_of_file_exception(stream):
raise ParseException('Unexpected end-of-stream', stream.get_location())


def _consume(stream, what):
_skip_whitespace(stream)
what_len = len(what)
if stream.peek(what_len) != what:
raise ParseException("Expected to read '{}'".format(what), stream.get_location())
raise ParseException("Expected to read '{}'".format(what),
stream.get_location())
stream.skip(what_len)


def _skip_characterse_and_whitespace(stream, num_char_to_skip):
stream.skip(num_char_to_skip)
return _skip_whitespace(stream)

_WHITESPACE_SET = set({b' ', b'\t', b'\n', b'\r'})


def _is_whitespace(char):
return char in _WHITESPACE_SET


def _skip_c_style_comment(stream):
comment_start_location = stream.get_location()
# skip the comment start
Expand Down Expand Up @@ -171,9 +180,10 @@ def _skip_cpp_style_comment(stream):
break
stream.skip()


def _skip_whitespace(stream):
'''skip whitespace. Returns the next character if a new position within the stream was
found; returns None if the end of the stream was hit.'''
'''skip whitespace. Returns the next character if a new position within the
stream was found; returns None if the end of the stream was hit.'''
while True:
next_char = stream.peek(allow_end_of_file=True)
if not _is_whitespace(next_char):
Expand All @@ -192,9 +202,12 @@ def _skip_whitespace(stream):
return next_char

_IDENTIFIER_SET = set(string.ascii_letters + string.digits + '_')


def _is_identifier(obj):
return chr(obj[0]) in _IDENTIFIER_SET


def _decode_escaped_character(char):
if char == b'b':
return b'\b'
Expand All @@ -205,6 +218,7 @@ def _decode_escaped_character(char):
elif char == b'\\' or char == b'\"':
return char


def _decode_string(stream, allow_identifier=False):
_skip_whitespace(stream)

Expand Down Expand Up @@ -254,6 +268,8 @@ def _decode_string(stream, allow_identifier=False):
return str(result, encoding='utf-8')

_NUMBER_SEPARATOR_SET = _WHITESPACE_SET.union(set({b',', b']', b'}', None}))


def _decode_number(stream, next_char):
"""Parse a number.
Expand All @@ -280,6 +296,7 @@ def _decode_number(stream, next_char):
return float(value)
return int(value)


def _decode_dict(stream, delimited=False):
"""
delimited -- if ``True``, parsing will stop once the end-of-dictionary
Expand Down Expand Up @@ -315,6 +332,7 @@ def _decode_dict(stream, delimited=False):

return result


def _parse_list(stream):
result = []
# skip '['
Expand All @@ -334,6 +352,7 @@ def _parse_list(stream):

return result


def _parse(stream):
next_char = _skip_whitespace(stream)

Expand Down Expand Up @@ -364,16 +383,27 @@ def _parse(stream):
except ValueError:
raise ParseException('Invalid character', stream.get_location())


def load(stream):
"""Load a SJSON object from a stream."""
return _decode_dict(ByteBufferInputStream(io.BufferedReader(stream)))


def loads(text):
"""Load a SJSON object from a string."""
return _decode_dict(MemoryInputStream(text.encode('utf-8')))


def dumps(obj, indent=None):
"""Dump an object to a string."""
import io
stream = io.StringIO()
dump(obj, stream, indent)
return stream.getvalue()


def dump(obj, fp, indent=None):
"""Dump an object to a stream."""
if not indent:
_indent = ''
elif isinstance(indent, numbers.Number):
Expand All @@ -382,7 +412,12 @@ def dumps(obj, indent=None):
_indent = ' ' * indent
else:
_indent = indent
return ''.join(_encode(obj, indent=_indent))

for e in _encode(obj, indent=_indent):
fp.write(e)

_ESCAPE_CHARACTER_SET = {'\n': '\\n', '\b': '\\b', '\t': '\\t', '\"': '\\"'}


def _escape_string(obj, quote=True):
"""Escape a string.
Expand All @@ -397,14 +432,15 @@ def _escape_string(obj, quote=True):
if quote:
yield '"'

for key, value in {'\n':'\\n', '\b':'\\b', '\t':'\\t', '\"':'\\"'}.items():
for key, value in _ESCAPE_CHARACTER_SET.items():
obj = obj.replace(key, value)

yield obj

if quote:
yield '"'


def _encode(obj, separators=(', ', '\n', ' = '), indent=0, level=0):
if obj is None:
yield 'null'
Expand All @@ -426,12 +462,15 @@ def _encode(obj, separators=(', ', '\n', ' = '), indent=0, level=0):
else:
raise RuntimeError("Unsupported object type")


def _indent(level, indent):
return indent * level


def _encode_key(k):
yield from _escape_string(k, False)


def _encode_list(obj, separators, indent, level):
yield '['
first = True
Expand All @@ -443,6 +482,7 @@ def _encode_list(obj, separators, indent, level):
yield from _encode(element, separators, indent, level+1)
yield ']'


def _encode_dict(obj, separators, indent, level):
if level > 0:
yield '{\n'
Expand Down

0 comments on commit 6966f50

Please sign in to comment.