diff --git a/NEWS b/NEWS index 46a393d5..7df532a3 100644 --- a/NEWS +++ b/NEWS @@ -4,7 +4,7 @@ Current release What's new in psycopg 2.9 ------------------------- -- Dropped support for Python 3.4, 3.5 (:tickets:#1000, #1197). +- Dropped support for Python 2.7, 3.4, 3.5 (:tickets:#1198, #1000, #1197). What's new in psycopg 2.8.6 ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -13,7 +13,7 @@ What's new in psycopg 2.8.6 (:ticket:`#1101`). - Fixed search of mxDateTime headers in virtualenvs (:ticket:`#996`). - Added missing values from errorcodes (:ticket:`#1133`). -- `cursor.query` reports the query of the last :sql:`COPY` opearation too +- `cursor.query` reports the query of the last :sql:`COPY` operation too (:ticket:`#1141`). - `~psycopg2.errorcodes` map and `~psycopg2.errors` classes updated to PostgreSQL 13. diff --git a/doc/src/install.rst b/doc/src/install.rst index a685cdef..c771c9b7 100644 --- a/doc/src/install.rst +++ b/doc/src/install.rst @@ -131,8 +131,7 @@ The current `!psycopg2` implementation supports: .. NOTE: keep consistent with setup.py and the /features/ page. -- Python version 2.7 -- Python 3 versions from 3.6 to 3.9 +- Python versions from 3.6 to 3.9 - PostgreSQL server versions from 7.4 to 13 - PostgreSQL client library version from 9.1 diff --git a/lib/_lru_cache.py b/lib/_lru_cache.py deleted file mode 100644 index 1e2c52d0..00000000 --- a/lib/_lru_cache.py +++ /dev/null @@ -1,104 +0,0 @@ -""" -LRU cache implementation for Python 2.7 - -Ported from http://code.activestate.com/recipes/578078/ and simplified for our -use (only support maxsize > 0 and positional arguments). -""" - -from collections import namedtuple -from functools import update_wrapper -from threading import RLock - -_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"]) - - -def lru_cache(maxsize=100): - """Least-recently-used cache decorator. - - Arguments to the cached function must be hashable. - - See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used - - """ - def decorating_function(user_function): - - cache = dict() - stats = [0, 0] # make statistics updateable non-locally - HITS, MISSES = 0, 1 # names for the stats fields - cache_get = cache.get # bound method to lookup key or return None - _len = len # localize the global len() function - lock = RLock() # linkedlist updates aren't threadsafe - root = [] # root of the circular doubly linked list - root[:] = [root, root, None, None] # initialize by pointing to self - nonlocal_root = [root] # make updateable non-locally - PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields - - assert maxsize and maxsize > 0, "maxsize %s not supported" % maxsize - - def wrapper(*args): - # size limited caching that tracks accesses by recency - key = args - with lock: - link = cache_get(key) - if link is not None: - # record recent use of the key by moving it to the - # front of the list - root, = nonlocal_root - link_prev, link_next, key, result = link - link_prev[NEXT] = link_next - link_next[PREV] = link_prev - last = root[PREV] - last[NEXT] = root[PREV] = link - link[PREV] = last - link[NEXT] = root - stats[HITS] += 1 - return result - result = user_function(*args) - with lock: - root, = nonlocal_root - if key in cache: - # getting here means that this same key was added to the - # cache while the lock was released. since the link - # update is already done, we need only return the - # computed result and update the count of misses. - pass - elif _len(cache) >= maxsize: - # use the old root to store the new key and result - oldroot = root - oldroot[KEY] = key - oldroot[RESULT] = result - # empty the oldest link and make it the new root - root = nonlocal_root[0] = oldroot[NEXT] - oldkey = root[KEY] - # oldvalue = root[RESULT] - root[KEY] = root[RESULT] = None - # now update the cache dictionary for the new links - del cache[oldkey] - cache[key] = oldroot - else: - # put result in a new link at the front of the list - last = root[PREV] - link = [last, root, key, result] - last[NEXT] = root[PREV] = cache[key] = link - stats[MISSES] += 1 - return result - - def cache_info(): - """Report cache statistics""" - with lock: - return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache)) - - def cache_clear(): - """Clear the cache and cache statistics""" - with lock: - cache.clear() - root = nonlocal_root[0] - root[:] = [root, root, None, None] - stats[:] = [0, 0] - - wrapper.__wrapped__ = user_function - wrapper.cache_info = cache_info - wrapper.cache_clear = cache_clear - return update_wrapper(wrapper, user_function) - - return decorating_function diff --git a/scripts/appveyor.py b/scripts/appveyor.py index fd939cf3..d2cb76da 100755 --- a/scripts/appveyor.py +++ b/scripts/appveyor.py @@ -696,7 +696,7 @@ class Options: def py_ver(self): """The Python version to build as 2 digits string.""" rv = os.environ['PY_VER'] - assert rv in ('27', '36', '37', '38', '39'), rv + assert rv in ('36', '37', '38', '39'), rv return rv @property @@ -779,11 +779,9 @@ class Options: def vs_ver(self): # https://wiki.python.org/moin/WindowsCompilers # https://www.appveyor.com/docs/windows-images-software/#python - # Py 2.7 = VS Ver. 9.0 (VS 2008) # Py 3.6--3.8 = VS Ver. 14.0 (VS 2015) # Py 3.9 = VS Ver. 16.0 (VS 2019) vsvers = { - '27': '9.0', '36': '14.0', '37': '14.0', '38': '14.0', diff --git a/tests/test_async_keyword.py b/tests/test_async_keyword.py deleted file mode 100755 index 162db1c6..00000000 --- a/tests/test_async_keyword.py +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env python - -# test_async_keyword.py - test for objects using 'async' as attribute/param -# -# Copyright (C) 2017-2019 Daniele Varrazzo -# Copyright (C) 2020 The Psycopg Team -# -# psycopg2 is free software: you can redistribute it and/or modify it -# under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# In addition, as a special exception, the copyright holders give -# permission to link this program with the OpenSSL library (or with -# modified versions of OpenSSL that use the same license as OpenSSL), -# and distribute linked combinations including the two. -# -# You must obey the GNU Lesser General Public License in all respects for -# all of the code used other than OpenSSL. -# -# psycopg2 is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public -# License for more details. - -import time -from select import select - -import psycopg2 -from psycopg2 import extras - -from .testconfig import dsn -import unittest -from .testutils import ConnectingTestCase, skip_before_postgres, slow - -from .test_replication import ReplicationTestCase, skip_repl_if_green -from psycopg2.extras import LogicalReplicationConnection, StopReplication - - -class AsyncTests(ConnectingTestCase): - def setUp(self): - ConnectingTestCase.setUp(self) - - self.sync_conn = self.conn - self.conn = self.connect(async=True) - - self.wait(self.conn) - - curs = self.conn.cursor() - curs.execute(''' - CREATE TEMPORARY TABLE table1 ( - id int PRIMARY KEY - )''') - self.wait(curs) - - def test_connection_setup(self): - cur = self.conn.cursor() - sync_cur = self.sync_conn.cursor() - del cur, sync_cur - - self.assert_(self.conn.async) - self.assert_(not self.sync_conn.async) - - # the async connection should be autocommit - self.assert_(self.conn.autocommit) - - # check other properties to be found on the connection - self.assert_(self.conn.server_version) - self.assert_(self.conn.protocol_version in (2, 3)) - self.assert_(self.conn.encoding in psycopg2.extensions.encodings) - - def test_async_subclass(self): - class MyConn(psycopg2.extensions.connection): - def __init__(self, dsn, async=0): - psycopg2.extensions.connection.__init__(self, dsn, async=async) - - conn = self.connect(connection_factory=MyConn, async=True) - self.assert_(isinstance(conn, MyConn)) - self.assert_(conn.async) - conn.close() - - def test_async_connection_error_message(self): - try: - cnn = psycopg2.connect('dbname=thisdatabasedoesntexist', async=True) - self.wait(cnn) - except psycopg2.Error as e: - self.assertNotEqual(str(e), "asynchronous connection failed", - "connection error reason lost") - else: - self.fail("no exception raised") - - -class CancelTests(ConnectingTestCase): - def setUp(self): - ConnectingTestCase.setUp(self) - - cur = self.conn.cursor() - cur.execute(''' - CREATE TEMPORARY TABLE table1 ( - id int PRIMARY KEY - )''') - self.conn.commit() - - @slow - @skip_before_postgres(8, 2) - def test_async_cancel(self): - async_conn = psycopg2.connect(dsn, async=True) - self.assertRaises(psycopg2.OperationalError, async_conn.cancel) - extras.wait_select(async_conn) - cur = async_conn.cursor() - cur.execute("select pg_sleep(10)") - time.sleep(1) - self.assertTrue(async_conn.isexecuting()) - async_conn.cancel() - self.assertRaises(psycopg2.extensions.QueryCanceledError, - extras.wait_select, async_conn) - cur.execute("select 1") - extras.wait_select(async_conn) - self.assertEqual(cur.fetchall(), [(1, )]) - - def test_async_connection_cancel(self): - async_conn = psycopg2.connect(dsn, async=True) - async_conn.close() - self.assertTrue(async_conn.closed) - - -class ConnectTestCase(unittest.TestCase): - def setUp(self): - self.args = None - - def connect_stub(dsn, connection_factory=None, async=False): - self.args = (dsn, connection_factory, async) - - self._connect_orig = psycopg2._connect - psycopg2._connect = connect_stub - - def tearDown(self): - psycopg2._connect = self._connect_orig - - def test_there_has_to_be_something(self): - self.assertRaises(TypeError, psycopg2.connect) - self.assertRaises(TypeError, psycopg2.connect, - connection_factory=lambda dsn, async=False: None) - self.assertRaises(TypeError, psycopg2.connect, - async=True) - - def test_factory(self): - def f(dsn, async=False): - pass - - psycopg2.connect(database='foo', host='baz', connection_factory=f) - self.assertDsnEqual(self.args[0], 'dbname=foo host=baz') - self.assertEqual(self.args[1], f) - self.assertEqual(self.args[2], False) - - psycopg2.connect("dbname=foo host=baz", connection_factory=f) - self.assertDsnEqual(self.args[0], 'dbname=foo host=baz') - self.assertEqual(self.args[1], f) - self.assertEqual(self.args[2], False) - - def test_async(self): - psycopg2.connect(database='foo', host='baz', async=1) - self.assertDsnEqual(self.args[0], 'dbname=foo host=baz') - self.assertEqual(self.args[1], None) - self.assert_(self.args[2]) - - psycopg2.connect("dbname=foo host=baz", async=True) - self.assertDsnEqual(self.args[0], 'dbname=foo host=baz') - self.assertEqual(self.args[1], None) - self.assert_(self.args[2]) - - -class AsyncReplicationTest(ReplicationTestCase): - @skip_before_postgres(9, 4) # slots require 9.4 - @skip_repl_if_green - def test_async_replication(self): - conn = self.repl_connect( - connection_factory=LogicalReplicationConnection, async=1) - if conn is None: - return - - cur = conn.cursor() - - self.create_replication_slot(cur, output_plugin='test_decoding') - self.wait(cur) - - cur.start_replication(self.slot) - self.wait(cur) - - self.make_replication_events() - - self.msg_count = 0 - - def consume(msg): - # just check the methods - "%s: %s" % (cur.io_timestamp, repr(msg)) - "%s: %s" % (cur.feedback_timestamp, repr(msg)) - - self.msg_count += 1 - if self.msg_count > 3: - cur.send_feedback(reply=True) - raise StopReplication() - - cur.send_feedback(flush_lsn=msg.data_start) - - # cannot be used in asynchronous mode - self.assertRaises(psycopg2.ProgrammingError, cur.consume_stream, consume) - - def process_stream(): - while True: - msg = cur.read_message() - if msg: - consume(msg) - else: - select([cur], [], []) - self.assertRaises(StopReplication, process_stream) - - -def test_suite(): - return unittest.TestLoader().loadTestsFromName(__name__) - - -if __name__ == "__main__": - unittest.main()