]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
Improvements to the adaptation documentation
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>
Mon, 12 Jul 2021 15:36:19 +0000 (17:36 +0200)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Mon, 12 Jul 2021 15:58:33 +0000 (17:58 +0200)
17 files changed:
docs/advanced/adapt.rst
docs/api/adapt.rst [new file with mode: 0644]
docs/api/errors.rst
docs/api/index.rst
docs/api/proto.rst [new file with mode: 0644]
docs/api/types.rst
docs/basic/adapt.rst
docs/pictures/adapt.drawio [new file with mode: 0644]
docs/pictures/adapt.svg [new file with mode: 0644]
psycopg/psycopg/_adapters_map.py
psycopg/psycopg/_transform.py
psycopg/psycopg/_typeinfo.py
psycopg/psycopg/adapt.py
psycopg/psycopg/connection.py
psycopg/psycopg/proto.py
psycopg/psycopg/types/__init__.py
psycopg_c/psycopg_c/_psycopg/transform.pyx

index 96555fbf86cf24dab16ff6ecc1e74f00107c373b..3e5bc468bb81cf517a8f5da672b52f4953b152e1 100644 (file)
@@ -16,50 +16,45 @@ returned.
     described in this page is useful if you intend to *customise* the
     adaptation rules.
 
-- The `~psycopg.types.TypeInfo` object allows to query type information from
-  a database, which can be used by the adapters: for instance to make them
-  able to decode arrays of base types or composite types.
-
-- The `Dumper` is the base object to perform conversion from a Python object
-  to a `!bytes` string understood by PostgreSQL. The string returned
-  *shouldn't be quoted*: the value will be passed to the database using
-  functions such as :pq:`PQexecParams()` so quoting and quotes escaping is not
-  necessary.
-
-- The `Loader` is the base object to perform the opposite operation: to read a
-  `!bytes` string from PostgreSQL and create a Python object.
-
-`!Dumper` and `!Loader` are abstract classes: concrete classes must implement
-the `~Dumper.dump()` and `~Loader.load()` methods. Psycopg provides
-implementation for several builtin Python and PostgreSQL types.
-
-Psycopg provides adapters for several builtin types, which can be used as the
-base to build more complex ones: they all live in the `psycopg.types`
-package.
-
-
-Dumpers and loaders configuration
----------------------------------
-
-Dumpers and loaders can be registered on different scopes: globally, per
-`~psycopg.Connection`, per `~psycopg.Cursor`, so that adaptation rules can
-be customised for specific needs within the same application: in order to do
-so you can use the *context* parameter of `Dumper.register()` and
-`Loader.register()`.
-
-When a `!Connection` is created, it inherits the global adapters
-configuration; when a `!Cursor` is created it inherits its `!Connection`
-configuration.
-
-.. note::
-
-    `!register()` is a class method on the base class, so if you
-    subclass `!Dumper` or `!Loader` you should call the ``.register()`` on the
-    class you created.
+- Adaptation configuration is performed by changing the
+  `~psycopg.proto.AdaptContext.adapters` object of objects implementing the
+  `~psycopg.proto.AdaptContext` protocols, for instance `~psycopg.Connection`
+  or `~psycopg.Cursor`.
+
+- Every context object derived from another context inherits its adapters
+  mapping: cursors created from a connection inherit the connection's
+  configuration. Connections obtain an adapters map from the global map
+  exposed as `psycopg.adapters`: changing the content of this object will
+  affect every connection created afterwards.
+
+  .. image:: ../pictures/adapt.svg
+     :align: center
+
+- The `!adapters` attribute are `AdaptersMap` instances, and contain the
+  mapping from Python types and `~psycopg.proto.Dumper` classes, and from
+  PostgreSQL oids to `~psycopg.proto.Loader` classes. Changing this mapping
+  (e.g. writing and registering your own adapters, or using a different
+  configuration of builtin adapters) affects how types are converted between
+  Python and PostgreSQL.
+
+  - Dumpers (objects implementing the `~psycopg.proto.Dumper` protocol) are
+    the objects used to perform the conversion from a Python object to a bytes
+    sequence in a format understood by PostgreSQL. The string returned
+    *shouldn't be quoted*: the value will be passed to the database using
+    functions such as :pq:`PQexecParams()` so quoting and quotes escaping is
+    not necessary. The dumper usually also suggests the server what type to
+    use, via its `~psycopg.proto.Dumper.oid` attribute.
+
+  - Loaders (objects implementing the `~psycopg.proto.Loader` protocol) are
+    the objects used to perform the opposite operation: reading a bytes
+    sequence from PostgreSQL and create a Python object out of it.
+
+  - Dumpers and loaders are instantiated on demand by a `~Transformer` object
+    when a query is executed.
 
 
 Example: handling infinity date
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+-------------------------------
 
 Suppose you want to work with the "infinity" date which is available in
 PostgreSQL but not handled by Python:
@@ -79,6 +74,8 @@ cursor):
 .. code:: python
 
     from datetime import date
+
+    # Subclass existing adapters so that the base case is handled normally.
     from psycopg.types.datetime import DateLoader, DateDumper
 
     class InfDateDumper(DateDumper):
@@ -95,6 +92,7 @@ cursor):
             else:
                 return super().load(data)
 
+    # The new classes can be registered globally, on a connection, on a cursor
     cur.adapters.register_dumper(date, InfDateDumper)
     cur.adapters.register_loader("date", InfDateLoader)
 
@@ -103,25 +101,29 @@ cursor):
     cur.execute("select '2020-12-31'::date, 'infinity'::date").fetchone()
     # (datetime.date(2020, 12, 31), datetime.date(9999, 12, 31))
 
+
+Example: PostgreSQL numeric to Python float
+-------------------------------------------
+
 .. admonition:: TODO
 
-    - Example: numeric to float
+    Write it
 
 
 Dumpers and loaders life cycle
 ------------------------------
 
-Registering dumpers and loaders will instruct `!psycopg` to use them
+Registering dumpers and loaders will instruct Psycopg to use them
 in the queries to follow, in the context where they have been registered.
 
-When a query is performed on a `!Cursor`, a `Transformer` object is created
-as a local context to manage conversions during the query, instantiating the
-required dumpers and loaders and dispatching the values to convert to the
-right instance.
+When a query is performed on a `~psycopg.Cursor`, a
+`~psycopg.adapt.Transformer` object is created as a local context to manage
+conversions during the query, instantiating the required dumpers and loaders
+and dispatching the values to convert to the right instance.
 
-- The `!Transformer` copies the adapters configuration from the `!Cursor`, thus
-  inheriting all the changes made to the global configuration, the current
-  `!Connection`, the `!Cursor`.
+- The `!Transformer` copies the adapters configuration from the `!Cursor`,
+  thus inheriting all the changes made to the global `psycopg.adapters`
+  configuration, the current `!Connection`, the `!Cursor`.
 
 - For every Python type passed as query argument, the `!Transformer` will
   instantiate a `!Dumper`. Usually all the objects of the same type will be
@@ -130,10 +132,17 @@ right instance.
   (for instance, a Python `int` might be better dumped as a PostgreSQL
   :sql:`integer`, :sql:`bigint`, :sql:`smallint` according to its value).
 
-- According to the placeholder used (``%s``, ``%b``, ``%t``), Psycopg may
-  pick a binary or a text dumper. When using the ``%s`` "`~PyFormat.AUTO`"
-  format, if the same type has both a text and a binary dumper registered, the
-  last one registered (using `Dumper.register()`) will be selected.
+- According to the placeholder used (``%s``, ``%b``, ``%t``), Psycopg may pick
+  a binary or a text dumper. When using the ``%s`` "`~PyFormat.AUTO`" format,
+  if the same type has both a text and a binary dumper registered, the last
+  one registered by `~AdaptersMap.register_dumper()` will be used.
+
+- Sometimes, just the Python type is not enough to infer the best PostgreSQL
+  type to use (for instance the PostgreSQL type of a Python list depends on
+  the objects it contains, whether to use an :sql:`integer` or :sql:`bigint`
+  depends on the number size...) In these cases the mechanism provided by
+  `~psycopg.proto.Dumper.get_key()` and `~psycopg.proto.Dumper.upgrade()` is
+  used.
 
 - For every OID returned by the query, the `!Transformer` will instantiate a
   `!Loader`. All the values with the same OID will be converted by the same
@@ -150,127 +159,3 @@ Querying will fail if a Python object for which there isn't a `!Dumper`
 registered (for the right `~psycopg.pq.Format`) is used as query parameter.
 If the query returns a data type whose OID doesn't have a `!Loader`, the
 value will be returned as a string (or bytes string for binary types).
-
-
-Objects involved in types adaptation
-------------------------------------
-
-.. admonition:: TODO
-
-    move to API section
-
-
-.. autoclass:: PyFormat
-    :members:
-
-
-.. autoclass:: Dumper(cls, context=None)
-
-    This is an abstract base class: subclasses *must* implement the `dump()`
-    method and specify the `format`.
-    They *may* implement `oid` (as attribute or property) in order to
-    override the oid type oid; if not PostgreSQL will try to infer the type
-    from the context, but this may fail in some contexts and may require a
-    cast.
-
-    :param cls: The type that will be managed by this dumper.
-    :type cls: type
-    :param context: The context where the transformation is performed. If not
-        specified the conversion might be inaccurate, for instance it will not
-        be possible to know the connection encoding or the server date format.
-    :type context: `~psycopg.Connection`, `~psycopg.Cursor`, or `Transformer`
-
-    .. attribute:: format
-        :type: pq.Format
-
-        The format this class dumps, `~Format.TEXT` or `~Format.BINARY`.
-        This is a class attribute.
-
-
-    .. automethod:: dump
-
-        The format returned by dump shouldn't contain quotes or escaped
-        values.
-
-    .. automethod:: quote
-
-        By default return the `dump()` value quoted and sanitised, so
-        that the result can be used to build a SQL string. This works well
-        for most types and you won't likely have to implement this method in a
-        subclass.
-
-        .. tip::
-
-            This method will be used by `~psycopg.sql.Literal` to convert a
-            value client-side.
-
-        This method only makes sense for text dumpers; the result of calling
-        it on a binary dumper is undefined. It might scratch your car, or burn
-        your cake. Don't tell me I didn't warn you.
-
-    .. autoattribute:: oid
-
-        .. admonition:: todo
-
-            Document how to find type OIDs in a database.
-
-    .. automethod:: register(cls, context=None)
-
-        You should call this method on the `Dumper` subclass you create,
-        passing the Python type you want to dump as *cls*.
-
-        If two dumpers of different `format` are registered for the same type,
-        the last one registered will be chosen by default when the query
-        doesn't specify a format (i.e. when the value is used with a ``%s``
-        "`~Format.AUTO`" placeholder).
-
-        :param cls: The type to manage.
-        :type cls: `!type` or `!str`
-        :param context: Where the dumper should be used. If `!None` the dumper
-            will be used globally.
-        :type context: `~psycopg.Connection`, `~psycopg.Cursor`, or `Transformer`
-
-        If *cls* is specified as string it will be lazy-loaded, so that it
-        will be possible to register it without importing it before. In this
-        case it should be the fully qualified name of the object (e.g.
-        ``"uuid.UUID"``).
-
-
-.. autoclass:: Loader(oid, context=None)
-
-    This is an abstract base class: subclasses *must* implement the `load()`
-    method and specify a `format`.
-
-    :param oid: The type that will be managed by this dumper.
-    :type oid: int
-    :param context: The context where the transformation is performed. If not
-        specified the conversion might be inaccurate, for instance it will not
-        be possible to know the connection encoding or the server date format.
-    :type context: `~psycopg.Connection`, `~psycopg.Cursor`, or `Transformer`
-
-    .. attribute:: format
-        :type: Format
-
-        The format this class can load, `~Format.TEXT` or `~Format.BINARY`.
-        This is a class attribute.
-
-    .. automethod:: load
-
-    .. automethod:: register(oid, context=None)
-
-        You should call this method on the `Loader` subclass you create,
-        passing the OID of the type you want to load as *oid* parameter.
-
-        :param oid: The PostgreSQL OID to manage.
-        :type oid: `!int`
-        :param context: Where the loader should be used. If `!None` the loader
-            will be used globally.
-        :type context: `~psycopg.Connection`, `~psycopg.Cursor`, or `Transformer`
-
-
-.. autoclass:: Transformer(context=None)
-
-    :param context: The context where the transformer should operate.
-    :type context: `~psycopg.Connection`, `~psycopg.Cursor`, or `Transformer`
-
-    TODO: finalise the interface of this object
diff --git a/docs/api/adapt.rst b/docs/api/adapt.rst
new file mode 100644 (file)
index 0000000..02553f3
--- /dev/null
@@ -0,0 +1,82 @@
+`adapt` -- Types adaptation
+===========================
+
+.. module:: psycopg.adapt
+
+The `!psycopg.adapt` module exposes a set of objects useful for the
+configuration of *data adaptation*, which is the conversion of Python objects
+to PostgreSQL data types and back.
+
+These objects are useful if you need to configure data adaptation, i.e.
+if you need to change the default way that Psycopg converts between types or
+if you want to adapt custom data types and objects. You don't need this object
+in the normal use of Psycopg.
+
+See :ref:`adaptation` for an overview of the Psycopg adaptation system.
+
+
+Dumpers and loaders
+-------------------
+
+.. autoclass:: Dumper(cls, context=None)
+
+    This is an abstract base class: subclasses *must* at least implement the
+    `dump()` method and specify the `format`.
+
+    The class implements the `~psycopg.proto.Dumper` protocol.
+
+    .. automethod:: dump
+
+    .. automethod:: quote
+
+    .. automethod:: get_key
+
+    .. automethod:: upgrade
+
+
+.. autoclass:: Loader(oid, context=None)
+
+    This is an abstract base class: subclasses *must* at least implement the
+    `!load()` method and specify a `format`.
+
+    The class implements the `~psycopg.proto.Loader` protocol.
+
+    .. automethod:: load
+
+
+Other objects used in adaptations
+---------------------------------
+
+.. autoclass:: PyFormat
+    :members:
+
+
+.. data:: psycopg.adapters
+
+   The global, default adapters map establishing how Python and PostgreSQL
+   types are converted into each other. This map is used as template when new
+   connections are created, using `psycopg.connect()`.
+
+   :type: `~psycopg.adapt.AdaptersMap`
+
+
+.. autoclass:: AdaptersMap
+
+   .. automethod:: register_dumper
+   .. automethod:: register_loader
+
+   .. attribute:: types
+
+       The object where to look up for types information (such as the mapping
+       between type names and oids in the specified context).
+
+       :type: `~psycopg.types.TypesRegistry`
+
+   .. automethod:: get_dumper
+   .. automethod:: get_loader
+
+
+.. autoclass:: Transformer(context=None)
+
+    :param context: The context where the transformer should operate.
+    :type context: `~psycopg.proto.AdaptContext`
index 815c7b20a114c98a99a6d9cc831b235e2e72746b..128dfbd7a0338739e8d06f6e6c53ea64ad79b3e4 100644 (file)
@@ -1,4 +1,4 @@
-`errors` -- package exceptions
+`errors` -- Package exceptions
 ==============================
 
 .. index::
index 8136c059faf5efe003496c0ad449f8cae2677c08..2064ebb4aa5fb6a581055b9e7ad146d69d4e9fc3 100644 (file)
@@ -2,13 +2,15 @@ Psycopg 3 API
 =============
 
 .. toctree::
-    :maxdepth: 1
+    :maxdepth: 2
     :caption: Contents:
 
     connections
     cursors
     sql
     errors
-    pool
+    adapt
     types
+    proto
+    pool
     pq
diff --git a/docs/api/proto.rst b/docs/api/proto.rst
new file mode 100644 (file)
index 0000000..85e0605
--- /dev/null
@@ -0,0 +1,81 @@
+`proto` -- Psycopg abstract classes
+===================================
+
+TODO: rename to abc
+
+The module exposes Psycopg definitions which can be used for static type
+checking.
+
+.. module:: psycopg.proto
+
+.. autoclass:: Dumper(cls, context=None)
+
+    :param cls: The type that will be managed by this dumper.
+    :type cls: type
+    :param context: The context where the transformation is performed. If not
+        specified the conversion might be inaccurate, for instance it will not
+        be possible to know the connection encoding or the server date format.
+    :type context: `AdaptContext` or None
+
+    A partial implementation of this protocol (implementing everyting except
+    `dump()`) is available as `psycopg.adapt.Dumper`.
+
+    .. attribute:: format
+        :type: pq.Format
+
+        The format this class dumps, `~Format.TEXT` or `~Format.BINARY`.
+        This is a class attribute.
+
+    .. automethod:: dump
+
+        The format returned by dump shouldn't contain quotes or escaped
+        values.
+
+    .. automethod:: quote
+
+        .. tip::
+
+            This method will be used by `~psycopg.sql.Literal` to convert a
+            value client-side.
+
+        This method only makes sense for text dumpers; the result of calling
+        it on a binary dumper is undefined. It might scratch your car, or burn
+        your cake. Don't tell me I didn't warn you.
+
+    .. autoattribute:: oid
+
+        If the oid is not specified, PostgreSQL will try to infer the type
+        from the context, but this may fail in some contexts and may require a
+        cast (e.g. specifying :samp:`%s::{type}` for its placeholder).
+
+        .. admonition:: todo
+
+            Document how to find type OIDs in a database.
+
+    .. automethod:: get_key
+    .. automethod:: upgrade
+
+
+.. autoclass:: Loader(oid, context=None)
+
+    :param oid: The type that will be managed by this dumper.
+    :type oid: int
+    :param context: The context where the transformation is performed. If not
+        specified the conversion might be inaccurate, for instance it will not
+        be possible to know the connection encoding or the server date format.
+    :type context: `AdaptContext` or None
+
+    A partial implementation of this protocol (implementing everyting except
+    `load()`) is available as `psycopg.adapt.Loader`.
+
+    .. attribute:: format
+        :type: Format
+
+        The format this class can load, `~Format.TEXT` or `~Format.BINARY`.
+        This is a class attribute.
+
+    .. automethod:: load
+
+
+.. autoclass:: AdaptContext
+    :members:
index 74d08ac637e37a708279d080733e16d310333214..bf63d09e64c0e05103891820e5e571b41d810f55 100644 (file)
@@ -2,8 +2,8 @@
 
 .. _psycopg.types:
 
-`!types` -- types mapping and adaptation
-========================================
+`!types` -- Types information and adapters
+==========================================
 
 .. module:: psycopg.types
 
@@ -66,6 +66,9 @@ and composite types using it as a subtypes.
         Python is demanded to a `Loader`.
 
 
+.. autoclass:: TypesRegistry
+
+
 The following `!TypeInfo` subclasses allow to fetch more specialised
 information from certain class of PostgreSQL types and to create more
 specialised adapters configurations.
index 2e851b60eb910ceed66ea0bd5161d5e8c09b8274..9d8a4c57c4b0f12bbb3267c67f4873eb517b9b83 100644 (file)
@@ -14,7 +14,8 @@ Many standard Python types are adapted into SQL and returned as Python
 objects when a query is executed.
 
 The following table shows the default mapping between Python and PostgreSQL
-types:
+types. In case you need to customise the conversion you should take a look at
+:ref:`adaptation`.
 
 TODO: complete table
 
diff --git a/docs/pictures/adapt.drawio b/docs/pictures/adapt.drawio
new file mode 100644 (file)
index 0000000..75f61ed
--- /dev/null
@@ -0,0 +1,107 @@
+<mxfile host="Electron" modified="2021-07-12T13:26:05.192Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.6.13 Chrome/89.0.4389.128 Electron/12.0.7 Safari/537.36" etag="kKU1DyIkJcQFc1Rxt__U" compressed="false" version="14.6.13" type="device">
+  <diagram id="THISp3X85jFCtBEH0bao" name="Page-1">
+    <mxGraphModel dx="675" dy="400" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
+      <root>
+        <mxCell id="0" />
+        <mxCell id="1" parent="0" />
+        <mxCell id="uy255Msn6vtulWmyCIR1-12" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontFamily=Courier New;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="uy255Msn6vtulWmyCIR1-29" target="uy255Msn6vtulWmyCIR1-11">
+          <mxGeometry relative="1" as="geometry">
+            <mxPoint x="280" y="210" as="sourcePoint" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-15" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;fontFamily=Courier New;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="uy255Msn6vtulWmyCIR1-30" target="uy255Msn6vtulWmyCIR1-14">
+          <mxGeometry relative="1" as="geometry">
+            <mxPoint x="280" y="320" as="sourcePoint" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-39" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontFamily=Courier New;" edge="1" parent="1" source="uy255Msn6vtulWmyCIR1-11" target="uy255Msn6vtulWmyCIR1-14">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-11" value=".adapters" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontFamily=Courier New;" vertex="1" parent="1">
+          <mxGeometry x="330" y="185" width="80" height="20" as="geometry" />
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-40" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontFamily=Courier New;" edge="1" parent="1" source="uy255Msn6vtulWmyCIR1-14" target="uy255Msn6vtulWmyCIR1-27">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-14" value=".adapters" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontFamily=Courier New;" vertex="1" parent="1">
+          <mxGeometry x="330" y="285" width="80" height="20" as="geometry" />
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-28" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontFamily=Courier New;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="uy255Msn6vtulWmyCIR1-31" target="uy255Msn6vtulWmyCIR1-27">
+          <mxGeometry relative="1" as="geometry">
+            <mxPoint x="280" y="440" as="sourcePoint" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-18" value=".cursor()" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontFamily=Courier New;" vertex="1" parent="1">
+          <mxGeometry x="220" y="220" width="80" height="20" as="geometry" />
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-26" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontFamily=Courier New;" edge="1" parent="1" source="uy255Msn6vtulWmyCIR1-19" target="uy255Msn6vtulWmyCIR1-25">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-34" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;fontFamily=Courier New;" edge="1" parent="1" source="uy255Msn6vtulWmyCIR1-19" target="uy255Msn6vtulWmyCIR1-29">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-19" value="&lt;b&gt;psycopg&lt;/b&gt;&lt;br&gt;&lt;font face=&quot;Helvetica&quot;&gt;module&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fontFamily=Courier New;" vertex="1" parent="1">
+          <mxGeometry x="160" y="75" width="120" height="50" as="geometry" />
+        </mxCell>
+        <UserObject label=".connect()" link="../api/connections.html" id="uy255Msn6vtulWmyCIR1-20">
+          <mxCell style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontFamily=Courier New;" vertex="1" parent="1">
+            <mxGeometry x="220" y="125" width="80" height="20" as="geometry" />
+          </mxCell>
+        </UserObject>
+        <mxCell id="uy255Msn6vtulWmyCIR1-21" value=".execute()" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontFamily=Courier New;" vertex="1" parent="1">
+          <mxGeometry x="220" y="320" width="80" height="20" as="geometry" />
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-37" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontFamily=Courier New;" edge="1" parent="1" source="uy255Msn6vtulWmyCIR1-25" target="uy255Msn6vtulWmyCIR1-11">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-25" value=".adapters" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontFamily=Courier New;" vertex="1" parent="1">
+          <mxGeometry x="330" y="90" width="80" height="20" as="geometry" />
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-27" value=".adapters" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontFamily=Courier New;" vertex="1" parent="1">
+          <mxGeometry x="330" y="385" width="80" height="20" as="geometry" />
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-35" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontFamily=Courier New;" edge="1" parent="1" source="uy255Msn6vtulWmyCIR1-29" target="uy255Msn6vtulWmyCIR1-30">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <UserObject label="&lt;b&gt;Connection&lt;/b&gt;&lt;br&gt;&lt;font face=&quot;Helvetica&quot;&gt;object&lt;/font&gt;" link="../api/connections.html" id="uy255Msn6vtulWmyCIR1-29">
+          <mxCell style="rounded=1;whiteSpace=wrap;html=1;fontFamily=Courier New;" vertex="1" parent="1">
+            <mxGeometry x="160" y="170" width="120" height="50" as="geometry" />
+          </mxCell>
+        </UserObject>
+        <mxCell id="uy255Msn6vtulWmyCIR1-36" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;fontFamily=Courier New;" edge="1" parent="1" source="uy255Msn6vtulWmyCIR1-30" target="uy255Msn6vtulWmyCIR1-31">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-30" value="&lt;b&gt;Cursor&lt;/b&gt;&lt;br&gt;&lt;font face=&quot;Helvetica&quot;&gt;object&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fontFamily=Courier New;" vertex="1" parent="1">
+          <mxGeometry x="160" y="270" width="120" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-31" value="&lt;b&gt;Transformer&lt;/b&gt;&lt;br&gt;&lt;font face=&quot;Helvetica&quot;&gt;object&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fontFamily=Courier New;" vertex="1" parent="1">
+          <mxGeometry x="160" y="370" width="120" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-46" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;fontFamily=Helvetica;endArrow=none;endFill=0;dashed=1;dashPattern=1 1;" edge="1" parent="1" source="uy255Msn6vtulWmyCIR1-41">
+          <mxGeometry relative="1" as="geometry">
+            <mxPoint x="310" y="100" as="targetPoint" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-41" value="Has a" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontFamily=Helvetica;" vertex="1" parent="1">
+          <mxGeometry x="300" y="55" width="40" height="20" as="geometry" />
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-45" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;fontFamily=Helvetica;endArrow=none;endFill=0;dashed=1;dashPattern=1 1;startSize=4;" edge="1" parent="1" source="uy255Msn6vtulWmyCIR1-42">
+          <mxGeometry relative="1" as="geometry">
+            <mxPoint x="220" y="150" as="targetPoint" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-42" value="Create" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontFamily=Helvetica;" vertex="1" parent="1">
+          <mxGeometry x="150" y="130" width="40" height="20" as="geometry" />
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-47" style="edgeStyle=none;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;fontFamily=Helvetica;endArrow=none;endFill=0;dashed=1;dashPattern=1 1;" edge="1" parent="1" source="uy255Msn6vtulWmyCIR1-43">
+          <mxGeometry relative="1" as="geometry">
+            <mxPoint x="370" y="150" as="targetPoint" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="uy255Msn6vtulWmyCIR1-43" value="Copy" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontFamily=Helvetica;" vertex="1" parent="1">
+          <mxGeometry x="394" y="130" width="40" height="20" as="geometry" />
+        </mxCell>
+      </root>
+    </mxGraphModel>
+  </diagram>
+</mxfile>
diff --git a/docs/pictures/adapt.svg b/docs/pictures/adapt.svg
new file mode 100644 (file)
index 0000000..2c39755
--- /dev/null
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="285px" height="366px" viewBox="-0.5 -0.5 285 366" style="background-color: rgb(255, 255, 255);"><defs/><g><path d="M 130 140 L 173.63 140" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 178.88 140 L 171.88 143.5 L 173.63 140 L 171.88 136.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 130 240 L 173.63 240" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 178.88 240 L 171.88 243.5 L 173.63 240 L 171.88 236.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 220 150 L 220 223.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 220 228.88 L 216.5 221.88 L 220 223.63 L 223.5 221.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="180" y="130" width="80" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 78px; height: 1px; padding-top: 140px; margin-left: 182px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Courier New; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">.adapters</div></div></div></foreignObject><text x="182" y="144" fill="#000000" font-family="Courier New" font-size="12px">.adapters</text></switch></g><path d="M 220 250 L 220 323.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 220 328.88 L 216.5 321.88 L 220 323.63 L 223.5 321.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="180" y="230" width="80" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 78px; height: 1px; padding-top: 240px; margin-left: 182px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Courier New; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">.adapters</div></div></div></foreignObject><text x="182" y="244" fill="#000000" font-family="Courier New" font-size="12px">.adapters</text></switch></g><path d="M 130 340 L 173.63 340" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 178.88 340 L 171.88 343.5 L 173.63 340 L 171.88 336.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="70" y="165" width="80" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 78px; height: 1px; padding-top: 175px; margin-left: 72px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Courier New; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">.cursor()</div></div></div></foreignObject><text x="72" y="179" fill="#000000" font-family="Courier New" font-size="12px">.cursor()</text></switch></g><path d="M 130 45 L 173.63 45" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 178.88 45 L 171.88 48.5 L 173.63 45 L 171.88 41.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><path d="M 70 70 L 70 108.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 70 113.88 L 66.5 106.88 L 70 108.63 L 73.5 106.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="10" y="20" width="120" height="50" rx="7.5" ry="7.5" fill="#ffffff" stroke="#000000" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 45px; margin-left: 11px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Courier New; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; "><b>psycopg</b><br /><font face="Helvetica">module</font></div></div></div></foreignObject><text x="70" y="49" fill="#000000" font-family="Courier New" font-size="12px" text-anchor="middle">psycopg...</text></switch></g><a xlink:href="../api/connections.html"><rect x="70" y="70" width="80" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 78px; height: 1px; padding-top: 80px; margin-left: 72px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Courier New; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">.connect()</div></div></div></foreignObject><text x="72" y="84" fill="#000000" font-family="Courier New" font-size="12px">.connect()</text></switch></g></a><rect x="70" y="265" width="80" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 78px; height: 1px; padding-top: 275px; margin-left: 72px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Courier New; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">.execute()</div></div></div></foreignObject><text x="72" y="279" fill="#000000" font-family="Courier New" font-size="12px">.execute()</text></switch></g><path d="M 220 55 L 220 123.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 220 128.88 L 216.5 121.88 L 220 123.63 L 223.5 121.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="180" y="35" width="80" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 78px; height: 1px; padding-top: 45px; margin-left: 182px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Courier New; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">.adapters</div></div></div></foreignObject><text x="182" y="49" fill="#000000" font-family="Courier New" font-size="12px">.adapters</text></switch></g><rect x="180" y="330" width="80" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 78px; height: 1px; padding-top: 340px; margin-left: 182px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; "><div style="display: inline-block; font-size: 12px; font-family: Courier New; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">.adapters</div></div></div></foreignObject><text x="182" y="344" fill="#000000" font-family="Courier New" font-size="12px">.adapters</text></switch></g><path d="M 70 165 L 70 208.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 70 213.88 L 66.5 206.88 L 70 208.63 L 73.5 206.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><a xlink:href="../api/connections.html"><rect x="10" y="115" width="120" height="50" rx="7.5" ry="7.5" fill="#ffffff" stroke="#000000" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 140px; margin-left: 11px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Courier New; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; "><b>Connection</b><br /><font face="Helvetica">object</font></div></div></div></foreignObject><text x="70" y="144" fill="#000000" font-family="Courier New" font-size="12px" text-anchor="middle">Connection...</text></switch></g></a><path d="M 70 265 L 70 308.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 70 313.88 L 66.5 306.88 L 70 308.63 L 73.5 306.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/><rect x="10" y="215" width="120" height="50" rx="7.5" ry="7.5" fill="#ffffff" stroke="#000000" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 240px; margin-left: 11px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Courier New; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; "><b>Cursor</b><br /><font face="Helvetica">object</font></div></div></div></foreignObject><text x="70" y="244" fill="#000000" font-family="Courier New" font-size="12px" text-anchor="middle">Cursor...</text></switch></g><rect x="10" y="315" width="120" height="50" rx="7.5" ry="7.5" fill="#ffffff" stroke="#000000" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 340px; margin-left: 11px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Courier New; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; "><b>Transformer</b><br /><font face="Helvetica">object</font></div></div></div></foreignObject><text x="70" y="344" fill="#000000" font-family="Courier New" font-size="12px" text-anchor="middle">Transformer...</text></switch></g><path d="M 167.14 20 L 160 45" fill="none" stroke="#000000" stroke-miterlimit="10" stroke-dasharray="1 1" pointer-events="stroke"/><rect x="150" y="0" width="40" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 10px; margin-left: 151px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">Has a</div></div></div></foreignObject><text x="170" y="14" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">Has a</text></switch></g><path d="M 40 89 L 70 95" fill="none" stroke="#000000" stroke-miterlimit="10" stroke-dasharray="1 1" pointer-events="stroke"/><rect x="0" y="75" width="40" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 85px; margin-left: 1px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">Create</div></div></div></foreignObject><text x="20" y="89" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">Create</text></switch></g><path d="M 244 89.55 L 220 95" fill="none" stroke="#000000" stroke-miterlimit="10" stroke-dasharray="1 1" pointer-events="stroke"/><rect x="244" y="75" width="40" height="20" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 85px; margin-left: 245px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">Copy</div></div></div></foreignObject><text x="264" y="89" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">Copy</text></switch></g></g><switch><g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/><a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank"><text text-anchor="middle" font-size="10px" x="50%" y="100%">Viewer does not support full SVG 1.1</text></a></switch></svg>
\ No newline at end of file
index ac6313a3359976d608ff06770620a6e778aaf7bf..3bef245f1e0caf8ac1b40e5852df66897a1338ab 100644 (file)
@@ -10,7 +10,7 @@ from typing import cast, TYPE_CHECKING
 from . import pq
 from . import errors as e
 from ._enums import PyFormat as PyFormat
-from .proto import AdaptContext, Dumper, Loader
+from .proto import Dumper, Loader
 from ._cmodule import _psycopg
 from ._typeinfo import TypesRegistry
 
@@ -20,9 +20,31 @@ if TYPE_CHECKING:
 RV = TypeVar("RV")
 
 
-class AdaptersMap(AdaptContext):
-    """
-    Map oids to Loaders and types to Dumpers.
+class AdaptersMap:
+    r"""
+    Establish how types should be converted between Python and PostgreSQL in
+    an `~psycopg.proto.AdaptContext`.
+
+    `!AdaptersMap` maps Python types to `~psycopg.adapt.Dumper` classes to
+    define how Python types are converted to PostgreSQL, and maps OIDs to
+    `~psycopg.adapt.Loader` classes to establish how query results are
+    converted to Python.
+
+    Every `!AdaptContext` object has an underlying `!AdaptersMap` defining how
+    types are converted in that context, exposed as the
+    `~psycopg.proto.AdaptContext.adapters` attribute: changing such map allows
+    to customise adaptation in a context without changing separated contexts.
+
+    When a context is created from another context (for instance when a
+    `~psycopg.Cursor` is created from a `~psycopg.Connection`), the parent's
+    `!adapters` are used as template for the child's `!adapters`, so that every
+    cursor created from the same connection use the connection's types
+    configuration, but separate connections have independent mappings. Once
+    created, `!AdaptersMap` are independent.
+
+    The connections adapters are initialised using a global `!AdptersMap`
+    template, exposed as `psycopg.adapters`: changing such mapping allows to
+    customise the type mapping for the entire application.
 
     The object can start empty or copy from another object of the same class.
     Copies are copy-on-write: if the maps are updated make a copy. This way
@@ -32,9 +54,10 @@ class AdaptersMap(AdaptContext):
 
     __module__ = "psycopg.adapt"
 
+    types: TypesRegistry
+
     _dumpers: Dict[PyFormat, Dict[Union[type, str], Type[Dumper]]]
     _loaders: List[Dict[int, Type[Loader]]]
-    types: TypesRegistry
 
     # Record if a dumper or loader has an optimised version.
     _optimised: Dict[type, type] = {}
@@ -73,6 +96,19 @@ class AdaptersMap(AdaptContext):
     ) -> None:
         """
         Configure the context to use *dumper* to convert object of type *cls*.
+
+        If two dumpers with different `~Dumper.format` are registered for the
+        same type, the last one registered will be chosen when the query
+        doesn't specify a format (i.e. when the value is used with a ``%s``
+        "`~PyFormat.AUTO`" placeholder).
+
+        :param cls: The type to manage.
+        :param dumper: The dumper to register for *cls*.
+
+        If *cls* is specified as string it will be lazy-loaded, so that it
+        will be possible to register it without importing it before. In this
+        case it should be the fully qualified name of the object (e.g.
+        ``"uuid.UUID"``).
         """
         if not isinstance(cls, (str, type)):
             raise TypeError(
@@ -96,6 +132,13 @@ class AdaptersMap(AdaptContext):
     ) -> None:
         """
         Configure the context to use *loader* to convert data of oid *oid*.
+
+        :param oid: The PostgreSQL OID or type name to manage.
+        :param loader: The loar to register for *oid*.
+
+        If `oid` is specified as string, it refers to a type name, which is
+        looked up in the `types` registry. `
+
         """
         if isinstance(oid, str):
             oid = self.types[oid].oid
index aa77115e3786b9064591b25a670c946cd1758842..11a549ca18abbaed3f5470c9026858d7d57cdfb0 100644 (file)
@@ -29,9 +29,11 @@ class Transformer(AdaptContext):
     """
     An object that can adapt efficiently between Python and PostgreSQL.
 
-    The life cycle of the object is the query, so it is assumed that stuff like
-    the server version or connection encoding will not change. It can have its
-    state so adapting several values of the same type can be optimised.
+    The life cycle of the object is the query, so it is assumed that attributes
+    such as the server version or the connection encoding will not change. The
+    object have its state so adapting several values of the same type can be
+    optimised.
+
     """
 
     __module__ = "psycopg.adapt"
index 386f832adc5539d588e12f01eacca8bf35c6f6c5..e8a15dd3e17e43fa8ba82996205d2731b2a96c43 100644 (file)
@@ -223,6 +223,8 @@ class TypesRegistry:
     Container for the information about types in a database.
     """
 
+    __module__ = "psycopg.types"
+
     def __init__(self, template: Optional["TypesRegistry"] = None):
         self._by_oid: Dict[int, TypeInfo]
         self._by_name: Dict[str, TypeInfo]
index 08578c865ad94ee3eafa5c4205f0431dae19c809..78196a6380770be407104a5dd63be12c6cb481a7 100644 (file)
@@ -7,46 +7,49 @@ Entry point into the adaptation system.
 from abc import ABC, abstractmethod
 from typing import Any, Optional, Type, Tuple, Union, TYPE_CHECKING
 
-from . import pq
+from . import pq, proto
 from . import _adapters_map
 from .proto import AdaptContext, Buffer as Buffer
 from ._enums import PyFormat as PyFormat
 from ._cmodule import _psycopg
 
 if TYPE_CHECKING:
-    from . import proto
     from .connection import BaseConnection
 
 AdaptersMap = _adapters_map.AdaptersMap
 
 
-class Dumper(ABC):
+class Dumper(proto.Dumper, ABC):
     """
     Convert Python object of the type *cls* to PostgreSQL representation.
     """
 
-    format: pq.Format
-
     # A class-wide oid, which will be used by default by instances unless
     # the subclass overrides it in init.
     _oid: int = 0
 
+    oid: int
+    """The oid to pass to the server, if known."""
+
     def __init__(self, cls: type, context: Optional[AdaptContext] = None):
         self.cls = cls
         self.connection: Optional["BaseConnection[Any]"] = (
             context.connection if context else None
         )
 
-        self.oid: int = self._oid
-        """The oid to pass to the server, if known."""
+        self.oid = self._oid
 
     @abstractmethod
     def dump(self, obj: Any) -> Buffer:
-        """Convert the object *obj* to PostgreSQL representation."""
         ...
 
     def quote(self, obj: Any) -> Buffer:
-        """Convert the object *obj* to escaped representation."""
+        """
+        By default return the `dump()` value quoted and sanitised, so
+        that the result can be used to build a SQL string. This works well
+        for most types and you won't likely have to implement this method in a
+        subclass.
+        """
         value = self.dump(obj)
 
         if self.connection:
@@ -59,31 +62,25 @@ class Dumper(ABC):
     def get_key(
         self, obj: Any, format: PyFormat
     ) -> Union[type, Tuple[type, ...]]:
-        """Return an alternative key to upgrade the dumper to represent *obj*
-
-        Normally the type of the object is all it takes to define how to dump
-        the object to the database. In a few cases this is not enough. Example
-
-        - Python int could be several Postgres types: int2, int4, int8, numeric
-        - Python lists should be dumped according to the type they contain
-          to convert them to e.g. array of strings, array of ints (which?...)
+        """
+        Implementation of the `~psycopg.proto.Dumper.get_key()` member of the
+        `~psycopg.proto.Dumper` protocol. Look at its definition for details.
 
-        In these cases a Dumper can implement `get_key()` and return a new
-        class, or sequence of classes, that can be used to indentify the same
-        dumper again.
+        This implementation returns the *cls* passed in the constructor.
+        Subclasses needing to specialise the PostgreSQL type according to the
+        *value* of the object dumped (not only according to to its type)
+        should override this class.
 
-        If a Dumper implements `get_key()` it should also implmement
-        `upgrade()`.
         """
         return self.cls
 
     def upgrade(self, obj: Any, format: PyFormat) -> "Dumper":
-        """Return a new dumper to manage *obj*.
+        """
+        Implementation of the `~psycopg.proto.Dumper.upgrade()` member of the
+        `~psycopg.proto.Dumper` protocol. Look at its definition for details.
 
-        Once `Transformer.get_dumper()` has been notified that this Dumper
-        class cannot handle *obj* itself it will invoke `upgrade()`, which
-        should return a new `Dumper` instance, and will be reused for every
-        objects for which `get_key()` returns the same result.
+        This implementation just returns *self*. If a subclass implements
+        `get_key()` it should probably override `!upgrade()` too.
         """
         return self
 
index 48f8bc40aa3eb5da4a8021e1867e3c931ead28ad..c00479641c639e5e9c4268e3b03adbb35c3c0f6e 100644 (file)
@@ -25,7 +25,7 @@ from . import encodings
 from .pq import ConnStatus, ExecStatus, TransactionStatus, Format
 from .sql import Composable
 from .rows import Row, RowFactory, tuple_row, TupleRow
-from .proto import AdaptContext, ConnectionType, Params, PQGen, PQGenConn
+from .proto import ConnectionType, Params, PQGen, PQGenConn
 from .proto import Query, RV
 from .compat import asynccontextmanager
 from .cursor import Cursor, AsyncCursor
@@ -79,7 +79,7 @@ NoticeHandler = Callable[[e.Diagnostic], None]
 NotifyHandler = Callable[[Notify], None]
 
 
-class BaseConnection(AdaptContext, Generic[Row]):
+class BaseConnection(Generic[Row]):
     """
     Base class for different types of connections.
 
index 16f10025a5c55e41f8d323e4b33b525c3c2be52a..1fa915a4e6dabbea81ba2ba22bc393613fb2114e 100644 (file)
@@ -59,39 +59,104 @@ class AdaptContext(Protocol):
     """
     A context describing how types are adapted.
 
-    Example of AdaptContext are connections, cursors, transformers.
+    Example of `~AdaptContext` are `~psycopg.Connection`, `~psycopg.Cursor`,
+    `~psycopg.adapt.Transformer`, `~psycopg.adapt.AdaptersMap`.
+
+    Note that this is a `~typing.Protocol`, so objects implementing
+    `!AdaptContext` don't need to explicitly inherit from this class.
+
     """
 
     @property
     def adapters(self) -> "AdaptersMap":
+        """The adapters configuration that this object uses."""
         ...
 
     @property
     def connection(self) -> Optional["BaseConnection[Any]"]:
+        """The connection used by this object, if available.
+
+        :rtype: `~psycopg.Connection` or `~psycopg.AsyncConnection` or `!None`
+        """
         ...
 
 
 class Dumper(Protocol):
+    """
+    Convert Python objects of type *cls* to PostgreSQL representation.
+    """
+
     format: pq.Format
     oid: int
+    """The oid to pass to the server, if known; 0 otherwise."""
 
     def __init__(self, cls: type, context: Optional[AdaptContext] = None):
         ...
 
     def dump(self, obj: Any) -> Buffer:
+        """Convert the object *obj* to PostgreSQL representation."""
         ...
 
     def quote(self, obj: Any) -> Buffer:
+        """Convert the object *obj* to escaped representation."""
         ...
 
     def get_key(self, obj: Any, format: PyFormat) -> DumperKey:
+        """Return an alternative key to upgrade the dumper to represent *obj*.
+
+        :param obj: The object to convert
+        :param format: The format to convert to
+
+        Normally the type of the object is all it takes to define how to dump
+        the object to the database. For instance, a Python `~datetime.date` can
+        be simply converted into a PostgreSQL :sql:`date`.
+
+        In a few cases, just the type is not enough. For example:
+
+        - A Python `~datetime.datetime` could be represented as a
+          :sql:`timestamptz` or a :sql:`timestamp`, according to whether it
+          specifies a `!tzinfo` or not.
+
+        - A Python int could be stored as several Postgres types: int2, int4,
+          int8, numeric. If a type too small is used, it may result in an
+          overflow. If a type too large is used, PostgreSQL may not want to
+          cast it to a smaller type.
+
+        - Python lists should be dumped according to the type they contain to
+          convert them to e.g. array of strings, array of ints (and which
+          size of int?...)
+
+        In these cases, a dumper can implement `!get_key()` and return a new
+        class, or sequence of classes, that can be used to indentify the same
+        dumper again. If the mechanism is not needed, the method should return
+        the same *cls* object passed in the constructor.
+
+        If a dumper implements `get_key()` it should also implmement
+        `upgrade()`.
+
+        """
         ...
 
     def upgrade(self, obj: Any, format: PyFormat) -> "Dumper":
+        """Return a new dumper to manage *obj*.
+
+        :param obj: The object to convert
+        :param format: The format to convert to
+
+        Once `Transformer.get_dumper()` has been notified by `get_key()` that
+        this Dumper class cannot handle *obj* itself, it will invoke
+        `!upgrade()`, which should return a new `Dumper` instance, which will
+        be reused for every objects for which `!get_key()` returns the same
+        result.
+        """
         ...
 
 
 class Loader(Protocol):
+    """
+    Convert PostgreSQL objects with OID *oid* to Python objects.
+    """
+
     format: pq.Format
 
     def __init__(self, oid: int, context: Optional[AdaptContext] = None):
index 04d5e35a03cbe576b6211013d77f25cef1effe90..05eb1cbcb8f782dce5f2f0273063145c6303538e 100644 (file)
@@ -6,4 +6,6 @@ psycopg types package
 
 from .. import _typeinfo
 
-TypeInfo = _typeinfo.TypeInfo  # exported here
+# Exposed here
+TypeInfo = _typeinfo.TypeInfo
+TypesRegistry = _typeinfo.TypesRegistry
index 414f3a04b13ac8cbdc43f50a49b4ffcf5af1296a..66627c2f24e084ba58ab5c1baaeebd8db02eaf77 100644 (file)
@@ -61,9 +61,11 @@ cdef class Transformer:
     """
     An object that can adapt efficiently between Python and PostgreSQL.
 
-    The life cycle of the object is the query, so it is assumed that stuff like
-    the server version or connection encoding will not change. It can have its
-    state so adapting several values of the same type can use optimisations.
+    The life cycle of the object is the query, so it is assumed that attributes
+    such as the server version or the connection encoding will not change. The
+    object have its state so adapting several values of the same type can be
+    optimised.
+
     """
 
     cdef readonly object connection