From ade7dba27cb856c3d2e3f6284aa3e909f8033e10 Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Thu, 1 Oct 2015 14:29:56 +0100 Subject: [PATCH 1/8] MSVC 2015 compiler support added to news file --- NEWS | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS b/NEWS index fd4fc6ba..81d24583 100644 --- a/NEWS +++ b/NEWS @@ -31,6 +31,7 @@ What's new in psycopg 2.6.1 - Fixed deadlock in multithread programs using OpenSSL (:ticket:`#290`). - Correctly unlock the connection after error in flush (:ticket:`#294`). - Fixed ``MinTimeLoggingCursor.callproc()`` (:ticket:`#309`). +- Added support for MSVC 2015 compiler (:ticket:`#350`). What's new in psycopg 2.6 From 98f2aad4baea7472a6643d6da7986e9b016a6f54 Mon Sep 17 00:00:00 2001 From: ClodoaldoPinto Date: Wed, 9 Sep 2015 11:16:59 -0300 Subject: [PATCH 2/8] Typo correction --- NEWS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 81d24583..3ba60dea 100644 --- a/NEWS +++ b/NEWS @@ -46,7 +46,7 @@ New features: 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`). From 9e6c3322d8640bca7007a222973d87d8ea60057c Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Thu, 1 Oct 2015 14:44:14 +0100 Subject: [PATCH 3/8] Fixed PersistentConnectionPool on Python 3 Fixes ticket #348. --- NEWS | 5 +++-- lib/pool.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/NEWS b/NEWS index 3ba60dea..651b74c4 100644 --- a/NEWS +++ b/NEWS @@ -20,8 +20,9 @@ What's new in psycopg 2.6.2 ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Report the server response status on errors (such as :ticket:`#281`). -- Raise NotSupportedError on unhandled server response status +- Raise `!NotSupportedError` on unhandled server response status (:ticket:`#352`). +- Fixed `!PersistentConnectionPool` on Python 3 (:ticket:`#348`). What's new in psycopg 2.6.1 @@ -30,7 +31,7 @@ What's new in psycopg 2.6.1 - Lists consisting of only `None` are escaped correctly (:ticket:`#285`). - Fixed deadlock in multithread programs using OpenSSL (:ticket:`#290`). - 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`). diff --git a/lib/pool.py b/lib/pool.py index 4f858ab1..8d7c4afb 100644 --- a/lib/pool.py +++ b/lib/pool.py @@ -204,8 +204,8 @@ class PersistentConnectionPool(AbstractConnectionPool): # we we'll need the thread module, to determine thread ids, so we # import it here and copy it in an instance variable - import thread - self.__thread = thread + import thread as _thread # work around for 2to3 bug - see ticket #348 + self.__thread = _thread def getconn(self): """Generate thread id and return a connection.""" From f635547ec690cc76b02da8e22772d41008fcc46e Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Thu, 1 Oct 2015 15:26:13 +0100 Subject: [PATCH 4/8] The wait_select callback can cancel a query using Ctrl-C Fixes #333. --- NEWS | 3 +++ doc/src/extras.rst | 3 +++ doc/src/faq.rst | 31 +++++++++++++++++++++++++++++++ lib/extras.py | 23 ++++++++++++++--------- 4 files changed, 51 insertions(+), 9 deletions(-) diff --git a/NEWS b/NEWS index 651b74c4..83ba166e 100644 --- a/NEWS +++ b/NEWS @@ -20,6 +20,9 @@ What's new in psycopg 2.6.2 ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Report the server response status on errors (such as :ticket:`#281`). +- 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`). - Fixed `!PersistentConnectionPool` on Python 3 (:ticket:`#348`). diff --git a/doc/src/extras.rst b/doc/src/extras.rst index 36ef0132..0e21ae58 100644 --- a/doc/src/extras.rst +++ b/doc/src/extras.rst @@ -611,3 +611,6 @@ Coroutine support .. autofunction:: wait_select(conn) + .. versionchanged:: 2.6.2 + allow to cancel a query using :kbd:`Ctrl-C`, see + :ref:`the FAQ ` for an example. diff --git a/doc/src/faq.rst b/doc/src/faq.rst index 8f2f1ecc..69273ba5 100644 --- a/doc/src/faq.rst +++ b/doc/src/faq.rst @@ -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. +.. _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 "", line 1, in + QueryCanceledError: canceling statement due to user request + + >>> cnn.rollback() + >>> # You can use the connection and cursor again from here + + .. _faq-compile: Problems compiling and deploying psycopg2 diff --git a/lib/extras.py b/lib/extras.py index c9f1cbcd..2713d6fc 100644 --- a/lib/extras.py +++ b/lib/extras.py @@ -575,15 +575,20 @@ def wait_select(conn): from psycopg2.extensions import POLL_OK, POLL_READ, POLL_WRITE while 1: - state = conn.poll() - if state == POLL_OK: - break - elif state == POLL_READ: - select.select([conn.fileno()], [], []) - elif state == POLL_WRITE: - select.select([], [conn.fileno()], []) - else: - raise conn.OperationalError("bad state from poll: %s" % state) + try: + state = conn.poll() + if state == POLL_OK: + break + elif state == POLL_READ: + select.select([conn.fileno()], [], []) + elif state == POLL_WRITE: + select.select([], [conn.fileno()], []) + else: + 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): From c73c1c577170f51c2dfadcdb61c34e4def82d709 Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Thu, 1 Oct 2015 17:02:43 +0100 Subject: [PATCH 5/8] Decref the ssl module after importing --- psycopg/psycopgmodule.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/psycopg/psycopgmodule.c b/psycopg/psycopgmodule.c index 737a7811..c77dce5b 100644 --- a/psycopg/psycopgmodule.c +++ b/psycopg/psycopgmodule.c @@ -234,13 +234,16 @@ psyco_register_type(PyObject *self, PyObject *args) static void psyco_libcrypto_threads_init(void) { + PyObject *m; + /* 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 that have already been set up */ #if PG_VERSION_NUM >= 80400 PQinitOpenSSL(1, 0); #endif + Py_DECREF(m); } else { /* might mean that Python has been compiled without OpenSSL support, From 9295bce154182863e19342b6a4c2e80a58187120 Mon Sep 17 00:00:00 2001 From: Oleksandr Shulgin Date: Tue, 13 Oct 2015 17:29:55 +0200 Subject: [PATCH 6/8] Add psycopg2.extensions.quote_ident. --- doc/src/extensions.rst | 13 +++++++++++++ lib/extensions.py | 2 +- psycopg/psycopgmodule.c | 38 ++++++++++++++++++++++++++++++++++++++ psycopg/utils.c | 4 ++-- tests/test_quote.py | 7 +++++++ 5 files changed, 61 insertions(+), 3 deletions(-) diff --git a/doc/src/extensions.rst b/doc/src/extensions.rst index 4db76b01..d96cca4f 100644 --- a/doc/src/extensions.rst +++ b/doc/src/extensions.rst @@ -221,6 +221,19 @@ functionalities defined by the |DBAPI|_. .. __: 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 protocol objects diff --git a/lib/extensions.py b/lib/extensions.py index d10e8ac6..b40e28b8 100644 --- a/lib/extensions.py +++ b/lib/extensions.py @@ -56,7 +56,7 @@ try: except ImportError: 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 ISQLQuote, Notify, Diagnostics, Column diff --git a/psycopg/psycopgmodule.c b/psycopg/psycopgmodule.c index c77dce5b..9906b7be 100644 --- a/psycopg/psycopgmodule.c +++ b/psycopg/psycopgmodule.c @@ -165,6 +165,42 @@ exit: return res; } + +#define psyco_quote_ident_doc "quote_ident(str, conn_or_curs) -> str" + +static PyObject * +psyco_quote_ident(PyObject *self, PyObject *args) +{ + const char *str = NULL; + char *quoted; + PyObject *obj, *result; + connectionObject *conn; + + if (!PyArg_ParseTuple(args, "sO", &str, &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; + } + + quoted = PQescapeIdentifier(conn->pgconn, str, strlen(str)); + if (!quoted) { + PyErr_NoMemory(); + return NULL; + } + result = conn_text_from_chars(conn, quoted); + PQfreemem(quoted); + + return result; +} + /** type registration **/ #define psyco_register_type_doc \ "register_type(obj, conn_or_curs) -> None -- register obj with psycopg type system\n\n" \ @@ -768,6 +804,8 @@ static PyMethodDef psycopgMethods[] = { METH_VARARGS|METH_KEYWORDS, psyco_parse_dsn_doc}, {"adapt", (PyCFunction)psyco_microprotocols_adapt, METH_VARARGS, psyco_microprotocols_adapt_doc}, + {"quote_ident", (PyCFunction)psyco_quote_ident, + METH_VARARGS, psyco_quote_ident_doc}, {"register_type", (PyCFunction)psyco_register_type, METH_VARARGS, psyco_register_type_doc}, diff --git a/psycopg/utils.c b/psycopg/utils.c index 836f6129..ec8e47c8 100644 --- a/psycopg/utils.c +++ b/psycopg/utils.c @@ -87,7 +87,7 @@ psycopg_escape_string(connectionObject *conn, const char *from, Py_ssize_t len, 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. * '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 * 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 * psycopg_escape_identifier_easy(const char *from, Py_ssize_t len) diff --git a/tests/test_quote.py b/tests/test_quote.py index e7b3c316..a24ab6d4 100755 --- a/tests/test_quote.py +++ b/tests/test_quote.py @@ -165,6 +165,13 @@ class TestQuotedString(ConnectingTestCase): self.assertEqual(q.encoding, 'utf_8') +class TestQuotedIdentifier(ConnectingTestCase): + 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"') + + def test_suite(): return unittest.TestLoader().loadTestsFromName(__name__) From 89bb6b0711f8ba59f7b8e81339ddaa53356233a2 Mon Sep 17 00:00:00 2001 From: Oleksandr Shulgin Date: Thu, 15 Oct 2015 11:52:18 +0200 Subject: [PATCH 7/8] Proper unicode handling in quote_ident. --- psycopg/psycopgmodule.c | 38 +++++++++++++++++++++++++++++--------- tests/test_quote.py | 13 ++++++++++++- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/psycopg/psycopgmodule.c b/psycopg/psycopgmodule.c index 9906b7be..cf70a4ad 100644 --- a/psycopg/psycopgmodule.c +++ b/psycopg/psycopgmodule.c @@ -166,17 +166,25 @@ exit: } -#define psyco_quote_ident_doc "quote_ident(str, conn_or_curs) -> str" +#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) +psyco_quote_ident(PyObject *self, PyObject *args, PyObject *kwargs) { - const char *str = NULL; - char *quoted; - PyObject *obj, *result; +#if PG_VERSION_NUM >= 90000 + PyObject *ident = NULL, *obj = NULL, *result = NULL; connectionObject *conn; + const char *str; + char *quoted = NULL; - if (!PyArg_ParseTuple(args, "sO", &str, &obj)) return 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; @@ -190,15 +198,27 @@ psyco_quote_ident(PyObject *self, PyObject *args) 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(); - return NULL; + 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 **/ @@ -802,10 +822,10 @@ static PyMethodDef psycopgMethods[] = { METH_VARARGS|METH_KEYWORDS, psyco_connect_doc}, {"parse_dsn", (PyCFunction)psyco_parse_dsn, 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, METH_VARARGS, psyco_microprotocols_adapt_doc}, - {"quote_ident", (PyCFunction)psyco_quote_ident, - METH_VARARGS, psyco_quote_ident_doc}, {"register_type", (PyCFunction)psyco_register_type, METH_VARARGS, psyco_register_type_doc}, diff --git a/tests/test_quote.py b/tests/test_quote.py index a24ab6d4..6e945624 100755 --- a/tests/test_quote.py +++ b/tests/test_quote.py @@ -23,7 +23,7 @@ # License for more details. import sys -from testutils import unittest, ConnectingTestCase +from testutils import unittest, ConnectingTestCase, skip_before_libpq import psycopg2 import psycopg2.extensions @@ -166,11 +166,22 @@ class TestQuotedString(ConnectingTestCase): 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(): return unittest.TestLoader().loadTestsFromName(__name__) From 109409bc951b7dd3e61712a65289e3458430656a Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Thu, 15 Oct 2015 11:06:44 +0100 Subject: [PATCH 8/8] Mention quote_ident() in NEWS file --- NEWS | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS b/NEWS index 83ba166e..5200c4dd 100644 --- a/NEWS +++ b/NEWS @@ -14,6 +14,7 @@ New features: - The attributes `~connection.notices` and `~connection.notifies` can be customized replacing them with any object exposing an `!append()` method (:ticket:`#326`). +- Added `~psycopg2.extensions.quote_ident()` function (:ticket:`#359`). What's new in psycopg 2.6.2