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.
This commit is contained in:
Daniele Varrazzo 2012-09-19 16:32:57 +01:00
parent d963b478e2
commit 7386b8327c
3 changed files with 100 additions and 30 deletions

View File

@ -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

View File

@ -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.

View File

@ -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