Merge branch 'master' into feature/replication-protocol

This commit is contained in:
Oleksandr Shulgin 2015-10-15 12:27:43 +02:00
commit 8e518d4954
10 changed files with 156 additions and 19 deletions

12
NEWS
View File

@ -14,14 +14,19 @@ New features:
- The attributes `~connection.notices` and `~connection.notifies` can be - The attributes `~connection.notices` and `~connection.notifies` can be
customized replacing them with any object exposing an `!append()` method customized replacing them with any object exposing an `!append()` method
(:ticket:`#326`). (:ticket:`#326`).
- Added `~psycopg2.extensions.quote_ident()` function (:ticket:`#359`).
What's new in psycopg 2.6.2 What's new in psycopg 2.6.2
^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Report the server response status on errors (such as :ticket:`#281`). - Report the server response status on errors (such as :ticket:`#281`).
- Raise NotSupportedError on unhandled server response status - The `~psycopg2.extras.wait_select` callback allows interrupting a
long-running query in an interactive shell using :kbd:`Ctrl-C`
(:ticket:`#333`).
- Raise `!NotSupportedError` on unhandled server response status
(:ticket:`#352`). (:ticket:`#352`).
- Fixed `!PersistentConnectionPool` on Python 3 (:ticket:`#348`).
What's new in psycopg 2.6.1 What's new in psycopg 2.6.1
@ -30,7 +35,8 @@ What's new in psycopg 2.6.1
- Lists consisting of only `None` are escaped correctly (:ticket:`#285`). - Lists consisting of only `None` are escaped correctly (:ticket:`#285`).
- Fixed deadlock in multithread programs using OpenSSL (:ticket:`#290`). - Fixed deadlock in multithread programs using OpenSSL (:ticket:`#290`).
- Correctly unlock the connection after error in flush (:ticket:`#294`). - Correctly unlock the connection after error in flush (:ticket:`#294`).
- Fixed ``MinTimeLoggingCursor.callproc()`` (:ticket:`#309`). - Fixed `!MinTimeLoggingCursor.callproc()` (:ticket:`#309`).
- Added support for MSVC 2015 compiler (:ticket:`#350`).
What's new in psycopg 2.6 What's new in psycopg 2.6
@ -45,7 +51,7 @@ New features:
Bug fixes: Bug fixes:
- Json apapter's `!str()` returns the adapted content instead of the `!repr()` - Json adapter's `!str()` returns the adapted content instead of the `!repr()`
(:ticket:`#191`). (:ticket:`#191`).

View File

@ -221,6 +221,19 @@ functionalities defined by the |DBAPI|_.
.. __: http://www.postgresql.org/docs/current/static/libpq-misc.html#LIBPQ-PQLIBVERSION .. __: http://www.postgresql.org/docs/current/static/libpq-misc.html#LIBPQ-PQLIBVERSION
.. function:: quote_ident(str, scope)
Return quoted identifier according to PostgreSQL quoting rules.
The *scope* must be a `connection` or a `cursor`, the underlying
connection encoding is used for any necessary character conversion.
Requires libpq >= 9.0.
.. seealso:: libpq docs for `PQescapeIdentifier()`__
.. __: http://www.postgresql.org/docs/current/static/libpq-exec.html#LIBPQ-PQESCAPEIDENTIFIER
.. _sql-adaptation-objects: .. _sql-adaptation-objects:
SQL adaptation protocol objects SQL adaptation protocol objects

View File

@ -979,3 +979,6 @@ Coroutine support
.. autofunction:: wait_select(conn) .. autofunction:: wait_select(conn)
.. versionchanged:: 2.6.2
allow to cancel a query using :kbd:`Ctrl-C`, see
:ref:`the FAQ <faq-interrupt-query>` for an example.

View File

@ -223,6 +223,37 @@ What are the advantages or disadvantages of using named cursors?
little memory on the client and to skip or discard parts of the result set. little memory on the client and to skip or discard parts of the result set.
.. _faq-interrupt-query:
.. cssclass:: faq
How do I interrupt a long-running query in an interactive shell?
Normally the interactive shell becomes unresponsive to :kbd:`Ctrl-C` when
running a query. Using a connection in green mode allows Python to
receive and handle the interrupt, although it may leave the connection
broken, if the async callback doesn't handle the `!KeyboardInterrupt`
correctly.
Starting from psycopg 2.6.2, the `~psycopg2.extras.wait_select` callback
can handle a :kbd:`Ctrl-C` correctly. For previous versions, you can use
`this implementation`__.
.. __: http://initd.org/psycopg/articles/2014/07/20/cancelling-postgresql-statements-python/
.. code-block:: pycon
>>> psycopg2.extensions.set_wait_callback(psycopg2.extensions.wait_select)
>>> cnn = psycopg2.connect('')
>>> cur = cnn.cursor()
>>> cur.execute("select pg_sleep(10)")
^C
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
QueryCanceledError: canceling statement due to user request
>>> cnn.rollback()
>>> # You can use the connection and cursor again from here
.. _faq-compile: .. _faq-compile:
Problems compiling and deploying psycopg2 Problems compiling and deploying psycopg2

View File

@ -56,7 +56,7 @@ try:
except ImportError: except ImportError:
pass pass
from psycopg2._psycopg import adapt, adapters, encodings, connection, cursor, lobject, Xid, libpq_version, parse_dsn from psycopg2._psycopg import adapt, adapters, encodings, connection, cursor, lobject, Xid, libpq_version, parse_dsn, quote_ident
from psycopg2._psycopg import string_types, binary_types, new_type, new_array_type, register_type from psycopg2._psycopg import string_types, binary_types, new_type, new_array_type, register_type
from psycopg2._psycopg import ISQLQuote, Notify, Diagnostics, Column from psycopg2._psycopg import ISQLQuote, Notify, Diagnostics, Column

View File

@ -735,6 +735,7 @@ def wait_select(conn):
from psycopg2.extensions import POLL_OK, POLL_READ, POLL_WRITE from psycopg2.extensions import POLL_OK, POLL_READ, POLL_WRITE
while 1: while 1:
try:
state = conn.poll() state = conn.poll()
if state == POLL_OK: if state == POLL_OK:
break break
@ -744,6 +745,10 @@ def wait_select(conn):
select.select([], [conn.fileno()], []) select.select([], [conn.fileno()], [])
else: else:
raise conn.OperationalError("bad state from poll: %s" % state) raise conn.OperationalError("bad state from poll: %s" % state)
except KeyboardInterrupt:
conn.cancel()
# the loop will be broken by a server error
continue
def _solve_conn_curs(conn_or_curs): def _solve_conn_curs(conn_or_curs):

View File

@ -204,8 +204,8 @@ class PersistentConnectionPool(AbstractConnectionPool):
# we we'll need the thread module, to determine thread ids, so we # we we'll need the thread module, to determine thread ids, so we
# import it here and copy it in an instance variable # import it here and copy it in an instance variable
import thread import thread as _thread # work around for 2to3 bug - see ticket #348
self.__thread = thread self.__thread = _thread
def getconn(self): def getconn(self):
"""Generate thread id and return a connection.""" """Generate thread id and return a connection."""

View File

@ -166,6 +166,62 @@ exit:
return res; return res;
} }
#define psyco_quote_ident_doc \
"quote_ident(str, conn_or_curs) -> str -- wrapper around PQescapeIdentifier\n\n" \
":Parameters:\n" \
" * `str`: A bytes or unicode object\n" \
" * `conn_or_curs`: A connection or cursor, required"
static PyObject *
psyco_quote_ident(PyObject *self, PyObject *args, PyObject *kwargs)
{
#if PG_VERSION_NUM >= 90000
PyObject *ident = NULL, *obj = NULL, *result = NULL;
connectionObject *conn;
const char *str;
char *quoted = NULL;
static char *kwlist[] = {"ident", "scope", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OO", kwlist, &ident, &obj)) {
return NULL;
}
if (PyObject_TypeCheck(obj, &cursorType)) {
conn = ((cursorObject*)obj)->conn;
}
else if (PyObject_TypeCheck(obj, &connectionType)) {
conn = (connectionObject*)obj;
}
else {
PyErr_SetString(PyExc_TypeError,
"argument 2 must be a connection or a cursor");
return NULL;
}
Py_INCREF(ident); /* for ensure_bytes */
if (!(ident = psycopg_ensure_bytes(ident))) { goto exit; }
str = Bytes_AS_STRING(ident);
quoted = PQescapeIdentifier(conn->pgconn, str, strlen(str));
if (!quoted) {
PyErr_NoMemory();
goto exit;
}
result = conn_text_from_chars(conn, quoted);
exit:
PQfreemem(quoted);
Py_XDECREF(ident);
return result;
#else
PyErr_SetString(NotSupportedError, "PQescapeIdentifier not available in libpq < 9.0");
return NULL;
#endif
}
/** type registration **/ /** type registration **/
#define psyco_register_type_doc \ #define psyco_register_type_doc \
"register_type(obj, conn_or_curs) -> None -- register obj with psycopg type system\n\n" \ "register_type(obj, conn_or_curs) -> None -- register obj with psycopg type system\n\n" \
@ -235,13 +291,16 @@ psyco_register_type(PyObject *self, PyObject *args)
static void static void
psyco_libcrypto_threads_init(void) psyco_libcrypto_threads_init(void)
{ {
PyObject *m;
/* importing the ssl module sets up Python's libcrypto callbacks */ /* importing the ssl module sets up Python's libcrypto callbacks */
if (PyImport_ImportModule("ssl") != NULL) { if ((m = PyImport_ImportModule("ssl"))) {
/* disable libcrypto setup in libpq, so it won't stomp on the callbacks /* disable libcrypto setup in libpq, so it won't stomp on the callbacks
that have already been set up */ that have already been set up */
#if PG_VERSION_NUM >= 80400 #if PG_VERSION_NUM >= 80400
PQinitOpenSSL(1, 0); PQinitOpenSSL(1, 0);
#endif #endif
Py_DECREF(m);
} }
else { else {
/* might mean that Python has been compiled without OpenSSL support, /* might mean that Python has been compiled without OpenSSL support,
@ -764,6 +823,8 @@ static PyMethodDef psycopgMethods[] = {
METH_VARARGS|METH_KEYWORDS, psyco_connect_doc}, METH_VARARGS|METH_KEYWORDS, psyco_connect_doc},
{"parse_dsn", (PyCFunction)psyco_parse_dsn, {"parse_dsn", (PyCFunction)psyco_parse_dsn,
METH_VARARGS|METH_KEYWORDS, psyco_parse_dsn_doc}, METH_VARARGS|METH_KEYWORDS, psyco_parse_dsn_doc},
{"quote_ident", (PyCFunction)psyco_quote_ident,
METH_VARARGS|METH_KEYWORDS, psyco_quote_ident_doc},
{"adapt", (PyCFunction)psyco_microprotocols_adapt, {"adapt", (PyCFunction)psyco_microprotocols_adapt,
METH_VARARGS, psyco_microprotocols_adapt_doc}, METH_VARARGS, psyco_microprotocols_adapt_doc},

View File

@ -87,7 +87,7 @@ psycopg_escape_string(connectionObject *conn, const char *from, Py_ssize_t len,
return to; return to;
} }
/* Escape a string to build a valid PostgreSQL identifier /* Escape a string to build a valid PostgreSQL identifier.
* *
* Allocate a new buffer on the Python heap containing the new string. * Allocate a new buffer on the Python heap containing the new string.
* 'len' is optional: if 0 the length is calculated. * 'len' is optional: if 0 the length is calculated.
@ -96,7 +96,7 @@ psycopg_escape_string(connectionObject *conn, const char *from, Py_ssize_t len,
* *
* WARNING: this function is not so safe to allow untrusted input: it does no * WARNING: this function is not so safe to allow untrusted input: it does no
* check for multibyte chars. Such a function should be built on * check for multibyte chars. Such a function should be built on
* PQescapeIndentifier, which is only available from PostgreSQL 9.0. * PQescapeIdentifier, which is only available from PostgreSQL 9.0.
*/ */
char * char *
psycopg_escape_identifier_easy(const char *from, Py_ssize_t len) psycopg_escape_identifier_easy(const char *from, Py_ssize_t len)

View File

@ -23,7 +23,7 @@
# License for more details. # License for more details.
import sys import sys
from testutils import unittest, ConnectingTestCase from testutils import unittest, ConnectingTestCase, skip_before_libpq
import psycopg2 import psycopg2
import psycopg2.extensions import psycopg2.extensions
@ -165,6 +165,24 @@ class TestQuotedString(ConnectingTestCase):
self.assertEqual(q.encoding, 'utf_8') self.assertEqual(q.encoding, 'utf_8')
class TestQuotedIdentifier(ConnectingTestCase):
@skip_before_libpq(9, 0)
def test_identifier(self):
from psycopg2.extensions import quote_ident
self.assertEqual(quote_ident('blah-blah', self.conn), '"blah-blah"')
self.assertEqual(quote_ident('quote"inside', self.conn), '"quote""inside"')
@skip_before_libpq(9, 0)
def test_unicode_ident(self):
from psycopg2.extensions import quote_ident
snowman = u"\u2603"
quoted = '"' + snowman + '"'
if sys.version_info[0] < 3:
self.assertEqual(quote_ident(snowman, self.conn), quoted.encode('utf8'))
else:
self.assertEqual(quote_ident(snowman, self.conn), quoted)
def test_suite(): def test_suite():
return unittest.TestLoader().loadTestsFromName(__name__) return unittest.TestLoader().loadTestsFromName(__name__)