Merge branch 'devel'

This commit is contained in:
Federico Di Gregorio 2011-02-13 12:28:18 +01:00
commit 3762c67cd4
17 changed files with 193 additions and 42 deletions

2
NEWS
View File

@ -28,6 +28,8 @@ What's new in psycopg 2.4
available. available.
- Added missing icon to ZPsycopgDA package, not available in Zope 2.12.9 - Added missing icon to ZPsycopgDA package, not available in Zope 2.12.9
(ticket #30). Bug report and patch by Pumukel. (ticket #30). Bug report and patch by Pumukel.
- Fixed conversion of negative infinity (ticket #40). Bug report and patch
by Marti Raudsepp.
What's new in psycopg 2.3.2 What's new in psycopg 2.3.2

View File

@ -16,7 +16,7 @@
# their work without bothering about the module dependencies. # their work without bothering about the module dependencies.
ALLOWED_PSYCOPG_VERSIONS = ('2.4-beta1',) ALLOWED_PSYCOPG_VERSIONS = ('2.4-beta1', '2.4-beta2')
import sys import sys
import time import time

View File

@ -89,14 +89,33 @@ by the `psycopg2.extensions.adapt()` function.
The `~cursor.execute()` method adapts its arguments to the The `~cursor.execute()` method adapts its arguments to the
`~psycopg2.extensions.ISQLQuote` protocol. Objects that conform to this `~psycopg2.extensions.ISQLQuote` protocol. Objects that conform to this
protocol expose a `!getquoted()` method returning the SQL representation protocol expose a `!getquoted()` method returning the SQL representation
of the object as a string. of the object as a string (the method must return `!bytes` in Python 3).
Optionally the conform object may expose a
`~psycopg2.extensions.ISQLQuote.prepare()` method.
The easiest way to adapt an object to an SQL string is to register an adapter There are two basic ways to have a Python object adapted to SQL:
function via the `~psycopg2.extensions.register_adapter()` function. The
adapter function must take the value to be adapted as argument and return a - the object itself is conform, or knows how to make itself conform. Such
conform object. A convenient object is the `~psycopg2.extensions.AsIs` object must expose a `__conform__()` method that will be called with the
wrapper, whose `!getquoted()` result is simply the `!str()`\ ing protocol object as argument. The object can check that the protocol is
conversion of the wrapped object. `!ISQLQuote`, in which case it can return `!self` (if the object also
implements `!getquoted()`) or a suitable wrapper object. This option is
viable if you are the author of the object and if the object is specifically
designed for the database (i.e. having Psycopg as a dependency and polluting
its interface with the required methods doesn't bother you). For a simple
example you can take a look to the source code for the
`psycopg2.extras.Inet` object.
- If implementing the `!ISQLQuote` interface directly in the object is not an
option, you can use an adaptation function, taking the object to be adapted
as argument and returning a conforming object. The adapter must be
registered via the `~psycopg2.extensions.register_adapter()` function. A
simple example wrapper is the `!psycopg2.extras.UUID_adapter` used by the
`~psycopg2.extras.register_uuid()` function.
A convenient object to write adapters is the `~psycopg2.extensions.AsIs`
wrapper, whose `!getquoted()` result is simply the `!str()`\ ing conversion of
the wrapped object.
.. index:: .. index::
single: Example; Types adaptation single: Example; Types adaptation

View File

@ -189,8 +189,9 @@ deal with Python objects adaptation:
.. method:: getquoted() .. method:: getquoted()
Subclasses or other conforming objects should return a valid SQL Subclasses or other conforming objects should return a valid SQL
string representing the wrapped object. The `!ISQLQuote` string representing the wrapped object. In Python 3 the SQL must be
implementation does nothing. returned in a `!bytes` object. The `!ISQLQuote` implementation does
nothing.
.. method:: prepare(conn) .. method:: prepare(conn)

View File

@ -94,6 +94,18 @@ Psycopg converts :sql:`decimal`\/\ :sql:`numeric` database types into Python `!D
documentation. If you find `!psycopg2.extensions.DECIMAL` not avalable, use documentation. If you find `!psycopg2.extensions.DECIMAL` not avalable, use
`!psycopg2._psycopg.DECIMAL` instead. `!psycopg2._psycopg.DECIMAL` instead.
Transferring binary data from PostgreSQL 9.0 doesn't work.
PostgreSQL 9.0 uses by default `the "hex" format`__ to transfer
:sql:`bytea` data: the format can't be parsed by the libpq 8.4 and
earlier. Three options to solve the problem are:
- set the bytea_output__ parameter to ``escape`` in the server;
- use ``SET bytea_output TO escape`` in the client before reading binary
data;
- upgrade the libpq library on the client to at least 9.0.
.. __: http://www.postgresql.org/docs/9.0/static/datatype-binary.html
.. __: http://www.postgresql.org/docs/9.0/static/runtime-config-client.html#GUC-BYTEA-OUTPUT
Best practices Best practices
-------------- --------------

View File

@ -233,15 +233,37 @@ the SQL string that would be sent to the database.
.. index:: .. index::
pair: Strings; Adaptation pair: Strings; Adaptation
single: Unicode; Adaptation single: Unicode; Adaptation
- String types: `!str`, `!unicode` are converted in SQL string syntax.
`!unicode` objects (`!str` in Python 3) are encoded in the connection
`~connection.encoding` to be sent to the backend: trying to send a character
not supported by the encoding will result in an error. Received data can be
converted either as `!str` or `!unicode`: see :ref:`unicode-handling` for
received, either `!str` or `!unicode`
.. index::
single: Buffer; Adaptation single: Buffer; Adaptation
single: bytea; Adaptation single: bytea; Adaptation
single: Binary string single: Binary string
- String types: `!str`, `!unicode` are converted in SQL string - Binary types: Python types such as `!bytes`, `!bytearray`, `!buffer`,
syntax. `!buffer` is converted in PostgreSQL binary string syntax, `!memoryview` are converted in PostgreSQL binary string syntax, suitable for
suitable for :sql:`bytea` fields. When reading textual fields, either :sql:`bytea` fields. Received data is returned as `!buffer` (in Python 2) or
`!str` or `!unicode` can be received: see `!memoryview` (in Python 3).
:ref:`unicode-handling`.
.. warning::
PostgreSQL 9 uses by default `a new "hex" format`__ to emit :sql:`bytea`
fields. Unfortunately this format can't be parsed by libpq versions
before 9.0. This means that using a library client with version lesser
than 9.0 to talk with a server 9.0 or later you may have problems
receiving :sql:`bytea` data. To work around this problem you can set the
`bytea_output`__ parameter to ``escape``, either in the server
configuration or in the client session using a query such as ``SET
bytea_output TO escape;`` before trying to receive binary data.
.. __: http://www.postgresql.org/docs/9.0/static/datatype-binary.html
.. __: http://www.postgresql.org/docs/9.0/static/runtime-config-client.html#GUC-BYTEA-OUTPUT
.. index:: .. index::
single: Adaptation; Date/Time objects single: Adaptation; Date/Time objects
@ -338,8 +360,8 @@ defined on the database connection (the `PostgreSQL encoding`__, available in
.. __: http://www.postgresql.org/docs/9.0/static/multibyte.html .. __: http://www.postgresql.org/docs/9.0/static/multibyte.html
.. __: http://docs.python.org/library/codecs.html#standard-encodings .. __: http://docs.python.org/library/codecs.html#standard-encodings
When reading data from the database, the strings returned are usually 8 bit When reading data from the database, in Python 2 the strings returned are
`!str` objects encoded in the database client encoding:: usually 8 bit `!str` objects encoded in the database client encoding::
>>> print conn.encoding >>> print conn.encoding
UTF8 UTF8
@ -356,9 +378,10 @@ When reading data from the database, the strings returned are usually 8 bit
>>> print type(x), repr(x) >>> print type(x), repr(x)
<type 'str'> '\xe0\xe8\xec\xf2\xf9\xa4' <type 'str'> '\xe0\xe8\xec\xf2\xf9\xa4'
In order to obtain `!unicode` objects instead, it is possible to In Python 3 instead the strings are automatically *decoded* in the connection
register a typecaster so that PostgreSQL textual types are automatically `~connection.encoding`, as the `!str` object can represent Unicode characters.
*decoded* using the current client encoding:: In Python 2 you must register a :ref:`typecaster
<type-casting-from-sql-to-python>` in order to receive `!unicode` objects::
>>> psycopg2.extensions.register_type(psycopg2.extensions.UNICODE, cur) >>> psycopg2.extensions.register_type(psycopg2.extensions.UNICODE, cur)
@ -375,9 +398,9 @@ the connection or globally: see the function
.. note:: .. note::
If you want to receive uniformly all your database input in Unicode, you In Python 2, if you want to receive uniformly all your database input in
can register the related typecasters globally as soon as Psycopg is Unicode, you can register the related typecasters globally as soon as
imported:: Psycopg is imported::
import psycopg2 import psycopg2
import psycopg2.extensions import psycopg2.extensions

View File

@ -835,14 +835,22 @@ class CompositeCaster(object):
# Store the transaction status of the connection to revert it after use # Store the transaction status of the connection to revert it after use
conn_status = conn.status conn_status = conn.status
# Use the correct schema
if '.' in name:
schema, tname = name.split('.', 1)
else:
tname = name
schema = 'public'
# get the type oid and attributes # get the type oid and attributes
curs.execute("""\ curs.execute("""\
SELECT t.oid, attname, atttypid SELECT t.oid, attname, atttypid
FROM pg_type t FROM pg_type t
JOIN pg_namespace ns ON typnamespace = ns.oid JOIN pg_namespace ns ON typnamespace = ns.oid
JOIN pg_attribute a ON attrelid = typrelid JOIN pg_attribute a ON attrelid = typrelid
WHERE typname = %s and nspname = 'public'; WHERE typname = %s and nspname = %s
""", (name, )) ORDER BY attnum;
""", (tname, schema))
recs = curs.fetchall() recs = curs.fetchall()
@ -858,7 +866,7 @@ WHERE typname = %s and nspname = 'public';
type_oid = recs[0][0] type_oid = recs[0][0]
type_attrs = [ (r[1], r[2]) for r in recs ] type_attrs = [ (r[1], r[2]) for r in recs ]
return CompositeCaster(name, type_oid, type_attrs) return CompositeCaster(tname, type_oid, type_attrs)
def register_composite(name, conn_or_curs, globally=False): def register_composite(name, conn_or_curs, globally=False):
"""Register a typecaster to convert a composite type into a tuple. """Register a typecaster to convert a composite type into a tuple.

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"></requestedExecutionLevel>
</requestedPrivileges>
</security>
</trustInfo>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.VC90.CRT" version="9.0.21022.8" processorArchitecture="amd64" publicKeyToken="1fc8b3b9a1e18e3b"></assemblyIdentity>
</dependentAssembly>
</dependency>
</assembly>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"></requestedExecutionLevel>
</requestedPrivileges>
</security>
</trustInfo>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.VC90.CRT" version="9.0.21022.8" processorArchitecture="x86" publicKeyToken="1fc8b3b9a1e18e3b"></assemblyIdentity>
</dependentAssembly>
</dependency>
</assembly>

View File

@ -42,8 +42,12 @@ pfloat_getquoted(pfloatObject *self, PyObject *args)
double n = PyFloat_AsDouble(self->wrapped); double n = PyFloat_AsDouble(self->wrapped);
if (isnan(n)) if (isnan(n))
rv = Bytes_FromString("'NaN'::float"); rv = Bytes_FromString("'NaN'::float");
else if (isinf(n)) else if (isinf(n)) {
rv = Bytes_FromString("'Infinity'::float"); if (n > 0)
rv = Bytes_FromString("'Infinity'::float");
else
rv = Bytes_FromString("'-Infinity'::float");
}
else { else {
rv = PyObject_Repr(self->wrapped); rv = PyObject_Repr(self->wrapped);

View File

@ -54,6 +54,8 @@ from distutils.errors import DistutilsFileError
from distutils.command.build_ext import build_ext from distutils.command.build_ext import build_ext
from distutils.sysconfig import get_python_inc from distutils.sysconfig import get_python_inc
from distutils.ccompiler import get_default_compiler from distutils.ccompiler import get_default_compiler
from distutils.dep_util import newer_group
from distutils.util import get_platform
try: try:
from distutils.command.build_py import build_py_2to3 as build_py from distutils.command.build_py import build_py_2to3 as build_py
except ImportError: except ImportError:
@ -73,7 +75,7 @@ except ImportError:
# Take a look at http://www.python.org/dev/peps/pep-0386/ # Take a look at http://www.python.org/dev/peps/pep-0386/
# for a consistent versioning pattern. # for a consistent versioning pattern.
PSYCOPG_VERSION = '2.4-beta1' PSYCOPG_VERSION = '2.4-beta2'
version_flags = ['dt', 'dec'] version_flags = ['dt', 'dec']
@ -149,6 +151,23 @@ class psycopg_build_ext(build_ext):
def get_pg_config(self, kind): def get_pg_config(self, kind):
return get_pg_config(kind, self.pg_config) return get_pg_config(kind, self.pg_config)
def build_extension(self, ext):
build_ext.build_extension(self, ext)
# For MSVC compiler and Python 2.6/2.7 (aka VS 2008), re-insert the
# Manifest into the resulting .pyd file.
sysVer = sys.version_info[:2]
if self.get_compiler().lower().startswith('msvc') and \
sysVer in ((2,6), (2,7)):
platform = get_platform()
# Default to the x86 manifest
manifest = '_psycopg.vc9.x86.manifest'
if platform == 'win-amd64':
manifest = '_psycopg.vc9.amd64.manifest'
self.compiler.spawn(['mt.exe', '-nologo', '-manifest',
os.path.join('psycopg', manifest),
'-outputresource:%s;2' % (os.path.join(self.build_lib, 'psycopg2', '_psycopg.pyd'))])
def finalize_win32(self): def finalize_win32(self):
"""Finalize build system configuration on win32 platform.""" """Finalize build system configuration on win32 platform."""
import struct import struct

View File

@ -143,7 +143,7 @@ class ConnectionTests(unittest.TestCase):
def test_weakref(self): def test_weakref(self):
from weakref import ref from weakref import ref
conn = psycopg2.connect(self.conn.dsn) conn = psycopg2.connect(dsn)
w = ref(conn) w = ref(conn)
conn.close() conn.close()
del conn del conn

View File

@ -136,9 +136,9 @@ class CursorTests(unittest.TestCase):
# timestamp will not be influenced by the pause in Python world. # timestamp will not be influenced by the pause in Python world.
curs.execute("""select clock_timestamp() from generate_series(1,2)""") curs.execute("""select clock_timestamp() from generate_series(1,2)""")
i = iter(curs) i = iter(curs)
t1 = i.next()[0] t1 = (i.next())[0] # the brackets work around a 2to3 bug
time.sleep(0.2) time.sleep(0.2)
t2 = i.next()[0] t2 = (i.next())[0]
self.assert_((t2 - t1).microseconds * 1e-6 < 0.1, self.assert_((t2 - t1).microseconds * 1e-6 < 0.1,
"named cursor records fetched in 2 roundtrips (delta: %s)" "named cursor records fetched in 2 roundtrips (delta: %s)"
% (t2 - t1)) % (t2 - t1))

View File

@ -6,6 +6,7 @@ dbname = os.environ.get('PSYCOPG2_TESTDB', 'psycopg2_test')
dbhost = os.environ.get('PSYCOPG2_TESTDB_HOST', None) dbhost = os.environ.get('PSYCOPG2_TESTDB_HOST', None)
dbport = os.environ.get('PSYCOPG2_TESTDB_PORT', None) dbport = os.environ.get('PSYCOPG2_TESTDB_PORT', None)
dbuser = os.environ.get('PSYCOPG2_TESTDB_USER', None) dbuser = os.environ.get('PSYCOPG2_TESTDB_USER', None)
dbpass = os.environ.get('PSYCOPG2_TESTDB_PASSWORD', None)
# Check if we want to test psycopg's green path. # Check if we want to test psycopg's green path.
green = os.environ.get('PSYCOPG2_TEST_GREEN', None) green = os.environ.get('PSYCOPG2_TEST_GREEN', None)
@ -29,5 +30,7 @@ if dbport is not None:
dsn += ' port=%s' % dbport dsn += ' port=%s' % dbport
if dbuser is not None: if dbuser is not None:
dsn += ' user=%s' % dbuser dsn += ' user=%s' % dbuser
if dbpass is not None:
dsn += ' password=%s' % dbpass

View File

@ -1,4 +1,5 @@
# testutils.py - utility module for psycopg2 testing. # testutils.py - utility module for psycopg2 testing.
# #
# Copyright (C) 2010-2011 Daniele Varrazzo <daniele.varrazzo@gmail.com> # Copyright (C) 2010-2011 Daniele Varrazzo <daniele.varrazzo@gmail.com>
# #
@ -190,21 +191,24 @@ def script_to_py3(script):
return script return script
import tempfile import tempfile
f = tempfile.NamedTemporaryFile(suffix=".py") f = tempfile.NamedTemporaryFile(suffix=".py", delete=False)
f.write(script.encode()) f.write(script.encode())
f.flush() f.flush()
filename = f.name
f.close()
# 2to3 is way too chatty # 2to3 is way too chatty
import logging import logging
logging.basicConfig(filename=os.devnull) logging.basicConfig(filename=os.devnull)
from lib2to3.main import main from lib2to3.main import main
if main("lib2to3.fixes", ['--no-diffs', '-w', '-n', f.name]): if main("lib2to3.fixes", ['--no-diffs', '-w', '-n', filename]):
raise Exception('py3 conversion failed') raise Exception('py3 conversion failed')
f2 = open(f.name) f2 = open(filename)
try: try:
return f2.read() return f2.read()
finally: finally:
f2.close() f2.close()
os.remove(filename)

View File

@ -113,6 +113,9 @@ class TypesBasicTests(unittest.TestCase):
self.failUnless(str(s) == "inf", "wrong float quoting: " + str(s)) self.failUnless(str(s) == "inf", "wrong float quoting: " + str(s))
self.failUnless(type(s) == float, "wrong float conversion: " + repr(s)) self.failUnless(type(s) == float, "wrong float conversion: " + repr(s))
s = self.execute("SELECT %s AS foo", (float("-inf"),))
self.failUnless(str(s) == "-inf", "wrong float quoting: " + str(s))
def testBinary(self): def testBinary(self):
if sys.version_info[0] < 3: if sys.version_info[0] < 3:
s = ''.join([chr(x) for x in range(256)]) s = ''.join([chr(x) for x in range(256)])

View File

@ -267,7 +267,7 @@ class HstoreTestCase(unittest.TestCase):
oids = HstoreAdapter.get_oids(self.conn) oids = HstoreAdapter.get_oids(self.conn)
try: try:
register_hstore(self.conn, globally=True) register_hstore(self.conn, globally=True)
conn2 = psycopg2.connect(self.conn.dsn) conn2 = psycopg2.connect(dsn)
try: try:
cur2 = self.conn.cursor() cur2 = self.conn.cursor()
cur2.execute("select 'a => b'::hstore") cur2.execute("select 'a => b'::hstore")
@ -489,8 +489,8 @@ class AdaptTypeTestCase(unittest.TestCase):
def test_register_on_connection(self): def test_register_on_connection(self):
self._create_type("type_ii", [("a", "integer"), ("b", "integer")]) self._create_type("type_ii", [("a", "integer"), ("b", "integer")])
conn1 = psycopg2.connect(self.conn.dsn) conn1 = psycopg2.connect(dsn)
conn2 = psycopg2.connect(self.conn.dsn) conn2 = psycopg2.connect(dsn)
try: try:
psycopg2.extras.register_composite("type_ii", conn1) psycopg2.extras.register_composite("type_ii", conn1)
curs1 = conn1.cursor() curs1 = conn1.cursor()
@ -507,8 +507,8 @@ class AdaptTypeTestCase(unittest.TestCase):
def test_register_globally(self): def test_register_globally(self):
self._create_type("type_ii", [("a", "integer"), ("b", "integer")]) self._create_type("type_ii", [("a", "integer"), ("b", "integer")])
conn1 = psycopg2.connect(self.conn.dsn) conn1 = psycopg2.connect(dsn)
conn2 = psycopg2.connect(self.conn.dsn) conn2 = psycopg2.connect(dsn)
try: try:
t = psycopg2.extras.register_composite("type_ii", conn1, globally=True) t = psycopg2.extras.register_composite("type_ii", conn1, globally=True)
try: try:
@ -525,6 +525,24 @@ class AdaptTypeTestCase(unittest.TestCase):
conn1.close() conn1.close()
conn2.close() conn2.close()
@skip_if_no_composite
def test_composite_namespace(self):
curs = self.conn.cursor()
curs.execute("""
select nspname from pg_namespace
where nspname = 'typens';
""")
if not curs.fetchone():
curs.execute("create schema typens;")
self.conn.commit()
self._create_type("typens.typens_ii",
[("a", "integer"), ("b", "integer")])
t = psycopg2.extras.register_composite(
"typens.typens_ii", self.conn)
curs.execute("select (4,8)::typens.typens_ii")
self.assertEqual(curs.fetchone()[0], (4,8))
def _create_type(self, name, fields): def _create_type(self, name, fields):
curs = self.conn.cursor() curs = self.conn.cursor()
try: try:
@ -534,11 +552,16 @@ class AdaptTypeTestCase(unittest.TestCase):
curs.execute("create type %s as (%s);" % (name, curs.execute("create type %s as (%s);" % (name,
", ".join(["%s %s" % p for p in fields]))) ", ".join(["%s %s" % p for p in fields])))
if '.' in name:
schema, name = name.split('.')
else:
schema = 'public'
curs.execute("""\ curs.execute("""\
SELECT t.oid SELECT t.oid
FROM pg_type t JOIN pg_namespace ns ON typnamespace = ns.oid FROM pg_type t JOIN pg_namespace ns ON typnamespace = ns.oid
WHERE typname = %s and nspname = 'public'; WHERE typname = %s and nspname = %s;
""", (name,)) """, (name, schema))
oid = curs.fetchone()[0] oid = curs.fetchone()[0]
self.conn.commit() self.conn.commit()
return oid return oid