From c756d580f2342846be05a4708c3162f9614f9fae Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Mon, 24 Sep 2012 00:49:44 +0100 Subject: [PATCH] Added documentation for range types and adaptation --- NEWS | 1 + doc/src/extras.rst | 83 ++++++++++++++++++++++++++++++++++++++++++++++ lib/_range.py | 79 ++++++++++++++++++++++++------------------- 3 files changed, 128 insertions(+), 35 deletions(-) diff --git a/NEWS b/NEWS index 679486bc..d4ddba89 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,7 @@ What's new in psycopg 2.4.6 --------------------------- + - Added support for PostgreSQL 9.2 range types. - Added support for backward scrollable cursors. Thanks to Jon Nelson for the initial patch (ticket #108). - connection.reset() implemented using DISCARD ALL on server versions diff --git a/doc/src/extras.rst b/doc/src/extras.rst index f3f10b12..ddefab2b 100644 --- a/doc/src/extras.rst +++ b/doc/src/extras.rst @@ -211,6 +211,89 @@ requires no adapter registration. +.. index:: + pair: range; Data types + +Range data types +^^^^^^^^^^^^^^^^ + +.. versionadded:: 2.4.6 + +Psycopg offers a `Range` Python type and supports adaptation between them and +PostgreSQL |range|_ types. Builtin |range| types are supported out-of-the-box; +user-defined |range| types can be adapted using `register_range()`. + +.. |range| replace:: :sql:`range` +.. _range: http://www.postgresql.org/docs/current/static/rangetypes.html + +.. autoclass:: Range + + This Python type is only used to pass and retrieve range values to and + from PostgreSQL and doesn't attempt to replicate the PostgreSQL range + features: it doesn't perform normalization and doesn't implement all the + operators__ supported by the database. + + .. __: http://www.postgresql.org/docs/current/static/functions-range.html#RANGE-OPERATORS-TABLE + + `!Range` objects are immutable, hashable, and support the ``in`` operator + (checking if an element is within the range). They can be tested for + equivalence but not for ordering. Empty ranges evaluate to `!False` in + boolean context, nonempty evaluate to `!True`. + + Although it is possible to instantiate `!Range` objects, the class doesn't + have an adapter registered, so you cannot normally pass these instances as + query arguments. To use range objects as query arguments you can either + use one of the provided subclasses, such as `NumericRange` or create a + custom subclass using `register_range()`. + + Object attributes: + + .. autoattribute:: isempty + .. autoattribute:: lower + .. autoattribute:: upper + .. autoattribute:: lower_inc + .. autoattribute:: upper_inc + .. autoattribute:: lower_inf + .. autoattribute:: upper_inf + + +The following `Range` subclasses map builtin PostgreSQL |range| types to +Python objects: they have an adapter registered so their instances can be +passed as query arguments. |range| values read from database queries are +automatically casted into instances of these classes. + +.. autoclass:: NumericRange +.. autoclass:: DateRange +.. autoclass:: DateTimeRange +.. autoclass:: DateTimeTZRange + +Custom |range| types (created with |CREATE TYPE|_ :sql:`... AS RANGE`) can be +adapted to a custom `Range` subclass: + +.. autofunction:: register_range + +.. autoclass:: RangeCaster + + Object attributes: + + .. attribute:: range + + The `!Range` subclass adapted. + + .. attribute:: adapter + + The `~psycopg2.extensions.ISQLQuote` responsible to adapt `!range`. + + .. attribute:: typecaster + + The object responsible for casting. + + .. attribute:: array_typecaster + + The object responsible for casting arrays, if available, else `!None`. + + + .. index:: pair: UUID; Data types diff --git a/lib/_range.py b/lib/_range.py index 7fb194fb..549efebf 100644 --- a/lib/_range.py +++ b/lib/_range.py @@ -31,26 +31,13 @@ from psycopg2.extensions import ISQLQuote, adapt, register_adapter from psycopg2.extensions import new_type, new_array_type, register_type class Range(object): - """Python representation for a PostgreSQL range type. + """Python representation for a PostgreSQL |range|_ type. - :param lower: lower bound for the range. None means unbound - :param upper: upper bound for the range. None means unbound + :param lower: lower bound for the range. `!None` means unbound + :param upper: upper bound for the range. `!None` means unbound :param bounds: one of the literal strings ``()``, ``[)``, ``(]``, ``[]``, representing whether the lower or upper bounds are included - :param empty: if true, the range is empty - - TODO: move this to the docs - - This Python type is only used to pass and retrieve range values to and - from PostgreSQL and doesn't attempt to replicate the PostgreSQL range - features: it doesn't perform normalization and doesn't implement all the - operators supported. - - Although it is possible to instantiate `!Range` objects, the class doesn't - have an adapter so you cannot normally pass these instances as query - arguments. To use range objects as query arguments you can either use one - of the provided subclasses, such as [TODO: the other] `IntRange` or create - a custom one using `register_range()`. + :param empty: if `!True`, the range is empty """ __slots__ = ('_lower', '_upper', '_bounds') @@ -75,41 +62,41 @@ class Range(object): @property def lower(self): - """The lower bound of the range. None if empty or unbound.""" + """The lower bound of the range. `!None` if empty or unbound.""" return self._lower @property def upper(self): - """The upper bound of the range. None if empty or unbound.""" + """The upper bound of the range. `!None` if empty or unbound.""" return self._upper @property def isempty(self): - """True if the range is empty.""" + """`!True` if the range is empty.""" return self._bounds is None @property def lower_inf(self): - """True if the range doesn't have a lower bound.""" + """`!True` if the range doesn't have a lower bound.""" if self._bounds is None: return False return self._lower is None @property def upper_inf(self): - """True if the range doesn't have an upper bound.""" + """`!True` if the range doesn't have an upper bound.""" if self._bounds is None: return False return self._upper is None @property def lower_inc(self): - """True if the lower bound is included in the range.""" + """`!True` if the lower bound is included in the range.""" if self._bounds is None: return False if self._lower is None: return False return self._bounds[0] == '[' @property def upper_inc(self): - """True if the upper bound is included in the range.""" + """`!True` if the upper bound is included in the range.""" if self._bounds is None: return False if self._upper is None: return False return self._bounds[1] == ']' @@ -151,19 +138,30 @@ class Range(object): def register_range(pgrange, pyrange, conn_or_curs, globally=False): - """Register a typecaster and an adapter for range a range type. - - :param pgrange: the name of the PostgreSQL range type - :param pyrange: a Range (strict) subclass, or just the name to give it - (the class will be available as the `!range` attribute of the returned - `RangeCaster`. + """Create and register an adapter and the typecasters to convert between + a PostgreSQL |range|_ type and a PostgreSQL `Range` subclass. + + :param pgrange: the name of the PostgreSQL |range| type. Can be + schema-qualified + :param pyrange: a `Range` strict subclass, or just a name to give to a new + class :param conn_or_curs: a connection or cursor used to find the oid of the range and its subtype; the typecaster is registered in a scope limited to this object, unless *globally* is set to `!True` :param globally: if `!False` (default) register the typecaster only on *conn_or_curs*, otherwise register it globally - :return: the registered `RangeCaster` instance responsible for the - conversion + :return: `RangeCaster` instance responsible for the conversion + + If a string is passed to *pyrange*, a new `Range` subclass is created + with such name and will be available as the `~RangeCaster.range` attribute + of the returned `RangeCaster` object. + + The function queries the database on *conn_or_curs* to inspect the + *pgrange* type. Raise `~psycopg2.ProgrammingError` if the type is not + found. If querying the database is not advisable, use directly the + `RangeCaster` class and register the adapter and typecasters using the + provided functions. + """ caster = RangeCaster._from_db(pgrange, pyrange, conn_or_curs) caster._register(not globally and conn_or_curs or None) @@ -219,7 +217,12 @@ class RangeAdapter(object): class RangeCaster(object): - """Helper class to convert between `Range` and PostgreSQL range types.""" + """Helper class to convert between `Range` and PostgreSQL range types. + + Objects of this class are usually created by `register_range()`. Manual + creation could be useful if querying the database is not advisable: in + this case the oids must be provided. + """ def __init__(self, pgrange, pyrange, oid, subtype_oid, array_oid=None): self.subtype_oid = subtype_oid self._create_ranges(pgrange, pyrange) @@ -237,7 +240,9 @@ class RangeCaster(object): def _create_ranges(self, pgrange, pyrange): """Create Range and RangeAdapter classes if needed.""" # if got a string create a new RangeAdapter concrete type (with a name) - # else take it as an adapter. + # else take it as an adapter. Passing an adapter should be considered + # an implementation detail and is not documented. It is currently used + # for the numeric ranges. self.adapter = None if isinstance(pgrange, basestring): self.adapter = type(pgrange, (RangeAdapter,), {}) @@ -378,7 +383,11 @@ where typname = %s and ns.nspname = %s; class NumericRange(Range): - """A `Range` suitable to pass Python numeric types to a PostgreSQL range.""" + """A `Range` suitable to pass Python numeric types to a PostgreSQL range. + + PostgreSQL types :sql:`int4range`, :sql:`int8range`, :sql:`numrange` are + casted into `!NumericRange` instances. + """ pass class DateRange(Range):