diff --git a/.travis.yml b/.travis.yml index 2fbf7010..51cba9a7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,12 +6,14 @@ language: python python: - 2.7 + - 3.7-dev - 3.6 - 3.5 - 3.4 install: - - python setup.py install + - pip install -U pip setuptools wheel + - pip install . - rm -rf psycopg2.egg-info - sudo scripts/travis_prepare.sh diff --git a/LICENSE b/LICENSE index 360a44f6..bdeaf9c4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ psycopg2 and the LGPL -===================== +--------------------- psycopg2 is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published @@ -29,15 +29,15 @@ If not, see . Alternative licenses -==================== +-------------------- If you prefer you can use the Zope Database Adapter ZPsycopgDA (i.e., -every file inside the ZPsycopgDA directory) user the ZPL license as +every file inside the ZPsycopgDA directory) using the ZPL license as published on the Zope web site, http://www.zope.org/Resources/ZPL. Also, the following BSD-like license applies (at your option) to the -files following the pattern psycopg/adapter*.{h,c} and -psycopg/microprotocol*.{h,c}: +files following the pattern ``psycopg/adapter*.{h,c}`` and +``psycopg/microprotocol*.{h,c}``: Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it diff --git a/NEWS b/NEWS index 09b19e5f..c44639da 100644 --- a/NEWS +++ b/NEWS @@ -17,10 +17,14 @@ Other changes: What's new in psycopg 2.7.5 ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +- Allow non-ascii chars in namedtuple fields (regression introduced fixing + :ticket:`#211`). +- Fixed adaptation of arrays of arrays of nulls (:ticket:`#325`). - Fixed building on Solaris 11 and derivatives such as SmartOS and illumos (:ticket:`#677`). - Maybe fixed building on MSYS2 (as reported in :ticket:`#658`). - Allow string subclasses in connection and other places (:ticket:`#679`). +- Don't raise an exception closing an unused named cursor (:ticket:`#716`). What's new in psycopg 2.7.4 diff --git a/README.rst b/README.rst index b5b048d4..a9785e3a 100644 --- a/README.rst +++ b/README.rst @@ -54,8 +54,8 @@ external libraries, by installing the `psycopg2-binary`_ package from PyPI:: The binary package is a practical choice for development and testing but in production it is advised to use the package built from sources. -.. _PyPI: https://pypi.python.org/pypi/psycopg2 -.. _psycopg2-binary: https://pypi.python.org/pypi/psycopg2-binary +.. _PyPI: https://pypi.org/project/psycopg2/ +.. _psycopg2-binary: https://pypi.org/project/psycopg2-binary/ .. _install: http://initd.org/psycopg/docs/install.html#install-from-source .. _faq: http://initd.org/psycopg/docs/faq.html#faq-compile diff --git a/doc/Makefile b/doc/Makefile index 2903b9d0..558d0a75 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -29,8 +29,6 @@ doctest: upload: # this command requires ssh configured to the proper target tar czf - -C html . | ssh psycoweb tar xzvf - -C docs/current - # this command requires a .pypirc with the right privileges - # python src/tools/pypi_docs_upload.py psycopg2 $$(pwd)/html clean: $(MAKE) $(SPHOPTS) -C src $@ diff --git a/doc/release.rst b/doc/release.rst index 3576cdc9..3ea4a9fa 100644 --- a/doc/release.rst +++ b/doc/release.rst @@ -100,5 +100,5 @@ Test packages may be uploaded on the `PyPI testing site`__ using:: assuming `proper configuration`__ of ``~/.pypirc``. -.. __: https://testpypi.python.org/pypi/psycopg2 +.. __: https://test.pypi.org/project/psycopg2/ .. __: https://wiki.python.org/moin/TestPyPI diff --git a/doc/src/advanced.rst b/doc/src/advanced.rst index d1683b8b..724cb281 100644 --- a/doc/src/advanced.rst +++ b/doc/src/advanced.rst @@ -485,7 +485,7 @@ details. You can check the `psycogreen`_ project for further informations and resources about the topic. .. _coroutine: http://en.wikipedia.org/wiki/Coroutine -.. _greenlet: http://pypi.python.org/pypi/greenlet +.. _greenlet: https://pypi.org/project/greenlet/ .. _green threads: http://en.wikipedia.org/wiki/Green_threads .. _Eventlet: http://eventlet.net/ .. _gevent: http://www.gevent.org/ diff --git a/doc/src/conf.py b/doc/src/conf.py index a27d6cf4..2c52a568 100644 --- a/doc/src/conf.py +++ b/doc/src/conf.py @@ -61,8 +61,8 @@ except ImportError: release = version intersphinx_mapping = { - 'py': ('http://docs.python.org/2', None), - 'py3': ('http://docs.python.org/3', None), + 'py': ('https://docs.python.org/2', None), + 'py3': ('https://docs.python.org/3', None), } # Pattern to generate links to the bug tracker diff --git a/doc/src/index.rst b/doc/src/index.rst index 852bbc2c..7ae073d7 100644 --- a/doc/src/index.rst +++ b/doc/src/index.rst @@ -48,6 +48,7 @@ Psycopg 2 is both Unicode and Python 3 friendly. errorcodes faq news + license .. ifconfig:: builder != 'text' diff --git a/doc/src/install.rst b/doc/src/install.rst index a858cbe3..f5524a56 100644 --- a/doc/src/install.rst +++ b/doc/src/install.rst @@ -45,7 +45,9 @@ Build prerequisites ^^^^^^^^^^^^^^^^^^^ The build prerequisites are to be met in order to install Psycopg from source -code, either from a source distribution package or from PyPI. +code, from a source distribution package, GitHub_ or from PyPI. + +.. _GitHub: https://github.com/psycopg/psycopg2 Psycopg is a C wrapper around the libpq_ PostgreSQL client library. To install it from sources you will need: @@ -140,7 +142,7 @@ Make sure to use an up-to-date version of :program:`pip` (you can upgrade it using something like ``pip install -U pip``) .. __: PyPI-binary_ -.. _PyPI-binary: https://pypi.python.org/pypi/psycopg2-binary/ +.. _PyPI-binary: https://pypi.org/project/psycopg2-binary/ .. _wheel: http://pythonwheels.com/ .. note:: @@ -302,10 +304,14 @@ Try the following. *In order:* - Google for `!psycopg2` *your error message*. Especially useful the week after the release of a new OS X version. -- Write to the `Mailing List`__. +- Write to the `Mailing List`_. + +- If you think that you have discovered a bug, test failure or missing feature + please raise a ticket in the `bug tracker`_. - Complain on your blog or on Twitter that `!psycopg2` is the worst package ever and about the quality time you have wasted figuring out the correct :envvar:`ARCHFLAGS`. Especially useful from the Starbucks near you. -.. __: https://lists.postgresql.org/mj/mj_wwwusr?func=lists-long-full&extra=psycopg +.. _mailing list: https://lists.postgresql.org/mj/mj_wwwusr?func=lists-long-full&extra=psycopg +.. _bug tracker: https://github.com/psycopg/psycopg2/issues diff --git a/doc/src/license.rst b/doc/src/license.rst new file mode 100644 index 00000000..53a4e724 --- /dev/null +++ b/doc/src/license.rst @@ -0,0 +1,7 @@ +.. index:: + single: License + +License +======= + +.. include:: ../../LICENSE diff --git a/doc/src/news.rst b/doc/src/news.rst index d5b11a69..053d6464 100644 --- a/doc/src/news.rst +++ b/doc/src/news.rst @@ -1,3 +1,7 @@ +.. index:: + single: Release notes + single: News + Release notes ============= diff --git a/lib/extras.py b/lib/extras.py index 1f85d532..9c26ccba 100644 --- a/lib/extras.py +++ b/lib/extras.py @@ -363,12 +363,15 @@ class NamedTupleCursor(_cursor): return def _make_nt(self): + # ascii except alnum and underscore + nochars = ' !"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~' + re_clean = _re.compile('[' + _re.escape(nochars) + ']') + def f(s): - # NOTE: Python 3 actually allows unicode chars in fields - s = _re.sub('[^a-zA-Z0-9_]', '_', s) + s = re_clean.sub('_', s) # Python identifier cannot start with numbers, namedtuple fields # cannot start with underscore. So... - if _re.match('^[0-9_]', s): + if s[0] == '_' or '0' <= s[0] <= '9': s = 'f' + s return s diff --git a/psycopg/adapter_list.c b/psycopg/adapter_list.c index dec17b4c..3fdff76a 100644 --- a/psycopg/adapter_list.c +++ b/psycopg/adapter_list.c @@ -38,13 +38,14 @@ list_quote(listObject *self) { /* adapt the list by calling adapt() recursively and then wrapping everything into "ARRAY[]" */ - PyObject *tmp = NULL, *str = NULL, *joined = NULL, *res = NULL; + PyObject *res = NULL; + PyObject **qs = NULL; + Py_ssize_t bufsize = 0; + char *buf = NULL, *ptr; /* list consisting of only NULL don't work with the ARRAY[] construct - * so we use the {NULL,...} syntax. Note however that list of lists where - * some element is a list of only null still fails: for that we should use - * the '{...}' syntax uniformly but we cannot do it in the current - * infrastructure. TODO in psycopg3 */ + * so we use the {NULL,...} syntax. The same syntax is also necessary + * to convert array of arrays containing only nulls. */ int all_nulls = 1; Py_ssize_t i, len; @@ -53,47 +54,95 @@ list_quote(listObject *self) /* empty arrays are converted to NULLs (still searching for a way to insert an empty array in postgresql */ - if (len == 0) return Bytes_FromString("'{}'"); + if (len == 0) { + res = Bytes_FromString("'{}'"); + goto exit; + } - tmp = PyTuple_New(len); + if (!(qs = PyMem_New(PyObject *, len))) { + PyErr_NoMemory(); + goto exit; + } + memset(qs, 0, len * sizeof(PyObject *)); - for (i=0; iwrapped, i); if (wrapped == Py_None) { Py_INCREF(psyco_null); - quoted = psyco_null; + qs[i] = psyco_null; } else { - quoted = microprotocol_getquoted(wrapped, - (connectionObject*)self->connection); - if (quoted == NULL) goto error; - all_nulls = 0; + if (!(qs[i] = microprotocol_getquoted( + wrapped, (connectionObject*)self->connection))) { + goto exit; + } + + /* Lists of arrays containing only nulls are also not supported + * by the ARRAY construct so we should do some special casing */ + if (!PyList_Check(wrapped) || Bytes_AS_STRING(qs[i])[0] == 'A') { + all_nulls = 0; + } } - - /* here we don't loose a refcnt: SET_ITEM does not change the - reference count and we are just transferring ownership of the tmp - object to the tuple */ - PyTuple_SET_ITEM(tmp, i, quoted); + bufsize += Bytes_GET_SIZE(qs[i]) + 1; /* this, and a comma */ } - /* now that we have a tuple of adapted objects we just need to join them - and put "ARRAY[] around the result */ - str = Bytes_FromString(", "); - joined = PyObject_CallMethod(str, "join", "(O)", tmp); - if (joined == NULL) goto error; + /* Create an array literal, usually ARRAY[...] but if the contents are + * all NULL or array of NULL we must use the '{...}' syntax + */ + if (!(ptr = buf = PyMem_Malloc(bufsize + 8))) { + PyErr_NoMemory(); + goto exit; + } - /* PG doesn't like ARRAY[NULL..] */ if (!all_nulls) { - res = Bytes_FromFormat("ARRAY[%s]", Bytes_AsString(joined)); - } else { - res = Bytes_FromFormat("'{%s}'", Bytes_AsString(joined)); + strcpy(ptr, "ARRAY["); + ptr += 6; + for (i = 0; i < len; i++) { + Py_ssize_t sl; + sl = Bytes_GET_SIZE(qs[i]); + memcpy(ptr, Bytes_AS_STRING(qs[i]), sl); + ptr += sl; + *ptr++ = ','; + } + *(ptr - 1) = ']'; + } + else { + *ptr++ = '\''; + *ptr++ = '{'; + for (i = 0; i < len; i++) { + /* in case all the adapted things are nulls (or array of nulls), + * the quoted string is either NULL or an array of the form + * '{NULL,...}', in which case we have to strip the extra quotes */ + char *s; + Py_ssize_t sl; + s = Bytes_AS_STRING(qs[i]); + sl = Bytes_GET_SIZE(qs[i]); + if (s[0] != '\'') { + memcpy(ptr, s, sl); + ptr += sl; + } + else { + memcpy(ptr, s + 1, sl - 2); + ptr += sl - 2; + } + *ptr++ = ','; + } + *(ptr - 1) = '}'; + *ptr++ = '\''; } - error: - Py_XDECREF(tmp); - Py_XDECREF(str); - Py_XDECREF(joined); + res = Bytes_FromStringAndSize(buf, ptr - buf); + +exit: + if (qs) { + for (i = 0; i < len; i++) { + PyObject *q = qs[i]; + Py_XDECREF(q); + } + PyMem_Free(qs); + } + PyMem_Free(buf); + return res; } diff --git a/psycopg/cursor_type.c b/psycopg/cursor_type.c index b7fd1870..d73bc3a4 100644 --- a/psycopg/cursor_type.c +++ b/psycopg/cursor_type.c @@ -59,6 +59,11 @@ psyco_curs_close(cursorObject *self) char buffer[128]; PGTransactionStatusType status; + if (!self->query) { + Dprintf("skipping named cursor close because unused"); + goto close; + } + if (self->conn) { status = PQtransactionStatus(self->conn->pgconn); } @@ -66,17 +71,18 @@ psyco_curs_close(cursorObject *self) status = PQTRANS_UNKNOWN; } - if (!(status == PQTRANS_UNKNOWN || status == PQTRANS_INERROR)) { - EXC_IF_NO_MARK(self); - PyOS_snprintf(buffer, 127, "CLOSE %s", self->qname); - if (pq_execute(self, buffer, 0, 0, 1) == -1) return NULL; - } - else { + if (status == PQTRANS_UNKNOWN || status == PQTRANS_INERROR) { Dprintf("skipping named curs close because tx status %d", (int)status); + goto close; } + + EXC_IF_NO_MARK(self); + PyOS_snprintf(buffer, 127, "CLOSE %s", self->qname); + if (pq_execute(self, buffer, 0, 0, 1) == -1) return NULL; } +close: self->closed = 1; Dprintf("psyco_curs_close: cursor at %p closed", self); diff --git a/psycopg/typecast_datetime.c b/psycopg/typecast_datetime.c index f24223cb..e34117dd 100644 --- a/psycopg/typecast_datetime.c +++ b/psycopg/typecast_datetime.c @@ -406,6 +406,11 @@ typecast_PYINTERVAL_cast(const char *str, Py_ssize_t len, PyObject *curs) } break; + case 'P': + PyErr_SetString(NotSupportedError, + "iso_8601 intervalstyle currently not supported"); + return NULL; + default: break; } diff --git a/scripts/travis_test.sh b/scripts/travis_test.sh index 0320654a..342e24cc 100755 --- a/scripts/travis_test.sh +++ b/scripts/travis_test.sh @@ -56,15 +56,15 @@ fi # Unsupported postgres versions that we still support # Images built by https://github.com/psycopg/psycopg2-wheels/tree/build-dinosaurs if [[ -n "$TEST_PAST" ]]; then - run_test 7.4 - run_test 8.0 - run_test 8.1 - run_test 8.2 - run_test 8.3 - run_test 8.4 - run_test 9.0 - run_test 9.1 run_test 9.2 + run_test 9.1 + run_test 9.0 + run_test 8.4 + run_test 8.3 + run_test 8.2 + run_test 8.1 + run_test 8.0 + run_test 7.4 fi # Postgres built from master diff --git a/tests/test_cursor.py b/tests/test_cursor.py index cc8db0f4..b3e03d9b 100755 --- a/tests/test_cursor.py +++ b/tests/test_cursor.py @@ -435,6 +435,11 @@ class CursorTests(ConnectingTestCase): self.assertEqual([(2,), (3,), (4,)], cur2.fetchmany(3)) self.assertEqual([(5,), (6,), (7,)], cur2.fetchall()) + @skip_before_postgres(8, 0) + def test_named_noop_close(self): + cur = self.conn.cursor('test') + cur.close() + @skip_before_postgres(8, 0) def test_scroll(self): cur = self.conn.cursor() diff --git a/tests/test_dates.py b/tests/test_dates.py index 74dfc9ab..bb5aee30 100755 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -438,6 +438,14 @@ class DatetimeTests(ConnectingTestCase, CommonDatetimeTestsMixin): r = cur.fetchone()[0] self.assertEqual(r, v, "%s -> %s != %s" % (s, r, v)) + @skip_before_postgres(8, 4) + def test_interval_iso_8601_not_supported(self): + # We may end up supporting, but no pressure for it + cur = self.conn.cursor() + cur.execute("set local intervalstyle to iso_8601") + cur.execute("select '1 day 2 hours'::interval") + self.assertRaises(psycopg2.NotSupportedError, cur.fetchone) + # Only run the datetime tests if psycopg was compiled with support. if not hasattr(psycopg2.extensions, 'PYDATETIME'): diff --git a/tests/test_extras_dictcursor.py b/tests/test_extras_dictcursor.py index 99bdeee6..d43980af 100755 --- a/tests/test_extras_dictcursor.py +++ b/tests/test_extras_dictcursor.py @@ -19,7 +19,7 @@ from datetime import timedelta import psycopg2 import psycopg2.extras import unittest -from .testutils import ConnectingTestCase, skip_before_postgres +from .testutils import ConnectingTestCase, skip_before_postgres, skip_before_python class ExtrasDictCursorTests(ConnectingTestCase): @@ -357,6 +357,14 @@ class NamedTupleCursorTest(ConnectingTestCase): self.assertEqual(rv.f_column_, 2) self.assertEqual(rv.f3, 3) + @skip_before_python(3) + @skip_before_postgres(8) + def test_nonascii_name(self): + curs = self.conn.cursor() + curs.execute('select 1 as \xe5h\xe9') + rv = curs.fetchone() + self.assertEqual(getattr(rv, '\xe5h\xe9'), 1) + def test_minimal_generation(self): # Instrument the class to verify it gets called the minimum number of times. from psycopg2.extras import NamedTupleCursor diff --git a/tests/test_types_basic.py b/tests/test_types_basic.py index a93265d9..76b9aa3f 100755 --- a/tests/test_types_basic.py +++ b/tests/test_types_basic.py @@ -224,16 +224,31 @@ class TypesBasicTests(ConnectingTestCase): curs.execute("insert into na (boola) values (%s)", ([True, None],)) curs.execute("insert into na (boola) values (%s)", ([None, None],)) - # TODO: array of array of nulls are not supported yet - # curs.execute("insert into na (textaa) values (%s)", ([[None]],)) + curs.execute("insert into na (textaa) values (%s)", ([[None]],)) curs.execute("insert into na (textaa) values (%s)", ([['a', None]],)) - # curs.execute("insert into na (textaa) values (%s)", ([[None, None]],)) - # curs.execute("insert into na (intaa) values (%s)", ([[None]],)) + curs.execute("insert into na (textaa) values (%s)", ([[None, None]],)) + + curs.execute("insert into na (intaa) values (%s)", ([[None]],)) curs.execute("insert into na (intaa) values (%s)", ([[42, None]],)) - # curs.execute("insert into na (intaa) values (%s)", ([[None, None]],)) - # curs.execute("insert into na (boolaa) values (%s)", ([[None]],)) + curs.execute("insert into na (intaa) values (%s)", ([[None, None]],)) + + curs.execute("insert into na (boolaa) values (%s)", ([[None]],)) curs.execute("insert into na (boolaa) values (%s)", ([[True, None]],)) - # curs.execute("insert into na (boolaa) values (%s)", ([[None, None]],)) + curs.execute("insert into na (boolaa) values (%s)", ([[None, None]],)) + + @testutils.skip_before_postgres(8, 2) + def testNestedArrays(self): + curs = self.conn.cursor() + for a in [ + [[1]], + [[None]], + [[None, None, None]], + [[None, None], [1, None]], + [[None, None], [None, None]], + [[[None, None], [None, None]]], + ]: + curs.execute("select %s::int[]", (a,)) + self.assertEqual(curs.fetchone()[0], a) @testutils.skip_from_python(3) def testTypeRoundtripBuffer(self): diff --git a/tests/test_types_extras.py b/tests/test_types_extras.py index 5cb13534..cda163b6 100755 --- a/tests/test_types_extras.py +++ b/tests/test_types_extras.py @@ -179,8 +179,8 @@ class HstoreTestCase(ConnectingTestCase): m = re.match(br'hstore\(ARRAY\[([^\]]+)\], ARRAY\[([^\]]+)\]\)', q) self.assert_(m, repr(q)) - kk = m.group(1).split(b", ") - vv = m.group(2).split(b", ") + kk = m.group(1).split(b",") + vv = m.group(2).split(b",") ii = list(zip(kk, vv)) ii.sort() diff --git a/tests/test_with.py b/tests/test_with.py index 1392d85f..f26f8f9c 100755 --- a/tests/test_with.py +++ b/tests/test_with.py @@ -26,7 +26,7 @@ import psycopg2 import psycopg2.extensions as ext import unittest -from .testutils import ConnectingTestCase +from .testutils import ConnectingTestCase, skip_before_postgres class WithTestCase(ConnectingTestCase): @@ -215,6 +215,11 @@ class WithCursorTestCase(WithTestCase): else: self.fail("where is my exception?") + @skip_before_postgres(8, 0) + def test_named_with_noop(self): + with self.conn.cursor('named') as cur: + pass + def test_suite(): return unittest.TestLoader().loadTestsFromName(__name__) diff --git a/tox.ini b/tox.ini index 17612e25..a0eafa4d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,9 @@ -# Tox (http://tox.testrun.org/) is a tool for running tests -# in multiple virtualenvs. This configuration file will run the -# test suite on all supported python versions. To use it, "pip install tox" -# and then run "tox" from this directory. - [tox] -envlist = py27 +envlist = py{27,34,35,36} [testenv] commands = make check +whitelist_externals = make [flake8] max-line-length = 85