From: Daniele Varrazzo Date: Mon, 12 Jul 2021 15:36:19 +0000 (+0200) Subject: Improvements to the adaptation documentation X-Git-Tag: 3.0.dev1~28^2~1 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=ffd8de7e8ef3e13f069905cfe07e987365935eba;p=thirdparty%2Fpsycopg.git Improvements to the adaptation documentation --- diff --git a/docs/advanced/adapt.rst b/docs/advanced/adapt.rst index 96555fbf8..3e5bc468b 100644 --- a/docs/advanced/adapt.rst +++ b/docs/advanced/adapt.rst @@ -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 index 000000000..02553f392 --- /dev/null +++ b/docs/api/adapt.rst @@ -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` diff --git a/docs/api/errors.rst b/docs/api/errors.rst index 815c7b20a..128dfbd7a 100644 --- a/docs/api/errors.rst +++ b/docs/api/errors.rst @@ -1,4 +1,4 @@ -`errors` -- package exceptions +`errors` -- Package exceptions ============================== .. index:: diff --git a/docs/api/index.rst b/docs/api/index.rst index 8136c059f..2064ebb4a 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -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 index 000000000..85e0605f2 --- /dev/null +++ b/docs/api/proto.rst @@ -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: diff --git a/docs/api/types.rst b/docs/api/types.rst index 74d08ac63..bf63d09e6 100644 --- a/docs/api/types.rst +++ b/docs/api/types.rst @@ -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. diff --git a/docs/basic/adapt.rst b/docs/basic/adapt.rst index 2e851b60e..9d8a4c57c 100644 --- a/docs/basic/adapt.rst +++ b/docs/basic/adapt.rst @@ -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 index 000000000..75f61ed62 --- /dev/null +++ b/docs/pictures/adapt.drawio @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/pictures/adapt.svg b/docs/pictures/adapt.svg new file mode 100644 index 000000000..2c397557b --- /dev/null +++ b/docs/pictures/adapt.svg @@ -0,0 +1,3 @@ + + +
.adapters
.adapters
.adapters
.adapters
.cursor()
.cursor()
psycopg
module
psycopg...
.connect()
.connect()
.execute()
.execute()
.adapters
.adapters
.adapters
.adapters
Connection
object
Connection...
Cursor
object
Cursor...
Transformer
object
Transformer...
Has a
Has a
Create
Create
Copy
Copy
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/psycopg/psycopg/_adapters_map.py b/psycopg/psycopg/_adapters_map.py index ac6313a33..3bef245f1 100644 --- a/psycopg/psycopg/_adapters_map.py +++ b/psycopg/psycopg/_adapters_map.py @@ -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 diff --git a/psycopg/psycopg/_transform.py b/psycopg/psycopg/_transform.py index aa77115e3..11a549ca1 100644 --- a/psycopg/psycopg/_transform.py +++ b/psycopg/psycopg/_transform.py @@ -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" diff --git a/psycopg/psycopg/_typeinfo.py b/psycopg/psycopg/_typeinfo.py index 386f832ad..e8a15dd3e 100644 --- a/psycopg/psycopg/_typeinfo.py +++ b/psycopg/psycopg/_typeinfo.py @@ -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] diff --git a/psycopg/psycopg/adapt.py b/psycopg/psycopg/adapt.py index 08578c865..78196a638 100644 --- a/psycopg/psycopg/adapt.py +++ b/psycopg/psycopg/adapt.py @@ -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 diff --git a/psycopg/psycopg/connection.py b/psycopg/psycopg/connection.py index 48f8bc40a..c00479641 100644 --- a/psycopg/psycopg/connection.py +++ b/psycopg/psycopg/connection.py @@ -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. diff --git a/psycopg/psycopg/proto.py b/psycopg/psycopg/proto.py index 16f10025a..1fa915a4e 100644 --- a/psycopg/psycopg/proto.py +++ b/psycopg/psycopg/proto.py @@ -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): diff --git a/psycopg/psycopg/types/__init__.py b/psycopg/psycopg/types/__init__.py index 04d5e35a0..05eb1cbcb 100644 --- a/psycopg/psycopg/types/__init__.py +++ b/psycopg/psycopg/types/__init__.py @@ -6,4 +6,6 @@ psycopg types package from .. import _typeinfo -TypeInfo = _typeinfo.TypeInfo # exported here +# Exposed here +TypeInfo = _typeinfo.TypeInfo +TypesRegistry = _typeinfo.TypesRegistry diff --git a/psycopg_c/psycopg_c/_psycopg/transform.pyx b/psycopg_c/psycopg_c/_psycopg/transform.pyx index 414f3a04b..66627c2f2 100644 --- a/psycopg_c/psycopg_c/_psycopg/transform.pyx +++ b/psycopg_c/psycopg_c/_psycopg/transform.pyx @@ -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