From 5f5f56e130378d411b0796e4af66963725385d89 Mon Sep 17 00:00:00 2001 From: Eric Du Date: Sun, 24 Jul 2011 08:33:21 +0800 Subject: [PATCH] rewrite json_parser to support both naive and aware datetime, date and time types --- docs/source/install.rst | 1 + pyres/json_parser.py | 70 +++++++++++++++++++---------------------- requirements.txt | 1 + setup.py | 3 +- tests/test_json.py | 52 ++++++++++++++++++++---------- 5 files changed, 73 insertions(+), 54 deletions(-) diff --git a/docs/source/install.rst b/docs/source/install.rst index e9db68e..c3c2f62 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -12,6 +12,7 @@ Those requirements are currently: itty==0.6.4 redis==1.34.1 pystache==0.2.0 + python-dateutil==1.5 If you'd rather install from the git repository, that's easy too:: diff --git a/pyres/json_parser.py b/pyres/json_parser.py index be80fb6..f9493d3 100644 --- a/pyres/json_parser.py +++ b/pyres/json_parser.py @@ -1,4 +1,5 @@ -from datetime import datetime +from datetime import datetime, date, time +from dateutil.parser import parse try: #import simplejson as json @@ -6,45 +7,40 @@ except ImportError: import simplejson as json -DATE_FORMAT = '%Y-%m-%dT%H:%M:%S' -DATE_PREFIX = '@D:' +SUPPORTED_TYPES = {datetime, date, time} +assert len(SUPPORTED_TYPES) == len({c.__name__ for c in SUPPORTED_TYPES}) +SUPPORTED_TYPES_NAME2CLASS = {c.__name__: c for c in SUPPORTED_TYPES} class CustomJSONEncoder(json.JSONEncoder): - - def default(self, o): - if isinstance(o, datetime): - return o.strftime(DATE_PREFIX + DATE_FORMAT) - return json.JSONEncoder.default(self, o) + def default(self, obj): + type_ = type(obj) + if type_ in SUPPORTED_TYPES: + if type_ in {datetime, date, time}: + return {'__type__': type_.__name__, '__value__': obj.isoformat()} + return json.JSONEncoder.default(self, obj) class CustomJSONDecoder(json.JSONDecoder): + def __init__(self, **kw): + json.JSONDecoder.__init__(self, object_hook=self.dict_to_object, **kw) + + def dict_to_object(self, d): + type_ = SUPPORTED_TYPES_NAME2CLASS.get(d.get('__type__')) + if type_ in SUPPORTED_TYPES: + if type_ in {datetime, date, time}: + dt = parse(d.get('__value__')) + if type_ is datetime: + return dt + elif type_ is date: + return dt.date() + else: + return dt.timetz() + return d + + +def dumps(obj): + return json.dumps(obj, cls=CustomJSONEncoder) + - def decode(self, json_string): - decoded = json.loads(json_string) - return self.convert(decoded) - - def convert(self, value): - if isinstance(value, basestring) and value.startswith(DATE_PREFIX): - try: - return datetime.strptime(value[len(DATE_PREFIX):], DATE_FORMAT) - except ValueError: - return value - elif isinstance(value, dict): - for k, v in value.iteritems(): - new = self.convert(v) - if new != v: - value[k] = new - elif isinstance(value, list): - for k, v in enumerate(value): - new = self.convert(v) - if new != v: - value[k] = new - return value - - -def dumps(values): - return json.dumps(values, cls=CustomJSONEncoder) - - -def loads(string): - return json.loads(string, cls=CustomJSONDecoder) +def loads(s): + return json.loads(s, cls=CustomJSONDecoder) diff --git a/requirements.txt b/requirements.txt index 1a7b5c6..d4c538b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ itty==0.6.2 redis>=1.34.1 pystache==0.1.0 setproctitle>=1.0 +python-dateutil==1.5 (2.0 is for python 3.x) diff --git a/setup.py b/setup.py index d5276d5..f198ca6 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,8 @@ 'itty>=0.6.2', 'redis>=1.34.1', 'pystache>=0.1.0', - 'setproctitle>=1.0' + 'setproctitle>=1.0', + 'python-dateutil==1.5' ], classifiers = [ 'Development Status :: 4 - Beta', diff --git a/tests/test_json.py b/tests/test_json.py index 1a619e6..a6c925f 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -1,24 +1,44 @@ from datetime import datetime +from dateutil.tz import tzutc from tests import PyResTests import pyres.json_parser as json class JSONTests(PyResTests): - def test_encode_decode_date(self): - dt = datetime(1972, 1, 22); - encoded = json.dumps({'dt': dt}) - decoded = json.loads(encoded) - assert decoded['dt'] == dt + def test(self): + naive_now = datetime.now() + aware_now = datetime.now(tzutc()) + for now in [naive_now, aware_now]: + for obj in [now, now.date(), now.timetz(), now.time()]: + encoded = json.dumps(obj) + decoded = json.loads(encoded) + assert obj == decoded - def test_dates_in_lists(self): - dates = [datetime.now() for i in range(50)] - decoded = json.loads(json.dumps(dates)) - for value in dates: - assert isinstance(value, datetime) + def test_in_list(self): + naive_now = datetime.now() + aware_now = datetime.now(tzutc()) + for now in [naive_now, aware_now]: + for obj in [now, now.date(), now.timetz(), now.time()]: + to_encode = [1, obj] + encoded = json.dumps(to_encode) + decoded = json.loads(encoded) + assert to_encode == decoded - def test_dates_in_dict(self): - dates = dict((i, datetime.now()) for i in range(50)) - decoded = json.loads(json.dumps(dates)) - for i, value in dates.items(): - assert isinstance(i, int) - assert isinstance(value, datetime) + def test_in_dict(self): + naive_now = datetime.now() + aware_now = datetime.now(tzutc()) + for now in [naive_now, aware_now]: + for obj in [now, now.date(), now.timetz(), now.time()]: + to_encode = dict(a=1, b=obj) + encoded = json.dumps(to_encode) + decoded = json.loads(encoded) + assert to_encode == decoded + def test_complex(self): + naive_now = datetime.now() + aware_now = datetime.now(tzutc()) + for now in [naive_now, aware_now]: + for obj in [now, now.date(), now.timetz(), now.time()]: + to_encode = dict(a=1, b=obj, c=dict(c1=2, c2=obj), d=[3, obj]) + encoded = json.dumps(to_encode) + decoded = json.loads(encoded) + assert to_encode == decoded