Adding sql module documentation

This commit is contained in:
Daniele Varrazzo 2017-01-01 08:12:05 +01:00
parent 41b9bfe401
commit 4a55b8018a
3 changed files with 153 additions and 24 deletions

View File

@ -44,6 +44,7 @@ Psycopg 2 is both Unicode and Python 3 friendly.
advanced
extensions
extras
sql
tz
pool
errorcodes

View File

@ -31,8 +31,29 @@ from psycopg2 import extensions as ext
class Composable(object):
"""Base class for objects that can be used to compose an SQL string."""
"""
Abstract base class for objects that can be used to compose an SQL string.
Composables can be passed directly to `~cursor.execute()` and
`~cursor.executemany()`.
Composables can be joined using the ``+`` operator: the result will be
a `Composed` instance containing the objects joined. The operator ``*`` is
also supported with an integer argument: the result is a `!Composed`
instance containing the left argument repeated as many times as requested.
.. automethod:: as_string
"""
def as_string(self, conn_or_curs):
"""
Return the string value of the object.
The object is evaluated in the context of the *conn_or_curs* argument.
The function is automatically invoked by `~cursor.execute()` and
`~cursor.executemany()` if a `!Composable` is passed instead of the
query string.
"""
raise NotImplementedError
def __add__(self, other):
@ -43,8 +64,26 @@ class Composable(object):
else:
return NotImplemented
def __mul__(self, n):
return Composed([self] * n)
class Composed(Composable):
"""
A `Composable` object obtained concatenating a sequence of `Composable`.
The object is usually created using `compose()` and the `Composable`
operators. However it is possible to create a `!Composed` directly
specifying a sequence of `Composable` as arguments.
Example::
>>> sql.Composed([sql.SQL("insert into "), sql.Identifier("table")]) \\
... .as_string(conn)
'insert into "table"'
.. automethod:: join
"""
def __init__(self, seq):
self._seq = []
for i in seq:
@ -70,10 +109,20 @@ class Composed(Composable):
else:
return NotImplemented
def __mul__(self, n):
return Composed(self._seq * n)
def join(self, joiner):
"""
Return a new `!Composed` interposing the *joiner* with the `!Composed` items.
The *joiner* must be a `SQL` or a string which will be interpreted as
an `SQL`.
Example::
>>> fields = sql.Identifier('foo') + sql.Identifier('bar') # a Composed
>>> fields.join(', ').as_string(conn)
'"foo", "bar"'
"""
if isinstance(joiner, basestring):
joiner = SQL(joiner)
elif not isinstance(joiner, SQL):
@ -93,10 +142,15 @@ class Composed(Composable):
class SQL(Composable):
def __init__(self, wrapped):
if not isinstance(wrapped, basestring):
"""
A `Composable` representing a snippet of SQL string to be included verbatim.
.. automethod:: join
"""
def __init__(self, string):
if not isinstance(string, basestring):
raise TypeError("SQL values must be strings")
self._wrapped = wrapped
self._wrapped = string
def __repr__(self):
return "sql.SQL(%r)" % (self._wrapped,)
@ -104,10 +158,21 @@ class SQL(Composable):
def as_string(self, conn_or_curs):
return self._wrapped
def __mul__(self, n):
return Composed([self] * n)
def join(self, seq):
"""
Join a sequence of `Composable` or a `Composed` and return a `!Composed`.
Use the object value to separate the *seq* elements.
Example::
>>> snip - sql.SQL(', ').join(map(sql.Identifier, ['foo', 'bar', 'baz']))
>>> snip.as_string(conn)
'"foo", "bar", "baz"'
"""
if isinstance(seq, Composed):
seq = seq._seq
rv = []
it = iter(seq)
try:
@ -123,15 +188,21 @@ class SQL(Composable):
class Identifier(Composable):
def __init__(self, wrapped):
if not isinstance(wrapped, basestring):
"""
A `Composable` representing an SQL identifer.
Identifiers usually represent names of database objects, such as tables
or fields. They follow `different rules`__ than SQL string literals for
escaping (e.g. they use double quotes).
.. __: https://www.postgresql.org/docs/current/static/sql-syntax-lexical.html# \
SQL-SYNTAX-IDENTIFIERS
"""
def __init__(self, string):
if not isinstance(string, basestring):
raise TypeError("SQL identifiers must be strings")
self._wrapped = wrapped
@property
def wrapped(self):
return self._wrapped
self._wrapped = string
def __repr__(self):
return "sql.Identifier(%r)" % (self._wrapped,)
@ -141,6 +212,11 @@ class Identifier(Composable):
class Literal(Composable):
"""
Represent an SQL value to be included in a query.
The object follows the normal :ref:`adaptation rules <python-types-adaptation>`
"""
def __init__(self, wrapped):
self._wrapped = wrapped
@ -166,11 +242,31 @@ class Literal(Composable):
return rv
def __mul__(self, n):
return Composed([self] * n)
class Placeholder(Composable):
"""A `Composable` representing a placeholder for query parameters.
If the name is specified, generate a named placeholder (e.g. ``%(name)s``),
otherwise generate a positional placeholder (e.g. ``%s``).
The object is useful to generate SQL queries with a variable number of
arguments.
Examples::
>>> sql.compose("insert into table (%s) values (%s)", [
... sql.SQL(', ').join(map(sql.Identifier, names)),
... sql.SQL(', ').join(sql.Placeholder() * 3)
... ]).as_string(conn)
'insert into table ("foo", "bar", "baz") values (%s, %s, %s)'
>>> sql.compose("insert into table (%s) values (%s)", [
... sql.SQL(', ').join(map(sql.Identifier, names)),
... sql.SQL(', ').join(map(sql.Placeholder, names))
... ]).as_string(conn)
'insert into table ("foo", "bar", "baz") values (%(foo)s, %(bar)s, %(baz)s)'
"""
def __init__(self, name=None):
if isinstance(name, basestring):
if ')' in name:
@ -185,9 +281,6 @@ class Placeholder(Composable):
return "sql.Placeholder(%r)" % (
self._name if self._name is not None else '',)
def __mul__(self, n):
return Composed([self] * n)
def as_string(self, conn_or_curs):
if self._name is not None:
return "%%(%s)s" % self._name
@ -204,7 +297,37 @@ re_compose = re.compile("""
""", re.VERBOSE)
def compose(sql, args=()):
def compose(sql, args=None):
"""
Merge an SQL string with some variable parts.
The *sql* string can contain placeholders such as `%s` or `%(name)s`.
If the string must contain a literal ``%`` symbol use ``%%``. Note that,
unlike `~cursor.execute()`, the replacement ``%%`` |=>| ``%`` is *always*
performed, even if there is no argument.
.. |=>| unicode:: 0x21D2 .. double right arrow
*args* must be a sequence or mapping (according to the placeholder style)
of `Composable` instances.
The value returned is a `Composed` instance obtained replacing the
arguments to the query placeholders.
Example::
>>> query = sql.compose(
... "select %s from %s", [
... sql.SQL(', ').join(
... [sql.Identifier('foo'), sql.Identifier('bar')]),
... sql.Identifier('table')])
>>> query.as_string(conn)
select "foo", "bar" from "table"'
"""
if args is None:
args = ()
phs = list(re_compose.finditer(sql))
# check placeholders consistent

View File

@ -208,6 +208,11 @@ class SQLTests(ConnectingTestCase):
self.assert_(isinstance(obj, sql.Composed))
self.assertEqual(obj.as_string(self.conn), '"foo", bar, 42')
obj = sql.SQL(", ").join(
sql.Composed([sql.Identifier('foo'), sql.SQL('bar'), sql.Literal(42)]))
self.assert_(isinstance(obj, sql.Composed))
self.assertEqual(obj.as_string(self.conn), '"foo", bar, 42')
class ComposedTest(ConnectingTestCase):
def test_class(self):