From 7386b8327cf4b77666a67e150ce660bf4ff489b6 Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Wed, 19 Sep 2012 16:32:57 +0100 Subject: [PATCH] Dropped keywords passthrough in Json adapter Pass a dumps function instead. Allow customizing by either arg passing or subclassing. The basic Json class now raises ImportError on getquoted() if json is not available, thus allowing using a customized Json subclass even when the json module is not available. --- doc/src/extras.rst | 1 + lib/_json.py | 78 +++++++++++++++++++++++++++----------- tests/test_types_extras.py | 51 +++++++++++++++++++++---- 3 files changed, 100 insertions(+), 30 deletions(-) diff --git a/doc/src/extras.rst b/doc/src/extras.rst index 8459b199..bfe6440c 100644 --- a/doc/src/extras.rst +++ b/doc/src/extras.rst @@ -152,6 +152,7 @@ versions the `simplejson`_ module is be used if available. Note that the last .. _simplejson: http://pypi.python.org/pypi/simplejson/ .. autoclass:: Json + :members: dumps .. autofunction:: register_json diff --git a/lib/_json.py b/lib/_json.py index 3d211482..c21d6c86 100644 --- a/lib/_json.py +++ b/lib/_json.py @@ -48,51 +48,83 @@ JSON_OID = 114 JSONARRAY_OID = 199 class Json(object): - """A wrapper to adapt a Python object to :sql:`json` data type. + """ + An `~psycopg2.extensions.ISQLQuote` wrapper to adapt a Python object to + :sql:`json` data type. `!Json` can be used to wrap any object supported by the underlying - `!json` module. Raise `!ImportError` if no module is available. + `!json` module. `~psycopg2.extensions.ISQLQuote.getquoted()` will raise + `!ImportError` if no module is available. - Any keyword argument will be passed to the underlying - :py:func:`json.dumps()` function, allowing extension and customization. :: + The basic usage is to wrap `!Json` around the object to be adapted:: curs.execute("insert into mytable (jsondata) values (%s)", - (Json({'a': 100}),)) + [Json({'a': 100})]) + + If you want to customize the adaptation from Python to PostgreSQL you can + either provide a custom *dumps* function:: + + curs.execute("insert into mytable (jsondata) values (%s)", + [Json({'a': 100}, dumps=simplejson.dumps)]) + + or you can subclass `!Json` overriding the `dumps()` method:: + + class MyJson(Json): + def dumps(self, obj): + return simplejson.dumps(obj) + + curs.execute("insert into mytable (jsondata) values (%s)", + [MyJson({'a': 100})]) .. note:: - You can use `~psycopg2.extensions.register_adapter()` to adapt Python - dictionaries to JSON:: + You can use `~psycopg2.extensions.register_adapter()` to adapt any + Python dictionary to JSON, either using `!Json` or any subclass or + factory creating a compatible adapter:: - psycopg2.extensions.register_adapter(dict, - psycopg2.extras.Json) + psycopg2.extensions.register_adapter(dict, psycopg2.extras.Json) - This setting is global though, so it is not compatible with the use of - `register_hstore()`. Any other object supported by the `!json` library - used by Psycopg can be registered the same way, but this will clobber - the default adaptation rule, so be careful to unwanted side effects. + This setting is global though, so it is not compatible with similar + adapters such as the one registered by `register_hstore()`. Any other + object supported by JSON can be registered the same way, but this will + clobber the default adaptation rule, so be careful to unwanted side + effects. """ - def __init__(self, adapted, **kwargs): + def __init__(self, adapted, dumps=None): self.adapted = adapted - self.kwargs = kwargs + + if dumps is not None: + self._dumps = dumps + elif json is not None: + self._dumps = json.dumps + else: + self._dumps = None def __conform__(self, proto): if proto is ISQLQuote: return self + def dumps(self, obj): + """Serialize *obj* in JSON format. + + The default is to call `!json.dumps()` or the *dumps* function + provided in the constructor. You can override this method to create a + customized JSON wrapper. + """ + dumps = self._dumps + if dumps is not None: + return dumps(obj) + else: + raise ImportError( + "json module not available: " + "you should provide a dumps function") + def getquoted(self): - s = json.dumps(self.adapted, **self.kwargs) + s = self.dumps(self.adapted) return QuotedString(s).getquoted() -# clobber the above class if json is not available -if json is None: - class Json(Json): - def __init__(self, adapted): - raise ImportError("no json module available") - - def register_json(conn_or_curs=None, globally=False, loads=None, oid=None, array_oid=None): """Create and register typecasters converting :sql:`json` type to Python objects. diff --git a/tests/test_types_extras.py b/tests/test_types_extras.py index 93c54d49..4588718e 100755 --- a/tests/test_types_extras.py +++ b/tests/test_types_extras.py @@ -781,6 +781,16 @@ class AdaptTypeTestCase(unittest.TestCase): return oid +def skip_if_json_module(f): + """Skip a test if no Python json module is available""" + def skip_if_json_module_(self): + if psycopg2.extras.json is not None: + return self.skipTest("json module is available") + + return f(self) + + return skip_if_json_module_ + def skip_if_no_json_module(f): """Skip a test if no Python json module is available""" def skip_if_no_json_module_(self): @@ -810,12 +820,20 @@ class JsonTestCase(unittest.TestCase): def tearDown(self): self.conn.close() + @skip_if_json_module def test_module_not_available(self): - from psycopg2.extras import json, Json - if json is not None: - return self.skipTest("json module is available") + from psycopg2.extras import Json + self.assertRaises(ImportError, Json(None).getquoted) - self.assertRaises(ImportError, Json, None) + @skip_if_json_module + def test_customizable_with_module_not_available(self): + from psycopg2.extras import Json + class MyJson(Json): + def dumps(self, obj): + assert obj is None + return "hi" + + self.assertEqual(MyJson(None).getquoted(), "'hi'") @skip_if_no_json_module def test_adapt(self): @@ -830,8 +848,7 @@ class JsonTestCase(unittest.TestCase): psycopg2.extensions.QuotedString(json.dumps(obj)).getquoted()) @skip_if_no_json_module - def test_adapt_extended(self): - """Json passes through kw arguments to dumps""" + def test_adapt_dumps(self): from psycopg2.extras import json, Json class DecimalEncoder(json.JSONEncoder): @@ -842,7 +859,27 @@ class JsonTestCase(unittest.TestCase): curs = self.conn.cursor() obj = Decimal('123.45') - self.assertEqual(curs.mogrify("%s", (Json(obj, cls=DecimalEncoder),)), + dumps = lambda obj: json.dumps(obj, cls=DecimalEncoder) + self.assertEqual(curs.mogrify("%s", (Json(obj, dumps=dumps),)), + b("'123.45'")) + + @skip_if_no_json_module + def test_adapt_subclass(self): + from psycopg2.extras import json, Json + + class DecimalEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, Decimal): + return float(obj) + return json.JSONEncoder.default(self, obj) + + class MyJson(Json): + def dumps(self, obj): + return json.dumps(obj, cls=DecimalEncoder) + + curs = self.conn.cursor() + obj = Decimal('123.45') + self.assertEqual(curs.mogrify("%s", (MyJson(obj),)), b("'123.45'")) @skip_if_no_json_module