Merge branch 'composite-custom' into devel

This commit is contained in:
Daniele Varrazzo 2012-09-27 00:38:00 +01:00
commit 33043cd038
4 changed files with 127 additions and 42 deletions

3
NEWS
View File

@ -3,6 +3,9 @@ What's new in psycopg 2.4.6
- Added support for backward scrollable cursors. Thanks to Jon Nelson - Added support for backward scrollable cursors. Thanks to Jon Nelson
for the initial patch (ticket #108). for the initial patch (ticket #108).
- Added a simple way to customize casting of composite types into Python
objects other than namedtuples. Many thanks to Ronan Dunklau and
Tobias Oberstein for the feature development.
- connection.reset() implemented using DISCARD ALL on server versions - connection.reset() implemented using DISCARD ALL on server versions
supporting it. supporting it.
- Fixed 'cursor()' arguments propagation in connection subclasses - Fixed 'cursor()' arguments propagation in connection subclasses

View File

@ -198,8 +198,8 @@ after a table row type) into a Python named tuple, or into a regular tuple if
>>> cur.fetchone()[0] >>> cur.fetchone()[0]
card(value=8, suit='hearts') card(value=8, suit='hearts')
Nested composite types are handled as expected, but the type of the composite Nested composite types are handled as expected, provided that the type of the
components must be registered as well. composite components are registered as well.
.. doctest:: .. doctest::
@ -214,10 +214,75 @@ components must be registered as well.
Adaptation from Python tuples to composite types is automatic instead and Adaptation from Python tuples to composite types is automatic instead and
requires no adapter registration. requires no adapter registration.
.. _custom-composite:
.. Note::
If you want to convert PostgreSQL composite types into something different
than a `!namedtuple` you can subclass the `CompositeCaster` overriding
`~CompositeCaster.make()`. For example, if you want to convert your type
into a Python dictionary you can use::
>>> class DictComposite(psycopg2.extras.CompositeCaster):
... def make(self, values):
... return dict(zip(self.attnames, values))
>>> psycopg2.extras.register_composite('card', cur,
... factory=DictComposite)
>>> cur.execute("select (8, 'hearts')::card")
>>> cur.fetchone()[0]
{'suit': 'hearts', 'value': 8}
.. autofunction:: register_composite .. autofunction:: register_composite
.. versionchanged:: 2.4.3
added support for array of composite types
.. versionchanged:: 2.4.6
added the *factory* parameter
.. autoclass:: CompositeCaster .. autoclass:: CompositeCaster
.. automethod:: make
.. versionadded:: 2.4.6
Object attributes:
.. attribute:: name
The name of the PostgreSQL type.
.. attribute:: schema
The schema where the type is defined.
.. versionadded:: 2.4.6
.. attribute:: oid
The oid of the PostgreSQL type.
.. attribute:: array_oid
The oid of the PostgreSQL array type, if available.
.. attribute:: type
The type of the Python objects returned. If :py:func:`collections.namedtuple()`
is available, it is a named tuple with attributes equal to the type
components. Otherwise it is just the `!tuple` object.
.. attribute:: attnames
List of component names of the type to be casted.
.. attribute:: atttypes
List of component type oids of the type to be casted.
.. index:: .. index::

View File

@ -792,35 +792,10 @@ class CompositeCaster(object):
querying the database at registration time is not desirable (such as when querying the database at registration time is not desirable (such as when
using an :ref:`asynchronous connections <async-support>`). using an :ref:`asynchronous connections <async-support>`).
.. attribute:: name
The name of the PostgreSQL type.
.. attribute:: oid
The oid of the PostgreSQL type.
.. attribute:: array_oid
The oid of the PostgreSQL array type, if available.
.. attribute:: type
The type of the Python objects returned. If :py:func:`collections.namedtuple()`
is available, it is a named tuple with attributes equal to the type
components. Otherwise it is just the `!tuple` object.
.. attribute:: attnames
List of component names of the type to be casted.
.. attribute:: atttypes
List of component type oids of the type to be casted.
""" """
def __init__(self, name, oid, attrs, array_oid=None): def __init__(self, name, oid, attrs, array_oid=None, schema=None):
self.name = name self.name = name
self.schema = schema
self.oid = oid self.oid = oid
self.array_oid = array_oid self.array_oid = array_oid
@ -844,8 +819,22 @@ class CompositeCaster(object):
"expecting %d components for the type %s, %d found instead" % "expecting %d components for the type %s, %d found instead" %
(len(self.atttypes), self.name, len(tokens))) (len(self.atttypes), self.name, len(tokens)))
return self._ctor(curs.cast(oid, token) values = [ curs.cast(oid, token)
for oid, token in zip(self.atttypes, tokens)) for oid, token in zip(self.atttypes, tokens) ]
return self.make(values)
def make(self, values):
"""Return a new Python object representing the data being casted.
*values* is the list of attributes, already casted into their Python
representation.
You can subclass this method to :ref:`customize the composite cast
<custom-composite>`.
"""
return self._ctor(values)
_re_tokenize = regex.compile(r""" _re_tokenize = regex.compile(r"""
\(? ([,)]) # an empty token, representing NULL \(? ([,)]) # an empty token, representing NULL
@ -927,10 +916,10 @@ ORDER BY attnum;
array_oid = recs[0][1] array_oid = recs[0][1]
type_attrs = [ (r[2], r[3]) for r in recs ] type_attrs = [ (r[2], r[3]) for r in recs ]
return CompositeCaster(tname, type_oid, type_attrs, return self(tname, type_oid, type_attrs,
array_oid=array_oid) array_oid=array_oid, schema=schema)
def register_composite(name, conn_or_curs, globally=False): def register_composite(name, conn_or_curs, globally=False, factory=None):
"""Register a typecaster to convert a composite type into a tuple. """Register a typecaster to convert a composite type into a tuple.
:param name: the name of a PostgreSQL composite type, e.g. created using :param name: the name of a PostgreSQL composite type, e.g. created using
@ -940,14 +929,15 @@ def register_composite(name, conn_or_curs, globally=False):
object, unless *globally* is set to `!True` object, unless *globally* is set to `!True`
:param globally: if `!False` (default) register the typecaster only on :param globally: if `!False` (default) register the typecaster only on
*conn_or_curs*, otherwise register it globally *conn_or_curs*, otherwise register it globally
:return: the registered `CompositeCaster` instance responsible for the :param factory: if specified it should be a `CompositeCaster` subclass: use
conversion it to :ref:`customize how to cast composite types <custom-composite>`
:return: the registered `CompositeCaster` or *factory* instance
.. versionchanged:: 2.4.3 responsible for the conversion
added support for array of composite types
""" """
caster = CompositeCaster._from_db(name, conn_or_curs) if factory is None:
factory = CompositeCaster
caster = factory._from_db(name, conn_or_curs)
_ext.register_type(caster.typecaster, not globally and conn_or_curs or None) _ext.register_type(caster.typecaster, not globally and conn_or_curs or None)
if caster.array_typecaster is not None: if caster.array_typecaster is not None:

View File

@ -532,6 +532,7 @@ class AdaptTypeTestCase(unittest.TestCase):
t = psycopg2.extras.register_composite("type_isd", self.conn) t = psycopg2.extras.register_composite("type_isd", self.conn)
self.assertEqual(t.name, 'type_isd') self.assertEqual(t.name, 'type_isd')
self.assertEqual(t.schema, 'public')
self.assertEqual(t.oid, oid) self.assertEqual(t.oid, oid)
self.assert_(issubclass(t.type, tuple)) self.assert_(issubclass(t.type, tuple))
self.assertEqual(t.attnames, ['anint', 'astring', 'adate']) self.assertEqual(t.attnames, ['anint', 'astring', 'adate'])
@ -655,6 +656,7 @@ class AdaptTypeTestCase(unittest.TestCase):
[("a", "integer"), ("b", "integer")]) [("a", "integer"), ("b", "integer")])
t = psycopg2.extras.register_composite( t = psycopg2.extras.register_composite(
"typens.typens_ii", self.conn) "typens.typens_ii", self.conn)
self.assertEqual(t.schema, 'typens')
curs.execute("select (4,8)::typens.typens_ii") curs.execute("select (4,8)::typens.typens_ii")
self.assertEqual(curs.fetchone()[0], (4,8)) self.assertEqual(curs.fetchone()[0], (4,8))
@ -736,7 +738,7 @@ class AdaptTypeTestCase(unittest.TestCase):
self.assertEqual(r[0], (2, 'test2')) self.assertEqual(r[0], (2, 'test2'))
self.assertEqual(r[1], [(3, 'testc', 2), (4, 'testd', 2)]) self.assertEqual(r[1], [(3, 'testc', 2), (4, 'testd', 2)])
@skip_if_no_hstore @skip_if_no_composite
def test_non_dbapi_connection(self): def test_non_dbapi_connection(self):
from psycopg2.extras import RealDictConnection from psycopg2.extras import RealDictConnection
from psycopg2.extras import register_composite from psycopg2.extras import register_composite
@ -760,6 +762,31 @@ class AdaptTypeTestCase(unittest.TestCase):
finally: finally:
conn.close() conn.close()
@skip_if_no_composite
def test_subclass(self):
oid = self._create_type("type_isd",
[('anint', 'integer'), ('astring', 'text'), ('adate', 'date')])
from psycopg2.extras import register_composite, CompositeCaster
class DictComposite(CompositeCaster):
def make(self, values):
return dict(zip(self.attnames, values))
t = register_composite('type_isd', self.conn, factory=DictComposite)
self.assertEqual(t.name, 'type_isd')
self.assertEqual(t.oid, oid)
curs = self.conn.cursor()
r = (10, 'hello', date(2011,1,2))
curs.execute("select %s::type_isd;", (r,))
v = curs.fetchone()[0]
self.assert_(isinstance(v, dict))
self.assertEqual(v['anint'], 10)
self.assertEqual(v['astring'], "hello")
self.assertEqual(v['adate'], date(2011,1,2))
def _create_type(self, name, fields): def _create_type(self, name, fields):
curs = self.conn.cursor() curs = self.conn.cursor()
try: try: