Added Json adapter

This commit is contained in:
Daniele Varrazzo 2012-09-19 04:12:20 +01:00
parent 64e3e1199e
commit b8e7f02256
3 changed files with 161 additions and 4 deletions

View File

@ -128,6 +128,33 @@ Additional data types
---------------------
.. _adapt-json:
.. index::
pair: JSON; Data types
pair: JSON; Adaptation
JSON adaptation
^^^^^^^^^^^^^^^
.. versionadded:: 2.4.6
Psycopg can use an underlying JSON_ module implementation to adapt Python
objects to and from the PostgreSQL |pgjson|_ data type. The library used
depends on the Python version: with Python 2.6 and following the
:py:mod:`json` module from the standard library is used; with previous
versions the `simplejson`_ module is be used if available. Note that the last
`!simplejson` version supporting Python 2.4 is the 2.0.9.
.. _JSON: http://www.json.org/
.. |pgjson| replace:: :sql:`json`
.. _pgjson: http://www.postgresql.org/docs/current/static/datatype-json.html
.. _simplejson: http://pypi.python.org/pypi/simplejson/
.. autoclass:: Json
.. _adapt-hstore:
.. index::

View File

@ -967,4 +967,59 @@ def register_composite(name, conn_or_curs, globally=False):
return caster
# import the best json implementation available
if sys.version_info[:2] >= (2,6):
import json
else:
try:
import simplejson as json
except ImportError:
json = None
class Json(object):
"""A 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. Any keyword argument will be passed to the
underlying :py:func:`json.dumps()` function, allowing extension and
customization. ::
curs.execute("insert into mytable (jsondata) values (%s)",
(Json({'a': 100}),))
.. note::
You can use `~psycopg2.extensions.register_adapter()` to adapt Python
dictionaries to 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.
"""
def __init__(self, adapted, **kwargs):
self.adapted = adapted
self.kwargs = kwargs
def __conform__(self, proto):
if proto is _ext.ISQLQuote:
return self
def getquoted(self):
s = json.dumps(self.adapted, **self.kwargs)
return _ext.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")
__all__ = filter(lambda k: not k.startswith('_'), locals().keys())

View File

@ -14,12 +14,9 @@
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
# License for more details.
try:
import decimal
except:
pass
import re
import sys
from decimal import Decimal
from datetime import date
from testutils import unittest, skip_if_no_uuid, skip_before_postgres
@ -784,6 +781,84 @@ class AdaptTypeTestCase(unittest.TestCase):
return oid
def skip_if_no_json_module(f):
"""Skip a test if no Python json module is available"""
def skip_if_no_json_module_(self):
if psycopg2.extras.json is None:
return self.skipTest("json module not available")
return f(self)
return skip_if_no_json_module_
def skip_if_no_json_type(f):
"""Skip a test if PostgreSQL json type is not available"""
def skip_if_no_json_type_(self):
curs = self.conn.cursor()
curs.execute("select oid from pg_type where typname = 'json'")
if not curs.fetchone():
return self.skipTest("json not available in test database")
return f(self)
return skip_if_no_json_type_
class JsonTestCase(unittest.TestCase):
def setUp(self):
self.conn = psycopg2.connect(dsn)
def tearDown(self):
self.conn.close()
def test_module_not_available(self):
from psycopg2.extras import json, Json
if json is not None:
return self.skipTest("json module is available")
self.assertRaises(ImportError, Json, None)
@skip_if_no_json_module
def test_adapt(self):
from psycopg2.extras import json, Json
objs = [None, "te'xt", 123, 123.45,
u'\xe0\u20ac', ['a', 100], {'a': 100} ]
curs = self.conn.cursor()
for obj in enumerate(objs):
self.assertEqual(curs.mogrify("%s", (Json(obj),)),
psycopg2.extensions.QuotedString(json.dumps(obj)).getquoted())
@skip_if_no_json_module
def test_adapt_extended(self):
"""Json passes through kw arguments to dumps"""
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)
curs = self.conn.cursor()
obj = Decimal('123.45')
self.assertEqual(curs.mogrify("%s", (Json(obj, cls=DecimalEncoder),)),
b("'123.45'"))
@skip_if_no_json_module
def test_register_on_dict(self):
from psycopg2.extras import Json
psycopg2.extensions.register_adapter(dict, Json)
try:
curs = self.conn.cursor()
obj = {'a': 123}
self.assertEqual(curs.mogrify("%s", (obj,)),
b("""'{"a": 123}'"""))
finally:
del psycopg2.extensions.adapters[dict, psycopg2.extensions.ISQLQuote]
def test_suite():
return unittest.TestLoader().loadTestsFromName(__name__)