From 7c7bbb9742734d1a1c0bc808d9730113249be4eb Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Sat, 16 Feb 2019 17:30:34 +0100 Subject: Added connection.pgconn_ptr and cursor.pgresult_ptr Allow interacting with libpq in Python via ctypes. See #782. --- psycopg/connection_type.c | 24 ++++++++++++++++++++++-- psycopg/cursor_type.c | 20 ++++++++++++++++++++ tests/test_connection.py | 15 +++++++++++++++ tests/test_cursor.py | 18 ++++++++++++++++++ tests/testutils.py | 14 ++++++++++++++ 5 files changed, 89 insertions(+), 2 deletions(-) diff --git a/psycopg/connection_type.c b/psycopg/connection_type.c index ca61a7f..d360d5d 100644 --- a/psycopg/connection_type.c +++ b/psycopg/connection_type.c @@ -1008,7 +1008,7 @@ psyco_conn_get_backend_pid(connectionObject *self, PyObject *dummy) /* get info about the connection */ -#define psyco_conn_info_get_doc \ +#define psyco_conn_info_doc \ "info -- Get connection info." static PyObject * @@ -1019,6 +1019,23 @@ psyco_conn_info_get(connectionObject *self) } +/* return the pointer to the PGconn structure */ + +#define psyco_conn_pgconn_ptr_doc \ +"pgconn_ptr -- Get the PGconn structure pointer." + +static PyObject * +psyco_conn_pgconn_ptr_get(connectionObject *self) +{ + if (self->pgconn) { + return PyLong_FromVoidPtr((void *)self->pgconn); + } + else { + Py_RETURN_NONE; + } +} + + /* reset the currect connection */ #define psyco_conn_reset_doc \ @@ -1270,7 +1287,10 @@ static struct PyGetSetDef connectionObject_getsets[] = { psyco_conn_deferrable_doc }, { "info", (getter)psyco_conn_info_get, NULL, - psyco_conn_info_get_doc }, + psyco_conn_info_doc }, + { "pgconn_ptr", + (getter)psyco_conn_pgconn_ptr_get, NULL, + psyco_conn_pgconn_ptr_doc }, {NULL} }; #undef EXCEPTION_GETTER diff --git a/psycopg/cursor_type.c b/psycopg/cursor_type.c index bae30ab..3153047 100644 --- a/psycopg/cursor_type.c +++ b/psycopg/cursor_type.c @@ -107,6 +107,8 @@ psyco_curs_close(cursorObject *self, PyObject *dummy) } close: + CLEARPGRES(self->pgres); + self->closed = 1; Dprintf("psyco_curs_close: cursor at %p closed", self); @@ -1716,6 +1718,21 @@ psyco_curs_scrollable_set(cursorObject *self, PyObject *pyvalue) } +#define psyco_curs_pgresult_ptr_doc \ +"pgresult_ptr -- Get the PGresult structure pointer." + +static PyObject * +psyco_curs_pgresult_ptr_get(cursorObject *self) +{ + if (self->pgres) { + return PyLong_FromVoidPtr((void *)self->pgres); + } + else { + Py_RETURN_NONE; + } +} + + /** the cursor object **/ /* iterator protocol */ @@ -1842,6 +1859,9 @@ static struct PyGetSetDef cursorObject_getsets[] = { (getter)psyco_curs_scrollable_get, (setter)psyco_curs_scrollable_set, psyco_curs_scrollable_doc, NULL }, + { "pgresult_ptr", + (getter)psyco_curs_pgresult_ptr_get, NULL, + psyco_curs_pgresult_ptr_doc, NULL }, {NULL} }; diff --git a/tests/test_connection.py b/tests/test_connection.py index e450883..9bad8c3 100755 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -26,6 +26,7 @@ import re import os import sys import time +import ctypes import threading import subprocess as sp from operator import attrgetter @@ -346,6 +347,20 @@ class ConnectionTests(ConnectingTestCase): # we can't do anything else in Python self.assertIsNotNone(capsule) + def test_pgconn_ptr(self): + conn = self.connect() + self.assert_(conn.pgconn_ptr is not None) + + f = self.libpq.PQserverVersion + f.argtypes = [ctypes.c_void_p] + f.restype = ctypes.c_int + ver = f(conn.pgconn_ptr) + self.assertEqual(ver, conn.server_version) + + conn.close() + self.assert_(conn.pgconn_ptr is None) + + class ParseDsnTestCase(ConnectingTestCase): def test_parse_dsn(self): from psycopg2 import ProgrammingError diff --git a/tests/test_cursor.py b/tests/test_cursor.py index bee469e..82338d3 100755 --- a/tests/test_cursor.py +++ b/tests/test_cursor.py @@ -23,6 +23,7 @@ # License for more details. import time +import ctypes import pickle import psycopg2 import psycopg2.extensions @@ -650,6 +651,23 @@ class CursorTests(ConnectingTestCase): [(i,) for i in range(5)]) self.assertEqual(cur.rowcount, 5) + @skip_before_postgres(9) + def test_pgresult_ptr(self): + curs = self.conn.cursor() + self.assert_(curs.pgresult_ptr is None) + + f = self.libpq.PQcmdStatus + f.argtypes = [ctypes.c_void_p] + f.restype = ctypes.c_char_p + + curs.execute("select 'x'") + self.assert_(curs.pgresult_ptr is not None) + status = f(curs.pgresult_ptr) + self.assertEqual(status, b'SELECT 1') + + curs.close() + self.assert_(curs.pgresult_ptr is None) + def test_suite(): return unittest.TestLoader().loadTestsFromName(__name__) diff --git a/tests/testutils.py b/tests/testutils.py index e4b6843..5f41789 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -26,10 +26,12 @@ import re import os import sys import types +import ctypes import select import platform import unittest from functools import wraps +from ctypes.util import find_library from .testconfig import dsn, repl_dsn from psycopg2.compat import text_type @@ -174,6 +176,18 @@ class ConnectingTestCase(unittest.TestCase): else: raise Exception("Unexpected result from poll: %r", state) + _libpq = None + + @property + def libpq(self): + """Return a ctypes wrapper for the libpq library""" + if ConnectingTestCase._libpq is not None: + return ConnectingTestCase._libpq + + libname = find_library('pq') + rv = ConnectingTestCase._libpq = ctypes.pydll.LoadLibrary(libname) + return rv + def decorate_all_tests(obj, *decorators): """ -- cgit v1.2.1 From 3b7c083c3d92b7c1cd22c5af53b9ee18dc331db3 Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Sat, 16 Feb 2019 18:07:28 +0100 Subject: Skip tests involving ctypes on Windows No idea about how to import libpq. --- tests/testutils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/testutils.py b/tests/testutils.py index 5f41789..227c044 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -185,6 +185,9 @@ class ConnectingTestCase(unittest.TestCase): return ConnectingTestCase._libpq libname = find_library('pq') + if libname is None and platform.system() == 'Windows': + raise self.skipTest("can't import libpq on windows") + rv = ConnectingTestCase._libpq = ctypes.pydll.LoadLibrary(libname) return rv -- cgit v1.2.1 From 80b7b845d2cfaddef3f623a1357048954308dc0a Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Sat, 16 Feb 2019 18:08:08 +0100 Subject: Added docs about pgconn_ptr, pgresult_ptr --- NEWS | 4 +++- doc/src/connection.rst | 18 +++++++++++++++++- doc/src/cursor.rst | 18 ++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/NEWS b/NEWS index 984d531..adc6685 100644 --- a/NEWS +++ b/NEWS @@ -17,7 +17,9 @@ New features: - Added `connection.info` object to retrieve various PostgreSQL connection information (:ticket:`#726`). - Added `~connection.get_native_connection()` to expose the raw ``PGconn`` - structure (:ticket:`#782`). + structure to C extensions via Capsule (:ticket:`#782`). +- Added `~connection.pgconn_ptr` and `~cursor.pgresult_ptr` to expose raw + C structures to Python and interact with libpq via ctypes (:ticket:`#782`). - `~psycopg2.sql.Identifier` can represent qualified names in SQL composition (:ticket:`#732`). - Added *fetch* parameter to `~psycopg2.extras.execute_values()` function diff --git a/doc/src/connection.rst b/doc/src/connection.rst index 179b7ba..9c04e83 100644 --- a/doc/src/connection.rst +++ b/doc/src/connection.rst @@ -738,11 +738,27 @@ The ``connection`` class Return `!True` if the connection is executing an asynchronous operation. + .. rubric:: Interoperation with other C API modules + .. attribute:: pgconn_ptr + + Return the internal `!PGconn*` as integer. Useful to pass the libpq + raw connection structure to C functions, e.g. via `ctypes`:: + + >>> import ctypes + >>> libpq = ctypes.pydll.LoadLibrary(ctypes.util.find_library('pq')) + >>> libpq.PQserverVersion.argtypes = [ctypes.c_void_p] + >>> libpq.PQserverVersion.restype = ctypes.c_int + >>> libpq.PQserverVersion(conn.pgconn_ptr) + 90611 + + .. versionadded:: 2.8 + + .. method:: get_native_connection() - Return the internal `PGconn*` wrapped in a PyCapsule object. This is + Return the internal `!PGconn*` wrapped in a PyCapsule object. This is only useful for passing the `libpq` raw connection associated to this connection object to other C-level modules that may have a use for it. diff --git a/doc/src/cursor.rst b/doc/src/cursor.rst index 1d7098f..c6b04cf 100644 --- a/doc/src/cursor.rst +++ b/doc/src/cursor.rst @@ -632,6 +632,24 @@ The ``cursor`` class using Unicode data instead of bytes. + .. rubric:: Interoperation with other C API modules + + .. attribute:: pgresult_ptr + + Return the cursor's internal `!PGresult*` as integer. Useful to pass + the libpq raw result structure to C functions, e.g. via `ctypes`:: + + >>> import ctypes + >>> libpq = ctypes.pydll.LoadLibrary(ctypes.util.find_library('pq')) + >>> libpq.PQcmdStatus.argtypes = [ctypes.c_void_p] + >>> libpq.PQcmdStatus.restype = ctypes.c_char_p + + >>> curs.execute("select 'x'") + >>> libpq.PQcmdStatus(curs.pgresult_ptr) + b'SELECT 1' + + .. versionadded:: 2.8 + .. testcode:: :hide: -- cgit v1.2.1