mirror of
https://github.com/psycopg/psycopg2.git
synced 2024-11-22 00:46:33 +03:00
Drop support for EOL Python 2.7
This commit is contained in:
parent
7babeccbec
commit
d956eaa3b1
4
NEWS
4
NEWS
|
@ -4,7 +4,7 @@ Current release
|
||||||
What's new in psycopg 2.9
|
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
|
What's new in psycopg 2.8.6
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
@ -13,7 +13,7 @@ What's new in psycopg 2.8.6
|
||||||
(:ticket:`#1101`).
|
(:ticket:`#1101`).
|
||||||
- Fixed search of mxDateTime headers in virtualenvs (:ticket:`#996`).
|
- Fixed search of mxDateTime headers in virtualenvs (:ticket:`#996`).
|
||||||
- Added missing values from errorcodes (:ticket:`#1133`).
|
- 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`).
|
(:ticket:`#1141`).
|
||||||
- `~psycopg2.errorcodes` map and `~psycopg2.errors` classes updated to
|
- `~psycopg2.errorcodes` map and `~psycopg2.errors` classes updated to
|
||||||
PostgreSQL 13.
|
PostgreSQL 13.
|
||||||
|
|
|
@ -131,8 +131,7 @@ The current `!psycopg2` implementation supports:
|
||||||
..
|
..
|
||||||
NOTE: keep consistent with setup.py and the /features/ page.
|
NOTE: keep consistent with setup.py and the /features/ page.
|
||||||
|
|
||||||
- Python version 2.7
|
- Python versions from 3.6 to 3.9
|
||||||
- Python 3 versions from 3.6 to 3.9
|
|
||||||
- PostgreSQL server versions from 7.4 to 13
|
- PostgreSQL server versions from 7.4 to 13
|
||||||
- PostgreSQL client library version from 9.1
|
- PostgreSQL client library version from 9.1
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
|
@ -696,7 +696,7 @@ class Options:
|
||||||
def py_ver(self):
|
def py_ver(self):
|
||||||
"""The Python version to build as 2 digits string."""
|
"""The Python version to build as 2 digits string."""
|
||||||
rv = os.environ['PY_VER']
|
rv = os.environ['PY_VER']
|
||||||
assert rv in ('27', '36', '37', '38', '39'), rv
|
assert rv in ('36', '37', '38', '39'), rv
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -779,11 +779,9 @@ class Options:
|
||||||
def vs_ver(self):
|
def vs_ver(self):
|
||||||
# https://wiki.python.org/moin/WindowsCompilers
|
# https://wiki.python.org/moin/WindowsCompilers
|
||||||
# https://www.appveyor.com/docs/windows-images-software/#python
|
# 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.6--3.8 = VS Ver. 14.0 (VS 2015)
|
||||||
# Py 3.9 = VS Ver. 16.0 (VS 2019)
|
# Py 3.9 = VS Ver. 16.0 (VS 2019)
|
||||||
vsvers = {
|
vsvers = {
|
||||||
'27': '9.0',
|
|
||||||
'36': '14.0',
|
'36': '14.0',
|
||||||
'37': '14.0',
|
'37': '14.0',
|
||||||
'38': '14.0',
|
'38': '14.0',
|
||||||
|
|
|
@ -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 <daniele.varrazzo@gmail.com>
|
|
||||||
# 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()
|
|
Loading…
Reference in New Issue
Block a user