diff --git a/MANIFEST.in b/MANIFEST.in index 00e4fc32..0d34fd3d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,10 +2,10 @@ recursive-include psycopg *.c *.h *.manifest recursive-include lib *.py recursive-include tests *.py recursive-include examples *.py somehackers.jpg whereareyou.jpg -recursive-include doc README SUCCESS COPYING.LESSER pep-0249.txt -recursive-include doc Makefile requirements.txt +include doc/README.rst doc/SUCCESS doc/COPYING.LESSER doc/pep-0249.txt +include doc/Makefile doc/requirements.txt recursive-include doc/src *.rst *.py *.css Makefile recursive-include scripts *.py *.sh include scripts/maketypes.sh scripts/buildtypes.py include AUTHORS README.rst INSTALL LICENSE NEWS -include PKG-INFO MANIFEST.in MANIFEST setup.py setup.cfg Makefile +include MANIFEST.in setup.py setup.cfg Makefile diff --git a/Makefile b/Makefile index 232f0d0b..a8f491e4 100644 --- a/Makefile +++ b/Makefile @@ -92,14 +92,9 @@ $(PACKAGE)/tests/%.py: tests/%.py $(PYTHON) setup.py build_py $(BUILD_OPT) touch $@ -$(SDIST): MANIFEST $(SOURCE) +$(SDIST): $(SOURCE) $(PYTHON) setup.py sdist $(SDIST_OPT) -MANIFEST: MANIFEST.in $(SOURCE) - # Run twice as MANIFEST.in includes MANIFEST - $(PYTHON) setup.py sdist --manifest-only - $(PYTHON) setup.py sdist --manifest-only - # docs depend on the build as it partly use introspection. doc/html/genindex.html: $(PLATLIB) $(PURELIB) $(SOURCE_DOC) $(MAKE) -C doc html @@ -111,5 +106,5 @@ doc/docs.zip: doc/html/genindex.html (cd doc/html && zip -r ../docs.zip *) clean: - rm -rf build MANIFEST + rm -rf build $(MAKE) -C doc clean diff --git a/NEWS b/NEWS index 9467fea7..49ed56a9 100644 --- a/NEWS +++ b/NEWS @@ -18,19 +18,28 @@ New features: customized replacing them with any object exposing an `!append()` method (:ticket:`#326`). - Added `~psycopg2.extensions.quote_ident()` function (:ticket:`#359`). +- Added `~connection.get_dsn_parameters()` connection method (:ticket:`#364`). 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 + (:ticket:`#352`). +- Allow overriding string adapter encoding with no connection (:ticket:`#331`). - 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`). +- Fixed segfault on `repr()` of an unitialized connection (:ticket:`#361`). +- Allow adapting bytes using QuotedString on Python 3 too (:ticket:`#365`). +- Added support for setuptools/wheel (:ticket:`#370`). +- Fix build on Windows with Python 3.5, VS 2015 (:ticket:`#380`). - Fixed `!errorcodes.lookup` initialization thread-safety (:ticket:`#382`). +- Fixed `!read()` exception propagation in copy_from (:ticket:`#412`). +- Fixed possible NULL TZ decref (:ticket:`#424`). +- `~psycopg2.errorcodes` map updated to PostgreSQL 9.5. What's new in psycopg 2.6.1 diff --git a/doc/src/advanced.rst b/doc/src/advanced.rst index e63fcff1..f2e279f8 100644 --- a/doc/src/advanced.rst +++ b/doc/src/advanced.rst @@ -47,7 +47,7 @@ it is the class where query building, execution and result type-casting into Python variables happens. The `~psycopg2.extras` module contains several examples of :ref:`connection -and cursor sublcasses `. +and cursor subclasses `. .. note:: diff --git a/doc/src/conf.py b/doc/src/conf.py index 18b81e07..94ffa349 100644 --- a/doc/src/conf.py +++ b/doc/src/conf.py @@ -42,9 +42,7 @@ master_doc = 'index' # General information about the project. project = u'Psycopg' -from datetime import date -year = date.today().year -copyright = u'2001-%s, Federico Di Gregorio, Daniele Varrazzo' % year +copyright = u'2001-2016, Federico Di Gregorio, Daniele Varrazzo' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/doc/src/connection.rst b/doc/src/connection.rst index cceef1e5..c99c8bd8 100644 --- a/doc/src/connection.rst +++ b/doc/src/connection.rst @@ -568,6 +568,29 @@ The ``connection`` class .. versionadded:: 2.0.12 + .. index:: + pair: Connection; Parameters + + .. method:: get_dsn_parameters() + + Get the effective dsn parameters for the connection as a dictionary. + + The *password* parameter is removed from the result. + + Example:: + + >>> conn.get_dsn_parameters() + {'dbname': 'test', 'user': 'postgres', 'port': '5432', 'sslmode': 'prefer'} + + Requires libpq >= 9.3. + + .. seealso:: libpq docs for `PQconninfo()`__ for details. + + .. __: http://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-PQCONNINFO + + .. versionadded:: 2.7 + + .. index:: pair: Transaction; Status diff --git a/doc/src/cursor.rst b/doc/src/cursor.rst index 73bb5375..45e62781 100644 --- a/doc/src/cursor.rst +++ b/doc/src/cursor.rst @@ -494,6 +494,9 @@ The ``cursor`` class .. rubric:: COPY-related methods + Efficiently copy data from file-like objects to the database and back. See + :ref:`copy` for an overview. + .. extension:: The :sql:`COPY` command is a PostgreSQL extension to the SQL standard. @@ -502,7 +505,7 @@ The ``cursor`` class .. method:: copy_from(file, table, sep='\\t', null='\\\\N', size=8192, columns=None) Read data *from* the file-like object *file* appending them to - the table named *table*. See :ref:`copy` for an overview. + the table named *table*. :param file: file-like object to read data from. It must have both `!read()` and `!readline()` methods. diff --git a/doc/src/install.rst b/doc/src/install.rst index ec1eeea8..3a95adc4 100644 --- a/doc/src/install.rst +++ b/doc/src/install.rst @@ -18,7 +18,7 @@ The current `!psycopg2` implementation supports: NOTE: keep consistent with setup.py and the /features/ page. - Python 2 versions from 2.5 to 2.7 -- Python 3 versions from 3.1 to 3.4 +- Python 3 versions from 3.1 to 3.5 - PostgreSQL versions from 7.4 to 9.4 .. _PostgreSQL: http://www.postgresql.org/ @@ -51,6 +51,16 @@ extension packages, *above all if you are a Windows or a Mac OS user*, please use a pre-compiled package and go straight to the :ref:`module usage ` avoid bothering with the gory details. +.. note:: + + Regardless of the way `!psycopg2` is installed, at runtime it will need to + use the libpq_ library. `!psycopg2` relies on the host OS to find the + library file (usually ``libpq.so`` or ``libpq.dll``): if the library is + installed in a standard location there is usually no problem; if the + library is in a non-standard location you will have to tell somehow + psycopg how to find it, which is OS-dependent (for instance setting a + suitable :envvar:`LD_LIBRARY_PATH` on Linux). + .. _install-from-package: @@ -95,7 +105,17 @@ Install from a package pair: Install; Windows **Microsoft Windows** - Jason Erickson maintains a packaged `Windows port of Psycopg`__ with + There are two options to install a precompiled `psycopg2` package under windows: + + **Option 1:** Using `pip`__ (Included in python 2.7.9+ and python 3.4+) + and a binary wheel package. Launch windows' command prompt (`cmd.exe`) + and execute the following command:: + + pip install psycopg2 + + .. __: https://pip.pypa.io/en/stable/installing/ + + **Option 2:** Jason Erickson maintains a packaged `Windows port of Psycopg`__ with installation executable. Download. Double click. Done. .. __: http://www.stickpeople.com/projects/python/win-psycopg/ diff --git a/doc/src/usage.rst b/doc/src/usage.rst index 9dd31df2..3b42aeb9 100644 --- a/doc/src/usage.rst +++ b/doc/src/usage.rst @@ -864,11 +864,19 @@ Using COPY TO and COPY FROM Psycopg `cursor` objects provide an interface to the efficient PostgreSQL |COPY|__ command to move data from files to tables and back. + +Currently no adaptation is provided between Python and PostgreSQL types on +|COPY|: the file can be any Python file-like object but its format must be in +the format accepted by `PostgreSQL COPY command`__ (data fromat, escaped +characters, etc). + +.. __: COPY_ + The methods exposed are: `~cursor.copy_from()` Reads data *from* a file-like object appending them to a database table - (:sql:`COPY table FROM file` syntax). The source file must have both + (:sql:`COPY table FROM file` syntax). The source file must provide both `!read()` and `!readline()` method. `~cursor.copy_to()` diff --git a/lib/errorcodes.py b/lib/errorcodes.py index aa5a723c..60181c1c 100644 --- a/lib/errorcodes.py +++ b/lib/errorcodes.py @@ -199,6 +199,8 @@ INVALID_ESCAPE_SEQUENCE = '22025' STRING_DATA_LENGTH_MISMATCH = '22026' TRIM_ERROR = '22027' ARRAY_SUBSCRIPT_ERROR = '2202E' +INVALID_TABLESAMPLE_REPEAT = '2202G' +INVALID_TABLESAMPLE_ARGUMENT = '2202H' FLOATING_POINT_EXCEPTION = '22P01' INVALID_TEXT_REPRESENTATION = '22P02' INVALID_BINARY_REPRESENTATION = '22P03' @@ -271,6 +273,7 @@ INVALID_SQLSTATE_RETURNED = '39001' NULL_VALUE_NOT_ALLOWED = '39004' TRIGGER_PROTOCOL_VIOLATED = '39P01' SRF_PROTOCOL_VIOLATED = '39P02' +EVENT_TRIGGER_PROTOCOL_VIOLATED = '39P03' # Class 3B - Savepoint Exception SAVEPOINT_EXCEPTION = '3B000' @@ -408,6 +411,7 @@ PLPGSQL_ERROR = 'P0000' RAISE_EXCEPTION = 'P0001' NO_DATA_FOUND = 'P0002' TOO_MANY_ROWS = 'P0003' +ASSERT_FAILURE = 'P0004' # Class XX - Internal Error INTERNAL_ERROR = 'XX000' diff --git a/psycopg/adapter_datetime.c b/psycopg/adapter_datetime.c index 0571837d..9d04df40 100644 --- a/psycopg/adapter_datetime.c +++ b/psycopg/adapter_datetime.c @@ -451,7 +451,7 @@ psyco_TimestampFromTicks(PyObject *self, PyObject *args) tz); exit: - Py_DECREF(tz); + Py_XDECREF(tz); Py_XDECREF(m); return res; } diff --git a/psycopg/adapter_qstring.c b/psycopg/adapter_qstring.c index 2e3ab0ae..8c5a8f10 100644 --- a/psycopg/adapter_qstring.c +++ b/psycopg/adapter_qstring.c @@ -36,44 +36,56 @@ static const char *default_encoding = "latin1"; /* qstring_quote - do the quote process on plain and unicode strings */ +const char * +_qstring_get_encoding(qstringObject *self) +{ + /* if the wrapped object is an unicode object we can encode it to match + conn->encoding but if the encoding is not specified we don't know what + to do and we raise an exception */ + if (self->conn) { + return self->conn->codec; + } + else { + return self->encoding ? self->encoding : default_encoding; + } +} + static PyObject * qstring_quote(qstringObject *self) { PyObject *str = NULL; char *s, *buffer = NULL; Py_ssize_t len, qlen; - const char *encoding = default_encoding; + const char *encoding; PyObject *rv = NULL; - /* if the wrapped object is an unicode object we can encode it to match - conn->encoding but if the encoding is not specified we don't know what - to do and we raise an exception */ - if (self->conn) { - encoding = self->conn->codec; - } - + encoding = _qstring_get_encoding(self); Dprintf("qstring_quote: encoding to %s", encoding); - if (PyUnicode_Check(self->wrapped) && encoding) { - str = PyUnicode_AsEncodedString(self->wrapped, encoding, NULL); - Dprintf("qstring_quote: got encoded object at %p", str); - if (str == NULL) goto exit; + if (PyUnicode_Check(self->wrapped)) { + if (encoding) { + str = PyUnicode_AsEncodedString(self->wrapped, encoding, NULL); + Dprintf("qstring_quote: got encoded object at %p", str); + if (str == NULL) goto exit; + } + else { + PyErr_SetString(PyExc_TypeError, + "missing encoding to encode unicode object"); + goto exit; + } } -#if PY_MAJOR_VERSION < 3 - /* if the wrapped object is a simple string, we don't know how to + /* if the wrapped object is a binary string, we don't know how to (re)encode it, so we pass it as-is */ - else if (PyString_Check(self->wrapped)) { + else if (Bytes_Check(self->wrapped)) { str = self->wrapped; /* INCREF to make it ref-wise identical to unicode one */ Py_INCREF(str); } -#endif /* if the wrapped object is not a string, this is an error */ else { - PyErr_SetString(PyExc_TypeError, - "can't quote non-string object (or missing encoding)"); + PyErr_SetString(PyExc_TypeError, "can't quote non-string object"); goto exit; } @@ -150,15 +162,34 @@ qstring_conform(qstringObject *self, PyObject *args) static PyObject * qstring_get_encoding(qstringObject *self) { - const char *encoding = default_encoding; - - if (self->conn) { - encoding = self->conn->codec; - } - + const char *encoding; + encoding = _qstring_get_encoding(self); return Text_FromUTF8(encoding); } +static int +qstring_set_encoding(qstringObject *self, PyObject *pyenc) +{ + int rv = -1; + const char *tmp; + char *cenc; + + /* get a C copy of the encoding (which may come from unicode) */ + Py_INCREF(pyenc); + if (!(pyenc = psycopg_ensure_bytes(pyenc))) { goto exit; } + if (!(tmp = Bytes_AsString(pyenc))) { goto exit; } + if (0 > psycopg_strdup(&cenc, tmp, 0)) { goto exit; } + + Dprintf("qstring_set_encoding: encoding set to %s", cenc); + PyMem_Free((void *)self->encoding); + self->encoding = cenc; + rv = 0; + +exit: + Py_XDECREF(pyenc); + return rv; +} + /** the QuotedString object **/ /* object member list */ @@ -183,7 +214,7 @@ static PyMethodDef qstringObject_methods[] = { static PyGetSetDef qstringObject_getsets[] = { { "encoding", (getter)qstring_get_encoding, - (setter)NULL, + (setter)qstring_set_encoding, "current encoding of the adapter" }, {NULL} }; @@ -216,6 +247,7 @@ qstring_dealloc(PyObject* obj) Py_CLEAR(self->wrapped); Py_CLEAR(self->buffer); Py_CLEAR(self->conn); + PyMem_Free((void *)self->encoding); Dprintf("qstring_dealloc: deleted qstring object at %p, refcnt = " FORMAT_CODE_PY_SSIZE_T, diff --git a/psycopg/adapter_qstring.h b/psycopg/adapter_qstring.h index b7b086f3..8abdc5f2 100644 --- a/psycopg/adapter_qstring.h +++ b/psycopg/adapter_qstring.h @@ -39,6 +39,9 @@ typedef struct { PyObject *buffer; connectionObject *conn; + + const char *encoding; + } qstringObject; #ifdef __cplusplus diff --git a/psycopg/connection_type.c b/psycopg/connection_type.c index 2c1dddf2..485a92b7 100644 --- a/psycopg/connection_type.c +++ b/psycopg/connection_type.c @@ -733,6 +733,37 @@ psyco_conn_get_parameter_status(connectionObject *self, PyObject *args) return conn_text_from_chars(self, val); } +/* get_dsn_parameters method - Get connection parameters */ + +#define psyco_conn_get_dsn_parameters_doc \ +"get_dsn_parameters() -- Get effective connection parameters.\n\n" + +static PyObject * +psyco_conn_get_dsn_parameters(connectionObject *self) +{ +#if PG_VERSION_NUM >= 90300 + PyObject *res = NULL; + PQconninfoOption *options = NULL; + + EXC_IF_CONN_CLOSED(self); + + if (!(options = PQconninfo(self->pgconn))) { + PyErr_NoMemory(); + goto exit; + } + + res = psycopg_dict_from_conninfo_options(options, /* include_password = */ 0); + +exit: + PQconninfoFree(options); + + return res; +#else + PyErr_SetString(NotSupportedError, "PQconninfo not available in libpq < 9.3"); + return NULL; +#endif +} + /* lobject method - allocate a new lobject */ @@ -977,6 +1008,8 @@ static struct PyMethodDef connectionObject_methods[] = { METH_NOARGS, psyco_conn_get_transaction_status_doc}, {"get_parameter_status", (PyCFunction)psyco_conn_get_parameter_status, METH_VARARGS, psyco_conn_get_parameter_status_doc}, + {"get_dsn_parameters", (PyCFunction)psyco_conn_get_dsn_parameters, + METH_NOARGS, psyco_conn_get_dsn_parameters_doc}, {"get_backend_pid", (PyCFunction)psyco_conn_get_backend_pid, METH_NOARGS, psyco_conn_get_backend_pid_doc}, {"lobject", (PyCFunction)psyco_conn_lobject, @@ -1171,7 +1204,7 @@ connection_repr(connectionObject *self) { return PyString_FromFormat( "", - self, self->dsn, self->closed); + self, (self->dsn ? self->dsn : ""), self->closed); } static int diff --git a/psycopg/cursor_type.c b/psycopg/cursor_type.c index cd8d5ca3..fe79bbf9 100644 --- a/psycopg/cursor_type.c +++ b/psycopg/cursor_type.c @@ -335,7 +335,7 @@ _psyco_curs_merge_query_args(cursorObject *self, PyErr_Fetch(&err, &arg, &trace); if (err && PyErr_GivenExceptionMatches(err, PyExc_TypeError)) { - Dprintf("psyco_curs_execute: TypeError exception catched"); + Dprintf("psyco_curs_execute: TypeError exception caught"); PyErr_NormalizeException(&err, &arg, &trace); if (PyObject_HasAttrString(arg, "args")) { diff --git a/psycopg/lobject.h b/psycopg/lobject.h index b9c8c3d8..73cf6192 100644 --- a/psycopg/lobject.h +++ b/psycopg/lobject.h @@ -60,8 +60,8 @@ RAISES_NEG HIDDEN int lobject_export(lobjectObject *self, const char *filename); RAISES_NEG HIDDEN Py_ssize_t lobject_read(lobjectObject *self, char *buf, size_t len); RAISES_NEG HIDDEN Py_ssize_t lobject_write(lobjectObject *self, const char *buf, size_t len); -RAISES_NEG HIDDEN long lobject_seek(lobjectObject *self, long pos, int whence); -RAISES_NEG HIDDEN long lobject_tell(lobjectObject *self); +RAISES_NEG HIDDEN Py_ssize_t lobject_seek(lobjectObject *self, Py_ssize_t pos, int whence); +RAISES_NEG HIDDEN Py_ssize_t lobject_tell(lobjectObject *self); RAISES_NEG HIDDEN int lobject_truncate(lobjectObject *self, size_t len); RAISES_NEG HIDDEN int lobject_close(lobjectObject *self); diff --git a/psycopg/lobject_int.c b/psycopg/lobject_int.c index 8788c100..279ef1e2 100644 --- a/psycopg/lobject_int.c +++ b/psycopg/lobject_int.c @@ -376,12 +376,12 @@ lobject_read(lobjectObject *self, char *buf, size_t len) /* lobject_seek - move the current position in the lo */ -RAISES_NEG long -lobject_seek(lobjectObject *self, long pos, int whence) +RAISES_NEG Py_ssize_t +lobject_seek(lobjectObject *self, Py_ssize_t pos, int whence) { PGresult *pgres = NULL; char *error = NULL; - long where; + Py_ssize_t where; Dprintf("lobject_seek: fd = %d, pos = %ld, whence = %d", self->fd, pos, whence); @@ -391,12 +391,12 @@ lobject_seek(lobjectObject *self, long pos, int whence) #ifdef HAVE_LO64 if (self->conn->server_version < 90300) { - where = (long)lo_lseek(self->conn->pgconn, self->fd, (int)pos, whence); + where = (Py_ssize_t)lo_lseek(self->conn->pgconn, self->fd, (int)pos, whence); } else { - where = lo_lseek64(self->conn->pgconn, self->fd, pos, whence); + where = (Py_ssize_t)lo_lseek64(self->conn->pgconn, self->fd, pos, whence); } #else - where = (long)lo_lseek(self->conn->pgconn, self->fd, (int)pos, whence); + where = (Py_ssize_t)lo_lseek(self->conn->pgconn, self->fd, (int)pos, whence); #endif Dprintf("lobject_seek: where = %ld", where); if (where < 0) @@ -412,12 +412,12 @@ lobject_seek(lobjectObject *self, long pos, int whence) /* lobject_tell - tell the current position in the lo */ -RAISES_NEG long +RAISES_NEG Py_ssize_t lobject_tell(lobjectObject *self) { PGresult *pgres = NULL; char *error = NULL; - long where; + Py_ssize_t where; Dprintf("lobject_tell: fd = %d", self->fd); @@ -426,12 +426,12 @@ lobject_tell(lobjectObject *self) #ifdef HAVE_LO64 if (self->conn->server_version < 90300) { - where = (long)lo_tell(self->conn->pgconn, self->fd); + where = (Py_ssize_t)lo_tell(self->conn->pgconn, self->fd); } else { - where = lo_tell64(self->conn->pgconn, self->fd); + where = (Py_ssize_t)lo_tell64(self->conn->pgconn, self->fd); } #else - where = (long)lo_tell(self->conn->pgconn, self->fd); + where = (Py_ssize_t)lo_tell(self->conn->pgconn, self->fd); #endif Dprintf("lobject_tell: where = %ld", where); if (where < 0) diff --git a/psycopg/lobject_type.c b/psycopg/lobject_type.c index a43325d4..d15eb20e 100644 --- a/psycopg/lobject_type.c +++ b/psycopg/lobject_type.c @@ -105,7 +105,7 @@ psyco_lobj_write(lobjectObject *self, PyObject *args) goto exit; } - rv = PyInt_FromLong((long)res); + rv = PyInt_FromSsize_t((Py_ssize_t)res); exit: Py_XDECREF(data); @@ -121,7 +121,7 @@ static PyObject * psyco_lobj_read(lobjectObject *self, PyObject *args) { PyObject *res; - long where, end; + Py_ssize_t where, end; Py_ssize_t size = -1; char *buffer; @@ -165,10 +165,10 @@ psyco_lobj_read(lobjectObject *self, PyObject *args) static PyObject * psyco_lobj_seek(lobjectObject *self, PyObject *args) { - long offset, pos=0; + Py_ssize_t offset, pos=0; int whence=0; - if (!PyArg_ParseTuple(args, "l|i", &offset, &whence)) + if (!PyArg_ParseTuple(args, "n|i", &offset, &whence)) return NULL; EXC_IF_LOBJ_CLOSED(self); @@ -187,8 +187,8 @@ psyco_lobj_seek(lobjectObject *self, PyObject *args) #else if (offset < INT_MIN || offset > INT_MAX) { PyErr_Format(InterfaceError, - "offset out of range (%ld): this psycopg version was not built " - "with lobject 64 API support", + "offset out of range (" FORMAT_CODE_PY_SSIZE_T "): " + "this psycopg version was not built with lobject 64 API support", offset); return NULL; } @@ -197,7 +197,7 @@ psyco_lobj_seek(lobjectObject *self, PyObject *args) if ((pos = lobject_seek(self, offset, whence)) < 0) return NULL; - return PyLong_FromLong(pos); + return PyInt_FromSsize_t(pos); } /* tell method - tell current position in the lobject */ @@ -208,7 +208,7 @@ psyco_lobj_seek(lobjectObject *self, PyObject *args) static PyObject * psyco_lobj_tell(lobjectObject *self, PyObject *args) { - long pos; + Py_ssize_t pos; EXC_IF_LOBJ_CLOSED(self); EXC_IF_LOBJ_LEVEL0(self); @@ -217,7 +217,7 @@ psyco_lobj_tell(lobjectObject *self, PyObject *args) if ((pos = lobject_tell(self)) < 0) return NULL; - return PyLong_FromLong(pos); + return PyInt_FromSsize_t(pos); } /* unlink method - unlink (destroy) the lobject */ @@ -274,9 +274,9 @@ psyco_lobj_get_closed(lobjectObject *self, void *closure) static PyObject * psyco_lobj_truncate(lobjectObject *self, PyObject *args) { - long len = 0; + Py_ssize_t len = 0; - if (!PyArg_ParseTuple(args, "|l", &len)) + if (!PyArg_ParseTuple(args, "|n", &len)) return NULL; EXC_IF_LOBJ_CLOSED(self); @@ -286,16 +286,16 @@ psyco_lobj_truncate(lobjectObject *self, PyObject *args) #ifdef HAVE_LO64 if (len > INT_MAX && self->conn->server_version < 90300) { PyErr_Format(NotSupportedError, - "len out of range (%ld): server version %d " - "does not support the lobject 64 API", + "len out of range (" FORMAT_CODE_PY_SSIZE_T "): " + "server version %d does not support the lobject 64 API", len, self->conn->server_version); return NULL; } #else if (len > INT_MAX) { PyErr_Format(InterfaceError, - "len out of range (%ld): this psycopg version was not built " - "with lobject 64 API support", + "len out of range (" FORMAT_CODE_PY_SSIZE_T "): " + "this psycopg version was not built with lobject 64 API support", len); return NULL; } diff --git a/psycopg/pqpath.c b/psycopg/pqpath.c index 6d6728ca..220ae246 100644 --- a/psycopg/pqpath.c +++ b/psycopg/pqpath.c @@ -170,11 +170,11 @@ pq_raise(connectionObject *conn, cursorObject *curs, PGresult **pgres) if (conn == NULL) { PyErr_SetString(DatabaseError, - "psycopg went psycotic and raised a null error"); + "psycopg went psychotic and raised a null error"); return; } - /* if the connection has somehow beed broken, we mark the connection + /* if the connection has somehow been broken, we mark the connection object as closed but requiring cleanup */ if (conn->pgconn != NULL && PQstatus(conn->pgconn) == CONNECTION_BAD) conn->closed = 2; @@ -916,7 +916,7 @@ pq_execute(cursorObject *curs, const char *query, int async, int no_result, int PyErr_SetString(OperationalError, PQerrorMessage(curs->conn->pgconn)); return -1; } - Dprintf("curs_execute: pg connection at %p OK", curs->conn->pgconn); + Dprintf("pq_execute: pg connection at %p OK", curs->conn->pgconn); Py_BEGIN_ALLOW_THREADS; pthread_mutex_lock(&(curs->conn->lock)); @@ -941,7 +941,7 @@ pq_execute(cursorObject *curs, const char *query, int async, int no_result, int Py_UNBLOCK_THREADS; } - /* dont let pgres = NULL go to pq_fetch() */ + /* don't let pgres = NULL go to pq_fetch() */ if (curs->pgres == NULL) { pthread_mutex_unlock(&(curs->conn->lock)); Py_BLOCK_THREADS; @@ -1409,7 +1409,11 @@ _pq_copy_in_v3(cursorObject *curs) Py_DECREF(str); } } - PyErr_Restore(t, ex, tb); + /* Clear the Py exception: it will be re-raised from the libpq */ + Py_XDECREF(t); + Py_XDECREF(ex); + Py_XDECREF(tb); + PyErr_Clear(); } res = PQputCopyEnd(curs->conn->pgconn, buf); } diff --git a/psycopg/psycopg.h b/psycopg/psycopg.h index adda12d9..3174f309 100644 --- a/psycopg/psycopg.h +++ b/psycopg/psycopg.h @@ -132,6 +132,9 @@ STEALS(1) HIDDEN PyObject * psycopg_ensure_bytes(PyObject *obj); STEALS(1) HIDDEN PyObject * psycopg_ensure_text(PyObject *obj); +HIDDEN PyObject *psycopg_dict_from_conninfo_options(PQconninfoOption *options, + int include_password); + /* Exceptions docstrings */ #define Error_doc \ "Base class for error exceptions." diff --git a/psycopg/psycopgmodule.c b/psycopg/psycopgmodule.c index c08cd70e..d4a4c947 100644 --- a/psycopg/psycopgmodule.c +++ b/psycopg/psycopgmodule.c @@ -123,8 +123,8 @@ static PyObject * psyco_parse_dsn(PyObject *self, PyObject *args, PyObject *kwargs) { char *err = NULL; - PQconninfoOption *options = NULL, *o; - PyObject *dict = NULL, *res = NULL, *dsn; + PQconninfoOption *options = NULL; + PyObject *res = NULL, *dsn; static char *kwlist[] = {"dsn", NULL}; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", kwlist, &dsn)) { @@ -145,26 +145,10 @@ psyco_parse_dsn(PyObject *self, PyObject *args, PyObject *kwargs) goto exit; } - if (!(dict = PyDict_New())) { goto exit; } - for (o = options; o->keyword != NULL; o++) { - if (o->val != NULL) { - PyObject *value; - if (!(value = Text_FromUTF8(o->val))) { goto exit; } - if (PyDict_SetItemString(dict, o->keyword, value) != 0) { - Py_DECREF(value); - goto exit; - } - Py_DECREF(value); - } - } - - /* success */ - res = dict; - dict = NULL; + res = psycopg_dict_from_conninfo_options(options, /* include_password = */ 1); exit: PQconninfoFree(options); /* safe on null */ - Py_XDECREF(dict); Py_XDECREF(dsn); return res; diff --git a/psycopg/utils.c b/psycopg/utils.c index ec8e47c8..1b10c4aa 100644 --- a/psycopg/utils.c +++ b/psycopg/utils.c @@ -247,3 +247,32 @@ psycopg_is_text_file(PyObject *f) } } +/* Make a dict out of PQconninfoOption array */ +PyObject * +psycopg_dict_from_conninfo_options(PQconninfoOption *options, int include_password) +{ + PyObject *dict, *res = NULL; + PQconninfoOption *o; + + if (!(dict = PyDict_New())) { goto exit; } + for (o = options; o->keyword != NULL; o++) { + if (o->val != NULL && + (include_password || strcmp(o->keyword, "password") != 0)) { + PyObject *value; + if (!(value = Text_FromUTF8(o->val))) { goto exit; } + if (PyDict_SetItemString(dict, o->keyword, value) != 0) { + Py_DECREF(value); + goto exit; + } + Py_DECREF(value); + } + } + + res = dict; + dict = NULL; + +exit: + Py_XDECREF(dict); + + return res; +} diff --git a/scripts/make_errorcodes.py b/scripts/make_errorcodes.py index 122e0d56..58d05b85 100755 --- a/scripts/make_errorcodes.py +++ b/scripts/make_errorcodes.py @@ -33,7 +33,7 @@ def main(): file_start = read_base_file(filename) # If you add a version to the list fix the docs (errorcodes.rst, err.rst) classes, errors = fetch_errors( - ['8.1', '8.2', '8.3', '8.4', '9.0', '9.1', '9.2', '9.3', '9.4']) + ['8.1', '8.2', '8.3', '8.4', '9.0', '9.1', '9.2', '9.3', '9.4', '9.5']) f = open(filename, "w") for line in file_start: diff --git a/setup.py b/setup.py index 210ad831..6414a88f 100644 --- a/setup.py +++ b/setup.py @@ -41,6 +41,7 @@ Programming Language :: Python :: 3.1 Programming Language :: Python :: 3.2 Programming Language :: Python :: 3.3 Programming Language :: Python :: 3.4 +Programming Language :: Python :: 3.5 Programming Language :: C Programming Language :: SQL Topic :: Database @@ -57,7 +58,10 @@ import os import sys import re import subprocess -from distutils.core import setup, Extension +try: + from setuptools import setup, Extension +except ImportError: + from distutils.core import setup, Extension from distutils.command.build_ext import build_ext from distutils.sysconfig import get_python_inc from distutils.ccompiler import get_default_compiler @@ -301,6 +305,10 @@ class psycopg_build_ext(build_ext): except AttributeError: ext_path = os.path.join(self.build_lib, 'psycopg2', '_psycopg.pyd') + # Make sure spawn() will work if compile() was never + # called. https://github.com/psycopg/psycopg2/issues/380 + if not self.compiler.initialized: + self.compiler.initialize() self.compiler.spawn( ['mt.exe', '-nologo', '-manifest', os.path.join('psycopg', manifest), @@ -343,7 +351,8 @@ class psycopg_build_ext(build_ext): self.libraries.append("advapi32") if self.compiler_is_msvc(): # MSVC requires an explicit "libpq" - self.libraries.remove("pq") + if "pq" in self.libraries: + self.libraries.remove("pq") self.libraries.append("secur32") self.libraries.append("libpq") self.libraries.append("shfolder") diff --git a/tests/test_connection.py b/tests/test_connection.py index e92c288b..8aa5a2b5 100755 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -446,6 +446,13 @@ class MakeDsnTestCase(ConnectingTestCase): self.assertRaises(psycopg2.ProgrammingError, ext.make_dsn, url, nosuch="param") + @skip_before_libpq(9, 3) + def test_get_dsn_parameters(self): + conn = self.connect() + d = conn.get_dsn_parameters() + self.assertEqual(d['dbname'], dbname) # the only param we can check reliably + self.assertNotIn('password', d) + class IsolationLevelsTestCase(ConnectingTestCase): diff --git a/tests/test_quote.py b/tests/test_quote.py index 6e945624..25d1d31c 100755 --- a/tests/test_quote.py +++ b/tests/test_quote.py @@ -23,12 +23,14 @@ # License for more details. import sys -from testutils import unittest, ConnectingTestCase, skip_before_libpq +import testutils +from testutils import unittest, ConnectingTestCase import psycopg2 import psycopg2.extensions from psycopg2.extensions import b + class QuotingTestCase(ConnectingTestCase): r"""Checks the correct quoting of strings and binary objects. @@ -51,7 +53,7 @@ class QuotingTestCase(ConnectingTestCase): data = """some data with \t chars to escape into, 'quotes' and \\ a backslash too. """ - data += "".join(map(chr, range(1,127))) + data += "".join(map(chr, range(1, 127))) curs = self.conn.cursor() curs.execute("SELECT %s;", (data,)) @@ -90,13 +92,13 @@ class QuotingTestCase(ConnectingTestCase): if server_encoding != "UTF8": return self.skipTest( "Unicode test skipped since server encoding is %s" - % server_encoding) + % server_encoding) data = u"""some data with \t chars to escape into, 'quotes', \u20ac euro sign and \\ a backslash too. """ - data += u"".join(map(unichr, [ u for u in range(1,65536) - if not 0xD800 <= u <= 0xDFFF ])) # surrogate area + data += u"".join(map(unichr, [u for u in range(1, 65536) + if not 0xD800 <= u <= 0xDFFF])) # surrogate area self.conn.set_client_encoding('UNICODE') psycopg2.extensions.register_type(psycopg2.extensions.UNICODE, self.conn) @@ -156,7 +158,7 @@ class QuotingTestCase(ConnectingTestCase): class TestQuotedString(ConnectingTestCase): - def test_encoding(self): + def test_encoding_from_conn(self): q = psycopg2.extensions.QuotedString('hi') self.assertEqual(q.encoding, 'latin1') @@ -166,13 +168,13 @@ class TestQuotedString(ConnectingTestCase): class TestQuotedIdentifier(ConnectingTestCase): - @skip_before_libpq(9, 0) + @testutils.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) + @testutils.skip_before_libpq(9, 0) def test_unicode_ident(self): from psycopg2.extensions import quote_ident snowman = u"\u2603" @@ -183,9 +185,59 @@ class TestQuotedIdentifier(ConnectingTestCase): self.assertEqual(quote_ident(snowman, self.conn), quoted) +class TestStringAdapter(ConnectingTestCase): + def test_encoding_default(self): + from psycopg2.extensions import adapt + a = adapt("hello") + self.assertEqual(a.encoding, 'latin1') + self.assertEqual(a.getquoted(), b("'hello'")) + + # NOTE: we can't really test an encoding different from utf8, because + # when encoding without connection the libpq will use parameters from + # a previous one, so what would happens depends jn the tests run order. + # egrave = u'\xe8' + # self.assertEqual(adapt(egrave).getquoted(), "'\xe8'") + + def test_encoding_error(self): + from psycopg2.extensions import adapt + snowman = u"\u2603" + a = adapt(snowman) + self.assertRaises(UnicodeEncodeError, a.getquoted) + + def test_set_encoding(self): + # Note: this works-ish mostly in case when the standard db connection + # we test with is utf8, otherwise the encoding chosen by PQescapeString + # may give bad results. + from psycopg2.extensions import adapt + snowman = u"\u2603" + a = adapt(snowman) + a.encoding = 'utf8' + self.assertEqual(a.encoding, 'utf8') + self.assertEqual(a.getquoted(), b("'\xe2\x98\x83'")) + + def test_connection_wins_anyway(self): + from psycopg2.extensions import adapt + snowman = u"\u2603" + a = adapt(snowman) + a.encoding = 'latin9' + + self.conn.set_client_encoding('utf8') + a.prepare(self.conn) + + self.assertEqual(a.encoding, 'utf_8') + self.assertEqual(a.getquoted(), b("'\xe2\x98\x83'")) + + @testutils.skip_before_python(3) + def test_adapt_bytes(self): + snowman = u"\u2603" + self.conn.set_client_encoding('utf8') + a = psycopg2.extensions.QuotedString(snowman.encode('utf8')) + a.prepare(self.conn) + self.assertEqual(a.getquoted(), b("'\xe2\x98\x83'")) + + def test_suite(): return unittest.TestLoader().loadTestsFromName(__name__) if __name__ == "__main__": unittest.main() - diff --git a/tests/test_types_basic.py b/tests/test_types_basic.py index 199dc1b6..248712b0 100755 --- a/tests/test_types_basic.py +++ b/tests/test_types_basic.py @@ -95,11 +95,11 @@ class TypesBasicTests(ConnectingTestCase): except ValueError: return self.skipTest("inf not available on this platform") s = self.execute("SELECT %s AS foo", (float("inf"),)) - 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)) s = self.execute("SELECT %s AS foo", (float("-inf"),)) - self.failUnless(str(s) == "-inf", "wrong float quoting: " + str(s)) + self.failUnless(str(s) == "-inf", "wrong float quoting: " + str(s)) def testBinary(self): if sys.version_info[0] < 3: @@ -192,6 +192,7 @@ class TypesBasicTests(ConnectingTestCase): self.assertRaises(psycopg2.DataError, psycopg2.extensions.STRINGARRAY, b(s), curs) + @testutils.skip_before_postgres(8, 2) def testArrayOfNulls(self): curs = self.conn.cursor() curs.execute(""" @@ -363,8 +364,8 @@ class AdaptSubclassTest(unittest.TestCase): try: self.assertEqual(b('b'), adapt(C()).getquoted()) finally: - del psycopg2.extensions.adapters[A, psycopg2.extensions.ISQLQuote] - del psycopg2.extensions.adapters[B, psycopg2.extensions.ISQLQuote] + del psycopg2.extensions.adapters[A, psycopg2.extensions.ISQLQuote] + del psycopg2.extensions.adapters[B, psycopg2.extensions.ISQLQuote] @testutils.skip_from_python(3) def test_no_mro_no_joy(self): @@ -377,8 +378,7 @@ class AdaptSubclassTest(unittest.TestCase): try: self.assertRaises(psycopg2.ProgrammingError, adapt, B()) finally: - del psycopg2.extensions.adapters[A, psycopg2.extensions.ISQLQuote] - + del psycopg2.extensions.adapters[A, psycopg2.extensions.ISQLQuote] @testutils.skip_before_python(3) def test_adapt_subtype_3(self): @@ -391,7 +391,7 @@ class AdaptSubclassTest(unittest.TestCase): try: self.assertEqual(b("a"), adapt(B()).getquoted()) finally: - del psycopg2.extensions.adapters[A, psycopg2.extensions.ISQLQuote] + del psycopg2.extensions.adapters[A, psycopg2.extensions.ISQLQuote] class ByteaParserTest(unittest.TestCase): @@ -479,6 +479,7 @@ class ByteaParserTest(unittest.TestCase): self.assertEqual(rv, tgt) + def skip_if_cant_cast(f): @wraps(f) def skip_if_cant_cast_(self, *args, **kwargs): @@ -498,4 +499,3 @@ def test_suite(): if __name__ == "__main__": unittest.main() - diff --git a/tests/testutils.py b/tests/testutils.py index 70eb2cc9..1d1ad054 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -101,7 +101,7 @@ class ConnectingTestCase(unittest.TestCase): self._conns except AttributeError, e: raise AttributeError( - "%s (did you remember calling ConnectingTestCase.setUp()?)" + "%s (did you forget to call ConnectingTestCase.setUp()?)" % e) if 'dsn' in kwargs: