From 323340abc60bc2a3d360737e487d66b169cc88b8 Mon Sep 17 00:00:00 2001 From: Jonathan Ballet Date: Sat, 13 Feb 2010 17:06:39 +0100 Subject: [PATCH] The documentation is now mostly doctest-able --- doc/advanced.rst | 104 ++++++++++++++++++++++++++------------------- doc/conf.py | 31 +++++++++++++- doc/connection.rst | 13 +++++- doc/cursor.rst | 48 +++++++++++++-------- doc/extensions.rst | 7 ++- doc/extras.rst | 37 ++++++++-------- doc/module.rst | 11 +++-- doc/usage.rst | 2 +- 8 files changed, 166 insertions(+), 87 deletions(-) diff --git a/doc/advanced.rst b/doc/advanced.rst index 8bdbb460..8f5dcf33 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -10,6 +10,13 @@ More advanced topics .. _subclassing-connection: .. _subclassing-cursor: +.. testsetup:: * + + import re + + cur.execute("CREATE TABLE atable (apoint point)") + conn.commit() + Connection and cursor factories ------------------------------- @@ -42,8 +49,8 @@ An example of cursor subclass performing logging is:: raise conn = psycopg2.connect(DSN) - curs = conn.cursor(cursor_factory=LoggingCursor) - curs.execute("INSERT INTO mytable VALUES (%s, %s, %s);", + cur = conn.cursor(cursor_factory=LoggingCursor) + cur.execute("INSERT INTO mytable VALUES (%s, %s, %s);", (10, 20, 30)) @@ -78,22 +85,24 @@ conversion of the wrapped object. single: Example; Types adaptation Example: mapping of a :class:`!Point` class into the |point|_ PostgreSQL -geometric type:: +geometric type: - from psycopg2.extensions import adapt, register_adapter, AsIs +.. doctest:: - class Point(object): - def __init__(self, x, y): - self.x = x - self.y = y + >>> from psycopg2.extensions import adapt, register_adapter, AsIs - def adapt_point(point): - return AsIs("'(%s, %s)'" % (adapt(point.x), adapt(point.y))) + >>> class Point(object): + ... def __init__(self, x, y): + ... self.x = x + ... self.y = y - register_adapter(Point, adapt_point) + >>> def adapt_point(point): + ... return AsIs("'(%s, %s)'" % (adapt(point.x), adapt(point.y))) - curs.execute("INSERT INTO atable (apoint) VALUES (%s)", - (Point(1.23, 4.56),)) + >>> register_adapter(Point, adapt_point) + + >>> cur.execute("INSERT INTO atable (apoint) VALUES (%s)", + ... (Point(1.23, 4.56),)) .. |point| replace:: :sql:`point` @@ -117,55 +126,54 @@ through an user-defined adapting function. An adapter function takes two arguments: the object string representation as returned by PostgreSQL and the cursor currently being read, and should return a new Python object. For example, the following function parses the PostgreSQL :sql:`point` -representation into the previously defined :class:`!Point` class:: +representation into the previously defined :class:`!Point` class: - def cast_point(value, curs): - if value is None: - return None - - # Convert from (f1, f2) syntax using a regular expression. - m = re.match(r"\(([^)]+),([^)]+)\)", value) - if m: - return Point(float(m.group(1)), float(m.group(2))) - else: - raise InterfaceError("bad point representation: %r" % value) + >>> def cast_point(value, cur): + ... if value is None: + ... return None + ... + ... # Convert from (f1, f2) syntax using a regular expression. + ... m = re.match(r"\(([^)]+),([^)]+)\)", value) + ... if m: + ... return Point(float(m.group(1)), float(m.group(2))) + ... else: + ... raise InterfaceError("bad point representation: %r" % value) In order to create a mapping from a PostgreSQL type (either standard or user-defined), its OID must be known. It can be retrieved either by the second -column of the :attr:`cursor.description`:: +column of the :attr:`cursor.description`: - curs.execute("SELECT NULL::point") - point_oid = curs.description[0][1] # usually returns 600 + >>> cur.execute("SELECT NULL::point") + >>> point_oid = cur.description[0][1] # usually returns 600 or by querying the system catalogs for the type name and namespace (the -namespace for system objects is :sql:`pg_catalog`):: +namespace for system objects is :sql:`pg_catalog`): - curs.execute(""" - SELECT pg_type.oid - FROM pg_type JOIN pg_namespace - ON typnamespace = pg_namespace.oid - WHERE typname = %(typename)s - AND nspname = %(namespace)s""", - {'typename': 'point', 'namespace': 'pg_catalog'}) + >>> cur.execute(""" + ... SELECT pg_type.oid + ... FROM pg_type JOIN pg_namespace + ... ON typnamespace = pg_namespace.oid + ... WHERE typname = %(typename)s + ... AND nspname = %(namespace)s""", + ... {'typename': 'point', 'namespace': 'pg_catalog'}) + >>> point_oid = cur.fetchone()[0] - point_oid = curs.fetchone()[0] +After you know the object OID, you must can and register the new type: -After you know the object OID, you must can and register the new type:: - - POINT = psycopg2.extensions.new_type((point_oid,), "POINT", cast_point) - psycopg2.extensions.register_type(POINT) + >>> POINT = psycopg2.extensions.new_type((point_oid,), "POINT", cast_point) + >>> psycopg2.extensions.register_type(POINT) The :func:`~psycopg2.extensions.new_type` function binds the object OIDs (more than one can be specified) to the adapter function. :func:`~psycopg2.extensions.register_type` completes the spell. Conversion is automatically performed when a column whose type is a registered OID is -read:: +read: - >>> curs.execute("SELECT '(10.2,20.3)'::point") - >>> point = curs.fetchone()[0] + >>> cur.execute("SELECT '(10.2,20.3)'::point") + >>> point = cur.fetchone()[0] >>> print type(point), point.x, point.y - 10.2 20.3 + 10.2 20.3 @@ -318,3 +326,11 @@ call:: print row +.. testcode:: + :hide: + + conn.rollback() + cur.execute("DROP TABLE atable") + conn.commit() + cur.close() + conn.close() diff --git a/doc/conf.py b/doc/conf.py index 5622b34f..5f23a29f 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -22,7 +22,8 @@ sys.path.append(os.path.abspath('.')) # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.ifconfig' ] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.ifconfig', + 'sphinx.ext.doctest'] # Specific extensions for Psycopg documentation. extensions += [ 'dbapi_extension', 'sql_role' ] @@ -222,3 +223,31 @@ latex_documents = [ # If false, no module index is generated. #latex_use_modindex = True + + +doctest_global_setup = """ + +import os +import psycopg2 + +def test_connect(): + try: + dsn = os.environ['PSYCOPG2_DSN'] + except KeyError: + assert False, "You need to set the environment variable PSYCOPG2_DSN" \ + " in order to test the documentation!" + return psycopg2.connect(dsn) + +conn = test_connect() +cur = conn.cursor() + +def create_test_table(): + try: + cur.execute("CREATE TABLE test (id SERIAL PRIMARY KEY, num INT, data TEXT)") + except: + conn.rollback() + cur.execute("DELETE FROM test") + cur.execute("SELECT setval('test_id_seq', 1, False)") + conn.commit() + +""" diff --git a/doc/connection.rst b/doc/connection.rst index b2428b19..f285a6ab 100644 --- a/doc/connection.rst +++ b/doc/connection.rst @@ -3,6 +3,11 @@ The ``connection`` class .. sectionauthor:: Daniele Varrazzo +.. testsetup:: + + from pprint import pprint + import psycopg2.extensions + .. class:: connection Handles the connection to a PostgreSQL database instance. It encapsulates @@ -155,12 +160,16 @@ The ``connection`` class .. attribute:: notices A list containing all the database messages sent to the client during - the session. :: + the session. + + .. doctest:: + :options: NORMALIZE_WHITESPACE >>> cur.execute("CREATE TABLE foo (id serial PRIMARY KEY);") - >>> conn.notices + >>> pprint(conn.notices) ['NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "foo_pkey" for table "foo"\n', 'NOTICE: CREATE TABLE will create implicit sequence "foo_id_seq" for serial column "foo.id"\n'] + >>> cur.execute("DROP TABLE foo;") To avoid a leak in case excessive notices are generated, only the last 50 messages are kept. diff --git a/doc/cursor.rst b/doc/cursor.rst index 5f9eea62..b46f1295 100644 --- a/doc/cursor.rst +++ b/doc/cursor.rst @@ -3,6 +3,19 @@ The ``cursor`` class .. sectionauthor:: Daniele Varrazzo +.. testsetup:: * + + from StringIO import StringIO + import sys + + create_test_table() + + # initial data + cur.executemany("INSERT INTO test (num, data) VALUES (%s, %s)", + [(100, "abc'def"), (None, 'dada'), (42, 'bar')]) + conn.commit() + + .. class:: cursor Allows Python code to execute PostgreSQL command in a database session. @@ -123,7 +136,7 @@ The ``cursor`` class Return a query string after arguments binding. The string returned is exactly the one that would be sent to the database running the - :meth:`~cursor.execute` method or similar. :: + :meth:`~cursor.execute` method or similar. >>> cur.mogrify("INSERT INTO test (num, data) VALUES (%s, %s)", (42, 'bar')) "INSERT INTO test (num, data) VALUES (42, E'bar')" @@ -189,7 +202,7 @@ The ``cursor`` class :class:`cursor` objects are iterable, so, instead of calling explicitly :meth:`~cursor.fetchone` in a loop, the object itself can - be used:: + be used: >>> cur.execute("SELECT * FROM test;") >>> for record in cur: @@ -197,17 +210,17 @@ The ``cursor`` class ... (1, 100, "abc'def") (2, None, 'dada') - (4, 42, 'bar') + (3, 42, 'bar') .. method:: fetchone() Fetch the next row of a query result set, returning a single tuple, - or ``None`` when no more data is available:: + or ``None`` when no more data is available: - >>> cur.execute("SELECT * FROM test WHERE id = %s", (4,)) + >>> cur.execute("SELECT * FROM test WHERE id = %s", (3,)) >>> cur.fetchone() - (4, 42, 'bar') + (3, 42, 'bar') A :exc:`~psycopg2.ProgrammingError` is raised if the previous call to |execute*|_ did not produce any result set or no call was issued @@ -224,13 +237,13 @@ The ``cursor`` class the number of rows to be fetched. The method should try to fetch as many rows as indicated by the size parameter. If this is not possible due to the specified number of rows not being available, fewer rows - may be returned:: + may be returned: >>> cur.execute("SELECT * FROM test;") >>> cur.fetchmany(2) [(1, 100, "abc'def"), (2, None, 'dada')] >>> cur.fetchmany(2) - [(4, 42, 'bar')] + [(3, 42, 'bar')] >>> cur.fetchmany(2) [] @@ -252,7 +265,7 @@ The ``cursor`` class >>> cur.execute("SELECT * FROM test;") >>> cur.fetchall() - [(1, 100, "abc'def"), (2, None, 'dada'), (4, 42, 'bar')] + [(1, 100, "abc'def"), (2, None, 'dada'), (3, 42, 'bar')] A :exc:`~psycopg2.ProgrammingError` is raised if the previous call to |execute*|_ did not produce any result set or no call was issued yet. @@ -278,7 +291,7 @@ The ``cursor`` class According to the |DBAPI|_, the exception raised for a cursor out of bound should have been :exc:`!IndexError`. The best option is - probably to catch both exceptions in your code:: + probably to catch both exceptions in your code: try: cur.scroll(1000 * 1000) @@ -359,7 +372,7 @@ The ``cursor`` class Read-only attribute containing the body of the last query sent to the backend (including bound arguments). ``None`` if no query has been - executed yet:: + executed yet: >>> cur.execute("INSERT INTO test (num, data) VALUES (%s, %s)", (42, 'bar')) >>> cur.query @@ -373,7 +386,7 @@ The ``cursor`` class .. attribute:: statusmessage Read-only attribute containing the message returned by the last - command:: + command: >>> cur.execute("INSERT INTO test (num, data) VALUES (%s, %s)", (42, 'bar')) >>> cur.statusmessage @@ -429,14 +442,13 @@ The ``cursor`` class The :obj:`!columns` argument is a sequence containing the name of the fields where the read data will be entered. Its length and column type should match the content of the read file. If not specifies, it - is assumed that the entire table matches the file structure. :: + is assumed that the entire table matches the file structure. >>> f = StringIO("42\tfoo\n74\tbar\n") >>> cur.copy_from(f, 'test', columns=('num', 'data')) - >>> cur.execute("select * from test where id > 5;") >>> cur.fetchall() - [(7, 42, 'foo'), (8, 74, 'bar')] + [(6, 42, 'foo'), (7, 74, 'bar')] .. versionchanged:: 2.0.6 added the :obj:`columns` parameter. @@ -452,11 +464,12 @@ The ``cursor`` class :obj:`!null` represents :sql:`NULL` values in the file. The :obj:`!columns` argument is a sequence of field names: if not - ``None`` only the specified fields will be included in the dump. :: + ``None`` only the specified fields will be included in the dump. >>> cur.copy_to(sys.stdout, 'test', sep="|") 1|100|abc'def 2|\N|dada + ... .. versionchanged:: 2.0.6 added the :obj:`columns` parameter. @@ -472,12 +485,13 @@ The ``cursor`` class open, writeable file for :sql:`COPY TO`. The optional :obj:`!size` argument, when specified for a :sql:`COPY FROM` statement, will be passed to :obj:`!file`\ 's read method to control the read buffer - size. :: + size. >>> cur.copy_expert("COPY test TO STDOUT WITH CSV HEADER", sys.stdout) id,num,data 1,100,abc'def 2,,dada + ... .. |COPY| replace:: :sql:`COPY` .. __: http://www.postgresql.org/docs/8.4/static/sql-copy.html diff --git a/doc/extensions.rst b/doc/extensions.rst index 465a9245..59bbc6ed 100644 --- a/doc/extensions.rst +++ b/doc/extensions.rst @@ -5,6 +5,11 @@ .. module:: psycopg2.extensions +.. testsetup:: * + + from psycopg2.extensions import AsIs, QuotedString, ISOLATION_LEVEL_AUTOCOMMIT + from psycopg2._psycopg import Binary + The module contains a few objects and function extending the minimum set of functionalities defined by the |DBAPI|_. @@ -94,7 +99,7 @@ deal with Python objects adaptation: .. method:: getquoted() - Return the :meth:`str` conversion of the wrapped object. :: + Return the :meth:`str` conversion of the wrapped object. >>> AsIs(42).getquoted() '42' diff --git a/doc/extras.rst b/doc/extras.rst index f723b28e..4ee673b6 100644 --- a/doc/extras.rst +++ b/doc/extras.rst @@ -5,6 +5,12 @@ .. module:: psycopg2.extras +.. testsetup:: + + import psycopg2.extras + + create_test_table() + This module is a generic place used to hold little helper functions and classes until a better place in the distribution is found. @@ -22,11 +28,13 @@ similar to the Python dictionaries instead of the tuples. You can use it either passing :class:`DictConnection` as :obj:`!connection_factory` argument to the :func:`~psycopg2.connect` function or passing :class:`DictCursor` as the :class:`!cursor_factory` argument to the :meth:`~connection.cursor` method -of a regular :class:`connection`. :: +of a regular :class:`connection`. - >>> conn = psycopg2.connect(database='test') - >>> cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) - >>> cur.execute("SELECT * FROM test") + >>> dict_cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) + >>> dict_cur.execute("INSERT INTO test (num, data) VALUES(%s, %s)", + ... (100, "abc'def")) + >>> dict_cur.execute("SELECT * FROM test") + >>> rec = dict_cur.fetchone() >>> rec['id'] 1 >>> rec['num'] @@ -85,17 +93,15 @@ UUID data type .. versionadded:: 2.0.9 .. versionchanged:: 2.0.13 added UUID array support. -:: - >>> psycopg2.extras.register_uuid() - - # Python UUID can be used in SQL queries + + >>> + >>> # Python UUID can be used in SQL queries + >>> import uuid >>> psycopg2.extensions.adapt(uuid.uuid4()).getquoted() - "'1ad2b180-c17e-46ad-ad94-e1f0dfc7c34b'::uuid" - - # PostgreSQL UUID are transformed into Python UUID objects. - >>> conn = psycopg2.connect(database='test') - >>> cur = conn.cursor() + "'...-...-...-...-...'::uuid" + >>> + >>> # PostgreSQL UUID are transformed into Python UUID objects. >>> cur.execute("SELECT 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::uuid") >>> cur.fetchone()[0] UUID('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11') @@ -112,12 +118,9 @@ UUID data type INET data type -------------- -:: >>> psycopg2.extras.register_inet() - - >>> conn = psycopg2.connect(database='test') - >>> cur = conn.cursor() + >>> cur.execute("SELECT '192.168.0.1'::inet") >>> cur.fetchone()[0].addr '192.168.0.1' diff --git a/doc/module.rst b/doc/module.rst index be3bfe31..3bb7559f 100644 --- a/doc/module.rst +++ b/doc/module.rst @@ -107,15 +107,18 @@ available through the following exceptions: .. extension:: The :attr:`~Error.pgerror` and :attr:`~Error.pgcode` attributes are - Psycopg extensions. :: + Psycopg extensions. + + .. doctest:: + :options: +NORMALIZE_WHITESPACE >>> try: ... cur.execute("SELECT * FROM barf") - >>> except Exception, e: - .... pass + ... except Exception, e: + ... pass >>> e.pgcode - >>> '42P01' + '42P01' >>> print e.pgerror ERROR: relation "barf" does not exist LINE 1: SELECT * FROM barf diff --git a/doc/usage.rst b/doc/usage.rst index e7212d98..9d86d6ed 100644 --- a/doc/usage.rst +++ b/doc/usage.rst @@ -15,7 +15,7 @@ basic commands:: # Connect to an existing database >>> conn = psycopg2.connect("dbname=test user=postgres") - # Open a curstor to perform database operations + # Open a cursor to perform database operations >>> cur = conn.cursor() # Execute a command: this creates a new table