From 308b32848c6ea7cf3f3de384b3ef019436d7e257 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 26 Sep 2020 14:17:11 +0200 Subject: [PATCH 01/84] Remove Webware Examples, move Tests and Docs up --- DBUtils/Examples/DBUtilsExample.py | 455 ------------------ DBUtils/Examples/Main.py | 21 - DBUtils/Examples/__init__.py | 1 - {DBUtils/Docs => Docs}/Doc.css | 0 {DBUtils/Docs => Docs}/DocUtils.css | 0 {DBUtils/Docs => Docs}/RelNotes-0.8.1.html | 0 {DBUtils/Docs => Docs}/RelNotes-0.9.1.html | 0 {DBUtils/Docs => Docs}/RelNotes-0.9.2.html | 0 {DBUtils/Docs => Docs}/RelNotes-0.9.3.html | 0 {DBUtils/Docs => Docs}/RelNotes-0.9.4.html | 0 {DBUtils/Docs => Docs}/RelNotes-1.0.html | 0 {DBUtils/Docs => Docs}/RelNotes-1.1.1.html | 0 {DBUtils/Docs => Docs}/RelNotes-1.1.html | 0 {DBUtils/Docs => Docs}/RelNotes-1.2.html | 0 {DBUtils/Docs => Docs}/RelNotes-1.3.html | 0 {DBUtils/Docs => Docs}/RelNotes-1.4.html | 0 {DBUtils/Docs => Docs}/UsersGuide.de.html | 0 {DBUtils/Docs => Docs}/UsersGuide.de.rst | 0 {DBUtils/Docs => Docs}/UsersGuide.html | 0 {DBUtils/Docs => Docs}/UsersGuide.rst | 0 {DBUtils/Docs => Docs}/dbdep.gif | Bin {DBUtils/Docs => Docs}/persist.gif | Bin {DBUtils/Docs => Docs}/pgdep.gif | Bin {DBUtils/Docs => Docs}/pool.gif | Bin {DBUtils/Tests => Tests}/TestPersistentDB.py | 0 {DBUtils/Tests => Tests}/TestPersistentPg.py | 0 {DBUtils/Tests => Tests}/TestPooledDB.py | 0 {DBUtils/Tests => Tests}/TestPooledPg.py | 0 .../Tests => Tests}/TestSimplePooledDB.py | 0 .../Tests => Tests}/TestSimplePooledPg.py | 0 {DBUtils/Tests => Tests}/TestSteadyDB.py | 0 {DBUtils/Tests => Tests}/TestSteadyPg.py | 0 .../Tests => Tests}/TestThreadingLocal.py | 0 {DBUtils/Tests => Tests}/__init__.py | 0 {DBUtils/Tests => Tests}/mock_db.py | 0 {DBUtils/Tests => Tests}/mock_pg.py | 0 36 files changed, 477 deletions(-) delete mode 100644 DBUtils/Examples/DBUtilsExample.py delete mode 100644 DBUtils/Examples/Main.py delete mode 100644 DBUtils/Examples/__init__.py rename {DBUtils/Docs => Docs}/Doc.css (100%) rename {DBUtils/Docs => Docs}/DocUtils.css (100%) rename {DBUtils/Docs => Docs}/RelNotes-0.8.1.html (100%) rename {DBUtils/Docs => Docs}/RelNotes-0.9.1.html (100%) rename {DBUtils/Docs => Docs}/RelNotes-0.9.2.html (100%) rename {DBUtils/Docs => Docs}/RelNotes-0.9.3.html (100%) rename {DBUtils/Docs => Docs}/RelNotes-0.9.4.html (100%) rename {DBUtils/Docs => Docs}/RelNotes-1.0.html (100%) rename {DBUtils/Docs => Docs}/RelNotes-1.1.1.html (100%) rename {DBUtils/Docs => Docs}/RelNotes-1.1.html (100%) rename {DBUtils/Docs => Docs}/RelNotes-1.2.html (100%) rename {DBUtils/Docs => Docs}/RelNotes-1.3.html (100%) rename {DBUtils/Docs => Docs}/RelNotes-1.4.html (100%) rename {DBUtils/Docs => Docs}/UsersGuide.de.html (100%) rename {DBUtils/Docs => Docs}/UsersGuide.de.rst (100%) rename {DBUtils/Docs => Docs}/UsersGuide.html (100%) rename {DBUtils/Docs => Docs}/UsersGuide.rst (100%) rename {DBUtils/Docs => Docs}/dbdep.gif (100%) rename {DBUtils/Docs => Docs}/persist.gif (100%) rename {DBUtils/Docs => Docs}/pgdep.gif (100%) rename {DBUtils/Docs => Docs}/pool.gif (100%) rename {DBUtils/Tests => Tests}/TestPersistentDB.py (100%) rename {DBUtils/Tests => Tests}/TestPersistentPg.py (100%) rename {DBUtils/Tests => Tests}/TestPooledDB.py (100%) rename {DBUtils/Tests => Tests}/TestPooledPg.py (100%) rename {DBUtils/Tests => Tests}/TestSimplePooledDB.py (100%) rename {DBUtils/Tests => Tests}/TestSimplePooledPg.py (100%) rename {DBUtils/Tests => Tests}/TestSteadyDB.py (100%) rename {DBUtils/Tests => Tests}/TestSteadyPg.py (100%) rename {DBUtils/Tests => Tests}/TestThreadingLocal.py (100%) rename {DBUtils/Tests => Tests}/__init__.py (100%) rename {DBUtils/Tests => Tests}/mock_db.py (100%) rename {DBUtils/Tests => Tests}/mock_pg.py (100%) diff --git a/DBUtils/Examples/DBUtilsExample.py b/DBUtils/Examples/DBUtilsExample.py deleted file mode 100644 index 8e40172..0000000 --- a/DBUtils/Examples/DBUtilsExample.py +++ /dev/null @@ -1,455 +0,0 @@ - -from MiscUtils.Configurable import Configurable -from WebKit.Examples.ExamplePage import ExamplePage - - -class DBConfig(Configurable): - """Database configuration.""" - - def defaultConfig(self): - return { - 'dbapi': 'pg', - 'database': 'demo', - 'user': 'demo', - 'password': 'demo', - 'mincached': 5, - 'maxcached': 25 - } - - def configFilename(self): - return 'Configs/Database.config' - - -# the database tables used in this example: -tables = ( - '''seminars ( - id varchar(4) primary key, - title varchar(64) unique not null, - cost money, - places_left smallint)''', - '''attendees ( - name varchar(64) not null, - seminar varchar(4), - paid boolean, - primary key(name, seminar), - foreign key (seminar) references seminars(id) on delete cascade)''') - - -class DBUtilsExample(ExamplePage): - """Example page for the DBUtils package.""" - - # Initialize the database class once when this class is loaded: - config = DBConfig().config() - if config.get('maxcached', None) is None: - dbmod_name = 'Persistent' - else: - dbmod_name = 'Pooled' - dbapi_name = config.pop('dbapi', 'pg') - if dbapi_name == 'pg': # use the PyGreSQL classic DB API - dbmod_name += 'Pg' - if 'database' in config: - config['dbname'] = config['database'] - del config['database'] - if 'password' in config: - config['passwd'] = config['password'] - del config['password'] - else: # use a DB-API 2 compliant module - dbmod_name += 'DB' - dbapi = dbmod = dbclass = dbstatus = None - try: - dbapi = __import__(dbapi_name) - try: - dbmod = getattr(__import__('DBUtils.' + dbmod_name), dbmod_name) - try: - if dbapi_name != 'pg': - config['creator'] = dbapi - dbclass = getattr(dbmod, dbmod_name)(**config) - except dbapi.Error as error: - dbstatus = str(error) - except Exception: - dbstatus = 'Could not connect to the database.' - except Exception: - dbstatus = 'Could not import DBUtils.%s.' % dbmod_name - except Exception: - dbstatus = 'Could not import %s.' % dbapi_name - - # Initialize the buttons - _actions = [] - _buttons = [] - for action in ( - 'create tables', 'list seminars', 'list attendees', - 'add seminar', 'add attendee'): - value = action.capitalize() - action = action.split() - action[1] = action[1].capitalize() - action = ''.join(action) - _actions.append(action) - _buttons.append( - '<input name="_action_%s" type="submit" value="%s">' - % (action, value)) - _buttons = tuple(_buttons) - - def title(self): - return "DBUtils Example" - - def actions(self): - return ExamplePage.actions(self) + self._actions - - def awake(self, transaction): - ExamplePage.awake(self, transaction) - self._output = [] - - def postAction(self, actionName): - self.writeBody() - del self._output - ExamplePage.postAction(self, actionName) - - def output(self, s): - self._output.append(s) - - def outputMsg(self, msg, error=False): - self._output.append( - '<p style="color:%s">%s</p>' % ('red' if error else 'green', msg)) - - def connection(self, shareable=True): - if self.dbstatus: - error = self.dbstatus - else: - try: - if self.dbmod_name == 'PooledDB': - return self.dbclass.connection(shareable) - else: - return self.dbclass.connection() - except self.dbapi.Error as error: - error = str(error) - except Exception: - error = 'Cannot connect to the database.' - self.outputMsg(error, True) - - def dedicated_connection(self): - return self.connection(False) - - def sqlEncode(self, s): - if s is None: - return 'null' - s = s.replace('\\', '\\\\').replace('\'', '\\\'') - return "'%s'" % s - - def createTables(self): - db = self.dedicated_connection() - if not db: - return - for table in tables: - self._output.append( - '<p>Creating the following table:</p><pre>%s</pre>' % table) - ddl = 'create table ' + table - try: - if self.dbapi_name == 'pg': - db.query(ddl) - else: - db.cursor().execute(ddl) - db.commit() - except self.dbapi.Error as error: - if self.dbapi_name != 'pg': - db.rollback() - self.outputMsg(error, True) - else: - self.outputMsg('The table was successfully created.') - db.close() - - def listSeminars(self): - id = self.request().field('id', None) - if id: - if not isinstance(id, list): - id = [id] - cmd = ','.join(map(self.sqlEncode, id)) - cmd = 'delete from seminars where id in (%s)' % cmd - db = self.dedicated_connection() - if not db: - return - try: - if self.dbapi_name == 'pg': - db.query('begin') - db.query(cmd) - db.query('end') - else: - db.cursor().execute(cmd) - db.commit() - except self.dbapi.Error as error: - try: - if self.dbapi_name == 'pg': - db.query('end') - else: - db.rollback() - except Exception: - pass - self.outputMsg(error, True) - return - else: - self.outputMsg('Entries deleted: %d' % len(id)) - db = self.connection() - if not db: - return - query = ('select id, title, cost, places_left' - ' from seminars order by title') - try: - if self.dbapi_name == 'pg': - result = db.query(query).getresult() - else: - cursor = db.cursor() - cursor.execute(query) - result = cursor.fetchall() - cursor.close() - except self.dbapi.Error as error: - self.outputMsg(error, True) - return - if not result: - self.outputMsg('There are no seminars in the database.', True) - return - wr = self.output - button = self._buttons[1].replace('List seminars', 'Delete') - wr('<h4>List of seminars in the database:</h4>') - wr('<form action=""><table border="1" cellspacing="0" cellpadding="2">' - '<tr><th>ID</th><th>Seminar title</th><th>Cost</th>' - '<th>Places left</th><th>%s</th></tr>' % button) - for id, title, cost, places in result: - if places is None: - places = 'unlimited' - if not cost: - cost = 'free' - wr('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>' - '<input type="checkbox" name="id" value="%s">' - '</td></tr>' % (id, title, cost, places, id)) - wr('</table></form>') - - def listAttendees(self): - id = self.request().field('id', None) - if id: - if not isinstance(id, list): - id = [id] - cmds = [ - 'delete from attendees where rpad(seminar,4)||name in (%s)' - % ','.join(map(self.sqlEncode, id))] - places = {} - for i in id: - i = i[:4].rstrip() - if i in places: - places[i] += 1 - else: - places[i] = 1 - for i, n in places.items(): - cmds.append( - 'update seminars set places_left=places_left+%d' - ' where id=%s' % (n, self.sqlEncode(i))) - db = self.dedicated_connection() - if not db: - return - try: - if self.dbapi_name == 'pg': - db.query('begin') - for cmd in cmds: - db.query(cmd) - db.query('end') - else: - for cmd in cmds: - db.cursor().execute(cmd) - db.commit() - except self.dbapi.Error as error: - if self.dbapi_name == 'pg': - db.query('end') - else: - db.rollback() - self.outputMsg(error, True) - return - else: - self.outputMsg('Entries deleted: %d' % len(id)) - db = self.connection() - if not db: - return - query = ('select a.name, s.id, s.title, a.paid' - ' from attendees a,seminars s' - ' where s.id=a.seminar order by a.name, s.title') - try: - if self.dbapi_name == 'pg': - result = db.query(query).getresult() - else: - cursor = db.cursor() - cursor.execute(query) - result = cursor.fetchall() - cursor.close() - except self.dbapi.Error as error: - self.outputMsg(error, True) - return - if not result: - self.outputMsg('There are no attendees in the database.', True) - return - wr = self.output - button = self._buttons[2].replace('List attendees', 'Delete') - wr('<h4>List of attendees in the database:</h4>') - wr('<form action=""><table border="1" cellspacing="0" cellpadding="2">' - '<tr><th>Name</th><th>Seminar</th><th>Paid</th>' - '<th>%s</th></tr>' % button) - for name, id, title, paid in result: - paid = 'Yes' if paid else 'No' - id = id.ljust(4) + name - wr('<tr><td>%s</td><td>%s</td><td>%s</td>' - '<td><input type="checkbox" name="id" value="%s"></td>' - '</tr>' % (name, title, paid, id)) - wr('</table></form>') - - def addSeminar(self): - wr = self.output - wr('<h4>Add a seminar entry to the database:</h4>') - wr('<form action=""><table>' - '<tr><th>ID</th><td><input name="id" type="text" ' - 'size="4" maxlength="4"></td></tr>' - '<tr><th>Title</th><td><input name="title" type="text" ' - 'size="40" maxlength="64"></td></tr>' - '<tr><th>Cost</th><td><input name="cost" type="text" ' - 'size="20" maxlength="20"></td></tr>' - '<tr><th>Places</th><td><input name="places" type="text" ' - 'size="20" maxlength="20"></td></tr>' - '<tr><td colspan="2" align="right">%s</td></tr>' - '</table></form>' % self._buttons[3]) - request = self.request() - if not request.hasField('id'): - return - values = [] - for name in ('id', 'title', 'cost', 'places'): - values.append(request.field(name, '').strip()) - if not values[0] or not values[1]: - self.outputMsg('You must enter a seminar ID and a title!') - return - if not values[2]: - values[2] = None - if not values[3]: - values[3] = None - db = self.dedicated_connection() - if not db: - return - cmd = ('insert into seminars values (%s,%s,%s,%s)' - % tuple(map(self.sqlEncode, values))) - try: - if self.dbapi_name == 'pg': - db.query('begin') - db.query(cmd) - db.query('end') - else: - db.cursor().execute(cmd) - db.commit() - except self.dbapi.Error as error: - if self.dbapi_name == 'pg': - db.query('end') - else: - db.rollback() - self.outputMsg(error, True) - else: - self.outputMsg('"%s" added to seminars.' % values[1]) - db.close() - - def addAttendee(self): - db = self.connection() - if not db: - return - query = ('select id, title from seminars' - ' where places_left is null or places_left>0 order by title') - try: - if self.dbapi_name == 'pg': - result = db.query(query).getresult() - else: - cursor = db.cursor() - cursor.execute(query) - result = cursor.fetchall() - cursor.close() - except self.dbapi.Error as error: - self.outputMsg(error, True) - return - if not result: - self.outputMsg('You have to define seminars first.') - return - sem = ['<select name="seminar" size="1">'] - for id, title in result: - sem.append('<option value="%s">%s</option>' % (id, title)) - sem.append('</select>') - sem = ''.join(sem) - wr = self.output - wr('<h4>Add an attendee entry to the database:</h4>') - wr('<form action=""><table>' - '<tr><th>Name</th><td><input name="name" type="text" ' - 'size="40" maxlength="64"></td></tr>' - '<tr><th>Seminar</th><td>%s</td></tr>' - '<tr><th>Paid</th><td>' - '<input type="radio" name="paid" value="t">Yes ' - '<input type="radio" name="paid" value="f" checked="checked">No' - '</td></tr><tr><td colspan="2" align="right">%s</td></tr>' - '</table></form>' % (sem, self._buttons[4])) - request = self.request() - if not request.hasField('name'): - return - values = [] - for name in ('name', 'seminar', 'paid'): - values.append(request.field(name, '').strip()) - if not values[0] or not values[1]: - self.outputMsg('You must enter a name and a seminar!') - return - db = self.dedicated_connection() - if not db: - return - try: - if self.dbapi_name == 'pg': - db.query('begin') - cmd = ('update seminars set places_left=places_left-1' - ' where id=%s' % self.sqlEncode(values[1])) - db.query(cmd) - cmd = ('select places_left from seminars' - ' where id=%s' % self.sqlEncode(values[1])) - if (db.query(cmd).getresult()[0][0] or 0) < 0: - raise self.dbapi.Error("No more places left.") - cmd = ('insert into attendees values (%s,%s,%s)' - % tuple(map(self.sqlEncode, values))) - db.query(cmd) - db.query('end') - else: - cursor = db.cursor() - cmd = ('update seminars set places_left=places_left-1' - ' where id=%s' % self.sqlEncode(values[1])) - cursor.execute(cmd) - cmd = ('select places_left from seminars' - ' where id=%s' % self.sqlEncode(values[1])) - cursor.execute(cmd) - if (cursor.fetchone()[0] or 0) < 0: - raise self.dbapi.Error("No more places left.") - cmd = ('insert into attendees values (%s,%s,%s)' - % tuple(map(self.sqlEncode, values))) - db.cursor().execute(cmd) - cursor.close() - db.commit() - except self.dbapi.Error as error: - if self.dbapi_name == 'pg': - db.query('end') - else: - db.rollback() - self.outputMsg(error, True) - else: - self.outputMsg('%s added to attendees.' % values[0]) - db.close() - - def writeContent(self): - wr = self.writeln - if self._output: - wr('\n'.join(self._output)) - wr('<p><a href="DBUtilsExample">Back</a></p>') - else: - wr('<h2>Welcome to the %s!</h2>' % self.title()) - wr('<h4>We are using DBUtils.%s and the %s database module.</h4>' - % (self.dbmod_name, self.dbapi_name)) - wr('<p>Configuration: %r</p>' % DBConfig().config()) - wr('<p>This example uses a small demo database ' - 'designed to track the attendees for a series of seminars ' - '(see <a href="http://www.linuxjournal.com/article/2605">"' - 'The Python DB-API"</a> by Andrew Kuchling).</p>') - wr('<form action="">' - '<p>%s (create the needed database tables first)</p>' - '<p>%s %s (list all database entries)</p>' - '<p>%s %s (add entries)</p>' - '</form>' % self._buttons) diff --git a/DBUtils/Examples/Main.py b/DBUtils/Examples/Main.py deleted file mode 100644 index 39bf4b6..0000000 --- a/DBUtils/Examples/Main.py +++ /dev/null @@ -1,21 +0,0 @@ - -from WebKit.Examples.ExamplePage import ExamplePage - - -class Main(ExamplePage): - - def writeContent(self): - self.writeln('''<h2>DBUtils example</h2> -<p>You can set the DBUtils parameters in the following file</p> -<ul> -<li><tt>Configs/Database.config</tt></li> -</ul> -<p>With the default settings,</p> -<ul> -<li>you must have the PostgreSQL database</li> -<li>and the PyGreSQL adapter installed, and</li> -<li>you must have created a database with the name "demo" and</li> -<li>a database user with the name "demo" and password "demo".</li> -</ul> -<p><a href="DBUtilsExample">Start the demo!</a></p> -''') diff --git a/DBUtils/Examples/__init__.py b/DBUtils/Examples/__init__.py deleted file mode 100644 index 71f8157..0000000 --- a/DBUtils/Examples/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# DBUtils Examples diff --git a/DBUtils/Docs/Doc.css b/Docs/Doc.css similarity index 100% rename from DBUtils/Docs/Doc.css rename to Docs/Doc.css diff --git a/DBUtils/Docs/DocUtils.css b/Docs/DocUtils.css similarity index 100% rename from DBUtils/Docs/DocUtils.css rename to Docs/DocUtils.css diff --git a/DBUtils/Docs/RelNotes-0.8.1.html b/Docs/RelNotes-0.8.1.html similarity index 100% rename from DBUtils/Docs/RelNotes-0.8.1.html rename to Docs/RelNotes-0.8.1.html diff --git a/DBUtils/Docs/RelNotes-0.9.1.html b/Docs/RelNotes-0.9.1.html similarity index 100% rename from DBUtils/Docs/RelNotes-0.9.1.html rename to Docs/RelNotes-0.9.1.html diff --git a/DBUtils/Docs/RelNotes-0.9.2.html b/Docs/RelNotes-0.9.2.html similarity index 100% rename from DBUtils/Docs/RelNotes-0.9.2.html rename to Docs/RelNotes-0.9.2.html diff --git a/DBUtils/Docs/RelNotes-0.9.3.html b/Docs/RelNotes-0.9.3.html similarity index 100% rename from DBUtils/Docs/RelNotes-0.9.3.html rename to Docs/RelNotes-0.9.3.html diff --git a/DBUtils/Docs/RelNotes-0.9.4.html b/Docs/RelNotes-0.9.4.html similarity index 100% rename from DBUtils/Docs/RelNotes-0.9.4.html rename to Docs/RelNotes-0.9.4.html diff --git a/DBUtils/Docs/RelNotes-1.0.html b/Docs/RelNotes-1.0.html similarity index 100% rename from DBUtils/Docs/RelNotes-1.0.html rename to Docs/RelNotes-1.0.html diff --git a/DBUtils/Docs/RelNotes-1.1.1.html b/Docs/RelNotes-1.1.1.html similarity index 100% rename from DBUtils/Docs/RelNotes-1.1.1.html rename to Docs/RelNotes-1.1.1.html diff --git a/DBUtils/Docs/RelNotes-1.1.html b/Docs/RelNotes-1.1.html similarity index 100% rename from DBUtils/Docs/RelNotes-1.1.html rename to Docs/RelNotes-1.1.html diff --git a/DBUtils/Docs/RelNotes-1.2.html b/Docs/RelNotes-1.2.html similarity index 100% rename from DBUtils/Docs/RelNotes-1.2.html rename to Docs/RelNotes-1.2.html diff --git a/DBUtils/Docs/RelNotes-1.3.html b/Docs/RelNotes-1.3.html similarity index 100% rename from DBUtils/Docs/RelNotes-1.3.html rename to Docs/RelNotes-1.3.html diff --git a/DBUtils/Docs/RelNotes-1.4.html b/Docs/RelNotes-1.4.html similarity index 100% rename from DBUtils/Docs/RelNotes-1.4.html rename to Docs/RelNotes-1.4.html diff --git a/DBUtils/Docs/UsersGuide.de.html b/Docs/UsersGuide.de.html similarity index 100% rename from DBUtils/Docs/UsersGuide.de.html rename to Docs/UsersGuide.de.html diff --git a/DBUtils/Docs/UsersGuide.de.rst b/Docs/UsersGuide.de.rst similarity index 100% rename from DBUtils/Docs/UsersGuide.de.rst rename to Docs/UsersGuide.de.rst diff --git a/DBUtils/Docs/UsersGuide.html b/Docs/UsersGuide.html similarity index 100% rename from DBUtils/Docs/UsersGuide.html rename to Docs/UsersGuide.html diff --git a/DBUtils/Docs/UsersGuide.rst b/Docs/UsersGuide.rst similarity index 100% rename from DBUtils/Docs/UsersGuide.rst rename to Docs/UsersGuide.rst diff --git a/DBUtils/Docs/dbdep.gif b/Docs/dbdep.gif similarity index 100% rename from DBUtils/Docs/dbdep.gif rename to Docs/dbdep.gif diff --git a/DBUtils/Docs/persist.gif b/Docs/persist.gif similarity index 100% rename from DBUtils/Docs/persist.gif rename to Docs/persist.gif diff --git a/DBUtils/Docs/pgdep.gif b/Docs/pgdep.gif similarity index 100% rename from DBUtils/Docs/pgdep.gif rename to Docs/pgdep.gif diff --git a/DBUtils/Docs/pool.gif b/Docs/pool.gif similarity index 100% rename from DBUtils/Docs/pool.gif rename to Docs/pool.gif diff --git a/DBUtils/Tests/TestPersistentDB.py b/Tests/TestPersistentDB.py similarity index 100% rename from DBUtils/Tests/TestPersistentDB.py rename to Tests/TestPersistentDB.py diff --git a/DBUtils/Tests/TestPersistentPg.py b/Tests/TestPersistentPg.py similarity index 100% rename from DBUtils/Tests/TestPersistentPg.py rename to Tests/TestPersistentPg.py diff --git a/DBUtils/Tests/TestPooledDB.py b/Tests/TestPooledDB.py similarity index 100% rename from DBUtils/Tests/TestPooledDB.py rename to Tests/TestPooledDB.py diff --git a/DBUtils/Tests/TestPooledPg.py b/Tests/TestPooledPg.py similarity index 100% rename from DBUtils/Tests/TestPooledPg.py rename to Tests/TestPooledPg.py diff --git a/DBUtils/Tests/TestSimplePooledDB.py b/Tests/TestSimplePooledDB.py similarity index 100% rename from DBUtils/Tests/TestSimplePooledDB.py rename to Tests/TestSimplePooledDB.py diff --git a/DBUtils/Tests/TestSimplePooledPg.py b/Tests/TestSimplePooledPg.py similarity index 100% rename from DBUtils/Tests/TestSimplePooledPg.py rename to Tests/TestSimplePooledPg.py diff --git a/DBUtils/Tests/TestSteadyDB.py b/Tests/TestSteadyDB.py similarity index 100% rename from DBUtils/Tests/TestSteadyDB.py rename to Tests/TestSteadyDB.py diff --git a/DBUtils/Tests/TestSteadyPg.py b/Tests/TestSteadyPg.py similarity index 100% rename from DBUtils/Tests/TestSteadyPg.py rename to Tests/TestSteadyPg.py diff --git a/DBUtils/Tests/TestThreadingLocal.py b/Tests/TestThreadingLocal.py similarity index 100% rename from DBUtils/Tests/TestThreadingLocal.py rename to Tests/TestThreadingLocal.py diff --git a/DBUtils/Tests/__init__.py b/Tests/__init__.py similarity index 100% rename from DBUtils/Tests/__init__.py rename to Tests/__init__.py diff --git a/DBUtils/Tests/mock_db.py b/Tests/mock_db.py similarity index 100% rename from DBUtils/Tests/mock_db.py rename to Tests/mock_db.py diff --git a/DBUtils/Tests/mock_pg.py b/Tests/mock_pg.py similarity index 100% rename from DBUtils/Tests/mock_pg.py rename to Tests/mock_pg.py From bef64b7d77f1df2f20efe5e09c5129865bfc5041 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 26 Sep 2020 14:24:03 +0200 Subject: [PATCH 02/84] Rename top folders to lower case --- {DBUtils => dbutils}/PersistentDB.py | 0 {DBUtils => dbutils}/PersistentPg.py | 0 {DBUtils => dbutils}/PooledDB.py | 0 {DBUtils => dbutils}/PooledPg.py | 0 {DBUtils => dbutils}/Properties.py | 0 {DBUtils => dbutils}/SimplePooledDB.py | 0 {DBUtils => dbutils}/SimplePooledPg.py | 0 {DBUtils => dbutils}/SteadyDB.py | 0 {DBUtils => dbutils}/SteadyPg.py | 0 {DBUtils => dbutils}/__init__.py | 0 {Docs => docs}/Doc.css | 0 {Docs => docs}/DocUtils.css | 0 {Docs => docs}/RelNotes-0.8.1.html | 0 {Docs => docs}/RelNotes-0.9.1.html | 0 {Docs => docs}/RelNotes-0.9.2.html | 0 {Docs => docs}/RelNotes-0.9.3.html | 0 {Docs => docs}/RelNotes-0.9.4.html | 0 {Docs => docs}/RelNotes-1.0.html | 0 {Docs => docs}/RelNotes-1.1.1.html | 0 {Docs => docs}/RelNotes-1.1.html | 0 {Docs => docs}/RelNotes-1.2.html | 0 {Docs => docs}/RelNotes-1.3.html | 0 {Docs => docs}/RelNotes-1.4.html | 0 {Docs => docs}/UsersGuide.de.html | 0 {Docs => docs}/UsersGuide.de.rst | 0 {Docs => docs}/UsersGuide.html | 0 {Docs => docs}/UsersGuide.rst | 0 {Docs => docs}/dbdep.gif | Bin {Docs => docs}/persist.gif | Bin {Docs => docs}/pgdep.gif | Bin {Docs => docs}/pool.gif | Bin {Tests => tests}/TestPersistentDB.py | 0 {Tests => tests}/TestPersistentPg.py | 0 {Tests => tests}/TestPooledDB.py | 0 {Tests => tests}/TestPooledPg.py | 0 {Tests => tests}/TestSimplePooledDB.py | 0 {Tests => tests}/TestSimplePooledPg.py | 0 {Tests => tests}/TestSteadyDB.py | 0 {Tests => tests}/TestSteadyPg.py | 0 {Tests => tests}/TestThreadingLocal.py | 0 {Tests => tests}/__init__.py | 0 {Tests => tests}/mock_db.py | 0 {Tests => tests}/mock_pg.py | 0 43 files changed, 0 insertions(+), 0 deletions(-) rename {DBUtils => dbutils}/PersistentDB.py (100%) rename {DBUtils => dbutils}/PersistentPg.py (100%) rename {DBUtils => dbutils}/PooledDB.py (100%) rename {DBUtils => dbutils}/PooledPg.py (100%) rename {DBUtils => dbutils}/Properties.py (100%) rename {DBUtils => dbutils}/SimplePooledDB.py (100%) rename {DBUtils => dbutils}/SimplePooledPg.py (100%) rename {DBUtils => dbutils}/SteadyDB.py (100%) rename {DBUtils => dbutils}/SteadyPg.py (100%) rename {DBUtils => dbutils}/__init__.py (100%) rename {Docs => docs}/Doc.css (100%) rename {Docs => docs}/DocUtils.css (100%) rename {Docs => docs}/RelNotes-0.8.1.html (100%) rename {Docs => docs}/RelNotes-0.9.1.html (100%) rename {Docs => docs}/RelNotes-0.9.2.html (100%) rename {Docs => docs}/RelNotes-0.9.3.html (100%) rename {Docs => docs}/RelNotes-0.9.4.html (100%) rename {Docs => docs}/RelNotes-1.0.html (100%) rename {Docs => docs}/RelNotes-1.1.1.html (100%) rename {Docs => docs}/RelNotes-1.1.html (100%) rename {Docs => docs}/RelNotes-1.2.html (100%) rename {Docs => docs}/RelNotes-1.3.html (100%) rename {Docs => docs}/RelNotes-1.4.html (100%) rename {Docs => docs}/UsersGuide.de.html (100%) rename {Docs => docs}/UsersGuide.de.rst (100%) rename {Docs => docs}/UsersGuide.html (100%) rename {Docs => docs}/UsersGuide.rst (100%) rename {Docs => docs}/dbdep.gif (100%) rename {Docs => docs}/persist.gif (100%) rename {Docs => docs}/pgdep.gif (100%) rename {Docs => docs}/pool.gif (100%) rename {Tests => tests}/TestPersistentDB.py (100%) rename {Tests => tests}/TestPersistentPg.py (100%) rename {Tests => tests}/TestPooledDB.py (100%) rename {Tests => tests}/TestPooledPg.py (100%) rename {Tests => tests}/TestSimplePooledDB.py (100%) rename {Tests => tests}/TestSimplePooledPg.py (100%) rename {Tests => tests}/TestSteadyDB.py (100%) rename {Tests => tests}/TestSteadyPg.py (100%) rename {Tests => tests}/TestThreadingLocal.py (100%) rename {Tests => tests}/__init__.py (100%) rename {Tests => tests}/mock_db.py (100%) rename {Tests => tests}/mock_pg.py (100%) diff --git a/DBUtils/PersistentDB.py b/dbutils/PersistentDB.py similarity index 100% rename from DBUtils/PersistentDB.py rename to dbutils/PersistentDB.py diff --git a/DBUtils/PersistentPg.py b/dbutils/PersistentPg.py similarity index 100% rename from DBUtils/PersistentPg.py rename to dbutils/PersistentPg.py diff --git a/DBUtils/PooledDB.py b/dbutils/PooledDB.py similarity index 100% rename from DBUtils/PooledDB.py rename to dbutils/PooledDB.py diff --git a/DBUtils/PooledPg.py b/dbutils/PooledPg.py similarity index 100% rename from DBUtils/PooledPg.py rename to dbutils/PooledPg.py diff --git a/DBUtils/Properties.py b/dbutils/Properties.py similarity index 100% rename from DBUtils/Properties.py rename to dbutils/Properties.py diff --git a/DBUtils/SimplePooledDB.py b/dbutils/SimplePooledDB.py similarity index 100% rename from DBUtils/SimplePooledDB.py rename to dbutils/SimplePooledDB.py diff --git a/DBUtils/SimplePooledPg.py b/dbutils/SimplePooledPg.py similarity index 100% rename from DBUtils/SimplePooledPg.py rename to dbutils/SimplePooledPg.py diff --git a/DBUtils/SteadyDB.py b/dbutils/SteadyDB.py similarity index 100% rename from DBUtils/SteadyDB.py rename to dbutils/SteadyDB.py diff --git a/DBUtils/SteadyPg.py b/dbutils/SteadyPg.py similarity index 100% rename from DBUtils/SteadyPg.py rename to dbutils/SteadyPg.py diff --git a/DBUtils/__init__.py b/dbutils/__init__.py similarity index 100% rename from DBUtils/__init__.py rename to dbutils/__init__.py diff --git a/Docs/Doc.css b/docs/Doc.css similarity index 100% rename from Docs/Doc.css rename to docs/Doc.css diff --git a/Docs/DocUtils.css b/docs/DocUtils.css similarity index 100% rename from Docs/DocUtils.css rename to docs/DocUtils.css diff --git a/Docs/RelNotes-0.8.1.html b/docs/RelNotes-0.8.1.html similarity index 100% rename from Docs/RelNotes-0.8.1.html rename to docs/RelNotes-0.8.1.html diff --git a/Docs/RelNotes-0.9.1.html b/docs/RelNotes-0.9.1.html similarity index 100% rename from Docs/RelNotes-0.9.1.html rename to docs/RelNotes-0.9.1.html diff --git a/Docs/RelNotes-0.9.2.html b/docs/RelNotes-0.9.2.html similarity index 100% rename from Docs/RelNotes-0.9.2.html rename to docs/RelNotes-0.9.2.html diff --git a/Docs/RelNotes-0.9.3.html b/docs/RelNotes-0.9.3.html similarity index 100% rename from Docs/RelNotes-0.9.3.html rename to docs/RelNotes-0.9.3.html diff --git a/Docs/RelNotes-0.9.4.html b/docs/RelNotes-0.9.4.html similarity index 100% rename from Docs/RelNotes-0.9.4.html rename to docs/RelNotes-0.9.4.html diff --git a/Docs/RelNotes-1.0.html b/docs/RelNotes-1.0.html similarity index 100% rename from Docs/RelNotes-1.0.html rename to docs/RelNotes-1.0.html diff --git a/Docs/RelNotes-1.1.1.html b/docs/RelNotes-1.1.1.html similarity index 100% rename from Docs/RelNotes-1.1.1.html rename to docs/RelNotes-1.1.1.html diff --git a/Docs/RelNotes-1.1.html b/docs/RelNotes-1.1.html similarity index 100% rename from Docs/RelNotes-1.1.html rename to docs/RelNotes-1.1.html diff --git a/Docs/RelNotes-1.2.html b/docs/RelNotes-1.2.html similarity index 100% rename from Docs/RelNotes-1.2.html rename to docs/RelNotes-1.2.html diff --git a/Docs/RelNotes-1.3.html b/docs/RelNotes-1.3.html similarity index 100% rename from Docs/RelNotes-1.3.html rename to docs/RelNotes-1.3.html diff --git a/Docs/RelNotes-1.4.html b/docs/RelNotes-1.4.html similarity index 100% rename from Docs/RelNotes-1.4.html rename to docs/RelNotes-1.4.html diff --git a/Docs/UsersGuide.de.html b/docs/UsersGuide.de.html similarity index 100% rename from Docs/UsersGuide.de.html rename to docs/UsersGuide.de.html diff --git a/Docs/UsersGuide.de.rst b/docs/UsersGuide.de.rst similarity index 100% rename from Docs/UsersGuide.de.rst rename to docs/UsersGuide.de.rst diff --git a/Docs/UsersGuide.html b/docs/UsersGuide.html similarity index 100% rename from Docs/UsersGuide.html rename to docs/UsersGuide.html diff --git a/Docs/UsersGuide.rst b/docs/UsersGuide.rst similarity index 100% rename from Docs/UsersGuide.rst rename to docs/UsersGuide.rst diff --git a/Docs/dbdep.gif b/docs/dbdep.gif similarity index 100% rename from Docs/dbdep.gif rename to docs/dbdep.gif diff --git a/Docs/persist.gif b/docs/persist.gif similarity index 100% rename from Docs/persist.gif rename to docs/persist.gif diff --git a/Docs/pgdep.gif b/docs/pgdep.gif similarity index 100% rename from Docs/pgdep.gif rename to docs/pgdep.gif diff --git a/Docs/pool.gif b/docs/pool.gif similarity index 100% rename from Docs/pool.gif rename to docs/pool.gif diff --git a/Tests/TestPersistentDB.py b/tests/TestPersistentDB.py similarity index 100% rename from Tests/TestPersistentDB.py rename to tests/TestPersistentDB.py diff --git a/Tests/TestPersistentPg.py b/tests/TestPersistentPg.py similarity index 100% rename from Tests/TestPersistentPg.py rename to tests/TestPersistentPg.py diff --git a/Tests/TestPooledDB.py b/tests/TestPooledDB.py similarity index 100% rename from Tests/TestPooledDB.py rename to tests/TestPooledDB.py diff --git a/Tests/TestPooledPg.py b/tests/TestPooledPg.py similarity index 100% rename from Tests/TestPooledPg.py rename to tests/TestPooledPg.py diff --git a/Tests/TestSimplePooledDB.py b/tests/TestSimplePooledDB.py similarity index 100% rename from Tests/TestSimplePooledDB.py rename to tests/TestSimplePooledDB.py diff --git a/Tests/TestSimplePooledPg.py b/tests/TestSimplePooledPg.py similarity index 100% rename from Tests/TestSimplePooledPg.py rename to tests/TestSimplePooledPg.py diff --git a/Tests/TestSteadyDB.py b/tests/TestSteadyDB.py similarity index 100% rename from Tests/TestSteadyDB.py rename to tests/TestSteadyDB.py diff --git a/Tests/TestSteadyPg.py b/tests/TestSteadyPg.py similarity index 100% rename from Tests/TestSteadyPg.py rename to tests/TestSteadyPg.py diff --git a/Tests/TestThreadingLocal.py b/tests/TestThreadingLocal.py similarity index 100% rename from Tests/TestThreadingLocal.py rename to tests/TestThreadingLocal.py diff --git a/Tests/__init__.py b/tests/__init__.py similarity index 100% rename from Tests/__init__.py rename to tests/__init__.py diff --git a/Tests/mock_db.py b/tests/mock_db.py similarity index 100% rename from Tests/mock_db.py rename to tests/mock_db.py diff --git a/Tests/mock_pg.py b/tests/mock_pg.py similarity index 100% rename from Tests/mock_pg.py rename to tests/mock_pg.py From f22adcec9b8eab2cf236808bd33f1220cb66526d Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 26 Sep 2020 16:15:28 +0200 Subject: [PATCH 03/84] Rename UsersGuide, make Changelog from ReleaseNotes --- docs/RelNotes-0.8.1.html | 19 -- docs/RelNotes-0.9.1.html | 29 --- docs/RelNotes-0.9.2.html | 26 --- docs/RelNotes-0.9.3.html | 34 --- docs/RelNotes-0.9.4.html | 26 --- docs/RelNotes-1.0.html | 49 ---- docs/RelNotes-1.1.1.html | 32 --- docs/RelNotes-1.1.html | 46 ---- docs/RelNotes-1.2.html | 26 --- docs/RelNotes-1.3.html | 26 --- docs/RelNotes-1.4.html | 26 --- docs/changelog.html | 270 ++++++++++++++++++++++ docs/changelog.rst | 211 +++++++++++++++++ docs/{Doc.css => doc.css} | 2 +- docs/{DocUtils.css => docutils.css} | 0 docs/{UsersGuide.de.html => main.de.html} | 4 +- docs/{UsersGuide.de.rst => main.de.rst} | 4 +- docs/{UsersGuide.html => main.html} | 4 +- docs/{UsersGuide.rst => main.rst} | 4 +- buildhtml.py => docs/make.py | 4 +- 20 files changed, 492 insertions(+), 350 deletions(-) delete mode 100644 docs/RelNotes-0.8.1.html delete mode 100644 docs/RelNotes-0.9.1.html delete mode 100644 docs/RelNotes-0.9.2.html delete mode 100644 docs/RelNotes-0.9.3.html delete mode 100644 docs/RelNotes-0.9.4.html delete mode 100644 docs/RelNotes-1.0.html delete mode 100644 docs/RelNotes-1.1.1.html delete mode 100644 docs/RelNotes-1.1.html delete mode 100644 docs/RelNotes-1.2.html delete mode 100644 docs/RelNotes-1.3.html delete mode 100644 docs/RelNotes-1.4.html create mode 100644 docs/changelog.html create mode 100644 docs/changelog.rst rename docs/{Doc.css => doc.css} (99%) rename docs/{DocUtils.css => docutils.css} (100%) rename docs/{UsersGuide.de.html => main.de.html} (99%) rename docs/{UsersGuide.de.rst => main.de.rst} (99%) rename docs/{UsersGuide.html => main.html} (99%) rename docs/{UsersGuide.rst => main.rst} (99%) rename buildhtml.py => docs/make.py (92%) diff --git a/docs/RelNotes-0.8.1.html b/docs/RelNotes-0.8.1.html deleted file mode 100644 index fbb16f8..0000000 --- a/docs/RelNotes-0.8.1.html +++ /dev/null @@ -1,19 +0,0 @@ -<!DOCTYPE html> -<html> -<head> -<title>DBUtils 0.8.1 Release Notes</title> -<link rel="stylesheet" href="Doc.css" type="text/css"> -</head> -<body> -<h1 class="header">DBUtils 0.8.1 Release Notes</h1> - -<p>DBUtils 0.8.1 was released on September 13, 2005.</p> - -<p>This is the first public release of DBUtils.</p> - -<div class="footer"> -DBUtils -(<a href="https://github.com/WebwareForPython/DBUtils">github.com/WebwareForPython/DBUtils</a>) -</div> -</body> -</html> diff --git a/docs/RelNotes-0.9.1.html b/docs/RelNotes-0.9.1.html deleted file mode 100644 index 55b96b4..0000000 --- a/docs/RelNotes-0.9.1.html +++ /dev/null @@ -1,29 +0,0 @@ -<!DOCTYPE html> -<html> -<head> -<title>DBUtils 0.9.1 Release Notes</title> -<link rel="stylesheet" href="Doc.css" type="text/css"> -</head> -<body> -<h1 class="header">DBUtils 0.9.1 Release Notes</h1> - -<p>DBUtils 0.9.1 was released on May 8, 2006.</p> - -<p>This is the second public release of DBUtils.</p> - -<h2>Changes:</h2> -<ul> -<li>Added <code>_closeable</code> attribute and made persistent connections -not closeable by default. This allows <code>PersistentDB</code> to be used -in the same way as you would use <code>PooledDB</code>.</li> -<li>Allowed arguments in the DB-API 2 <code>cursor()</code> method. -MySQLdb is using this to specify cursor classes. (Suggested by Michael Palmer.)</li> -<li>Improved the documentation and added a User's Guide.</li> -</ul> - -<div class="footer"> -DBUtils -(<a href="https://github.com/WebwareForPython/DBUtils">github.com/WebwareForPython/DBUtils</a>) -</div> -</body> -</html> diff --git a/docs/RelNotes-0.9.2.html b/docs/RelNotes-0.9.2.html deleted file mode 100644 index b3e566d..0000000 --- a/docs/RelNotes-0.9.2.html +++ /dev/null @@ -1,26 +0,0 @@ -<!DOCTYPE html> -<html> -<head> -<title>DBUtils 0.9.2 Release Notes</title> -<link rel="stylesheet" href="Doc.css" type="text/css"> -</head> -<body> -<h1 class="header">DBUtils 0.9.2 Release Notes</h1> - -<p>DBUtils 0.9.2 was released on September 22, 2006.</p> - -<p>This is the third public release of DBUtils.</p> - -<h2>Changes:</h2> -<ul> -<li>Renamed <code>SolidDB</code> to <code>SteadyDB</code> to avoid confusion -with the <i>solidDB</i> storage engine.</li> -<li>Accordingly, renamed <code>SolidPg</code> to <code>SteadyPg</code>.</li> -</ul> - -<div class="footer"> -DBUtils -(<a href="https://github.com/WebwareForPython/DBUtils">github.com/WebwareForPython/DBUtils</a>) -</div> -</body> -</html> diff --git a/docs/RelNotes-0.9.3.html b/docs/RelNotes-0.9.3.html deleted file mode 100644 index 72f4fb8..0000000 --- a/docs/RelNotes-0.9.3.html +++ /dev/null @@ -1,34 +0,0 @@ -<!DOCTYPE html> -<html> -<head> -<title>DBUtils 0.9.3 Release Notes</title> -<link rel="stylesheet" href="Doc.css" type="text/css"> -</head> -<body> -<h1 class="header">DBUtils 0.9.3 Release Notes</h1> - -<p>DBUtils 0.9.3 was released on May 21, 2007.</p> - -<p>This is the fourth public release of DBUtils.</p> - -<h2>Changes:</h2> -<ul> -<li>Support custom creator functions for database connections. -These can now be used as the first parameter instead of an DB-API module -(suggested by Ezio Vernacotola).</li> -<li>Added destructor for steady connections.</li> -<li>Use <a href="https://pypi.python.org/pypi/setuptools">setuptools</a> -if available.</li> -<li>Some code cleanup.</li> -<li>Some fixes in the documentation. -Added <a href="UsersGuide.zh.html">Chinese translation</a> -of the <a href="UsersGuide.html">User's Guide</a>, -kindly contributed by <a href="http://blog.csdn.net/gashero">gashero</a>.</li> -</ul> - -<div class="footer"> -DBUtils -(<a href="https://github.com/WebwareForPython/DBUtils">github.com/WebwareForPython/DBUtils</a>) -</div> -</body> -</html> diff --git a/docs/RelNotes-0.9.4.html b/docs/RelNotes-0.9.4.html deleted file mode 100644 index 674a6c8..0000000 --- a/docs/RelNotes-0.9.4.html +++ /dev/null @@ -1,26 +0,0 @@ -<!DOCTYPE html> -<html> -<head> -<title>DBUtils 0.9.4 Release Notes</title> -<link rel="stylesheet" href="Doc.css" type="text/css"> -</head> -<body> -<h1 class="header">DBUtils 0.9.4 Release Notes</h1> - -<p>DBUtils 0.9.4 was released on July 7, 2007.</p> - -<p>This is the fifth public release of DBUtils.</p> - -<p>This release fixes a problem in the destructor code and has been -supplemented with a German User's Guide.</p> - -<p>Please note that the <code>dbapi</code> parameter has been renamed to -<code>creator</code> in the last release since you can now pass custom -creator functions for database connections instead of DB-API 2 modules.</p> - -<div class="footer"> -DBUtils -(<a href="https://github.com/WebwareForPython/DBUtils">github.com/WebwareForPython/DBUtils</a>) -</div> -</body> -</html> diff --git a/docs/RelNotes-1.0.html b/docs/RelNotes-1.0.html deleted file mode 100644 index 2606375..0000000 --- a/docs/RelNotes-1.0.html +++ /dev/null @@ -1,49 +0,0 @@ -<!DOCTYPE html> -<html> -<head> -<title>DBUtils 1.0 Release Notes</title> -<link rel="stylesheet" href="Doc.css" type="text/css"> -</head> -<body> -<h1 class="header">DBUtils 1.0 Release Notes</h1> - -<p>DBUtils 1.0 was released on November 29, 2008.</p> - -<p>This is the sixth public release of DBUtils.</p> - -<h2>Changes:</h2> -<ul> -<li>Added a <code>failures</code> parameter for configuring the exception classes for -which the failover mechanisms is applied (as suggested by Matthew Harriger).</li> -<li>Added a <code>closeable</code> parameter for configuring whether connections -can be closed (otherwise closing connections will be silently ignored).</li> -<li>It is now possible to override defaults via the <code>creator.dbapi</code> -and <code>creator.threadsafety</code> attributes.</li> -<li>Added alias method <code>dedicated_connection</code> for -<code>connection(shareable=False)</code>.</li> -<li>Added a <code>version</code> attribute to all exported classes.</li> -<li>Where <code>0</code> has the meaning "unlimited", parameters can now be also -set to <code>None</code> instead.</li> -<li>It turned out that <code>threading.local</code> does not work properly with -<code>mod_wsgi</code>, so we use the Python implementation for thread-local data -even when a faster <code>threading.local</code> implementation is available. -A new parameter <code>threadlocal</code> allows you to pass an arbitrary class -such as <code>threading.local</code> if you know it works in your environment.</li> -</ul> - -<h2>Bugfixes and Improvements:</h2> -<ul> -<li>In some cases, when instance initialization failed or referenced objects -were already destroyed, finalizers could throw exceptions or create infinite -recursion (problem reported by Gregory Pinero and Jehiah Czebotar).</li> -<li>DBUtils now tries harder to find the underlying DB-API 2 module if only a -connection creator function is specified. This had not worked before with the -MySQLdb module (problem reported by Gregory Pinero).</li> -</ul> - -<div class="footer"> -DBUtils -(<a href="https://github.com/WebwareForPython/DBUtils">github.com/WebwareForPython/DBUtils</a>) -</div> -</body> -</html> diff --git a/docs/RelNotes-1.1.1.html b/docs/RelNotes-1.1.1.html deleted file mode 100644 index b780067..0000000 --- a/docs/RelNotes-1.1.1.html +++ /dev/null @@ -1,32 +0,0 @@ -<!DOCTYPE html> -<html> -<head> -<title>DBUtils 1.1.1 Release Notes</title> -<link rel="stylesheet" href="Doc.css" type="text/css"> -</head> -<body> -<h1 class="header">DBUtils 1.1.1 Release Notes</h1> - -<p>DBUtils 1.1.1 was released on 02/04/17.</p> - -<p>This bugfix release is the eight public release of DBUtils.</p> - -<p>It is intended to be used with Python versions 2.3 to 2.7</p> - -<h2>Improvements:</h2> -<ul> -<li>Reopen <code>SteadyDB</code> connections when commit or rollback fails -(suggested by Ben Hoyt).</li> -</ul> - -<h2>Bugfixes:</h2> -<ul> -<li>Fixed a problem when running under Jython (reported by Vitaly Kruglikov).</li> -</ul> - -<div class="footer"> -DBUtils -(<a href="https://github.com/WebwareForPython/DBUtils">github.com/WebwareForPython/DBUtils</a>) -</div> -</body> -</html> diff --git a/docs/RelNotes-1.1.html b/docs/RelNotes-1.1.html deleted file mode 100644 index 6cdb156..0000000 --- a/docs/RelNotes-1.1.html +++ /dev/null @@ -1,46 +0,0 @@ -<!DOCTYPE html> -<html> -<head> -<title>DBUtils 1.1 Release Notes</title> -<link rel="stylesheet" href="Doc.css" type="text/css"> -</head> -<body> -<h1 class="header">DBUtils 1.1 Release Notes</h1> - -<p>DBUtils 1.1 was released on August 14, 2011.</p> - -<p>This is the seventh public release of DBUtils.</p> - -<h2>Improvements:</h2> -<ul> -<li>The transparent reopening of connections is actually an undesired behavior -if it happens during database transactions. In these cases, the transaction -should fail and the error be reported back to the application instead of the -rest of the transaction being executed in a new connection and therefore in -a new transaction. Therefore DBUtils now allows suspending the transparent -reopening during transactions. All you need to do is indicate the beginning -of a transaction by calling the <code>begin()</code> method of the connection. -DBUtils makes sure that this method always exists, even if the database driver -does not support it.</li> -<li>If the database driver supports a <code>ping()</code> method, then DBUtils -can use it to check whether connections are alive instead of just trying -to use the connection and reestablishing it in case it was dead. Since these -checks are done at the expense of some performance, you have exact control -when these are executed via the new <code>ping</code> parameter.</li> -<li><code>PooledDB</code> has got another new parameter <code>reset</code> for -controlling how connections are reset before being put back into the pool.</li> -</ul> - -<h2>Bugfixes:</h2> -<ul> -<li>Fixed propagation of error messages when the connection was lost.</li> -<li>Fixed an issue with the <code>setoutputsize()</code> cursor method.</li> -<li>Fixed some minor issues with the <code>DBUtilsExample</code> for Webware.</li> -</ul> - -<div class="footer"> -DBUtils -(<a href="https://github.com/WebwareForPython/DBUtils">github.com/WebwareForPython/DBUtils</a>) -</div> -</body> -</html> diff --git a/docs/RelNotes-1.2.html b/docs/RelNotes-1.2.html deleted file mode 100644 index a90eae2..0000000 --- a/docs/RelNotes-1.2.html +++ /dev/null @@ -1,26 +0,0 @@ -<!DOCTYPE html> -<html> -<head> -<title>DBUtils 1.2 Release Notes</title> -<link rel="stylesheet" href="Doc.css" type="text/css"> -</head> -<body> -<h1 class="header">DBUtils 1.2 Release Notes</h1> - -<p>DBUtils 1.2 was released on 02/05/17.</p> - -<p>This is the ninth public release of DBUtils.</p> - -<p>It is intended to be used with Python versions 2.6 and newer.</p> - -<h2>Improvements:</h2> -<ul> -<li>Python 3 is now supported.</li> -</ul> - -<div class="footer"> -DBUtils -(<a href="https://github.com/WebwareForPython/DBUtils">github.com/WebwareForPython/DBUtils</a>) -</div> -</body> -</html> diff --git a/docs/RelNotes-1.3.html b/docs/RelNotes-1.3.html deleted file mode 100644 index 05f8614..0000000 --- a/docs/RelNotes-1.3.html +++ /dev/null @@ -1,26 +0,0 @@ -<!DOCTYPE html> -<html> -<head> -<title>DBUtils 1.3 Release Notes</title> -<link rel="stylesheet" href="Doc.css" type="text/css"> -</head> -<body> -<h1 class="header">DBUtils 1.3 Release Notes</h1> - -<p>DBUtils 1.3 was released on 03/08/18.</p> - -<p>This is the tenth public release of DBUtils.</p> - -<p>It is intended to be used with Python versions 2.6, 2.7 or 3.4 - 3.7.</p> - -<h2>Improvements:</h2> -<ul> -<li>Supports context handlers for connections and cursors.</li> -</ul> - -<div class="footer"> -DBUtils -(<a href="https://github.com/WebwareForPython/DBUtils">github.com/WebwareForPython/DBUtils</a>) -</div> -</body> -</html> diff --git a/docs/RelNotes-1.4.html b/docs/RelNotes-1.4.html deleted file mode 100644 index 0565fd0..0000000 --- a/docs/RelNotes-1.4.html +++ /dev/null @@ -1,26 +0,0 @@ -<!DOCTYPE html> -<html> -<head> -<title>DBUtils 1.4 Release Notes</title> -<link rel="stylesheet" href="Doc.css" type="text/css"> -</head> -<body> -<h1 class="header">DBUtils 1.4 Release Notes</h1> - -<p>DBUtils 1.4 was released on 09/26/20.</p> - -<p>This version is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> - -<h2>Improvements:</h2> -<ul> -<li>The <code>SteadyDB</code> and <code>SteadyPg</code> classes only reconnect -after the <code>maxusage</code> limit has been reached when the connection is -not currently inside a transaction.</li> -</ul> - -<div class="footer"> -DBUtils -(<a href="https://github.com/WebwareForPython/DBUtils">github.com/WebwareForPython/DBUtils</a>) -</div> -</body> -</html> diff --git a/docs/changelog.html b/docs/changelog.html new file mode 100644 index 0000000..1ecb86c --- /dev/null +++ b/docs/changelog.html @@ -0,0 +1,270 @@ +<!DOCTYPE html> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> +<meta charset="utf-8"/> +<meta name="generator" content="Docutils 0.15.2: http://docutils.sourceforge.net/" /> +<title>Changelog for DBUtils</title> +<link rel="stylesheet" href="Doc.css" type="text/css" /> +</head> +<body> +<div class="document" id="changelog-for-dbutils"> +<h1 class="title">Changelog for DBUtils</h1> + +<div class="contents topic" id="contents"> +<p class="topic-title first">Contents</p> +<ul class="simple"> +<li><p><a class="reference internal" href="#id1" id="id21">2.0</a></p> +<ul> +<li><p><a class="reference internal" href="#changes" id="id22">Changes:</a></p></li> +</ul> +</li> +<li><p><a class="reference internal" href="#id2" id="id23">1.4</a></p> +<ul> +<li><p><a class="reference internal" href="#improvements" id="id24">Improvements:</a></p></li> +</ul> +</li> +<li><p><a class="reference internal" href="#id3" id="id25">1.3</a></p> +<ul> +<li><p><a class="reference internal" href="#id4" id="id26">Improvements:</a></p></li> +</ul> +</li> +<li><p><a class="reference internal" href="#id5" id="id27">1.2</a></p></li> +<li><p><a class="reference internal" href="#id6" id="id28">1.1.1</a></p> +<ul> +<li><p><a class="reference internal" href="#id7" id="id29">Improvements:</a></p></li> +<li><p><a class="reference internal" href="#bugfixes" id="id30">Bugfixes:</a></p></li> +</ul> +</li> +<li><p><a class="reference internal" href="#id8" id="id31">1.1</a></p> +<ul> +<li><p><a class="reference internal" href="#id9" id="id32">Improvements:</a></p></li> +<li><p><a class="reference internal" href="#id10" id="id33">Bugfixes:</a></p></li> +</ul> +</li> +<li><p><a class="reference internal" href="#id11" id="id34">1.0</a></p> +<ul> +<li><p><a class="reference internal" href="#id12" id="id35">Changes:</a></p></li> +<li><p><a class="reference internal" href="#bugfixes-and-improvements" id="id36">Bugfixes and Improvements:</a></p></li> +</ul> +</li> +<li><p><a class="reference internal" href="#id13" id="id37">0.9.4</a></p></li> +<li><p><a class="reference internal" href="#id14" id="id38">0.9.3</a></p> +<ul> +<li><p><a class="reference internal" href="#id15" id="id39">Changes:</a></p></li> +</ul> +</li> +<li><p><a class="reference internal" href="#id16" id="id40">0.9.2</a></p> +<ul> +<li><p><a class="reference internal" href="#id17" id="id41">Changes:</a></p></li> +</ul> +</li> +<li><p><a class="reference internal" href="#id18" id="id42">0.9.1</a></p> +<ul> +<li><p><a class="reference internal" href="#id19" id="id43">Changes:</a></p></li> +</ul> +</li> +<li><p><a class="reference internal" href="#id20" id="id44">0.8.1 - 2005-09-13</a></p></li> +</ul> +</div> +<div class="section" id="id1"> +<h1>2.0</h1> +<p>DBUtils 2.0 was released on September 26, 2020.</p> +<p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> +<div class="section" id="changes"> +<h2>Changes:</h2> +<ul class="simple"> +<li><p>DBUtils does not act as a Webware plugin anymore, it is now just an ordinary +Python package (of course it could be used as such also before).</p></li> +<li><p>The top-level packages and folders have been renamed to lower-case. +Particularly, you need to import <span class="docutils literal">dbutils</span> instead of <span class="docutils literal">DBUtils</span> now.</p></li> +<li><p>The Webware <span class="docutils literal">Examples</span> folder has been removed.</p></li> +<li><p>This changelog file has been created from the former release notes.</p></li> +</ul> +</div> +</div> +<div class="section" id="id2"> +<h1>1.4</h1> +<p>DBUtils 1.4 was released on August 26, 2020.</p> +<p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> +<div class="section" id="improvements"> +<h2>Improvements:</h2> +<ul class="simple"> +<li><p>The <span class="docutils literal">SteadyDB</span> and <span class="docutils literal">SteadyPg</span> classes only reconnect after the +<span class="docutils literal">maxusage</span> limit has been reached when the connection is not currently +inside a transaction.</p></li> +</ul> +</div> +</div> +<div class="section" id="id3"> +<h1>1.3</h1> +<p>DBUtils 1.3 was released on March 3, 2018.</p> +<p>It is intended to be used with Python versions 2.6, 2.7 and 3.4 to 3.7.</p> +<div class="section" id="id4"> +<h2>Improvements:</h2> +<ul class="simple"> +<li><p>This version now supports context handlers for connections and cursors.</p></li> +</ul> +</div> +</div> +<div class="section" id="id5"> +<h1>1.2</h1> +<p>DBUtils 1.2 was released on February 5, 2017.</p> +<p>It is intended to be used with Python versions 2.6, 2.7 and 3.0 to 3.6.</p> +</div> +<div class="section" id="id6"> +<h1>1.1.1</h1> +<p>DBUtils 1.1.1 was released on February 4, 2017.</p> +<p>It is intended to be used with Python versions 2.3 to 2.7.</p> +<div class="section" id="id7"> +<h2>Improvements:</h2> +<ul class="simple"> +<li><p>Reopen <span class="docutils literal">SteadyDB</span> connections when commit or rollback fails +(suggested by Ben Hoyt).</p></li> +</ul> +</div> +<div class="section" id="bugfixes"> +<h2>Bugfixes:</h2> +<ul class="simple"> +<li><p>Fixed a problem when running under Jython (reported by Vitaly Kruglikov).</p></li> +</ul> +</div> +</div> +<div class="section" id="id8"> +<h1>1.1</h1> +<p>DBUtils 1.1 was released on August 14, 2011.</p> +<div class="section" id="id9"> +<h2>Improvements:</h2> +<ul class="simple"> +<li><p>The transparent reopening of connections is actually an undesired behavior +if it happens during database transactions. In these cases, the transaction +should fail and the error be reported back to the application instead of the +rest of the transaction being executed in a new connection and therefore in +a new transaction. Therefore DBUtils now allows suspending the transparent +reopening during transactions. All you need to do is indicate the beginning +of a transaction by calling the <span class="docutils literal">begin()</span> method of the connection. +DBUtils makes sure that this method always exists, even if the database +driver does not support it.</p></li> +<li><p>If the database driver supports a <span class="docutils literal">ping()</span> method, then DBUtils can use it +to check whether connections are alive instead of just trying to use the +connection and reestablishing it in case it was dead. Since these checks are +done at the expense of some performance, you have exact control when these +are executed via the new <span class="docutils literal">ping</span> parameter.</p></li> +<li><p><span class="docutils literal">PooledDB</span> has got another new parameter <span class="docutils literal">reset</span> for controlling how +connections are reset before being put back into the pool.</p></li> +</ul> +</div> +<div class="section" id="id10"> +<h2>Bugfixes:</h2> +<div class="system-message"> +<p class="system-message-title">System Message: WARNING/2 (<span class="docutils literal">changelog.rst</span>, line 100)</p> +<p>Title underline too short.</p> +<pre class="literal-block">Bugfixes: +--------</pre> +</div> +<ul class="simple"> +<li><p>Fixed propagation of error messages when the connection was lost.</p></li> +<li><p>Fixed an issue with the <span class="docutils literal">setoutputsize()</span> cursor method.</p></li> +<li><p>Fixed some minor issues with the <span class="docutils literal">DBUtilsExample</span> for Webware.</p></li> +</ul> +</div> +</div> +<div class="section" id="id11"> +<h1>1.0</h1> +<p>DBUtils 1.0 was released on November 29, 2008.</p> +<p>It is intended to be used with Python versions 2.2 to 2.6.</p> +<div class="section" id="id12"> +<h2>Changes:</h2> +<ul class="simple"> +<li><p>Added a <span class="docutils literal">failures</span> parameter for configuring the exception classes for +which the failover mechanisms is applied (as suggested by Matthew Harriger).</p></li> +<li><p>Added a <span class="docutils literal">closeable</span> parameter for configuring whether connections can be +closed (otherwise closing connections will be silently ignored).</p></li> +<li><p>It is now possible to override defaults via the <span class="docutils literal">creator.dbapi</span> and +<span class="docutils literal">creator.threadsafety</span> attributes.</p></li> +<li><p>Added an alias method <span class="docutils literal">dedicated_connection</span> as a shorthand for +<span class="docutils literal">connection(shareable=False)</span>.</p></li> +<li><p>Added a version attribute to all exported classes.</p></li> +<li><p>Where the value <span class="docutils literal">0</span> has the meaning "unlimited", parameters can now be also +set to the value <span class="docutils literal">None</span> instead.</p></li> +<li><p>It turned out that <span class="docutils literal">threading.local</span> does not work properly with +<span class="docutils literal">mod_wsgi</span>, so we use the Python implementation for thread-local data +even when a faster <span class="docutils literal">threading.local</span> implementation is available. +A new parameter <span class="docutils literal">threadlocal</span> allows you to pass an arbitrary class +such as <span class="docutils literal">threading.local</span> if you know it works in your environment.</p></li> +</ul> +</div> +<div class="section" id="bugfixes-and-improvements"> +<h2>Bugfixes and Improvements:</h2> +<ul class="simple"> +<li><p>In some cases, when instance initialization failed or referenced objects +were already destroyed, finalizers could throw exceptions or create infinite +recursion (problem reported by Gregory Pinero and Jehiah Czebotar).</p></li> +<li><p>DBUtils now tries harder to find the underlying DB-API 2 module if only a +connection creator function is specified. This had not worked before with +the MySQLdb module (problem reported by Gregory Pinero).</p></li> +</ul> +</div> +</div> +<div class="section" id="id13"> +<h1>0.9.4</h1> +<p>DBUtils 0.9.4 was released on July 7, 2007.</p> +<p>This release fixes a problem in the destructor code and has been supplemented +with a German User's Guide.</p> +<p>Again, please note that the <span class="docutils literal">dbapi</span> parameter has been renamed to <span class="docutils literal">creator</span> +in the last release, since you can now pass custom creator functions +for database connections instead of DB-API 2 modules.</p> +</div> +<div class="section" id="id14"> +<h1>0.9.3</h1> +<p>DBUtils 0.9.3 was released on May 21, 2007.</p> +<div class="section" id="id15"> +<h2>Changes:</h2> +<ul class="simple"> +<li><p>Support custom creator functions for database connections. +These can now be used as the first parameter instead of an DB-API module +(suggested by Ezio Vernacotola).</p></li> +<li><p>Added destructor for steady connections.</p></li> +<li><p>Use <a class="reference external" href="https://github.com/pypa/setuptools">setuptools</a> if available.</p></li> +<li><p>Some code cleanup.</p></li> +<li><p>Some fixes in the documentation. +Added Chinese translation of the User's Guide, kindly contributed by gashero.</p></li> +</ul> +</div> +</div> +<div class="section" id="id16"> +<h1>0.9.2</h1> +<p>DBUtils 0.9.2 was released on September 22, 2006.</p> +<p>It is intended to be used with Python versions 2.2 to 2.5.</p> +<div class="section" id="id17"> +<h2>Changes:</h2> +<ul class="simple"> +<li><p>Renamed <span class="docutils literal">SolidDB</span> to <span class="docutils literal">SteadyDB</span> to avoid confusion with the "solidDB" +storage engine. Accordingly, renamed <span class="docutils literal">SolidPg</span> to <span class="docutils literal">SteadyPg</span>.</p></li> +</ul> +</div> +</div> +<div class="section" id="id18"> +<h1>0.9.1</h1> +<p>DBUtils 0.9.1 was released on May 8, 2006.</p> +<p>It is intended to be used with Python versions 2.2 to 2.4.</p> +<div class="section" id="id19"> +<h2>Changes:</h2> +<ul class="simple"> +<li><p>Added <span class="docutils literal">_closeable</span> attribute and made persistent connections not closeable +by default. This allows <span class="docutils literal">PersistentDB</span> to be used in the same way as you +would use <span class="docutils literal">PooledDB</span>.</p></li> +<li><p>Allowed arguments in the DB-API 2 <span class="docutils literal">cursor()</span> method. MySQLdb is using this +to specify cursor classes. (Suggested by Michael Palmer.)</p></li> +<li><p>Improved the documentation and added a User's Guide.</p></li> +</ul> +</div> +</div> +<div class="section" id="id20"> +<h1>0.8.1 - 2005-09-13</h1> +<p>DBUtils 0.8.1 was released on September 13, 2005.</p> +<p>It is intended to be used with Python versions 2.0 to 2.4.</p> +<p>This is the first public release of DBUtils.</p> +</div> +</div> +</body> +</html> diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..fb97129 --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1,211 @@ +Changelog for DBUtils ++++++++++++++++++++++ + +.. contents:: Contents + +2.0 +=== + +DBUtils 2.0 was released on September 26, 2020. + +It is intended to be used with Python versions 2.7 and 3.5 to 3.9. + +Changes: +-------- + +* DBUtils does not act as a Webware plugin anymore, it is now just an ordinary + Python package (of course it could be used as such also before). +* The top-level packages and folders have been renamed to lower-case. + Particularly, you need to import ``dbutils`` instead of ``DBUtils`` now. +* The Webware ``Examples`` folder has been removed. +* This changelog file has been created from the former release notes. + +1.4 +=== + +DBUtils 1.4 was released on August 26, 2020. + +It is intended to be used with Python versions 2.7 and 3.5 to 3.9. + +Improvements: +------------- + +* The ``SteadyDB`` and ``SteadyPg`` classes only reconnect after the + ``maxusage`` limit has been reached when the connection is not currently + inside a transaction. + +1.3 +=== + +DBUtils 1.3 was released on March 3, 2018. + +It is intended to be used with Python versions 2.6, 2.7 and 3.4 to 3.7. + +Improvements: +------------- + +* This version now supports context handlers for connections and cursors. + +1.2 +=== + +DBUtils 1.2 was released on February 5, 2017. + +It is intended to be used with Python versions 2.6, 2.7 and 3.0 to 3.6. + +1.1.1 +===== + +DBUtils 1.1.1 was released on February 4, 2017. + +It is intended to be used with Python versions 2.3 to 2.7. + +Improvements: +------------- + +* Reopen ``SteadyDB`` connections when commit or rollback fails + (suggested by Ben Hoyt). + +Bugfixes: +--------- + +* Fixed a problem when running under Jython (reported by Vitaly Kruglikov). + +1.1 +=== + +DBUtils 1.1 was released on August 14, 2011. + +Improvements: +------------- + +* The transparent reopening of connections is actually an undesired behavior + if it happens during database transactions. In these cases, the transaction + should fail and the error be reported back to the application instead of the + rest of the transaction being executed in a new connection and therefore in + a new transaction. Therefore DBUtils now allows suspending the transparent + reopening during transactions. All you need to do is indicate the beginning + of a transaction by calling the ``begin()`` method of the connection. + DBUtils makes sure that this method always exists, even if the database + driver does not support it. +* If the database driver supports a ``ping()`` method, then DBUtils can use it + to check whether connections are alive instead of just trying to use the + connection and reestablishing it in case it was dead. Since these checks are + done at the expense of some performance, you have exact control when these + are executed via the new ``ping`` parameter. +* ``PooledDB`` has got another new parameter ``reset`` for controlling how + connections are reset before being put back into the pool. + +Bugfixes: +-------- + +* Fixed propagation of error messages when the connection was lost. +* Fixed an issue with the ``setoutputsize()`` cursor method. +* Fixed some minor issues with the ``DBUtilsExample`` for Webware. + + +1.0 +=== + +DBUtils 1.0 was released on November 29, 2008. + +It is intended to be used with Python versions 2.2 to 2.6. + +Changes: +-------- + +* Added a ``failures`` parameter for configuring the exception classes for + which the failover mechanisms is applied (as suggested by Matthew Harriger). +* Added a ``closeable`` parameter for configuring whether connections can be + closed (otherwise closing connections will be silently ignored). +* It is now possible to override defaults via the ``creator.dbapi`` and + ``creator.threadsafety`` attributes. +* Added an alias method ``dedicated_connection`` as a shorthand for + ``connection(shareable=False)``. +* Added a version attribute to all exported classes. +* Where the value ``0`` has the meaning "unlimited", parameters can now be also + set to the value ``None`` instead. +* It turned out that ``threading.local`` does not work properly with + ``mod_wsgi``, so we use the Python implementation for thread-local data + even when a faster ``threading.local`` implementation is available. + A new parameter ``threadlocal`` allows you to pass an arbitrary class + such as ``threading.local`` if you know it works in your environment. + +Bugfixes and Improvements: +-------------------------- + +* In some cases, when instance initialization failed or referenced objects + were already destroyed, finalizers could throw exceptions or create infinite + recursion (problem reported by Gregory Pinero and Jehiah Czebotar). +* DBUtils now tries harder to find the underlying DB-API 2 module if only a + connection creator function is specified. This had not worked before with + the MySQLdb module (problem reported by Gregory Pinero). + +0.9.4 +===== + +DBUtils 0.9.4 was released on July 7, 2007. + +This release fixes a problem in the destructor code and has been supplemented +with a German User's Guide. + +Again, please note that the ``dbapi`` parameter has been renamed to ``creator`` +in the last release, since you can now pass custom creator functions +for database connections instead of DB-API 2 modules. + +0.9.3 +===== + +DBUtils 0.9.3 was released on May 21, 2007. + +Changes: +-------- + +* Support custom creator functions for database connections. + These can now be used as the first parameter instead of an DB-API module + (suggested by Ezio Vernacotola). +* Added destructor for steady connections. +* Use setuptools_ if available. +* Some code cleanup. +* Some fixes in the documentation. + Added Chinese translation of the User's Guide, kindly contributed by gashero. + +.. _setuptools: https://github.com/pypa/setuptools + +0.9.2 +===== + +DBUtils 0.9.2 was released on September 22, 2006. + +It is intended to be used with Python versions 2.2 to 2.5. + +Changes: +-------- + +* Renamed ``SolidDB`` to ``SteadyDB`` to avoid confusion with the "solidDB" + storage engine. Accordingly, renamed ``SolidPg`` to ``SteadyPg``. + +0.9.1 +===== + +DBUtils 0.9.1 was released on May 8, 2006. + +It is intended to be used with Python versions 2.2 to 2.4. + +Changes: +-------- +* Added ``_closeable`` attribute and made persistent connections not closeable + by default. This allows ``PersistentDB`` to be used in the same way as you + would use ``PooledDB``. +* Allowed arguments in the DB-API 2 ``cursor()`` method. MySQLdb is using this + to specify cursor classes. (Suggested by Michael Palmer.) +* Improved the documentation and added a User's Guide. + +0.8.1 - 2005-09-13 +================== + +DBUtils 0.8.1 was released on September 13, 2005. + +It is intended to be used with Python versions 2.0 to 2.4. + +This is the first public release of DBUtils. diff --git a/docs/Doc.css b/docs/doc.css similarity index 99% rename from docs/Doc.css rename to docs/doc.css index 391e17c..c815371 100644 --- a/docs/Doc.css +++ b/docs/doc.css @@ -6,7 +6,7 @@ /* First import default style for pages created with Docutils: */ -@import url(DocUtils.css); +@import url(docutils.css); /* Customization for Webware goes here: */ diff --git a/docs/DocUtils.css b/docs/docutils.css similarity index 100% rename from docs/DocUtils.css rename to docs/docutils.css diff --git a/docs/UsersGuide.de.html b/docs/main.de.html similarity index 99% rename from docs/UsersGuide.de.html rename to docs/main.de.html index 948e12a..10a3eef 100644 --- a/docs/UsersGuide.de.html +++ b/docs/main.de.html @@ -11,12 +11,12 @@ <h1 class="title">Benutzeranleitung für DBUtils</h1> <dl class="docinfo simple"> <dt class="version">Version</dt> -<dd class="version">1.4</dd> +<dd class="version">2.0</dd> <dt class="released">Released</dt> <dd class="released"><p>09/26/20</p> </dd> <dt class="translations">Translations</dt> -<dd class="translations"><p><a class="reference external" href="UsersGuide.html">English</a> | German</p> +<dd class="translations"><p><a class="reference external" href="main.html">English</a> | German</p> </dd> </dl> <div class="contents topic" id="inhalt"> diff --git a/docs/UsersGuide.de.rst b/docs/main.de.rst similarity index 99% rename from docs/UsersGuide.de.rst rename to docs/main.de.rst index ee59e63..00013ac 100644 --- a/docs/UsersGuide.de.rst +++ b/docs/main.de.rst @@ -1,11 +1,11 @@ Benutzeranleitung für DBUtils +++++++++++++++++++++++++++++ -:Version: 1.4 +:Version: 2.0 :Released: 09/26/20 :Translations: English_ | German -.. _English: UsersGuide.html +.. _English: main.html .. contents:: Inhalt diff --git a/docs/UsersGuide.html b/docs/main.html similarity index 99% rename from docs/UsersGuide.html rename to docs/main.html index 2ec476e..9ec6f26 100644 --- a/docs/UsersGuide.html +++ b/docs/main.html @@ -11,12 +11,12 @@ <h1 class="title">DBUtils User's Guide</h1> <dl class="docinfo simple"> <dt class="version">Version</dt> -<dd class="version">1.4</dd> +<dd class="version">2.0</dd> <dt class="released">Released</dt> <dd class="released"><p>09/26/20</p> </dd> <dt class="translations">Translations</dt> -<dd class="translations"><p>English | <a class="reference external" href="UsersGuide.de.html">German</a></p> +<dd class="translations"><p>English | <a class="reference external" href="main.de.html">German</a></p> </dd> </dl> <div class="contents topic" id="contents"> diff --git a/docs/UsersGuide.rst b/docs/main.rst similarity index 99% rename from docs/UsersGuide.rst rename to docs/main.rst index 5c17298..edbfd31 100644 --- a/docs/UsersGuide.rst +++ b/docs/main.rst @@ -1,11 +1,11 @@ DBUtils User's Guide ++++++++++++++++++++ -:Version: 1.4 +:Version: 2.0 :Released: 09/26/20 :Translations: English | German_ -.. _German: UsersGuide.de.html +.. _German: main.de.html .. contents:: Contents diff --git a/buildhtml.py b/docs/make.py similarity index 92% rename from buildhtml.py rename to docs/make.py index 420a64c..fb82e88 100755 --- a/buildhtml.py +++ b/docs/make.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/python3.8 """Build HMTL from reST files.""" @@ -10,7 +10,7 @@ print("Creating the documentation...") -for rst_file in glob(join('DBUtils', 'Docs', '*.rst')): +for rst_file in glob('*.rst'): name = splitext(rst_file)[0] lang = splitext(name)[1] if lang.startswith('.'): From 5b9fe81108f87fdfa689332b7183c4d8dfb2b721 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 26 Sep 2020 16:29:26 +0200 Subject: [PATCH 04/84] Rename all modules to lower-case Also remove Properties.py since this is no longer a Webware plug-in. --- MANIFEST.in | 12 ++++++++++-- dbutils/Properties.py | 19 ------------------- dbutils/{PersistentDB.py => persistent_db.py} | 0 dbutils/{PersistentPg.py => persistent_pg.py} | 0 dbutils/{PooledDB.py => pooled_db.py} | 0 dbutils/{PooledPg.py => pooled_pg.py} | 0 ...{SimplePooledDB.py => simple_pooled_db.py} | 0 ...{SimplePooledPg.py => simple_pooled_pg.py} | 0 dbutils/{SteadyDB.py => steady_db.py} | 0 dbutils/{SteadyPg.py => steady_pg.py} | 0 Release.md => release.md | 0 setup.py | 4 ++-- ...tPersistentDB.py => test_persistend_db.py} | 0 ...tPersistentPg.py => test_persistend_pg.py} | 0 tests/{TestPooledDB.py => test_pooled_db.py} | 0 tests/{TestPooledPg.py => test_pooled_pg.py} | 0 ...lePooledDB.py => test_simple_pooled_db.py} | 0 ...lePooledPg.py => test_simple_pooled_pg.py} | 0 tests/{TestSteadyDB.py => test_steady_db.py} | 0 tests/{TestSteadyPg.py => test_steady_pg.py} | 0 ...eadingLocal.py => test_threading_local.py} | 0 tox.ini | 7 ++----- 22 files changed, 14 insertions(+), 28 deletions(-) delete mode 100644 dbutils/Properties.py rename dbutils/{PersistentDB.py => persistent_db.py} (100%) rename dbutils/{PersistentPg.py => persistent_pg.py} (100%) rename dbutils/{PooledDB.py => pooled_db.py} (100%) rename dbutils/{PooledPg.py => pooled_pg.py} (100%) rename dbutils/{SimplePooledDB.py => simple_pooled_db.py} (100%) rename dbutils/{SimplePooledPg.py => simple_pooled_pg.py} (100%) rename dbutils/{SteadyDB.py => steady_db.py} (100%) rename dbutils/{SteadyPg.py => steady_pg.py} (100%) rename Release.md => release.md (100%) rename tests/{TestPersistentDB.py => test_persistend_db.py} (100%) rename tests/{TestPersistentPg.py => test_persistend_pg.py} (100%) rename tests/{TestPooledDB.py => test_pooled_db.py} (100%) rename tests/{TestPooledPg.py => test_pooled_pg.py} (100%) rename tests/{TestSimplePooledDB.py => test_simple_pooled_db.py} (100%) rename tests/{TestSimplePooledPg.py => test_simple_pooled_pg.py} (100%) rename tests/{TestSteadyDB.py => test_steady_db.py} (100%) rename tests/{TestSteadyPg.py => test_steady_pg.py} (100%) rename tests/{TestThreadingLocal.py => test_threading_local.py} (100%) diff --git a/MANIFEST.in b/MANIFEST.in index 14ffe38..b4efc19 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,11 @@ -exclude Release.md setversion.py +include MANIFEST.in + +include LICENSE include README.md -include DBUtils/Docs/* + +recursive-include docs *.rst make.py *.html *.css *.png *.gif + +prune docs/_build +exclude release.md setversion.py + +global-exclude *.py[co] __pycache__ diff --git a/dbutils/Properties.py b/dbutils/Properties.py deleted file mode 100644 index da7426e..0000000 --- a/dbutils/Properties.py +++ /dev/null @@ -1,19 +0,0 @@ -name = 'DBUtils' - -version = (1, 4, 0) - -docs = [ - {'name': "User's Guide", 'file': 'UsersGuide.html'}, -] - -status = 'beta' - -requiredPyVersion = (2, 7, 0) - -synopsis = ( - "DBUtils provides database related support classes and functions" - " to Webware. There is plenty of useful reusable code here.") - -WebKitConfig = { - 'examplePages': ['DBUtilsExample'] -} diff --git a/dbutils/PersistentDB.py b/dbutils/persistent_db.py similarity index 100% rename from dbutils/PersistentDB.py rename to dbutils/persistent_db.py diff --git a/dbutils/PersistentPg.py b/dbutils/persistent_pg.py similarity index 100% rename from dbutils/PersistentPg.py rename to dbutils/persistent_pg.py diff --git a/dbutils/PooledDB.py b/dbutils/pooled_db.py similarity index 100% rename from dbutils/PooledDB.py rename to dbutils/pooled_db.py diff --git a/dbutils/PooledPg.py b/dbutils/pooled_pg.py similarity index 100% rename from dbutils/PooledPg.py rename to dbutils/pooled_pg.py diff --git a/dbutils/SimplePooledDB.py b/dbutils/simple_pooled_db.py similarity index 100% rename from dbutils/SimplePooledDB.py rename to dbutils/simple_pooled_db.py diff --git a/dbutils/SimplePooledPg.py b/dbutils/simple_pooled_pg.py similarity index 100% rename from dbutils/SimplePooledPg.py rename to dbutils/simple_pooled_pg.py diff --git a/dbutils/SteadyDB.py b/dbutils/steady_db.py similarity index 100% rename from dbutils/SteadyDB.py rename to dbutils/steady_db.py diff --git a/dbutils/SteadyPg.py b/dbutils/steady_pg.py similarity index 100% rename from dbutils/SteadyPg.py rename to dbutils/steady_pg.py diff --git a/Release.md b/release.md similarity index 100% rename from Release.md rename to release.md diff --git a/setup.py b/setup.py index 7e97f95..478fe8e 100755 --- a/setup.py +++ b/setup.py @@ -43,6 +43,6 @@ url='https://webwareforpython.github.io/DBUtils/', platforms=['any'], license='MIT License', - packages=['DBUtils', 'DBUtils.Examples', 'DBUtils.Tests'], - package_data={'DBUtils': ['Docs/*']} + packages=['dbutils'], + zip_safe=False, ) diff --git a/tests/TestPersistentDB.py b/tests/test_persistend_db.py similarity index 100% rename from tests/TestPersistentDB.py rename to tests/test_persistend_db.py diff --git a/tests/TestPersistentPg.py b/tests/test_persistend_pg.py similarity index 100% rename from tests/TestPersistentPg.py rename to tests/test_persistend_pg.py diff --git a/tests/TestPooledDB.py b/tests/test_pooled_db.py similarity index 100% rename from tests/TestPooledDB.py rename to tests/test_pooled_db.py diff --git a/tests/TestPooledPg.py b/tests/test_pooled_pg.py similarity index 100% rename from tests/TestPooledPg.py rename to tests/test_pooled_pg.py diff --git a/tests/TestSimplePooledDB.py b/tests/test_simple_pooled_db.py similarity index 100% rename from tests/TestSimplePooledDB.py rename to tests/test_simple_pooled_db.py diff --git a/tests/TestSimplePooledPg.py b/tests/test_simple_pooled_pg.py similarity index 100% rename from tests/TestSimplePooledPg.py rename to tests/test_simple_pooled_pg.py diff --git a/tests/TestSteadyDB.py b/tests/test_steady_db.py similarity index 100% rename from tests/TestSteadyDB.py rename to tests/test_steady_db.py diff --git a/tests/TestSteadyPg.py b/tests/test_steady_pg.py similarity index 100% rename from tests/TestSteadyPg.py rename to tests/test_steady_pg.py diff --git a/tests/TestThreadingLocal.py b/tests/test_threading_local.py similarity index 100% rename from tests/TestThreadingLocal.py rename to tests/test_threading_local.py diff --git a/tox.ini b/tox.ini index cfc48a4..78dd8dc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,6 @@ [tox] envlist = py{27,35,36,37,38,39}, flake8 -[pytest] -python_files=Test*.py - [testenv] setenv = PYTHONPATH = {toxinidir} @@ -14,8 +11,8 @@ commands = [testenv:flake8] basepython = - python + python3.8 deps = flake8 commands = - flake8 DBUtils + flake8 *.py dbutils From 1513377fd16de472325bd83cdd0216007e69bcf3 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 26 Sep 2020 18:01:47 +0200 Subject: [PATCH 05/84] Use the new lower-case module names everywhere Also use the bump2version tool instead of setversion.py. --- .bumpversion.cfg | 22 ++++ README.md | 2 +- dbutils/__init__.py | 14 +- dbutils/persistent_db.py | 7 +- dbutils/persistent_pg.py | 7 +- dbutils/pooled_db.py | 7 +- dbutils/pooled_pg.py | 7 +- dbutils/simple_pooled_db.py | 4 +- dbutils/simple_pooled_pg.py | 4 +- dbutils/steady_db.py | 4 +- dbutils/steady_pg.py | 4 +- docs/changelog.html | 7 +- docs/changelog.rst | 3 +- docs/main.de.html | 3 - docs/main.de.rst | 1 - docs/main.html | 3 - docs/main.rst | 1 - docs/make.py | 16 +-- setup.py | 2 +- setversion.py | 123 ------------------ tests/__init__.py | 2 +- tests/mock_pg.py | 4 +- ...persistend_db.py => test_persistent_db.py} | 122 +++++++++-------- ...persistend_pg.py => test_persistent_pg.py} | 112 ++++++++-------- tests/test_pooled_db.py | 68 +++++----- tests/test_pooled_pg.py | 34 +++-- tests/test_simple_pooled_db.py | 69 +++++----- tests/test_simple_pooled_pg.py | 49 ++++--- tests/test_steady_db.py | 56 ++++---- tests/test_steady_pg.py | 34 +++-- tests/test_threading_local.py | 68 +++++----- tox.ini | 2 +- 32 files changed, 350 insertions(+), 511 deletions(-) create mode 100644 .bumpversion.cfg delete mode 100755 setversion.py rename tests/{test_persistend_db.py => test_persistent_db.py} (74%) rename tests/{test_persistend_pg.py => test_persistent_pg.py} (64%) diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..9eab400 --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,22 @@ +[bumpversion] +current_version = 2.0 + +[bumpversion:file:setup.py] +search = __version__ = '{current_version}' +replace = __version__ = '{new_version}' + +[bumpversion:file:dbutils/__init__.py] +search = __version__ = '{current_version}' +replace = __version__ = '{new_version}' + +[bumpversion:file:README.md] +search = The current version {current_version} +replace = The current version {new_version} + +[bumpversion:file:docs/main.rst] +search = :Version: {current_version} +search = :Version: {new_version} + +[bumpversion:file:docs/main.de.rst] +search = :Version: {current_version} +search = :Version: {new_version} diff --git a/README.md b/README.md index 481657f..3697d59 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,6 @@ to a database that can be used in all kinds of multi-threaded environments like Webware for Python or other web application servers. The suite supports DB-API 2 compliant database interfaces and the classic PyGreSQL interface. -The current version of DBUtils supports Python versions 2.7 and 3.5 - 3.8. +The current version 2.0 of DBUtils supports Python versions 2.7 and 3.5 - 3.8. The DBUtils home page can be found here: https://webwareforpython.github.io/DBUtils/ diff --git a/dbutils/__init__.py b/dbutils/__init__.py index 7fd5939..adfdbf7 100644 --- a/dbutils/__init__.py +++ b/dbutils/__init__.py @@ -1,12 +1,8 @@ -# DBUtils package +# DBUtils main package __all__ = [ - 'SimplePooledPg', 'SteadyPg', 'PooledPg', 'PersistentPg', - 'SimplePooledDB', 'SteadyDB', 'PooledDB', 'PersistentDB' -] + '__version__', + 'simple_pooled_pg', 'steady_pg', 'pooled_pg', 'persistent_pg', + 'simple_pooled_db', 'steady_db', 'pooled_db', 'persistent_db'] -__version__ = '1.4' - - -def InstallInWebKit(appServer): - pass +__version__ = '2.0' diff --git a/dbutils/persistent_db.py b/dbutils/persistent_db.py index ae383d4..ac26c11 100644 --- a/dbutils/persistent_db.py +++ b/dbutils/persistent_db.py @@ -64,7 +64,7 @@ every connection to your local database 'mydb' to be reused 1000 times: import pgdb # import used DB-API 2 module - from DBUtils.PersistentDB import PersistentDB + from dbutils.persistent_db import PersistentDB persist = PersistentDB(pgdb, 1000, database='mydb') Once you have set up the generator with these parameters, you can @@ -111,9 +111,8 @@ """ -from DBUtils.SteadyDB import connect - -__version__ = '1.4' +from . import __version__ +from .steady_db import connect try: # Prefer the pure Python version of threading.local. diff --git a/dbutils/persistent_pg.py b/dbutils/persistent_pg.py index 08ea171..b14960e 100644 --- a/dbutils/persistent_pg.py +++ b/dbutils/persistent_pg.py @@ -54,7 +54,7 @@ For instance, if you want every connection to your local database 'mydb' to be reused 1000 times: - from DBUtils.PersistentPg import PersistentPg + from dbutils.persistent_pg import PersistentPg persist = PersistentPg(5, dbname='mydb') Once you have set up the generator with these parameters, you can @@ -102,9 +102,8 @@ """ -from DBUtils.SteadyPg import SteadyPgConnection - -__version__ = '1.4' +from . import __version__ +from .steady_pg import SteadyPgConnection try: # Prefer the pure Python version of threading.local. diff --git a/dbutils/pooled_db.py b/dbutils/pooled_db.py index 3336177..0b13c96 100644 --- a/dbutils/pooled_db.py +++ b/dbutils/pooled_db.py @@ -74,7 +74,7 @@ want a pool of at least five connections to your local database 'mydb': import pgdb # import used DB-API 2 module - from DBUtils.PooledDB import PooledDB + from dbutils.pooled_db import PooledDB pool = PooledDB(pgdb, 5, database='mydb') Once you have set up the connection pool you can request @@ -141,9 +141,8 @@ from threading import Condition -from DBUtils.SteadyDB import connect - -__version__ = '1.4' +from . import __version__ +from .steady_db import connect class PooledDBError(Exception): diff --git a/dbutils/pooled_pg.py b/dbutils/pooled_pg.py index c451dec..2f8b937 100644 --- a/dbutils/pooled_pg.py +++ b/dbutils/pooled_pg.py @@ -55,7 +55,7 @@ For instance, if you want a pool of at least five connections to your local database 'mydb': - from DBUtils.PooledPg import PooledPg + from dbutils.pooled_pg import PooledPg pool = PooledPg(5, dbname='mydb') Once you have set up the connection pool you can request @@ -113,9 +113,8 @@ except ImportError: # Python 3 from queue import Queue, Empty, Full -from DBUtils.SteadyPg import SteadyPgConnection - -__version__ = '1.4' +from . import __version__ +from .steady_pg import SteadyPgConnection class PooledPgError(Exception): diff --git a/dbutils/simple_pooled_db.py b/dbutils/simple_pooled_db.py index 3318163..3e1b406 100644 --- a/dbutils/simple_pooled_db.py +++ b/dbutils/simple_pooled_db.py @@ -26,7 +26,7 @@ to be cached in the pool and the connection parameters, e.g. import pgdb # import used DB-API 2 module - from DBUtils.SimplePooledDB import PooledDB + from dbutils.simple_pooled_db import PooledDB dbpool = PooledDB(pgdb, 5, host=..., database=..., user=..., ...) you can demand database connections from that pool, @@ -73,7 +73,7 @@ """ -__version__ = '1.4' +from . import __version__ class PooledDBError(Exception): diff --git a/dbutils/simple_pooled_pg.py b/dbutils/simple_pooled_pg.py index 84cd1a2..4927a35 100644 --- a/dbutils/simple_pooled_pg.py +++ b/dbutils/simple_pooled_pg.py @@ -26,7 +26,7 @@ number of connections to be cached in the pool and the connection parameters, e.g. - from DBUtils.SimplePooledPg import PooledPg + from dbutils.simple_pooled_pg import PooledPg dbpool = PooledPg(5, host=..., database=..., user=..., ...) you can demand database connections from that pool, @@ -70,7 +70,7 @@ from pg import DB as PgConnection -__version__ = '1.4' +from . import __version__ class PooledPgConnection: diff --git a/dbutils/steady_db.py b/dbutils/steady_db.py index 6ad6423..13918d6 100644 --- a/dbutils/steady_db.py +++ b/dbutils/steady_db.py @@ -58,7 +58,7 @@ without further notice. import pgdb # import used DB-API 2 module - from DBUtils.SteadyDB import connect + from dbutils.steady_db import connect db = connect(pgdb, 10000, ["set datestyle to german"], host=..., database=..., user=..., ...) ... @@ -92,7 +92,7 @@ import sys -__version__ = '1.4' +from . import __version__ try: baseint = (int, long) diff --git a/dbutils/steady_pg.py b/dbutils/steady_pg.py index 5c10be8..a75b025 100644 --- a/dbutils/steady_pg.py +++ b/dbutils/steady_pg.py @@ -45,7 +45,7 @@ database is lost or has been used too often, it will be automatically reset, without further notice. - from DBUtils.SteadyPg import SteadyPgConnection + from dbutils.steady_pg import SteadyPgConnection db = SteadyPgConnection(10000, ["set datestyle to german"], host=..., dbname=..., user=..., ...) ... @@ -72,7 +72,7 @@ from pg import DB as PgConnection -__version__ = '1.4' +from . import __version__ try: baseint = (int, long) diff --git a/docs/changelog.html b/docs/changelog.html index 1ecb86c..7db17cd 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -78,6 +78,7 @@ <h2>Changes:</h2> <li><p>The top-level packages and folders have been renamed to lower-case. Particularly, you need to import <span class="docutils literal">dbutils</span> instead of <span class="docutils literal">DBUtils</span> now.</p></li> <li><p>The Webware <span class="docutils literal">Examples</span> folder has been removed.</p></li> +<li><p>The internal naming conventions have been changed to comply with PEP8.</p></li> <li><p>This changelog file has been created from the former release notes.</p></li> </ul> </div> @@ -155,12 +156,6 @@ <h2>Improvements:</h2> </div> <div class="section" id="id10"> <h2>Bugfixes:</h2> -<div class="system-message"> -<p class="system-message-title">System Message: WARNING/2 (<span class="docutils literal">changelog.rst</span>, line 100)</p> -<p>Title underline too short.</p> -<pre class="literal-block">Bugfixes: ---------</pre> -</div> <ul class="simple"> <li><p>Fixed propagation of error messages when the connection was lost.</p></li> <li><p>Fixed an issue with the <span class="docutils literal">setoutputsize()</span> cursor method.</p></li> diff --git a/docs/changelog.rst b/docs/changelog.rst index fb97129..bf41adf 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -18,6 +18,7 @@ Changes: * The top-level packages and folders have been renamed to lower-case. Particularly, you need to import ``dbutils`` instead of ``DBUtils`` now. * The Webware ``Examples`` folder has been removed. +* The internal naming conventions have been changed to comply with PEP8. * This changelog file has been created from the former release notes. 1.4 @@ -97,7 +98,7 @@ Improvements: connections are reset before being put back into the pool. Bugfixes: --------- +--------- * Fixed propagation of error messages when the connection was lost. * Fixed an issue with the ``setoutputsize()`` cursor method. diff --git a/docs/main.de.html b/docs/main.de.html index 10a3eef..b26431d 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -12,9 +12,6 @@ <h1 class="title">Benutzeranleitung für DBUtils</h1> <dl class="docinfo simple"> <dt class="version">Version</dt> <dd class="version">2.0</dd> -<dt class="released">Released</dt> -<dd class="released"><p>09/26/20</p> -</dd> <dt class="translations">Translations</dt> <dd class="translations"><p><a class="reference external" href="main.html">English</a> | German</p> </dd> diff --git a/docs/main.de.rst b/docs/main.de.rst index 00013ac..f0fda86 100644 --- a/docs/main.de.rst +++ b/docs/main.de.rst @@ -2,7 +2,6 @@ +++++++++++++++++++++++++++++ :Version: 2.0 -:Released: 09/26/20 :Translations: English_ | German .. _English: main.html diff --git a/docs/main.html b/docs/main.html index 9ec6f26..a6625bf 100644 --- a/docs/main.html +++ b/docs/main.html @@ -12,9 +12,6 @@ <h1 class="title">DBUtils User's Guide</h1> <dl class="docinfo simple"> <dt class="version">Version</dt> <dd class="version">2.0</dd> -<dt class="released">Released</dt> -<dd class="released"><p>09/26/20</p> -</dd> <dt class="translations">Translations</dt> <dd class="translations"><p>English | <a class="reference external" href="main.de.html">German</a></p> </dd> diff --git a/docs/main.rst b/docs/main.rst index edbfd31..43b7905 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -2,7 +2,6 @@ ++++++++++++++++++++ :Version: 2.0 -:Released: 09/26/20 :Translations: English | German_ .. _German: main.de.html diff --git a/docs/make.py b/docs/make.py index fb82e88..f2153df 100755 --- a/docs/make.py +++ b/docs/make.py @@ -5,7 +5,7 @@ from __future__ import print_function from glob import glob -from os.path import splitext, join +from os.path import splitext from docutils.core import publish_file print("Creating the documentation...") @@ -25,13 +25,11 @@ with open(rst_file, encoding='utf-8-sig') as source: with open(html_file, 'w', encoding='utf-8') as destination: publish_file(writer_name='html5', - source=source, destination=destination, - settings_overrides = dict( - stylesheet_path='Doc.css', - embed_stylesheet=False, - toc_backlinks=False, - language_code=lang - ) - ) + source=source, destination=destination, + settings_overrides=dict( + stylesheet_path='Doc.css', + embed_stylesheet=False, + toc_backlinks=False, + language_code=lang)) print("Done.") diff --git a/setup.py b/setup.py index 478fe8e..5f54dfe 100755 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ warnings.filterwarnings('ignore', 'Unknown distribution option') -__version__ = '1.4' +__version__ = '2.0' readme = open('README.md').read() diff --git a/setversion.py b/setversion.py deleted file mode 100755 index 2164a07..0000000 --- a/setversion.py +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env python - -"""Set version. - -This script sets the DBUtils version number information -consistently in all files of the distribution. - -""" - -from __future__ import print_function - -import os -import sys -import re -from glob import glob - -# Version format is (Major, Minor, Sub, Alpha/Beta/etc) -# The Sub is optional, and if 0 is not returned. -# Examples: (0, 8, 1, 'b1'), (0, 8, 2) or (0, 9, 0, 'rc1') -# releaseDate format should be 'MM/DD/YY'. - -# Update this to change the current version and release date: -# version = ('X', 'Y', 0) -version = (1, 4, 0) -# releaseDate = '@@/@@/@@' -releaseDate = '09/26/20' - -# Verbose output (output unchanged files also): -verbose = False - -path = os.path.dirname(os.path.abspath(sys.argv[0])) -sys.path.append(path) -os.chdir(path) -print("Setversion", path) - - -def versionString(version): - """Create version string. - - For a sequence containing version information such as (2, 0, 0, 'pre'), - this returns a printable string such as '2.0pre'. - The micro version number is only excluded from the string if it is zero. - - """ - ver = list(map(str, version)) - numbers, rest = ver[:2 if ver[2] == '0' else 3], ver[3:] - return '.'.join(numbers) + '-'.join(rest) - - -versionString = versionString(version) - -if versionString == 'X.Y': - print("Please set the version.") - sys.exit(1) -if releaseDate == '@@/@@/@@': - print("Please set the release date.") - sys.exit(1) - - -class Replacer: - """Class to handle substitutions in a file.""" - - def __init__(self, *args): - self._subs = list(args) - - def add(self, search, replace): - self._subs.append((re.compile(search, re.M), replace)) - - def replaceInStr(self, data): - for search, replace in self._subs: - data = re.sub(search, replace, data) - return data - - def replaceInFile(self, filename): - data = open(filename).read() - newData = self.replaceInStr(data) - if data == newData: - if verbose: - print("Unchanged", filename) - else: - print("Updating", filename) - open(filename, 'w').write(newData) - - def replaceGlob(self, pattern): - for file in glob(pattern): - if os.path.exists(file): - self.replaceInFile(file) - - -pyReplace = Replacer() -pyReplace.add(r"(__version__\s*=\s*)'.*'", r"\g<1>%s" % repr(versionString)) - -propReplace = Replacer() -propReplace.add(r"(version\s*=\s*).*", r"\g<1>%s" % repr(version)) -propReplace.add(r"(releaseDate\s*=\s*).*", r"\g<1>%s" % repr(releaseDate)) - -htmlReplace = Replacer() -htmlReplace.add( - r"<!--\s*version\s*-->[^<]*<!--\s*/version\s*-->", - r"<!-- version --> %s <!-- /version -->" % versionString) -htmlReplace.add( - r"<!--\s*relDate\s*-->[^<]*<!--\s*/relDate\s*-->", - r"<!-- relDate --> %s <!-- /relDate -->" % releaseDate) - -rstReplace = Replacer() -rstReplace.add( - r"^:(.+)?: (X|\d+)\.(Y|\d+)(\.\d+)?$", r":\1: %s" % versionString) -rstReplace.add( - r"^:(.+)?: (@|\d){2}/(@|\d){2}/(@|\d){2}$", r":\1: %s" % releaseDate) - -# Replace in Python files: -pyReplace.replaceGlob('*.py') -pyReplace.replaceGlob('DBUtils/*.py') -pyReplace.replaceGlob('DBUtils/*/*.py') - -# Replace in Properties files: -propReplace.replaceGlob('DBUtils/Properties.py') - -# Replace in existing HTML: -htmlReplace.replaceGlob('DBUtils/Docs/*.html') - -# Replace in reStructuredText files: -rstReplace.replaceGlob('DBUtils/Docs/*.rst') diff --git a/tests/__init__.py b/tests/__init__.py index 3195867..ec99602 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -# DBUtils Tests +# DBUtils tests diff --git a/tests/mock_pg.py b/tests/mock_pg.py index 48fe998..7b4fcdc 100644 --- a/tests/mock_pg.py +++ b/tests/mock_pg.py @@ -22,10 +22,10 @@ class ProgrammingError(DatabaseError): def connect(*args, **kwargs): - return pgConnection(*args, **kwargs) + return PgConnection(*args, **kwargs) -class pgConnection: +class PgConnection: """The underlying pg API connection class.""" def __init__(self, dbname=None, user=None): diff --git a/tests/test_persistend_db.py b/tests/test_persistent_db.py similarity index 74% rename from tests/test_persistend_db.py rename to tests/test_persistent_db.py index 6f7ec53..bf1f8c6 100644 --- a/tests/test_persistend_db.py +++ b/tests/test_persistent_db.py @@ -13,11 +13,9 @@ import unittest -import DBUtils.Tests.mock_db as dbapi +from . import mock_db as dbapi -from DBUtils.PersistentDB import PersistentDB, local - -__version__ = '1.4' +from dbutils.persistent_db import PersistentDB, local class TestPersistentDB(unittest.TestCase): @@ -25,19 +23,17 @@ class TestPersistentDB(unittest.TestCase): def setUp(self): dbapi.threadsafety = 1 - def test0_CheckVersion(self): - from DBUtils import __version__ as DBUtilsVersion - self.assertEqual(DBUtilsVersion, __version__) - from DBUtils.PersistentDB import __version__ as PersistentDBVersion - self.assertEqual(PersistentDBVersion, __version__) + def test_version(self): + from dbutils import __version__, persistent_db + self.assertEqual(persistent_db.__version__, __version__) self.assertEqual(PersistentDB.version, __version__) - def test1_NoThreadsafety(self): - from DBUtils.PersistentDB import NotSupportedError + def test_no_threadsafety(self): + from dbutils.persistent_db import NotSupportedError for dbapi.threadsafety in (None, 0): self.assertRaises(NotSupportedError, PersistentDB, dbapi) - def test2_Close(self): + def test_close(self): for closeable in (False, True): persist = PersistentDB(dbapi, closeable=closeable) db = persist.connection() @@ -51,7 +47,7 @@ def test2_Close(self): db._close() self.assertFalse(db._con.valid) - def test3_Connection(self): + def test_connection(self): persist = PersistentDB(dbapi) db = persist.connection() db_con = db._con @@ -65,26 +61,26 @@ def test3_Connection(self): db2.close() db.close() - def test4_Threads(self): - numThreads = 3 + def test_threads(self): + num_threads = 3 persist = PersistentDB(dbapi, closeable=True) try: from queue import Queue, Empty except ImportError: # Python 2 from Queue import Queue, Empty - queryQueue, resultQueue = [], [] - for i in range(numThreads): - queryQueue.append(Queue(1)) - resultQueue.append(Queue(1)) + query_queue, result_queue = [], [] + for i in range(num_threads): + query_queue.append(Queue(1)) + result_queue.append(Queue(1)) - def runQueries(i): + def run_queries(i): this_db = persist.connection() while 1: try: try: - q = queryQueue[i].get(1, 1) + q = query_queue[i].get(1, 1) except TypeError: - q = queryQueue[i].get(1) + q = query_queue[i].get(1) except Empty: q = None if not q: @@ -105,82 +101,82 @@ def runQueries(i): cursor.close() r = '%d(%d): %s' % (i, db._usage, r) try: - resultQueue[i].put(r, 1, 1) + result_queue[i].put(r, 1, 1) except TypeError: - resultQueue[i].put(r, 1) + result_queue[i].put(r, 1) db.close() from threading import Thread threads = [] - for i in range(numThreads): - thread = Thread(target=runQueries, args=(i,)) + for i in range(num_threads): + thread = Thread(target=run_queries, args=(i,)) threads.append(thread) thread.start() - for i in range(numThreads): + for i in range(num_threads): try: - queryQueue[i].put('ping', 1, 1) + query_queue[i].put('ping', 1, 1) except TypeError: - queryQueue[i].put('ping', 1) - for i in range(numThreads): + query_queue[i].put('ping', 1) + for i in range(num_threads): try: - r = resultQueue[i].get(1, 1) + r = result_queue[i].get(1, 1) except TypeError: - r = resultQueue[i].get(1) + r = result_queue[i].get(1) self.assertEqual(r, '%d(0): ok - thread alive' % i) self.assertTrue(threads[i].is_alive()) - for i in range(numThreads): + for i in range(num_threads): for j in range(i + 1): try: - queryQueue[i].put('select test%d' % j, 1, 1) - r = resultQueue[i].get(1, 1) + query_queue[i].put('select test%d' % j, 1, 1) + r = result_queue[i].get(1, 1) except TypeError: - queryQueue[i].put('select test%d' % j, 1) - r = resultQueue[i].get(1) + query_queue[i].put('select test%d' % j, 1) + r = result_queue[i].get(1) self.assertEqual(r, '%d(%d): test%d' % (i, j + 1, j)) try: - queryQueue[1].put('select test4', 1, 1) + query_queue[1].put('select test4', 1, 1) except TypeError: - queryQueue[1].put('select test4', 1) + query_queue[1].put('select test4', 1) try: - r = resultQueue[1].get(1, 1) + r = result_queue[1].get(1, 1) except TypeError: - r = resultQueue[1].get(1) + r = result_queue[1].get(1) self.assertEqual(r, '1(3): test4') try: - queryQueue[1].put('close', 1, 1) - r = resultQueue[1].get(1, 1) + query_queue[1].put('close', 1, 1) + r = result_queue[1].get(1, 1) except TypeError: - queryQueue[1].put('close', 1) - r = resultQueue[1].get(1) + query_queue[1].put('close', 1) + r = result_queue[1].get(1) self.assertEqual(r, '1(3): ok - connection closed') for j in range(2): try: - queryQueue[1].put('select test%d' % j, 1, 1) - r = resultQueue[1].get(1, 1) + query_queue[1].put('select test%d' % j, 1, 1) + r = result_queue[1].get(1, 1) except TypeError: - queryQueue[1].put('select test%d' % j, 1) - r = resultQueue[1].get(1) + query_queue[1].put('select test%d' % j, 1) + r = result_queue[1].get(1) self.assertEqual(r, '1(%d): test%d' % (j + 1, j)) - for i in range(numThreads): + for i in range(num_threads): self.assertTrue(threads[i].is_alive()) try: - queryQueue[i].put('ping', 1, 1) + query_queue[i].put('ping', 1, 1) except TypeError: - queryQueue[i].put('ping', 1) - for i in range(numThreads): + query_queue[i].put('ping', 1) + for i in range(num_threads): try: - r = resultQueue[i].get(1, 1) + r = result_queue[i].get(1, 1) except TypeError: - r = resultQueue[i].get(1) + r = result_queue[i].get(1) self.assertEqual(r, '%d(%d): ok - thread alive' % (i, i + 1)) self.assertTrue(threads[i].is_alive()) - for i in range(numThreads): + for i in range(num_threads): try: - queryQueue[i].put(None, 1, 1) + query_queue[i].put(None, 1, 1) except TypeError: - queryQueue[i].put(None, 1) + query_queue[i].put(None, 1) - def test5_MaxUsage(self): + def test_maxusage(self): persist = PersistentDB(dbapi, 20) db = persist.connection() self.assertEqual(db._maxusage, 20) @@ -196,7 +192,7 @@ def test5_MaxUsage(self): self.assertEqual(db._con.num_uses, j) self.assertEqual(db._con.num_queries, j) - def test6_SetSession(self): + def test_setsession(self): persist = PersistentDB(dbapi, 3, ('set datestyle',)) db = persist.connection() self.assertEqual(db._maxusage, 3) @@ -214,7 +210,7 @@ def test6_SetSession(self): cursor.close() self.assertEqual(db._con.session, ['datestyle']) - def test7_ThreadLocal(self): + def test_threadlocal(self): persist = PersistentDB(dbapi) self.assertTrue(isinstance(persist.thread, local)) @@ -224,7 +220,7 @@ class threadlocal: persist = PersistentDB(dbapi, threadlocal=threadlocal) self.assertTrue(isinstance(persist.thread, threadlocal)) - def test8_PingCheck(self): + def test_ping_check(self): Connection = dbapi.Connection Connection.has_ping = True Connection.num_pings = 0 @@ -276,7 +272,7 @@ def test8_PingCheck(self): Connection.has_ping = False Connection.num_pings = 0 - def test9_FailedTransaction(self): + def test_failed_transaction(self): persist = PersistentDB(dbapi) db = persist.connection() cursor = db.cursor() diff --git a/tests/test_persistend_pg.py b/tests/test_persistent_pg.py similarity index 64% rename from tests/test_persistend_pg.py rename to tests/test_persistent_pg.py index f5ba411..bed067b 100644 --- a/tests/test_persistend_pg.py +++ b/tests/test_persistent_pg.py @@ -13,23 +13,19 @@ import unittest -import DBUtils.Tests.mock_pg as pg +from . import mock_pg as pg -from DBUtils.PersistentPg import PersistentPg - -__version__ = '1.4' +from dbutils.persistent_pg import PersistentPg class TestPersistentPg(unittest.TestCase): - def test0_CheckVersion(self): - from DBUtils import __version__ as DBUtilsVersion - self.assertEqual(DBUtilsVersion, __version__) - from DBUtils.PersistentPg import __version__ as PersistentPgVersion - self.assertEqual(PersistentPgVersion, __version__) + def test_version(self): + from dbutils import __version__, persistent_pg + self.assertEqual(persistent_pg.__version__, __version__) self.assertEqual(PersistentPg.version, __version__) - def test1_Close(self): + def test_close(self): for closeable in (False, True): persist = PersistentPg(closeable=closeable) db = persist.connection() @@ -45,26 +41,26 @@ def test1_Close(self): db._close() self.assertFalse(db._con.db and db._con.valid) - def test2_Threads(self): - numThreads = 3 + def test_threads(self): + num_threads = 3 persist = PersistentPg() try: from queue import Queue, Empty except ImportError: # Python 2 from Queue import Queue, Empty - queryQueue, resultQueue = [], [] - for i in range(numThreads): - queryQueue.append(Queue(1)) - resultQueue.append(Queue(1)) + query_queue, result_queue = [], [] + for i in range(num_threads): + query_queue.append(Queue(1)) + result_queue.append(Queue(1)) - def runQueries(i): + def run_queries(i): this_db = persist.connection().db while 1: try: try: - q = queryQueue[i].get(1, 1) + q = query_queue[i].get(1, 1) except TypeError: - q = queryQueue[i].get(1) + q = query_queue[i].get(1) except Empty: q = None if not q: @@ -82,80 +78,80 @@ def runQueries(i): r = db.query(q) r = '%d(%d): %s' % (i, db._usage, r) try: - resultQueue[i].put(r, 1, 1) + result_queue[i].put(r, 1, 1) except TypeError: - resultQueue[i].put(r, 1) + result_queue[i].put(r, 1) db.close() from threading import Thread threads = [] - for i in range(numThreads): - thread = Thread(target=runQueries, args=(i,)) + for i in range(num_threads): + thread = Thread(target=run_queries, args=(i,)) threads.append(thread) thread.start() - for i in range(numThreads): + for i in range(num_threads): try: - queryQueue[i].put('ping', 1, 1) + query_queue[i].put('ping', 1, 1) except TypeError: - queryQueue[i].put('ping', 1) - for i in range(numThreads): + query_queue[i].put('ping', 1) + for i in range(num_threads): try: - r = resultQueue[i].get(1, 1) + r = result_queue[i].get(1, 1) except TypeError: - r = resultQueue[i].get(1) + r = result_queue[i].get(1) self.assertEqual(r, '%d(0): ok - thread alive' % i) self.assertTrue(threads[i].is_alive()) - for i in range(numThreads): + for i in range(num_threads): for j in range(i + 1): try: - queryQueue[i].put('select test%d' % j, 1, 1) - r = resultQueue[i].get(1, 1) + query_queue[i].put('select test%d' % j, 1, 1) + r = result_queue[i].get(1, 1) except TypeError: - queryQueue[i].put('select test%d' % j, 1) - r = resultQueue[i].get(1) + query_queue[i].put('select test%d' % j, 1) + r = result_queue[i].get(1) self.assertEqual(r, '%d(%d): test%d' % (i, j + 1, j)) try: - queryQueue[1].put('select test4', 1, 1) - r = resultQueue[1].get(1, 1) + query_queue[1].put('select test4', 1, 1) + r = result_queue[1].get(1, 1) except TypeError: - queryQueue[1].put('select test4', 1) - r = resultQueue[1].get(1) + query_queue[1].put('select test4', 1) + r = result_queue[1].get(1) self.assertEqual(r, '1(3): test4') try: - queryQueue[1].put('close', 1, 1) - r = resultQueue[1].get(1, 1) + query_queue[1].put('close', 1, 1) + r = result_queue[1].get(1, 1) except TypeError: - queryQueue[1].put('close', 1) - r = resultQueue[1].get(1) + query_queue[1].put('close', 1) + r = result_queue[1].get(1) self.assertEqual(r, '1(3): ok - connection closed') for j in range(2): try: - queryQueue[1].put('select test%d' % j, 1, 1) - r = resultQueue[1].get(1, 1) + query_queue[1].put('select test%d' % j, 1, 1) + r = result_queue[1].get(1, 1) except TypeError: - queryQueue[1].put('select test%d' % j, 1) - r = resultQueue[1].get(1) + query_queue[1].put('select test%d' % j, 1) + r = result_queue[1].get(1) self.assertEqual(r, '1(%d): test%d' % (j + 1, j)) - for i in range(numThreads): + for i in range(num_threads): self.assertTrue(threads[i].is_alive()) try: - queryQueue[i].put('ping', 1, 1) + query_queue[i].put('ping', 1, 1) except TypeError: - queryQueue[i].put('ping', 1) - for i in range(numThreads): + query_queue[i].put('ping', 1) + for i in range(num_threads): try: - r = resultQueue[i].get(1, 1) + r = result_queue[i].get(1, 1) except TypeError: - r = resultQueue[i].get(1) + r = result_queue[i].get(1) self.assertEqual(r, '%d(%d): ok - thread alive' % (i, i + 1)) self.assertTrue(threads[i].is_alive()) - for i in range(numThreads): + for i in range(num_threads): try: - queryQueue[i].put(None, 1, 1) + query_queue[i].put(None, 1, 1) except TypeError: - queryQueue[i].put(None, 1) + query_queue[i].put(None, 1) - def test3_MaxUsage(self): + def test_maxusage(self): persist = PersistentPg(20) db = persist.connection() self.assertEqual(db._maxusage, 20) @@ -167,7 +163,7 @@ def test3_MaxUsage(self): self.assertEqual(db._usage, j) self.assertEqual(db.num_queries, j) - def test4_SetSession(self): + def test_setsession(self): persist = PersistentPg(3, ('set datestyle',)) db = persist.connection() self.assertEqual(db._maxusage, 3) @@ -179,7 +175,7 @@ def test4_SetSession(self): db.query('select test') self.assertEqual(db.db.session, ['datestyle']) - def test5_FailedTransaction(self): + def test_failed_transaction(self): persist = PersistentPg() db = persist.connection() db._con.close() diff --git a/tests/test_pooled_db.py b/tests/test_pooled_db.py index 04ee5b8..860b7f5 100644 --- a/tests/test_pooled_db.py +++ b/tests/test_pooled_db.py @@ -13,30 +13,26 @@ import unittest -import DBUtils.Tests.mock_db as dbapi +from . import mock_db as dbapi -from DBUtils.PooledDB import ( +from dbutils.pooled_db import ( PooledDB, SharedDBConnection, InvalidConnection, TooManyConnections) -__version__ = '1.4' - class TestPooledDB(unittest.TestCase): - def test00_CheckVersion(self): - from DBUtils import __version__ as DBUtilsVersion - self.assertEqual(DBUtilsVersion, __version__) - from DBUtils.PooledDB import __version__ as PooledDBVersion - self.assertEqual(PooledDBVersion, __version__) + def test_version(self): + from dbutils import __version__, pooled_db + self.assertEqual(pooled_db.__version__, __version__) self.assertEqual(PooledDB.version, __version__) - def test01_NoThreadsafety(self): - from DBUtils.PooledDB import NotSupportedError + def test_no_threadsafety(self): + from dbutils.pooled_db import NotSupportedError for threadsafety in (None, 0): dbapi.threadsafety = threadsafety self.assertRaises(NotSupportedError, PooledDB, dbapi) - def test02_Threadsafety(self): + def test_threadsafety(self): for threadsafety in (1, 2, 3): dbapi.threadsafety = threadsafety pool = PooledDB(dbapi, 0, 0, 1) @@ -48,7 +44,7 @@ def test02_Threadsafety(self): self.assertEqual(pool._maxshared, 0) self.assertFalse(hasattr(pool, '_shared_cache')) - def test03_CreateConnection(self): + def test_create_connection(self): for threadsafety in (1, 2): dbapi.threadsafety = threadsafety shareable = threadsafety > 1 @@ -67,7 +63,7 @@ def test03_CreateConnection(self): self.assertTrue(hasattr(pool, '_setsession')) self.assertIsNone(pool._setsession) con = pool._idle_cache[0] - from DBUtils.SteadyDB import SteadyDBConnection + from dbutils.steady_db import SteadyDBConnection self.assertTrue(isinstance(con, SteadyDBConnection)) self.assertTrue(hasattr(con, '_maxusage')) self.assertEqual(con._maxusage, 0) @@ -167,7 +163,7 @@ def test03_CreateConnection(self): self.assertEqual(con._maxusage, 3) self.assertEqual(con._setsession_sql, ('set datestyle',)) - def test04_CloseConnection(self): + def test_close_connection(self): for threadsafety in (1, 2): dbapi.threadsafety = threadsafety shareable = threadsafety > 1 @@ -189,7 +185,7 @@ def test04_CloseConnection(self): self.assertEqual(shared_con.shared, 1) self.assertTrue(hasattr(shared_con, 'con')) self.assertEqual(shared_con.con, con) - from DBUtils.SteadyDB import SteadyDBConnection + from dbutils.steady_db import SteadyDBConnection self.assertTrue(isinstance(con, SteadyDBConnection)) self.assertTrue(hasattr(con, '_con')) db_con = con._con @@ -247,7 +243,7 @@ def test04_CloseConnection(self): if shareable: self.assertEqual(len(pool._shared_cache), 0) - def test05_CloseAll(self): + def test_close_all(self): for threadsafety in (1, 2): dbapi.threadsafety = threadsafety shareable = threadsafety > 1 @@ -304,7 +300,7 @@ def close_shared(what=closed): del cache self.assertEqual(closed, ['idle', 'shared']) - def test06_ShareableConnection(self): + def test_shareable_connection(self): for threadsafety in (1, 2): dbapi.threadsafety = threadsafety shareable = threadsafety > 1 @@ -394,7 +390,7 @@ def test06_ShareableConnection(self): else: self.assertEqual(len(pool._idle_cache), 3) - def test08_MinMaxCached(self): + def test_min_max_cached(self): for threadsafety in (1, 2): dbapi.threadsafety = threadsafety shareable = threadsafety > 1 @@ -475,7 +471,7 @@ def test08_MinMaxCached(self): if shareable: self.assertEqual(len(pool._shared_cache), 0) - def test08_MaxShared(self): + def test_max_shared(self): for threadsafety in (1, 2): dbapi.threadsafety = threadsafety shareable = threadsafety > 1 @@ -514,7 +510,7 @@ def test08_MaxShared(self): if shareable: self.assertEqual(len(pool._shared_cache), 7) - def test09_SortShared(self): + def test_sort_shared(self): dbapi.threadsafety = 2 pool = PooledDB(dbapi, 0, 4, 4) cache = [] @@ -533,7 +529,7 @@ def test09_SortShared(self): db = pool.connection() self.assertIs(db._con, cache[3]._con) - def test10_EquallyShared(self): + def test_equally_shared(self): for threadsafety in (1, 2): dbapi.threadsafety = threadsafety shareable = threadsafety > 1 @@ -568,7 +564,7 @@ def test10_EquallyShared(self): if shareable: self.assertEqual(len(pool._shared_cache), 0) - def test11_ManyShared(self): + def test_many_shared(self): for threadsafety in (1, 2): dbapi.threadsafety = threadsafety shareable = threadsafety > 1 @@ -619,7 +615,7 @@ def test11_ManyShared(self): else: self.assertEqual(len(pool._idle_cache), 35) - def test12_Rollback(self): + def test_rollback(self): for threadsafety in (1, 2): dbapi.threadsafety = threadsafety pool = PooledDB(dbapi, 0, 1) @@ -651,7 +647,7 @@ def test12_Rollback(self): 'doit1', 'commit', 'dont1', 'rollback', 'doit2', 'commit', 'rollback']) - def test13_MaxConnections(self): + def test_maxconnections(self): for threadsafety in (1, 2): dbapi.threadsafety = threadsafety shareable = threadsafety > 1 @@ -856,7 +852,7 @@ def connection(): session, ['rollback', 'rollback', 'thread', 'rollback']) del db - def test14_MaxUsage(self): + def test_maxusage(self): for threadsafety in (1, 2): dbapi.threadsafety = threadsafety for maxusage in (0, 3, 7): @@ -891,7 +887,7 @@ def test14_MaxUsage(self): self.assertEqual(db._con._con.num_uses, j + 1) self.assertEqual(db._con._con.num_queries, j) - def test15_SetSession(self): + def test_setsession(self): for threadsafety in (1, 2): dbapi.threadsafety = threadsafety setsession = ('set time zone', 'set datestyle') @@ -920,7 +916,7 @@ def test15_SetSession(self): self.assertEqual( db._con._con.session, ['time zone', 'datestyle', 'test2']) - def test16_OneThreadTwoConnections(self): + def test_one_thread_two_connections(self): for threadsafety in (1, 2): dbapi.threadsafety = threadsafety shareable = threadsafety > 1 @@ -977,7 +973,7 @@ def test16_OneThreadTwoConnections(self): self.assertNotEqual(db1, db2) self.assertNotEqual(db1._con, db2._con) - def test17_ThreeThreadsTwoConnections(self): + def test_tnree_threads_two_connections(self): for threadsafety in (1, 2): dbapi.threadsafety = threadsafety pool = PooledDB(dbapi, 2, 2, 0, 2, True) @@ -1039,7 +1035,7 @@ def connection(): self.assertNotEqual(db1._con, db2._con) self.assertEqual(db1._con, db1_con) - def test18_PingCheck(self): + def test_ping_check(self): Connection = dbapi.Connection Connection.has_ping = True Connection.num_pings = 0 @@ -1107,7 +1103,7 @@ def test18_PingCheck(self): Connection.has_ping = False Connection.num_pings = 0 - def test19_FailedTransaction(self): + def test_failed_transaction(self): dbapi.threadsafety = 2 pool = PooledDB(dbapi, 0, 1, 1) db = pool.connection() @@ -1136,7 +1132,7 @@ def test19_FailedTransaction(self): db._con._con.close() cursor.execute('select test') - def test20_SharedInTransaction(self): + def test_shared_in_transaction(self): dbapi.threadsafety = 2 pool = PooledDB(dbapi, 0, 1, 1) db = pool.connection() @@ -1164,7 +1160,7 @@ def test20_SharedInTransaction(self): db = pool.connection() self.assertIs(db._con, db1._con) - def test21_ResetTransaction(self): + def test_reset_transaction(self): pool = PooledDB(dbapi, 1, 1, 0) db = pool.connection() db.begin() @@ -1189,13 +1185,13 @@ def test21_ResetTransaction(self): class TestSharedDBConnection(unittest.TestCase): - def test01_CreateConnection(self): + def test_create_connection(self): db_con = dbapi.connect() con = SharedDBConnection(db_con) self.assertEqual(con.con, db_con) self.assertEqual(con.shared, 1) - def test01_ShareAndUnshare(self): + def test_share_and_unshare(self): con = SharedDBConnection(dbapi.connect()) self.assertEqual(con.shared, 1) con.share() @@ -1207,7 +1203,7 @@ def test01_ShareAndUnshare(self): con.unshare() self.assertEqual(con.shared, 1) - def test02_Comparison(self): + def test_comparison(self): con1 = SharedDBConnection(dbapi.connect()) con1.con._transaction = False con2 = SharedDBConnection(dbapi.connect()) diff --git a/tests/test_pooled_pg.py b/tests/test_pooled_pg.py index cd4103b..0379693 100644 --- a/tests/test_pooled_pg.py +++ b/tests/test_pooled_pg.py @@ -13,23 +13,19 @@ import unittest -import DBUtils.Tests.mock_pg # noqa +from . import mock_pg # noqa -from DBUtils.PooledPg import PooledPg, InvalidConnection - -__version__ = '1.4' +from dbutils.pooled_pg import PooledPg, InvalidConnection class TestPooledPg(unittest.TestCase): - def test0_CheckVersion(self): - from DBUtils import __version__ as DBUtilsVersion - self.assertEqual(DBUtilsVersion, __version__) - from DBUtils.PooledPg import __version__ as PooledPgVersion - self.assertEqual(PooledPgVersion, __version__) + def test_version(self): + from dbutils import __version__, pooled_pg + self.assertEqual(pooled_pg.__version__, __version__) self.assertEqual(PooledPg.version, __version__) - def test1_CreateConnection(self): + def test_create_connection(self): pool = PooledPg( 1, 1, 0, False, None, None, False, 'PooledPgTestDB', user='PooledPgTestUser') @@ -43,7 +39,7 @@ def test1_CreateConnection(self): self.assertFalse(pool._reset) db_con = pool._cache.get(0) pool._cache.put(db_con, 0) - from DBUtils.SteadyPg import SteadyPgConnection + from dbutils.steady_pg import SteadyPgConnection self.assertTrue(isinstance(db_con, SteadyPgConnection)) db = pool.connection() self.assertEqual(pool._cache.qsize(), 0) @@ -77,14 +73,14 @@ def test1_CreateConnection(self): self.assertEqual(db._maxusage, 3) self.assertEqual(db._setsession_sql, ('set datestyle',)) - def test2_CloseConnection(self): + def test_close_connection(self): pool = PooledPg( 0, 1, 0, False, None, None, False, 'PooledPgTestDB', user='PooledPgTestUser') db = pool.connection() self.assertTrue(hasattr(db, '_con')) db_con = db._con - from DBUtils.SteadyPg import SteadyPgConnection + from dbutils.steady_pg import SteadyPgConnection self.assertTrue(isinstance(db_con, SteadyPgConnection)) self.assertTrue(hasattr(pool, '_cache')) self.assertEqual(pool._cache.qsize(), 0) @@ -105,7 +101,7 @@ def test2_CloseConnection(self): self.assertEqual(pool._cache.qsize(), 1) self.assertEqual(pool._cache.get(0), db_con) - def test3_MinMaxCached(self): + def test_min_max_cached(self): pool = PooledPg(3) self.assertTrue(hasattr(pool, '_cache')) self.assertEqual(pool._cache.qsize(), 3) @@ -151,8 +147,8 @@ def test3_MinMaxCached(self): cache.pop().close() self.assertEqual(pool._cache.qsize(), 5) - def test4_MaxConnections(self): - from DBUtils.PooledPg import TooManyConnections + def test_max_connections(self): + from dbutils.pooled_pg import TooManyConnections pool = PooledPg(1, 2, 3) self.assertEqual(pool._cache.qsize(), 1) cache = [pool.connection() for i in range(3)] @@ -205,7 +201,7 @@ def connection(): self.assertEqual(session, ['thread']) del db - def test5_OneThreadTwoConnections(self): + def test_one_thread_two_connections(self): pool = PooledPg(2) db1 = pool.connection() for i in range(5): @@ -228,7 +224,7 @@ def test5_OneThreadTwoConnections(self): db2.query('select test') self.assertEqual(db2.num_queries, 8) - def test6_ThreeThreadsTwoConnections(self): + def test_three_threads_two_connections(self): pool = PooledPg(2, 2, 2, True) try: from queue import Queue, Empty @@ -268,7 +264,7 @@ def connection(): self.assertNotEqual(db1._con, db2._con) self.assertEqual(db1._con, db1_con) - def test7_ResetTransaction(self): + def test_reset_transaction(self): pool = PooledPg(1) db = pool.connection() db.begin() diff --git a/tests/test_simple_pooled_db.py b/tests/test_simple_pooled_db.py index 28d2764..3c6ede1 100644 --- a/tests/test_simple_pooled_db.py +++ b/tests/test_simple_pooled_db.py @@ -14,49 +14,37 @@ import unittest -import DBUtils.Tests.mock_db as dbapi +from . import mock_db as dbapi -from DBUtils import SimplePooledDB - -__version__ = '1.4' - - -def versionString(version): - """Create version string.""" - ver = [str(v) for v in version] - numbers, rest = ver[:2 if ver[2] == '0' else 3], ver[3:] - return '.'.join(numbers) + '-'.join(rest) +from dbutils import simple_pooled_db class TestSimplePooledDB(unittest.TestCase): - def my_dbpool(self, mythreadsafety, maxConnections): + def my_db_pool(self, mythreadsafety, maxConnections): threadsafety = dbapi.threadsafety dbapi.threadsafety = mythreadsafety try: - return SimplePooledDB.PooledDB( + return simple_pooled_db.PooledDB( dbapi, maxConnections, 'SimplePooledDBTestDB', 'SimplePooledDBTestUser') finally: dbapi.threadsafety = threadsafety - def test0_check_version(self): - from DBUtils import __version__ as DBUtilsVersion - self.assertEqual(DBUtilsVersion, __version__) - from DBUtils.Properties import version - self.assertEqual(versionString(version), __version__) - self.assertEqual(SimplePooledDB.__version__, __version__) - self.assertEqual(SimplePooledDB.PooledDB.version, __version__) + def test_version(self): + from dbutils import __version__ + self.assertEqual(simple_pooled_db.__version__, __version__) + self.assertEqual(simple_pooled_db.PooledDB.version, __version__) - def test1_no_threadsafety(self): + def test_no_threadsafety(self): for threadsafety in (None, -1, 0, 4): self.assertRaises( - SimplePooledDB.NotSupportedError, - self.my_dbpool, threadsafety, 1) + simple_pooled_db.NotSupportedError, + self.my_db_pool, threadsafety, 1) - def test2_create_connection(self): + def test_create_connection(self): for threadsafety in (1, 2, 3): - dbpool = self.my_dbpool(threadsafety, 1) + dbpool = self.my_db_pool(threadsafety, 1) db = dbpool.connection() self.assertTrue(hasattr(db, 'cursor')) self.assertTrue(hasattr(db, 'open_cursors')) @@ -66,41 +54,44 @@ def test2_create_connection(self): self.assertTrue(hasattr(db, 'user')) self.assertEqual(db.user, 'SimplePooledDBTestUser') cursor = db.cursor() + self.assertIsNotNone(cursor) self.assertEqual(db.open_cursors, 1) del cursor - def test3_close_connection(self): + def test_close_connection(self): for threadsafety in (1, 2, 3): - dbpool = self.my_dbpool(threadsafety, 1) - db = dbpool.connection() + db_pool = self.my_db_pool(threadsafety, 1) + db = db_pool.connection() self.assertEqual(db.open_cursors, 0) cursor1 = db.cursor() + self.assertIsNotNone(cursor1) self.assertEqual(db.open_cursors, 1) db.close() self.assertFalse(hasattr(db, 'open_cursors')) - db = dbpool.connection() + db = db_pool.connection() self.assertTrue(hasattr(db, 'database')) self.assertEqual(db.database, 'SimplePooledDBTestDB') self.assertTrue(hasattr(db, 'user')) self.assertEqual(db.user, 'SimplePooledDBTestUser') self.assertEqual(db.open_cursors, 1) cursor2 = db.cursor() + self.assertIsNotNone(cursor2) self.assertEqual(db.open_cursors, 2) del cursor2 del cursor1 - def test4_two_connections(self): + def test_two_connections(self): for threadsafety in (1, 2, 3): - dbpool = self.my_dbpool(threadsafety, 2) - db1 = dbpool.connection() + db_pool = self.my_db_pool(threadsafety, 2) + db1 = db_pool.connection() cursors1 = [db1.cursor() for i in range(5)] - db2 = dbpool.connection() + db2 = db_pool.connection() self.assertNotEqual(db1, db2) cursors2 = [db2.cursor() for i in range(7)] self.assertEqual(db1.open_cursors, 5) self.assertEqual(db2.open_cursors, 7) db1.close() - db1 = dbpool.connection() + db1 = db_pool.connection() self.assertNotEqual(db1, db2) self.assertTrue(hasattr(db1, 'cursor')) for i in range(3): @@ -111,8 +102,8 @@ def test4_two_connections(self): del cursors2 del cursors1 - def test5_threadsafety_1(self): - dbpool = self.my_dbpool(1, 2) + def test_threadsafety_1(self): + db_pool = self.my_db_pool(1, 2) try: from queue import Queue, Empty except ImportError: # Python 2 @@ -120,7 +111,7 @@ def test5_threadsafety_1(self): queue = Queue(3) def connection(): - queue.put(dbpool.connection()) + queue.put(db_pool.connection()) from threading import Thread threads = [Thread(target=connection).start() for i in range(3)] @@ -145,9 +136,9 @@ def connection(): self.assertNotEqual(db1, db3) self.assertNotEqual(db1._con, db3._con) - def test6_threadsafety_2(self): + def test_threadsafety_2(self): for threadsafety in (2, 3): - dbpool = self.my_dbpool(threadsafety, 2) + dbpool = self.my_db_pool(threadsafety, 2) db1 = dbpool.connection() db2 = dbpool.connection() cursors = [dbpool.connection().cursor() for i in range(100)] diff --git a/tests/test_simple_pooled_pg.py b/tests/test_simple_pooled_pg.py index 7ff7690..251fd72 100644 --- a/tests/test_simple_pooled_pg.py +++ b/tests/test_simple_pooled_pg.py @@ -13,28 +13,25 @@ import unittest -import DBUtils.Tests.mock_pg # noqa +from . import mock_pg # noqa -from DBUtils import SimplePooledPg - -__version__ = '1.4' +from dbutils import simple_pooled_pg class TestSimplePooledPg(unittest.TestCase): - def my_dbpool(self, maxConnections): - return SimplePooledPg.PooledPg( + def my_db_pool(self, maxConnections): + return simple_pooled_pg.PooledPg( maxConnections, 'SimplePooledPgTestDB', 'SimplePooledPgTestUser') - def test0_check_version(self): - from DBUtils import __version__ as DBUtilsVersion - self.assertEqual(DBUtilsVersion, __version__) - self.assertEqual(SimplePooledPg.__version__, __version__) - self.assertEqual(SimplePooledPg.PooledPg.version, __version__) + def test_version(self): + from dbutils import __version__ + self.assertEqual(simple_pooled_pg.__version__, __version__) + self.assertEqual(simple_pooled_pg.PooledPg.version, __version__) - def test1_create_connection(self): - dbpool = self.my_dbpool(1) - db = dbpool.connection() + def test_create_connection(self): + db_pool = self.my_db_pool(1) + db = db_pool.connection() self.assertTrue(hasattr(db, 'query')) self.assertTrue(hasattr(db, 'num_queries')) self.assertEqual(db.num_queries, 0) @@ -45,15 +42,15 @@ def test1_create_connection(self): db.query('select 1') self.assertEqual(db.num_queries, 1) - def test2_close_connection(self): - dbpool = self.my_dbpool(1) - db = dbpool.connection() + def test_close_connection(self): + db_pool = self.my_db_pool(1) + db = db_pool.connection() self.assertEqual(db.num_queries, 0) db.query('select 1') self.assertEqual(db.num_queries, 1) db.close() self.assertFalse(hasattr(db, 'num_queries')) - db = dbpool.connection() + db = db_pool.connection() self.assertTrue(hasattr(db, 'dbname')) self.assertEqual(db.dbname, 'SimplePooledPgTestDB') self.assertTrue(hasattr(db, 'user')) @@ -62,12 +59,12 @@ def test2_close_connection(self): db.query('select 1') self.assertEqual(db.num_queries, 2) - def test3_two_connections(self): - dbpool = self.my_dbpool(2) - db1 = dbpool.connection() + def test_two_connections(self): + db_pool = self.my_db_pool(2) + db1 = db_pool.connection() for i in range(5): db1.query('select 1') - db2 = dbpool.connection() + db2 = db_pool.connection() self.assertNotEqual(db1, db2) self.assertNotEqual(db1._con, db2._con) for i in range(7): @@ -75,7 +72,7 @@ def test3_two_connections(self): self.assertEqual(db1.num_queries, 5) self.assertEqual(db2.num_queries, 7) db1.close() - db1 = dbpool.connection() + db1 = db_pool.connection() self.assertNotEqual(db1, db2) self.assertNotEqual(db1._con, db2._con) self.assertTrue(hasattr(db1, 'query')) @@ -85,8 +82,8 @@ def test3_two_connections(self): db2.query('select 1') self.assertEqual(db2.num_queries, 8) - def test4_threads(self): - dbpool = self.my_dbpool(2) + def test_threads(self): + db_pool = self.my_db_pool(2) try: from queue import Queue, Empty except ImportError: # Python 2 @@ -94,7 +91,7 @@ def test4_threads(self): queue = Queue(3) def connection(): - queue.put(dbpool.connection()) + queue.put(db_pool.connection()) from threading import Thread threads = [Thread(target=connection).start() for i in range(3)] diff --git a/tests/test_steady_db.py b/tests/test_steady_db.py index f9c7d15..79342a6 100644 --- a/tests/test_steady_db.py +++ b/tests/test_steady_db.py @@ -12,24 +12,20 @@ import unittest -import DBUtils.Tests.mock_db as dbapi +from . import mock_db as dbapi -from DBUtils.SteadyDB import ( +from dbutils.steady_db import ( connect as SteadyDBconnect, SteadyDBConnection, SteadyDBCursor) -__version__ = '1.4' - class TestSteadyDB(unittest.TestCase): - def test00_CheckVersion(self): - from DBUtils import __version__ as DBUtilsVersion - self.assertEqual(DBUtilsVersion, __version__) - from DBUtils.SteadyDB import __version__ as SteadyDBVersion - self.assertEqual(SteadyDBVersion, __version__) - self.assertEqual(SteadyDBConnection.version, __version__) + def test_version(self): + from dbutils import __version__, steady_db + self.assertEqual(steady_db.__version__, __version__) + self.assertEqual(steady_db.SteadyDBConnection.version, __version__) - def test01_MockedConnection(self): + def test_mocked_connection(self): db = dbapi.connect( 'SteadyDBTestDB', user='SteadyDBTestUser') db.__class__.has_ping = False @@ -101,7 +97,7 @@ def test01_MockedConnection(self): db.__class__.has_ping = False db.__class__.num_pings = 0 - def test02_BrokenConnection(self): + def test_broken_connection(self): self.assertRaises(TypeError, SteadyDBConnection, None) self.assertRaises(TypeError, SteadyDBCursor, None) db = SteadyDBconnect(dbapi, database='ok') @@ -119,7 +115,7 @@ def test02_BrokenConnection(self): cursor.close() self.assertRaises(dbapi.OperationalError, db.cursor, 'error') - def test03_Close(self): + def test_close(self): for closeable in (False, True): db = SteadyDBconnect(dbapi, closeable=closeable) self.assertTrue(db._con.valid) @@ -132,7 +128,7 @@ def test03_Close(self): db._close() self.assertFalse(db._con.valid) - def test04_Connection(self): + def test_connection(self): db = SteadyDBconnect( dbapi, 0, None, None, None, True, 'SteadyDBTestDB', user='SteadyDBTestUser') @@ -249,7 +245,7 @@ def test04_Connection(self): self.assertEqual( db._con.session, ['doit', 'commit', 'dont', 'rollback']) - def test05_ConnectionContextHandler(self): + def test_connection_context_handler(self): db = SteadyDBconnect( dbapi, 0, None, None, None, True, 'SteadyDBTestDB', user='SteadyDBTestUser') @@ -267,7 +263,7 @@ def test05_ConnectionContextHandler(self): self.assertTrue(error) self.assertEqual(db._con.session, ['commit', 'rollback']) - def test06_CursorContextHandler(self): + def test_cursor_context_handler(self): db = SteadyDBconnect( dbapi, 0, None, None, None, True, 'SteadyDBTestDB', user='SteadyDBTestUser') @@ -278,7 +274,7 @@ def test06_CursorContextHandler(self): self.assertEqual(cursor.fetchone(), 'test') self.assertEqual(db._con.open_cursors, 0) - def test07_ConnectionCreatorFunction(self): + def test_connection_creator_function(self): db1 = SteadyDBconnect( dbapi, 0, None, None, None, True, 'SteadyDBTestDB', user='SteadyDBTestUser') @@ -293,7 +289,7 @@ def test07_ConnectionCreatorFunction(self): db2.close() db1.close() - def test08_ConnectionMaxUsage(self): + def test_connection_maxusage(self): db = SteadyDBconnect(dbapi, 10) cursor = db.cursor() for i in range(100): @@ -343,7 +339,7 @@ def test08_ConnectionMaxUsage(self): self.assertEqual(db._con.num_uses, 1) self.assertEqual(db._con.num_queries, 1) - def test09_ConnectionSetSession(self): + def test_connection_setsession(self): db = SteadyDBconnect(dbapi, 3, ('set time zone', 'set datestyle')) self.assertTrue(hasattr(db, '_usage')) self.assertEqual(db._usage, 0) @@ -401,7 +397,7 @@ def test09_ConnectionSetSession(self): self.assertEqual(db._con.num_queries, 1) self.assertEqual(db._con.session, ['time zone', 'datestyle']) - def test10_ConnectionFailures(self): + def test_connection_failures(self): db = SteadyDBconnect(dbapi) db.close() db.cursor() @@ -416,7 +412,7 @@ def test10_ConnectionFailures(self): db.close() db.cursor() - def test11_ConnectionFailureError(self): + def test_connection_failure_error(self): db = SteadyDBconnect(dbapi) cursor = db.cursor() db.close() @@ -425,7 +421,7 @@ def test11_ConnectionFailureError(self): db.close() self.assertRaises(dbapi.ProgrammingError, cursor.execute, 'error') - def test12_ConnectionSetSizes(self): + def test_connection_set_sizes(self): db = SteadyDBconnect(dbapi) cursor = db.cursor() cursor.execute('get sizes') @@ -450,7 +446,7 @@ def test12_ConnectionSetSizes(self): result = cursor.fetchone() self.assertEqual(result, ([6, 42, 7], {None: 7, 3: 15, 9: 42})) - def test13_ConnectionPingCheck(self): + def test_connection_ping_check(self): Connection = dbapi.Connection Connection.has_ping = False Connection.num_pings = 0 @@ -539,7 +535,7 @@ def test13_ConnectionPingCheck(self): Connection.has_ping = False Connection.num_pings = 0 - def test14_BeginTransaction(self): + def test_begin_transaction(self): db = SteadyDBconnect(dbapi, database='ok') cursor = db.cursor() cursor.close() @@ -562,7 +558,7 @@ def test14_BeginTransaction(self): cursor.execute('select test12') self.assertEqual(cursor.fetchone(), 'test12') - def test15_WithBeginExtension(self): + def test_with_begin_extension(self): db = SteadyDBconnect(dbapi, database='ok') db._con._begin_called_with = None @@ -576,7 +572,7 @@ def begin(a, b=None, c=7): self.assertEqual(cursor.fetchone(), 'test13') self.assertEqual(db._con._begin_called_with, (42, 6, 7)) - def test16_CancelTransaction(self): + def test_cancel_transaction(self): db = SteadyDBconnect(dbapi, database='ok') cursor = db.cursor() db.begin() @@ -586,7 +582,7 @@ def test16_CancelTransaction(self): cursor.execute('select test14') self.assertEqual(cursor.fetchone(), 'test14') - def test17_WithCancelExtension(self): + def test_with_cancel_extension(self): db = SteadyDBconnect(dbapi, database='ok') db._con._cancel_called = None @@ -601,7 +597,7 @@ def cancel(): db.cancel() self.assertEqual(db._con._cancel_called, 'yes') - def test18_ResetTransaction(self): + def test_reset_transaction(self): db = SteadyDBconnect(dbapi, database='ok') db.begin() self.assertFalse(db._con.session) @@ -613,7 +609,7 @@ def test18_ResetTransaction(self): db.close() self.assertEqual(db._con.session, ['rollback']) - def test19_CommitError(self): + def test_commit_error(self): db = SteadyDBconnect(dbapi, database='ok') db.begin() self.assertFalse(db._con.session) @@ -635,7 +631,7 @@ def test19_CommitError(self): self.assertEqual(db._con.session, ['commit']) self.assertTrue(db._con.valid) - def test20_RollbackError(self): + def test_rollback_error(self): db = SteadyDBconnect(dbapi, database='ok') db.begin() self.assertFalse(db._con.session) diff --git a/tests/test_steady_pg.py b/tests/test_steady_pg.py index e566a7b..df88b77 100644 --- a/tests/test_steady_pg.py +++ b/tests/test_steady_pg.py @@ -15,23 +15,19 @@ import unittest import sys -import DBUtils.Tests.mock_pg as pg +from . import mock_pg as pg -from DBUtils.SteadyPg import SteadyPgConnection - -__version__ = '1.4' +from dbutils.steady_pg import SteadyPgConnection class TestSteadyPg(unittest.TestCase): - def test0_CheckVersion(self): - from DBUtils import __version__ as DBUtilsVersion - self.assertEqual(DBUtilsVersion, __version__) - from DBUtils.SteadyPg import __version__ as SteadyPgVersion - self.assertEqual(SteadyPgVersion, __version__) - self.assertEqual(SteadyPgConnection.version, __version__) + def test_version(self): + from dbutils import __version__, steady_pg + self.assertEqual(steady_pg.__version__, __version__) + self.assertEqual(steady_pg.SteadyPgConnection.version, __version__) - def test1_MockedConnection(self): + def test_mocked_connection(self): PgConnection = pg.DB db = PgConnection( 'SteadyPgTestDB', user='SteadyPgTestUser') @@ -80,7 +76,7 @@ def test1_MockedConnection(self): self.assertRaises(pg.InternalError, db.query, 'select test') self.assertRaises(pg.InternalError, db.get_tables) - def test2_BrokenConnection(self): + def test_broken_connection(self): self.assertRaises(TypeError, SteadyPgConnection, 'wrong') db = SteadyPgConnection(dbname='ok') InternalError = sys.modules[db._con.__module__].InternalError @@ -89,7 +85,7 @@ def test2_BrokenConnection(self): del db self.assertRaises(InternalError, SteadyPgConnection, dbname='error') - def test3_Close(self): + def test_close(self): for closeable in (False, True): db = SteadyPgConnection(closeable=closeable) self.assertTrue(db._con.db and db._con.valid) @@ -104,7 +100,7 @@ def test3_Close(self): db._close() self.assertFalse(db._con.db and db._con.valid) - def test4_Connection(self): + def test_connection(self): db = SteadyPgConnection( 0, None, 1, 'SteadyPgTestDB', user='SteadyPgTestUser') self.assertTrue(hasattr(db, 'db')) @@ -184,7 +180,7 @@ def test4_Connection(self): self.assertEqual(db._usage, 1) self.assertEqual(db.num_queries, 0) - def test5_ConnectionContextHandler(self): + def test_connection_context_handler(self): db = SteadyPgConnection( 0, None, 1, 'SteadyPgTestDB', user='SteadyPgTestUser') self.assertEqual(db.session, []) @@ -202,7 +198,7 @@ def test5_ConnectionContextHandler(self): self.assertEqual( db._con.session, ['begin', 'commit', 'begin', 'rollback']) - def test6_ConnectionMaxUsage(self): + def test_connection_maxusage(self): db = SteadyPgConnection(10) for i in range(100): r = db.query('select test%d' % i) @@ -250,7 +246,7 @@ def test6_ConnectionMaxUsage(self): self.assertEqual(db._usage, 1) self.assertEqual(db.num_queries, 1) - def test7_ConnectionSetSession(self): + def test_connection_setsession(self): db = SteadyPgConnection(3, ('set time zone', 'set datestyle')) self.assertTrue(hasattr(db, 'num_queries')) self.assertEqual(db.num_queries, 0) @@ -271,7 +267,7 @@ def test7_ConnectionSetSession(self): self.assertEqual(db.num_queries, 0) self.assertEqual(db.session, ['time zone', 'datestyle', 'test']) - def test8_Begin(self): + def test_begin(self): for closeable in (False, True): db = SteadyPgConnection(closeable=closeable) db.begin() @@ -291,7 +287,7 @@ def test8_Begin(self): self.assertEqual(db.begin('select sql:begin'), 'sql:begin') self.assertEqual(db.num_queries, 2) - def test9_End(self): + def test_end(self): for closeable in (False, True): db = SteadyPgConnection(closeable=closeable) db.begin() diff --git a/tests/test_threading_local.py b/tests/test_threading_local.py index 424ea2f..b44c613 100644 --- a/tests/test_threading_local.py +++ b/tests/test_threading_local.py @@ -3,41 +3,39 @@ import unittest from threading import Thread -from DBUtils.PersistentDB import local - -__version__ = '1.4' +from dbutils.persistent_db import local class TestThreadingLocal(unittest.TestCase): - def test0_GetAttr(self): - mydata = local() - mydata.number = 42 - self.assertEqual(mydata.number, 42) + def test_getattr(self): + my_data = local() + my_data.number = 42 + self.assertEqual(my_data.number, 42) - def test1_Dict(self): - mydata = local() - mydata.number = 42 - self.assertEqual(mydata.__dict__, {'number': 42}) - mydata.__dict__.setdefault('widgets', []) - self.assertEqual(mydata.widgets, []) + def test_dict(self): + my_data = local() + my_data.number = 42 + self.assertEqual(my_data.__dict__, {'number': 42}) + my_data.__dict__.setdefault('widgets', []) + self.assertEqual(my_data.widgets, []) - def test2_ThreadLocal(self): + def test_threadlocal(self): def f(): - items = sorted(mydata.__dict__.items()) + items = sorted(my_data.__dict__.items()) log.append(items) - mydata.number = 11 - log.append(mydata.number) - mydata = local() - mydata.number = 42 + my_data.number = 11 + log.append(my_data.number) + my_data = local() + my_data.number = 42 log = [] thread = Thread(target=f) thread.start() thread.join() self.assertEqual(log, [[], 11]) - self.assertEqual(mydata.number, 42) + self.assertEqual(my_data.number, 42) - def test3_SubClass(self): + def test_subclass(self): class MyLocal(local): number = 2 @@ -52,36 +50,36 @@ def __init__(self, **kw): def squared(self): return self.number ** 2 - mydata = MyLocal(color='red') - self.assertEqual(mydata.number, 2) - self.assertEqual(mydata.color, 'red') - del mydata.color - self.assertEqual(mydata.squared(), 4) + my_data = MyLocal(color='red') + self.assertEqual(my_data.number, 2) + self.assertEqual(my_data.color, 'red') + del my_data.color + self.assertEqual(my_data.squared(), 4) def f(): - items = sorted(mydata.__dict__.items()) + items = sorted(my_data.__dict__.items()) log.append(items) - mydata.number = 7 - log.append(mydata.number) + my_data.number = 7 + log.append(my_data.number) log = [] thread = Thread(target=f) thread.start() thread.join() self.assertEqual(log, [[('color', 'red'), ('initialized', 1)], 7]) - self.assertEqual(mydata.number, 2) - self.assertFalse(hasattr(mydata, 'color')) + self.assertEqual(my_data.number, 2) + self.assertFalse(hasattr(my_data, 'color')) class MyLocal(local): __slots__ = 'number' - mydata = MyLocal() - mydata.number = 42 - mydata.color = 'red' + my_data = MyLocal() + my_data.number = 42 + my_data.color = 'red' thread = Thread(target=f) thread.start() thread.join() - self.assertEqual(mydata.number, 7) + self.assertEqual(my_data.number, 7) if __name__ == '__main__': diff --git a/tox.ini b/tox.ini index 78dd8dc..0e56291 100644 --- a/tox.ini +++ b/tox.ini @@ -15,4 +15,4 @@ basepython = deps = flake8 commands = - flake8 *.py dbutils + flake8 dbutils tests docs setup.py From f067391bb59ebd76b7e57ba7860ec9c42fce6c7f Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 26 Sep 2020 18:18:01 +0200 Subject: [PATCH 06/84] Convert images from GIF to PNG --- MANIFEST.in | 2 +- docs/dbdep.gif | Bin 2653 -> 0 bytes docs/dbdep.png | Bin 0 -> 3331 bytes docs/main.de.html | 8 ++++---- docs/main.de.rst | 8 ++++---- docs/main.html | 8 ++++---- docs/main.rst | 8 ++++---- docs/persist.gif | Bin 7378 -> 0 bytes docs/persist.png | Bin 0 -> 7063 bytes docs/pgdep.gif | Bin 2426 -> 0 bytes docs/pgdep.png | Bin 0 -> 3129 bytes docs/pool.gif | Bin 13454 -> 0 bytes docs/pool.png | Bin 0 -> 11963 bytes 13 files changed, 17 insertions(+), 17 deletions(-) delete mode 100644 docs/dbdep.gif create mode 100644 docs/dbdep.png delete mode 100644 docs/persist.gif create mode 100644 docs/persist.png delete mode 100644 docs/pgdep.gif create mode 100644 docs/pgdep.png delete mode 100644 docs/pool.gif create mode 100644 docs/pool.png diff --git a/MANIFEST.in b/MANIFEST.in index b4efc19..9896216 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,7 +3,7 @@ include MANIFEST.in include LICENSE include README.md -recursive-include docs *.rst make.py *.html *.css *.png *.gif +recursive-include docs *.rst make.py *.html *.css *.png prune docs/_build exclude release.md setversion.py diff --git a/docs/dbdep.gif b/docs/dbdep.gif deleted file mode 100644 index 8554b62b242c5f9e58916c99d1ea419c18701f18..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2653 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 L02qKg5CH%?zBcu_ diff --git a/docs/dbdep.png b/docs/dbdep.png new file mode 100644 index 0000000000000000000000000000000000000000..666e3a78c5e9507e722a77a28f6d55f780f1333c GIT binary patch literal 3331 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 N002ovPDHLkV1mFbDR}?@ literal 0 HcmV?d00001 diff --git a/docs/main.de.html b/docs/main.de.html index b26431d..3827321 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -117,10 +117,10 @@ <h1>Module</h1> </table> <p>Die Abhängigkeiten der Module in der Variante für beliebige DB-API-2-Adapter sind im folgenden Diagramm dargestellt:</p> -<img alt="dbdep.gif" src="dbdep.gif" /> +<img alt="dbdep.png" src="dbdep.png" /> <p>Die Abhängigkeiten der Module in der Variante für den klassischen PyGreSQL-Adapter sehen ähnlich aus:</p> -<img alt="pgdep.gif" src="pgdep.gif" /> +<img alt="pgdep.png" src="pgdep.png" /> </div> <div class="section" id="download"> <h1>Download</h1> @@ -202,7 +202,7 @@ <h2>PersistentDB</h2> geschlossen werden.</p> <p>Das folgende Diagramm zeigt die beteiligten Verbindungsschichten, wenn Sie <span class="docutils literal">PersistentDB</span>-Datenbankverbindungen einsetzen:</p> -<img alt="persist.gif" src="persist.gif" /> +<img alt="persist.png" src="persist.png" /> <p>Immer wenn ein Thread eine Datenbankverbindung zum ersten Mal öffnet, wird eine neue Datenbankverbindung geöffnet, die von da an immer wieder für genau diesen Thread verwendet wird. Wenn der Thread die Datenbankverbindung schließt, @@ -227,7 +227,7 @@ <h2>PooledDB</h2> dies bemerkt, wiederverwendet werden.</p> <p>Das folgende Diagramm zeigt die beteiligten Verbindungsschichten, wenn Sie <span class="docutils literal">PooledDB</span>-Datenbankverbindungen einsetzen:</p> -<img alt="pool.gif" src="pool.gif" /> +<img alt="pool.png" src="pool.png" /> <p>Wie im Diagramm angedeutet, kann <span class="docutils literal">PooledDB</span> geöffnete Datenbankverbindungen den verschiedenen Threads beliebig zuteilen. Dies geschieht standardmäßig, wenn Sie den Verbindungspool mit einem positiven Wert für <span class="docutils literal">maxshared</span> einrichten diff --git a/docs/main.de.rst b/docs/main.de.rst index f0fda86..75580f7 100644 --- a/docs/main.de.rst +++ b/docs/main.de.rst @@ -54,12 +54,12 @@ DB-API-2-Datenbankadaptern, und einer Gruppe zur Verwendung mit dem klassischen Die Abhängigkeiten der Module in der Variante für beliebige DB-API-2-Adapter sind im folgenden Diagramm dargestellt: -.. image:: dbdep.gif +.. image:: dbdep.png Die Abhängigkeiten der Module in der Variante für den klassischen PyGreSQL-Adapter sehen ähnlich aus: -.. image:: pgdep.gif +.. image:: pgdep.png Download @@ -166,7 +166,7 @@ geschlossen werden. Das folgende Diagramm zeigt die beteiligten Verbindungsschichten, wenn Sie ``PersistentDB``-Datenbankverbindungen einsetzen: -.. image:: persist.gif +.. image:: persist.png Immer wenn ein Thread eine Datenbankverbindung zum ersten Mal öffnet, wird eine neue Datenbankverbindung geöffnet, die von da an immer wieder für genau @@ -196,7 +196,7 @@ dies bemerkt, wiederverwendet werden. Das folgende Diagramm zeigt die beteiligten Verbindungsschichten, wenn Sie ``PooledDB``-Datenbankverbindungen einsetzen: -.. image:: pool.gif +.. image:: pool.png Wie im Diagramm angedeutet, kann ``PooledDB`` geöffnete Datenbankverbindungen den verschiedenen Threads beliebig zuteilen. Dies geschieht standardmäßig, wenn diff --git a/docs/main.html b/docs/main.html index a6625bf..52a6944 100644 --- a/docs/main.html +++ b/docs/main.html @@ -116,10 +116,10 @@ <h1>Modules</h1> </table> <p>The dependencies of the modules in the universal DB-API 2 variant are as indicated in the following diagram:</p> -<img alt="dbdep.gif" src="dbdep.gif" /> +<img alt="dbdep.png" src="dbdep.png" /> <p>The dependencies of the modules in the classic PyGreSQL variant are similar:</p> -<img alt="pgdep.gif" src="pgdep.gif" /> +<img alt="pgdep.png" src="pgdep.png" /> </div> <div class="section" id="download"> <h1>Download</h1> @@ -194,7 +194,7 @@ <h2>PersistentDB</h2> connections to a database, using any DB-API 2 database module.</p> <p>The following diagram shows the connection layers involved when you are using <span class="docutils literal">PersistentDB</span> connections:</p> -<img alt="persist.gif" src="persist.gif" /> +<img alt="persist.png" src="persist.png" /> <p>Whenever a thread opens a database connection for the first time, a new connection to the database will be opened that will be used from now on for this specific thread. When the thread closes the database connection, @@ -216,7 +216,7 @@ <h2>PooledDB</h2> DB-API 2 database module.</p> <p>The following diagram shows the connection layers involved when you are using <span class="docutils literal">PooledDB</span> connections:</p> -<img alt="pool.gif" src="pool.gif" /> +<img alt="pool.png" src="pool.png" /> <p>As the diagram indicates, <span class="docutils literal">PooledDB</span> can share opened database connections between different threads. This will happen by default if you set up the connection pool with a positive value of <span class="docutils literal">maxshared</span> and the underlying diff --git a/docs/main.rst b/docs/main.rst index 43b7905..59b0b70 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -53,12 +53,12 @@ the other one for use with the classic PyGreSQL module. The dependencies of the modules in the universal DB-API 2 variant are as indicated in the following diagram: -.. image:: dbdep.gif +.. image:: dbdep.png The dependencies of the modules in the classic PyGreSQL variant are similar: -.. image:: pgdep.gif +.. image:: pgdep.png Download @@ -157,7 +157,7 @@ connections to a database, using any DB-API 2 database module. The following diagram shows the connection layers involved when you are using ``PersistentDB`` connections: -.. image:: persist.gif +.. image:: persist.png Whenever a thread opens a database connection for the first time, a new connection to the database will be opened that will be used from now on @@ -184,7 +184,7 @@ DB-API 2 database module. The following diagram shows the connection layers involved when you are using ``PooledDB`` connections: -.. image:: pool.gif +.. image:: pool.png As the diagram indicates, ``PooledDB`` can share opened database connections between different threads. This will happen by default if you set up the diff --git a/docs/persist.gif b/docs/persist.gif deleted file mode 100644 index 43c3d0176ab4fc7bfd38467e7cc95159ab46fa04..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7378 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 EJG2-cng9R* diff --git a/docs/persist.png b/docs/persist.png new file mode 100644 index 0000000000000000000000000000000000000000..e76856d7be4aaee2d008839e503b5c7d513f279d GIT binary patch literal 7063 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 literal 0 HcmV?d00001 diff --git a/docs/pgdep.gif b/docs/pgdep.gif deleted file mode 100644 index 9bdfaced5b7a62636c713fa34879e583ba254cbc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2426 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 diff --git a/docs/pgdep.png b/docs/pgdep.png new file mode 100644 index 0000000000000000000000000000000000000000..f235e436be4d796378650186b8fbd7fa1bc94e84 GIT binary patch literal 3129 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 TVXvr+00000NkvXXu0mjfk(cn* literal 0 HcmV?d00001 diff --git a/docs/pool.gif b/docs/pool.gif deleted file mode 100644 index 105b3c8b17a13656184440407c4e139e9856ccb1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13454 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 diff --git a/docs/pool.png b/docs/pool.png new file mode 100644 index 0000000000000000000000000000000000000000..c6677b04881669f261033623168bbe5f49118b04 GIT binary patch literal 11963 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 E2RKJmU;qFB literal 0 HcmV?d00001 From d01143878ab5d2ce0dad0c359c0896077a0b1baf Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 26 Sep 2020 20:30:52 +0200 Subject: [PATCH 07/84] Adapt documentation --- docs/changelog.html | 1 + docs/changelog.rst | 1 + docs/dbdep.png | Bin 3331 -> 0 bytes docs/dependencies_db.png | Bin 0 -> 4541 bytes docs/dependencies_pg.png | Bin 0 -> 4305 bytes docs/main.de.html | 281 +++++++++++++++++---------------------- docs/main.de.rst | 279 +++++++++++++++++--------------------- docs/main.html | 253 ++++++++++++++++------------------- docs/main.rst | 247 +++++++++++++++------------------- docs/persist.png | Bin 7063 -> 0 bytes docs/persistent.png | Bin 0 -> 7038 bytes docs/pgdep.png | Bin 3129 -> 0 bytes docs/pool.png | Bin 11963 -> 0 bytes docs/pooled.png | Bin 0 -> 12053 bytes 14 files changed, 470 insertions(+), 592 deletions(-) delete mode 100644 docs/dbdep.png create mode 100644 docs/dependencies_db.png create mode 100644 docs/dependencies_pg.png delete mode 100644 docs/persist.png create mode 100644 docs/persistent.png delete mode 100644 docs/pgdep.png delete mode 100644 docs/pool.png create mode 100644 docs/pooled.png diff --git a/docs/changelog.html b/docs/changelog.html index 7db17cd..fc37f48 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -79,6 +79,7 @@ <h2>Changes:</h2> Particularly, you need to import <span class="docutils literal">dbutils</span> instead of <span class="docutils literal">DBUtils</span> now.</p></li> <li><p>The Webware <span class="docutils literal">Examples</span> folder has been removed.</p></li> <li><p>The internal naming conventions have been changed to comply with PEP8.</p></li> +<li><p>The documentation has been adapted to reflect the changes in this version.</p></li> <li><p>This changelog file has been created from the former release notes.</p></li> </ul> </div> diff --git a/docs/changelog.rst b/docs/changelog.rst index bf41adf..25b7d88 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -19,6 +19,7 @@ Changes: Particularly, you need to import ``dbutils`` instead of ``DBUtils`` now. * The Webware ``Examples`` folder has been removed. * The internal naming conventions have been changed to comply with PEP8. +* The documentation has been adapted to reflect the changes in this version. * This changelog file has been created from the former release notes. 1.4 diff --git a/docs/dbdep.png b/docs/dbdep.png deleted file mode 100644 index 666e3a78c5e9507e722a77a28f6d55f780f1333c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3331 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 N002ovPDHLkV1mFbDR}?@ diff --git a/docs/dependencies_db.png b/docs/dependencies_db.png new file mode 100644 index 0000000000000000000000000000000000000000..f460fc02d6bf86de7e2ff5caf79729dc3badcbf9 GIT binary patch literal 4541 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 OE979~Y+Yp;koteBsk^NJ literal 0 HcmV?d00001 diff --git a/docs/dependencies_pg.png b/docs/dependencies_pg.png new file mode 100644 index 0000000000000000000000000000000000000000..380def9a0bba68c76ba73cd87e9d946f6c8147b6 GIT binary patch literal 4305 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 literal 0 HcmV?d00001 diff --git a/docs/main.de.html b/docs/main.de.html index 3827321..b744c1f 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -19,108 +19,108 @@ <h1 class="title">Benutzeranleitung für DBUtils</h1> <div class="contents topic" id="inhalt"> <p class="topic-title first">Inhalt</p> <ul class="simple"> -<li><p><a class="reference internal" href="#zusammenfassung" id="id4">Zusammenfassung</a></p></li> -<li><p><a class="reference internal" href="#module" id="id5">Module</a></p></li> -<li><p><a class="reference internal" href="#download" id="id6">Download</a></p></li> -<li><p><a class="reference internal" href="#installation" id="id7">Installation</a></p> +<li><p><a class="reference internal" href="#zusammenfassung" id="id5">Zusammenfassung</a></p></li> +<li><p><a class="reference internal" href="#module" id="id6">Module</a></p></li> +<li><p><a class="reference internal" href="#download" id="id7">Download</a></p></li> +<li><p><a class="reference internal" href="#installation" id="id8">Installation</a></p> <ul> -<li><p><a class="reference internal" href="#installation-als-eigenstandiges-paket" id="id8">Installation als eigenständiges Paket</a></p></li> -<li><p><a class="reference internal" href="#installation-als-unterpaket-plug-in-von-webware-for-python" id="id9">Installation als Unterpaket (Plug-In) von Webware for Python</a></p></li> +<li><p><a class="reference internal" href="#id1" id="id9">Installation</a></p></li> </ul> </li> <li><p><a class="reference internal" href="#anforderungen" id="id10">Anforderungen</a></p></li> <li><p><a class="reference internal" href="#funktionalitat" id="id11">Funktionalität</a></p> <ul> -<li><p><a class="reference internal" href="#simplepooleddb" id="id12">SimplePooledDB</a></p></li> -<li><p><a class="reference internal" href="#steadydb" id="id13">SteadyDB</a></p></li> -<li><p><a class="reference internal" href="#persistentdb" id="id14">PersistentDB</a></p></li> -<li><p><a class="reference internal" href="#pooleddb" id="id15">PooledDB</a></p></li> +<li><p><a class="reference internal" href="#simplepooleddb-simple-pooled-db" id="id12">SimplePooledDB (simple_pooled_db)</a></p></li> +<li><p><a class="reference internal" href="#steadydbconnection-steady-db" id="id13">SteadyDBConnection (steady_db)</a></p></li> +<li><p><a class="reference internal" href="#persistentdb-persistent-db" id="id14">PersistentDB (persistent_db)</a></p></li> +<li><p><a class="reference internal" href="#pooleddb-pooled-db" id="id15">PooledDB (pooled_db)</a></p></li> <li><p><a class="reference internal" href="#die-qual-der-wahl" id="id16">Die Qual der Wahl</a></p></li> </ul> </li> <li><p><a class="reference internal" href="#benutzung" id="id17">Benutzung</a></p> <ul> -<li><p><a class="reference internal" href="#id1" id="id18">PersistentDB</a></p></li> -<li><p><a class="reference internal" href="#id2" id="id19">PooledDB</a></p></li> -<li><p><a class="reference internal" href="#benutzung-in-webware-for-python" id="id20">Benutzung in Webware for Python</a></p></li> +<li><p><a class="reference internal" href="#id2" id="id18">PersistentDB (persistent_db)</a></p></li> +<li><p><a class="reference internal" href="#id3" id="id19">PooledDB (pooled_db)</a></p></li> </ul> </li> -<li><p><a class="reference internal" href="#anmerkungen" id="id21">Anmerkungen</a></p></li> -<li><p><a class="reference internal" href="#zukunft" id="id22">Zukunft</a></p></li> -<li><p><a class="reference internal" href="#fehlermeldungen-und-feedback" id="id23">Fehlermeldungen und Feedback</a></p></li> -<li><p><a class="reference internal" href="#links" id="id24">Links</a></p></li> -<li><p><a class="reference internal" href="#autoren" id="id25">Autoren</a></p></li> -<li><p><a class="reference internal" href="#copyright-und-lizenz" id="id26">Copyright und Lizenz</a></p></li> +<li><p><a class="reference internal" href="#anmerkungen" id="id20">Anmerkungen</a></p></li> +<li><p><a class="reference internal" href="#zukunft" id="id21">Zukunft</a></p></li> +<li><p><a class="reference internal" href="#fehlermeldungen-und-feedback" id="id22">Fehlermeldungen und Feedback</a></p></li> +<li><p><a class="reference internal" href="#links" id="id23">Links</a></p></li> +<li><p><a class="reference internal" href="#autoren" id="id24">Autoren</a></p></li> +<li><p><a class="reference internal" href="#copyright-und-lizenz" id="id25">Copyright und Lizenz</a></p></li> </ul> </div> <div class="section" id="zusammenfassung"> <h1>Zusammenfassung</h1> <p><a class="reference external" href="https://github.com/WebwareForPython/DBUtils">DBUtils</a> ist eine Sammlung von Python-Modulen, mit deren Hilfe man in <a class="reference external" href="https://www.python.org">Python</a> geschriebene Multithread-Anwendungen auf sichere und effiziente Weise an -Datenbanken anbinden kann. DBUtils wurde mit Blick auf <a class="reference external" href="https://webwareforpython.github.io/w4py/">Webware for Python</a> -als Anwendung und <a class="reference external" href="http://www.pygresql.org/">PyGreSQL</a> als <a class="reference external" href="https://www.postgresql.org/">PostgreSQL</a>-Datenbankadapter entwickelt, -kann aber für beliebige Python-Anwendungen und beliebige auf <a class="reference external" href="https://www.python.org/dev/peps/pep-0249/">DB-API 2</a> -beruhende Python-Datenbankadapter verwendet werden.</p> +Datenbanken anbinden kann.</p> +<p>DBUtils wurde ursprünglich speziell für <a class="reference external" href="https://webwareforpython.github.io/w4py/">Webware for Python</a> als Anwendung +und <a class="reference external" href="https://www.pygresql.org/">PyGreSQL</a> als <a class="reference external" href="https://www.postgresql.org/">PostgreSQL</a>-Datenbankadapter entwickelt, +kann aber inzwischen für beliebige Python-Anwendungen und beliebige +auf <a class="reference external" href="https://www.python.org/dev/peps/pep-0249/">DB-API 2</a> beruhende Python-Datenbankadapter verwendet werden.</p> </div> <div class="section" id="module"> <h1>Module</h1> <p>DBUtils ist als Python-Package realisiert worden, das aus zwei verschiedenen Gruppen von Modulen besteht: Einer Gruppe zur Verwendung mit beliebigen -DB-API-2-Datenbankadaptern, und einer Gruppe zur Verwendung mit dem klassischen PyGreSQL-Datenbankadapter-Modul.</p> +DB-API-2-Datenbankadaptern, und einer Gruppe zur Verwendung mit dem klassischen +PyGreSQL-Datenbankadapter-Modul.</p> <table> <colgroup> -<col style="width: 29%" /> -<col style="width: 71%" /> +<col style="width: 28%" /> +<col style="width: 72%" /> </colgroup> <thead> <tr><th class="head" colspan="2"><p>Allgemeine Variante für beliebige DB-API-2-Adapter</p></th> </tr> </thead> <tbody> -<tr><td><p>SteadyDB.py</p></td> +<tr><td><p>steady_db</p></td> <td><p>Gehärtete DB-API-2-Datenbankverbindungen</p></td> </tr> -<tr><td><p>PooledDB.py</p></td> +<tr><td><p>pooled_db</p></td> <td><p>Pooling für DB-API-2-Datenbankverbindungen</p></td> </tr> -<tr><td><p>PersistentDB.py</p></td> +<tr><td><p>persistent_db</p></td> <td><p>Persistente DB-API-2-Datenbankverbindungen</p></td> </tr> -<tr><td><p>SimplePooledDB.py</p></td> +<tr><td><p>simple_pooled_db</p></td> <td><p>Einfaches Pooling für DB-API 2</p></td> </tr> </tbody> </table> <table> <colgroup> -<col style="width: 29%" /> -<col style="width: 71%" /> +<col style="width: 28%" /> +<col style="width: 72%" /> </colgroup> <thead> <tr><th class="head" colspan="2"><p>Variante speziell für den klassischen PyGreSQL-Adapter</p></th> </tr> </thead> <tbody> -<tr><td><p>SteadyPg.py</p></td> +<tr><td><p>steady_pg</p></td> <td><p>Gehärtete klassische PyGreSQL-Verbindungen</p></td> </tr> -<tr><td><p>PooledPg.py</p></td> +<tr><td><p>pooled_pg</p></td> <td><p>Pooling für klassische PyGreSQL-Verbindungen</p></td> </tr> -<tr><td><p>PersistentPg.py</p></td> +<tr><td><p>persistent_pg</p></td> <td><p>Persistente klassische PyGreSQL-Verbindungen</p></td> </tr> -<tr><td><p>SimplePooledPg.py</p></td> +<tr><td><p>simple_pooled_pg</p></td> <td><p>Einfaches Pooling für klassisches PyGreSQL</p></td> </tr> </tbody> </table> <p>Die Abhängigkeiten der Module in der Variante für beliebige DB-API-2-Adapter sind im folgenden Diagramm dargestellt:</p> -<img alt="dbdep.png" src="dbdep.png" /> +<img alt="dependencies_db.png" src="dependencies_db.png" /> <p>Die Abhängigkeiten der Module in der Variante für den klassischen PyGreSQL-Adapter sehen ähnlich aus:</p> -<img alt="pgdep.png" src="pgdep.png" /> +<img alt="depdependencies_pg.png" src="depdependencies_pg.png" /> </div> <div class="section" id="download"> <h1>Download</h1> @@ -132,31 +132,19 @@ <h1>Download</h1> </div> <div class="section" id="installation"> <h1>Installation</h1> -<div class="section" id="installation-als-eigenstandiges-paket"> -<h2>Installation als eigenständiges Paket</h2> -<p>Wenn Sie DBUtils für andere Anwendungen als Webware for Python verwenden -möchten, empfiehlt es sich, das Paket auf die übliche Weise zu installieren:</p> +<div class="section" id="id1"> +<h2>Installation</h2> +<p>Das Paket kann auf die übliche Weise installiert werden:</p> <pre class="literal-block">python setup.py install</pre> -<p>Sie können auch <a class="reference external" href="https://pip.pypa.io/">pip</a> für Download und Installation verwenden:</p> +<p>Noch einfacher ist, das Paket in einem Schritt mit <a class="reference external" href="https://pip.pypa.io/">pip</a> automatisch +herunterzuladen und zu installieren:</p> <pre class="literal-block">pip install DBUtils</pre> </div> -<div class="section" id="installation-als-unterpaket-plug-in-von-webware-for-python"> -<h2>Installation als Unterpaket (Plug-In) von Webware for Python</h2> -<p>Wenn Sie DBUtils nur als Ergänzung für das Web-Framework Webware for Python -verwenden wollen, sollten Sie DBUtils als Webware-Plug-In installieren:</p> -<pre class="literal-block">python setup.py install --install-lib=/pfad/zu/Webware</pre> -<p>Ersetzen Sie <span class="docutils literal">/pfad/zu/Webware</span> hierbei durch den Pfad zum Wurzelverzeichnis -der Installation von Webware for Python. Sie müssen auch das Installationsskript -von Webware for Python laufen lassen, wenn dies noch nicht geschehen ist, oder -wenn Sie DBUtils in die Webware-Dokumentation integrieren wollen:</p> -<pre class="literal-block">cd /pfad/zu/Webware -python install.py</pre> -</div> </div> <div class="section" id="anforderungen"> <h1>Anforderungen</h1> -<p>DBUtils unterstützt die <a class="reference external" href="https://www.python.org">Python</a> Versionen 2.7 und 3.5 bis 3.8.</p> -<p>Die Module in der Variante für klassisches PyGreSQL benötigen <a class="reference external" href="http://www.pygresql.org/">PyGreSQL</a> +<p>DBUtils unterstützt die <a class="reference external" href="https://www.python.org">Python</a> Versionen 2.7 und 3.5 bis 3.9.</p> +<p>Die Module in der Variante für klassisches PyGreSQL benötigen <a class="reference external" href="https://www.pygresql.org/">PyGreSQL</a> Version 4.0 oder höher, während die Module in der allgemeinen Variante für DB-API 2 mit jedem beliebigen Python-Datenbankadapter-Modul zusammenarbeiten, das auf <a class="reference external" href="https://www.python.org/dev/peps/pep-0249/">DB-API 2</a> basiert.</p> @@ -165,44 +153,49 @@ <h1>Anforderungen</h1> <h1>Funktionalität</h1> <p>Dieser Abschnitt verwendet nur die Bezeichnungen der DB-API-2-Variante, aber Entsprechendes gilt auch für die PyGreSQL-Variante.</p> -<div class="section" id="simplepooleddb"> -<h2>SimplePooledDB</h2> -<p><span class="docutils literal">DBUtils.SimplePooledDB</span> ist eine sehr elementare Referenz-Implementierung -eines Pools von Datenbankverbindungen. Hiermit ist ein Vorratsspeicher an -Datenbankverbindungen gemeint, aus dem sich die Python-Anwendung bedienen kann. -Diese Implementierung ist weit weniger ausgefeilt als das eigentliche -<span class="docutils literal">PooledDB</span>-Modul und stellt insbesondere keine Ausfallsicherung zur Verfügung. -<span class="docutils literal">DBUtils.SimplePooledDB</span> ist im Wesentlichen identisch mit dem zu Webware for -Python gehörenden Modul <span class="docutils literal">MiscUtils.DBPool</span>. Es ist eher zur Verdeutlichung -des Konzepts gedacht, als zum Einsatz im produktiven Betrieb.</p> +<p>DBUtils installiert sich als Paket <span class="docutils literal">dbutils</span>, das alle hier beschriebenen +Module enthält. Jedes dieser Modul enthält im Wesentlichen eine Klasse, die +einen analogen Namen trägt und die jeweilige Funktionalität bereitstellt. +So enthält z.B. das Modul <span class="docutils literal">dbutils.pooled_db</span> die Klasse <span class="docutils literal">PooledDB</span>.</p> +<div class="section" id="simplepooleddb-simple-pooled-db"> +<h2>SimplePooledDB (simple_pooled_db)</h2> +<p>Die Klasse <span class="docutils literal">SimplePooledDB</span> in <span class="docutils literal">dbutils.simple_pooled_db</span> ist eine sehr +elementare Referenz-Implementierung eines Pools von Datenbankverbindungen. +Hiermit ist ein Vorratsspeicher an Datenbankverbindungen gemeint, aus dem sich +die Python-Anwendung bedienen kann. Diese Implementierung ist weit weniger +ausgefeilt als das eigentliche <span class="docutils literal">pooled_db</span>-Modul und stellt insbesondere +keine Ausfallsicherung zur Verfügung. <span class="docutils literal">dbutils.simple_pooled_db</span> ist im +Wesentlichen identisch mit dem zu Webware for Python gehörenden Modul +<span class="docutils literal">MiscUtils.DBPool</span>. Es ist eher zur Verdeutlichung des Konzepts gedacht, +als zum Einsatz im produktiven Betrieb.</p> </div> -<div class="section" id="steadydb"> -<h2>SteadyDB</h2> -<p><span class="docutils literal">DBUtils.SteadyDB</span> ist ein Modul, das "gehärtete" Datenbankverbindungen -bereitstellt, denen gewöhnlichen Verbindungen eines DB-API-2-Datenbankadapters -zugrunde liegen. Eine "gehärtete" Verbindung wird bei Zugriff automatisch, -ohne dass die Anwendung dies bemerkt, wieder geöffnet, wenn sie geschlossen -wurde, die Datenbankverbindung unterbrochen wurde, oder wenn sie öfter als -ein optionales Limit genutzt wurde.</p> +<div class="section" id="steadydbconnection-steady-db"> +<h2>SteadyDBConnection (steady_db)</h2> +<p>Die Klasse <span class="docutils literal">SteadyDBConnection</span> im Modul <span class="docutils literal">dbutils.steady_db</span> stellt +"gehärtete" Datenbankverbindungen bereit, denen gewöhnlichen Verbindungen +eines DB-API-2-Datenbankadapters zugrunde liegen. Eine "gehärtete" Verbindung +wird bei Zugriff automatisch, ohne dass die Anwendung dies bemerkt, wieder +geöffnet, wenn sie geschlossen wurde, die Datenbankverbindung unterbrochen +wurde, oder wenn sie öfter als ein optionales Limit genutzt wurde.</p> <p>Ein typisches Beispiel wo dies benötig wird, ist, wenn die Datenbank neu gestartet wurde, während Ihre Anwendung immer noch läuft und Verbindungen zur Datenbank offen hat, oder wenn Ihre Anwendung auf eine entfernte Datenbank über ein Netzwerk zugreift, das durch eine Firewall geschützt ist, und die Firewall neu gestartet wurde und dabei ihren Verbindungsstatus verloren hat.</p> -<p>Normalerweise benutzen Sie das <span class="docutils literal">SteadyDB</span>-Modul nicht direkt; es wird aber -von den beiden nächsten Modulen benötigt, <span class="docutils literal">PersistentDB</span> und <span class="docutils literal">PooledDB</span>.</p> +<p>Normalerweise benutzen Sie das <span class="docutils literal">steady_db</span>-Modul nicht direkt; es wird aber +von den beiden nächsten Modulen benötigt, <span class="docutils literal">persistent_db</span> und <span class="docutils literal">pooled_db</span>.</p> </div> -<div class="section" id="persistentdb"> -<h2>PersistentDB</h2> -<p><span class="docutils literal">DBUtils.PersistentDB</span> stellt gehärtete, thread-affine, persistente -Datenbankverbindungen zur Verfügung, unter Benutzung eines beliebigen -DB-API-2-Datenbankadapters. Mit "thread-affin" und "persistent" ist -hierbei gemeint, dass die einzelnen Datenbankverbindungen den jeweiligen -Threads fest zugeordnet bleiben und während der Laufzeit des Threads nicht -geschlossen werden.</p> +<div class="section" id="persistentdb-persistent-db"> +<h2>PersistentDB (persistent_db)</h2> +<p>Die Klasse <span class="docutils literal">PersistentDB</span> im Modul <span class="docutils literal">dbutils.persistent_db</span> stellt +gehärtete, thread-affine, persistente Datenbankverbindungen zur Verfügung, +unter Benutzung eines beliebigen DB-API-2-Datenbankadapters. Mit "thread-affin" +und "persistent" ist hierbei gemeint, dass die einzelnen Datenbankverbindungen +den jeweiligen Threads fest zugeordnet bleiben und während der Laufzeit des +Threads nicht geschlossen werden.</p> <p>Das folgende Diagramm zeigt die beteiligten Verbindungsschichten, wenn Sie -<span class="docutils literal">PersistentDB</span>-Datenbankverbindungen einsetzen:</p> -<img alt="persist.png" src="persist.png" /> +<span class="docutils literal">persistent_db</span>-Datenbankverbindungen einsetzen:</p> +<img alt="persistent.png" src="persistent.png" /> <p>Immer wenn ein Thread eine Datenbankverbindung zum ersten Mal öffnet, wird eine neue Datenbankverbindung geöffnet, die von da an immer wieder für genau diesen Thread verwendet wird. Wenn der Thread die Datenbankverbindung schließt, @@ -210,25 +203,25 @@ <h2>PersistentDB</h2> gleiche Thread wieder eine Datenbankverbindung anfordert, diese gleiche bereits geöffnete Datenbankverbindung wieder verwendet werden kann. Die Verbindung wird automatisch geschlossen, wenn der Thread beendet wird.</p> -<p>Kurz gesagt versucht <span class="docutils literal">PersistentDB</span> Datenbankverbindungen wiederzuverwerten, +<p>Kurz gesagt versucht <span class="docutils literal">persistent_db</span> Datenbankverbindungen wiederzuverwerten, um die Gesamteffizienz der Datenbankzugriffe Ihrer Multithread-Anwendungen zu steigern, aber es wird dabei sichergestellt, dass verschiedene Threads niemals die gleiche Verbindung benutzen.</p> -<p>Daher arbeitet <span class="docutils literal">PersistentDB</span> sogar dann problemlos, wenn der zugrunde +<p>Daher arbeitet <span class="docutils literal">persistent_db</span> sogar dann problemlos, wenn der zugrunde liegende DB-API-2-Datenbankadapter nicht thread-sicher auf der Verbindungsebene ist, oder wenn parallele Threads Parameter der Datenbank-Sitzung verändern oder Transaktionen mit mehreren SQL-Befehlen durchführen.</p> </div> -<div class="section" id="pooleddb"> -<h2>PooledDB</h2> -<p><span class="docutils literal">DBUtils.PooledDB</span> stellt, unter Benutzung eines beliebigen -DB-API-2-Datenbankadapters, einen Pool von gehärteten, thread-sicheren -Datenbankverbindungen zur Verfügung, die automatisch, ohne dass die Anwendung -dies bemerkt, wiederverwendet werden.</p> +<div class="section" id="pooleddb-pooled-db"> +<h2>PooledDB (pooled_db)</h2> +<p>Die Klasse <span class="docutils literal">PooledDB</span> im Modul <span class="docutils literal">dbutils.pooled_db</span> stellt, unter Benutzung +eines beliebigen DB-API-2-Datenbankadapters, einen Pool von gehärteten, +thread-sicheren Datenbankverbindungen zur Verfügung, die automatisch, ohne dass +die Anwendung dies bemerkt, wiederverwendet werden.</p> <p>Das folgende Diagramm zeigt die beteiligten Verbindungsschichten, wenn Sie -<span class="docutils literal">PooledDB</span>-Datenbankverbindungen einsetzen:</p> -<img alt="pool.png" src="pool.png" /> -<p>Wie im Diagramm angedeutet, kann <span class="docutils literal">PooledDB</span> geöffnete Datenbankverbindungen +<span class="docutils literal">pooled_db</span>-Datenbankverbindungen einsetzen:</p> +<img alt="pooled.png" src="pooled.png" /> +<p>Wie im Diagramm angedeutet, kann <span class="docutils literal">pooled_db</span> geöffnete Datenbankverbindungen den verschiedenen Threads beliebig zuteilen. Dies geschieht standardmäßig, wenn Sie den Verbindungspool mit einem positiven Wert für <span class="docutils literal">maxshared</span> einrichten und der zugrunde liegende DB-API-2-Datenbankadapter auf der Verbindungsebene @@ -244,23 +237,23 @@ <h2>PooledDB</h2> Datenbankverbindungen zurückgegeben, damit sie wiederverwertet werden kann.</p> <p>Wenn der zugrunde liegende DB-API-Datenbankadapter nicht thread-sicher ist, werden Thread-Locks verwendet, um sicherzustellen, dass die -<span class="docutils literal">PooledDB</span>-Verbindungen dennoch thread-sicher sind. Sie brauchen sich also +<span class="docutils literal">pooled_db</span>-Verbindungen dennoch thread-sicher sind. Sie brauchen sich also hierum keine Sorgen zu machen, aber Sie sollten darauf achten, dedizierte Datenbankverbindungen zu verwenden, sobald Sie Parameter der Datenbanksitzung verändern oder Transaktionen mit mehreren SQL-Befehlen ausführen.</p> </div> <div class="section" id="die-qual-der-wahl"> <h2>Die Qual der Wahl</h2> -<p>Sowohl <span class="docutils literal">PersistentDB</span> als auch <span class="docutils literal">PooledDB</span> dienen dem gleichen Zweck, +<p>Sowohl <span class="docutils literal">persistent_db</span> als auch <span class="docutils literal">pooled_db</span> dienen dem gleichen Zweck, nämlich die Effizienz des Datenbankzugriffs durch Wiederverwendung von Datenbankverbindungen zu steigern, und dabei gleichzeitig die Stabilität zu gewährleisten, selbst wenn die Datenbankverbindung unterbrochen wird.</p> <p>Welches der beiden Module sollte also verwendet werden? Nach den obigen -Erklärungen ist es klar, dass <span class="docutils literal">PersistentDB</span> dann sinnvoller ist, wenn +Erklärungen ist es klar, dass <span class="docutils literal">persistent_db</span> dann sinnvoller ist, wenn Ihre Anwendung eine gleich bleibende Anzahl Threads verwendet, die häufig auf die Datenbank zugreifen. In diesem Fall werden Sie ungefähr die gleiche Anzahl geöffneter Datenbankverbindungen erhalten. Wenn jedoch Ihre Anwendung -häufig Threads beendet und neu startet, dann ist <span class="docutils literal">PooledDB</span> die bessere +häufig Threads beendet und neu startet, dann ist <span class="docutils literal">pooled_db</span> die bessere Lösung, die auch mehr Möglichkeiten zur Feineinstellung zur Verbesserung der Effizienz erlaubt, insbesondere bei Verwendung eines thread-sicheren DB-API-2-Datenbankadapters.</p> @@ -274,17 +267,17 @@ <h1>Benutzung</h1> der Initialisierung auch einige Unterschiede, sowohl zwischen den "Pooled"- und den "Persistent"-Varianten, als auch zwischen den DB-API-2- und den PyGreSQL-Varianten.</p> -<p>Wir werden hier nur auf das <span class="docutils literal">PersistentDB</span>-Modul und das etwas kompliziertere -<span class="docutils literal">PooledDB</span>-Modul eingehen. Einzelheiten zu den anderen Modulen finden Sie +<p>Wir werden hier nur auf das <span class="docutils literal">persistent_db</span>-Modul und das etwas kompliziertere +<span class="docutils literal">pooled_db</span>-Modul eingehen. Einzelheiten zu den anderen Modulen finden Sie in deren Docstrings. Unter Verwendung der Python-Interpreter-Konsole können Sie -sich die Dokumentation des <span class="docutils literal">PooledDB</span>-Moduls wie folgt anzeigen lassen (dies +sich die Dokumentation des <span class="docutils literal">pooled_db</span>-Moduls wie folgt anzeigen lassen (dies funktioniert entsprechend auch mit den anderen Modulen):</p> -<pre class="literal-block">help(PooledDB)</pre> -<div class="section" id="id1"> -<h2>PersistentDB</h2> -<p>Wenn Sie das <span class="docutils literal">PersistentDB</span>-Modul einsetzen möchten, müssen Sie zuerst einen +<pre class="literal-block">help(pooled_db)</pre> +<div class="section" id="id2"> +<h2>PersistentDB (persistent_db)</h2> +<p>Wenn Sie das <span class="docutils literal">persistent_db</span>-Modul einsetzen möchten, müssen Sie zuerst einen Generator für die von Ihnen gewünschte Art von Datenbankverbindungen einrichten, -indem Sie eine Instanz der Klasse <span class="docutils literal">PersistentDB</span> erzeugen, wobei Sie folgende +indem Sie eine Instanz der Klasse <span class="docutils literal">persistent_db</span> erzeugen, wobei Sie folgende Parameter angeben müssen:</p> <ul> <li><p><span class="docutils literal">creator</span>: entweder eine Funktion, die neue DB-API-2-Verbindungen @@ -319,14 +312,14 @@ <h2>PersistentDB</h2> möchten, dass jede Verbindung Ihrer lokalen Datenbank <span class="docutils literal">meinedb</span> 1000 mal wiederverwendet werden soll, sieht die Initialisierung so aus:</p> <pre class="literal-block">import pgdb # importiere das verwendete DB-API-2-Modul -from DBUtils.PersistentDB import PersistentDB +from dbutils.persistent_db import PersistentDB persist = PersistentDB(pgdb, 1000, database='meinedb')</pre> <p>Nachdem Sie den Generator mit diesen Parametern eingerichtet haben, können Sie derartige Datenbankverbindungen von da an wie folgt anfordern:</p> <pre class="literal-block">db = persist.connection()</pre> <p>Sie können diese Verbindungen verwenden, als wären sie gewöhnliche DB-API-2-Datenbankverbindungen. Genauer genommen erhalten Sie die -"gehärtete" <span class="docutils literal">SteadyDB</span>-Version der zugrunde liegenden DB-API-2-Verbindung.</p> +"gehärtete" <span class="docutils literal">steady_db</span>-Version der zugrunde liegenden DB-API-2-Verbindung.</p> <p>Wenn Sie eine solche persistente Verbindung mit <span class="docutils literal">db.close()</span> schließen, wird dies stillschweigend ignoriert, denn sie würde beim nächsten Zugriff sowieso wieder geöffnet, und das wäre nicht im Sinne persistenter Verbindungen. @@ -344,11 +337,11 @@ <h2>PersistentDB</h2> <span class="docutils literal">mod_wsgi</span> hier Probleme bereitet, da es Daten, die mit <span class="docutils literal">threading.local</span> gespeichert wurden, zwischen Requests löscht).</p> </div> -<div class="section" id="id2"> -<h2>PooledDB</h2> -<p>Wenn Sie das <span class="docutils literal">PooledDB</span>-Modul einsetzen möchten, müssen Sie zuerst einen +<div class="section" id="id3"> +<h2>PooledDB (pooled_db)</h2> +<p>Wenn Sie das <span class="docutils literal">pooled_db</span>-Modul einsetzen möchten, müssen Sie zuerst einen Pool für die von Ihnen gewünschte Art von Datenbankverbindungen einrichten, -indem Sie eine Instanz der Klasse <span class="docutils literal">PooledDB</span> erzeugen, wobei Sie folgende +indem Sie eine Instanz der Klasse <span class="docutils literal">pooled_db</span> erzeugen, wobei Sie folgende Parameter angeben müssen:</p> <ul> <li><p><span class="docutils literal">creator</span>: entweder eine Funktion, die neue DB-API-2-Verbindungen @@ -403,14 +396,14 @@ <h2>PooledDB</h2> und einen Pool von mindestens fünf Datenbankverbindungen zu Ihrer Datenbank <span class="docutils literal">meinedb</span> verwenden möchten, dann sieht die Initialisierung so aus:</p> <pre class="literal-block">import pgdb # importiere das verwendete DB-API-2-Modul -from DBUtils.PooledDB import PooledDB +from dbutils.pooled_db import PooledDB pool = PooledDB(pgdb, 5, database='meinedb')</pre> <p>Nachdem Sie den Pool für Datenbankverbindungen so eingerichtet haben, können Sie Verbindungen daraus wie folgt anfordern:</p> <pre class="literal-block">db = pool.connection()</pre> <p>Sie können diese Verbindungen verwenden, als wären sie gewöhnliche DB-API-2-Datenbankverbindungen. Genauer genommen erhalten Sie die -"gehärtete" <span class="docutils literal">SteadyDB</span>-Version der zugrunde liegenden DB-API-2-Verbindung.</p> +"gehärtete" <span class="docutils literal">steady_db</span>-Version der zugrunde liegenden DB-API-2-Verbindung.</p> <p>Bitte beachten Sie, dass die Verbindung von anderen Threads mitgenutzt werden kann, wenn Sie den Parameter <span class="docutils literal">maxshared</span> auf einen Wert größer als Null gesetzt haben, und der zugrunde liegende DB-API-2-Datenbankadapter dies erlaubt. @@ -441,48 +434,24 @@ <h2>PooledDB</h2> ausgesetzt wird, und dass die Verbindung zurückgerollt wird, bevor sie wieder an den Verbindungspool zurückgegeben wird.</p> </div> -<div class="section" id="benutzung-in-webware-for-python"> -<h2>Benutzung in Webware for Python</h2> -<p>Wenn Sie DBUtils verwenden, um von Servlets des Web-Frameworks <a class="reference external" href="https://webwareforpython.github.io/w4py/">Webware -for Python</a> auf eine Datenbank zuzugreifen, dann müssen Sie sicherstellen, -dass die Generatoren zur Erzeugung von Datenbankverbindungen nur einmal -eingerichtet werden, wenn die Anwendung startet, und nicht jedes Mal, wenn -eine Servlet-Instanz erzeugt wird. Den hierfür nötigen Code können Sie -bei der Basis-Servlet-Klasse einfügen, dort wo das Modul oder die Klasse -initialisiert wird, oder Sie können die Funktion <span class="docutils literal">contextInitialize()</span> -im <span class="docutils literal">__init__.py</span>-Skript Ihres Anwendungskontextes verwenden.</p> -<p>Das zusammen mit DButils ausgelieferte Verzeichnis <span class="docutils literal">Examples</span> enthält -einen Beispielkontext für Webware for Python, der eine kleine Demo-Datenbank -verwendet, um Teilnehmer an einer Seminarreihe zu verwalten (die Idee für -dieses Beispiel wurde dem Artikel "<a class="reference external" href="http://www.linuxjournal.com/article/2605">The Python DB-API</a>" von Andrew Kuchling -entnommen).</p> -<p>Der Beispielkontext kann konfiguriert werden, indem entweder eine Konfig-Datei -<span class="docutils literal">Configs/Database.config</span> angelegt wird, oder indem die Standard-Parameter -direkt im Beispielservlet <span class="docutils literal">Examples/DBUtilsExample.py</span> geändert werden. -Auf diese Weise können Sie einen passenden Datenbanknutzer und sein Passwort -festlegen, sowie den zugrunde liegenden Datenbankadapter auswählen (das -klassische PyGreSQL-Modul oder irgendein DB-API-2-Modul). Wenn der Parameter -<span class="docutils literal">maxcached</span> vorhanden ist, verwendet das Beispielservlet die -<span class="docutils literal">Pooled</span>-Variante, andernfalls die <span class="docutils literal">Persistent</span>-Variante.</p> -</div> </div> <div class="section" id="anmerkungen"> <h1>Anmerkungen</h1> -<p>Wenn Sie einen der bekannten "Object-Relational Mapper" <a class="reference external" href="http://www.sqlobject.org/">SQLObject</a> oder -<a class="reference external" href="http://www.sqlalchemy.org">SQLAlchemy</a> verwenden, dann benötigen Sie DBUtils nicht, denn diese haben +<p>Wenn Sie einen der bekannten "Object-Relational Mapper" <a class="reference external" href="http://sqlobject.org/">SQLObject</a> oder +<a class="reference external" href="https://www.sqlalchemy.org">SQLAlchemy</a> verwenden, dann benötigen Sie DBUtils nicht, denn diese haben ihre eigenen Mechanismen zum Pooling von Datenbankverbindungen eingebaut. Tatsächlich hat SQLObject 2 (SQL-API) das Pooling in eine separate Schicht ausgelagert, in der Code von DBUtils verwendet wird.</p> <p>Wenn Sie eine Lösung verwenden wie den Apache-Webserver mit <a class="reference external" href="http://modpython.org/">mod_python</a> oder <a class="reference external" href="https://github.com/GrahamDumpleton/mod_wsgi">mod_wsgi</a>, dann sollten Sie bedenken, dass Ihr Python-Code normalerweise im Kontext der Kindprozesse des Webservers läuft. Wenn Sie also das -<span class="docutils literal">PooledDB</span>-Modul einsetzen, und mehrere dieser Kindprozesse laufen, dann +<span class="docutils literal">pooled_db</span>-Modul einsetzen, und mehrere dieser Kindprozesse laufen, dann werden Sie ebensoviele Pools mit Datenbankverbindungen erhalten. Wenn diese Prozesse viele Threads laufen lassen, dann mag dies eine sinnvoller Ansatz sein, wenn aber diese Prozesse nicht mehr als einen Worker-Thread starten, wie im Fall des Multi-Processing Moduls "prefork" für den Apache-Webserver, dann sollten Sie auf eine Middleware für das Connection-Pooling zurückgreifen, -die Multi-Processing unterstützt, wie zum Beispiel <a class="reference external" href="http://www.pgpool.net/">pgpool</a> oder <a class="reference external" href="https://pgbouncer.github.io/">pgbouncer</a> +die Multi-Processing unterstützt, wie zum Beispiel <a class="reference external" href="https://www.pgpool.net/">pgpool</a> oder <a class="reference external" href="https://pgbouncer.github.io/">pgbouncer</a> für die PostgreSQL-Datenbank.</p> </div> <div class="section" id="zukunft"> @@ -491,7 +460,7 @@ <h1>Zukunft</h1> <ul class="simple"> <li><p>Alternativ zur Obergrenze in der Anzahl der Nutzung einer Datenbankverbindung könnte eine maximale Lebensdauer für die Verbindung implementiert werden.</p></li> -<li><p>Es könnten Module <span class="docutils literal">MonitorDB</span> und <span class="docutils literal">MonitorPg</span> hinzugefügt werden, die +<li><p>Es könnten Module <span class="docutils literal">monitor_db</span> und <span class="docutils literal">monitor_pg</span> hinzugefügt werden, die in einem separaten Thread ständig den "idle pool" und eventuell auch den "shared pool" bzw. die persistenten Verbindungen überwachen. Wenn eine unterbrochene Datenbankverbindung entdeckt wird, wird diese automatisch durch @@ -509,10 +478,8 @@ <h1>Zukunft</h1> </div> <div class="section" id="fehlermeldungen-und-feedback"> <h1>Fehlermeldungen und Feedback</h1> -<p>Bitte Senden Sie Fehlermeldungen, Patches und Feedback direkt an den -Autor (unter Verwendung der unten angegebenen E-Mail-Adresse).</p> -<p>Probleme, die Webware betreffen, können auch in der <a class="reference external" href="https://lists.sourceforge.net/lists/listinfo/webware-discuss">Webware for Python -mailing list</a> diskutiert werden.</p> +<p>Fehlermeldungen, Patches und Feedback können Sie als <a class="reference external" href="https://github.com/WebwareForPython/DBUtils/issues">Issues</a> oder +<a class="reference external" href="https://github.com/WebwareForPython/DBUtils/pulls">Pull Requests</a> auf der <a class="reference external" href="https://github.com/WebwareForPython/DBUtils">GitHub-Projektseite</a> von DBUtils übermitteln.</p> </div> <div class="section" id="links"> <h1>Links</h1> @@ -523,18 +490,18 @@ <h1>Links</h1> <li><p><a class="reference external" href="https://webwareforpython.github.io/w4py/">Webware for Python</a> Framework</p></li> <li><p>Python <a class="reference external" href="https://www.python.org/dev/peps/pep-0249/">DB-API 2</a></p></li> <li><p><a class="reference external" href="https://www.postgresql.org/">PostgreSQL</a> Datenbank</p></li> -<li><p><a class="reference external" href="http://www.pygresql.org/">PyGreSQL</a> Python-Adapter for PostgreSQL</p></li> -<li><p><a class="reference external" href="http://www.pgpool.net/">pgpool</a> Middleware für Connection-Pooling mit PostgreSQL</p></li> +<li><p><a class="reference external" href="https://www.pygresql.org/">PyGreSQL</a> Python-Adapter for PostgreSQL</p></li> +<li><p><a class="reference external" href="https://www.pgpool.net/">pgpool</a> Middleware für Connection-Pooling mit PostgreSQL</p></li> <li><p><a class="reference external" href="https://pgbouncer.github.io/">pgbouncer</a> Middleware für Connection-Pooling mit PostgreSQL</p></li> -<li><p><a class="reference external" href="http://www.sqlobject.org/">SQLObject</a> Objekt-relationaler Mapper</p></li> -<li><p><a class="reference external" href="http://www.sqlalchemy.org">SQLAlchemy</a> Objekt-relationaler Mapper</p></li> +<li><p><a class="reference external" href="http://sqlobject.org/">SQLObject</a> Objekt-relationaler Mapper</p></li> +<li><p><a class="reference external" href="https://www.sqlalchemy.org">SQLAlchemy</a> Objekt-relationaler Mapper</p></li> </ul> </div> <div class="section" id="autoren"> <h1>Autoren</h1> <dl class="field-list simple"> <dt>Autor</dt> -<dd><p>Christoph Zwerschke <<a class="reference external" href="mailto:cito@online.de">cito@online.de</a>></p> +<dd><p><a class="reference external" href="https://github.com/Cito">Christoph Zwerschke</a></p> </dd> <dt>Beiträge</dt> <dd><p>DBUtils benutzt Code, Anmerkungen und Vorschläge von @@ -547,7 +514,7 @@ <h1>Autoren</h1> </div> <div class="section" id="copyright-und-lizenz"> <h1>Copyright und Lizenz</h1> -<p>Copyright © 2005-2018 Christoph Zwerschke. +<p>Copyright © 2005-2020 Christoph Zwerschke. Alle Rechte vorbehalten.</p> <p>DBUtils ist freie und quelloffene Software, lizenziert unter der <a class="reference external" href="https://opensource.org/licenses/MIT">MIT-Lizenz</a>.</p> diff --git a/docs/main.de.rst b/docs/main.de.rst index 75580f7..3742b5b 100644 --- a/docs/main.de.rst +++ b/docs/main.de.rst @@ -14,10 +14,12 @@ Zusammenfassung DBUtils_ ist eine Sammlung von Python-Modulen, mit deren Hilfe man in Python_ geschriebene Multithread-Anwendungen auf sichere und effiziente Weise an -Datenbanken anbinden kann. DBUtils wurde mit Blick auf `Webware for Python`_ -als Anwendung und PyGreSQL_ als PostgreSQL_-Datenbankadapter entwickelt, -kann aber für beliebige Python-Anwendungen und beliebige auf `DB-API 2`_ -beruhende Python-Datenbankadapter verwendet werden. +Datenbanken anbinden kann. + +DBUtils wurde ursprünglich speziell für `Webware for Python`_ als Anwendung +und PyGreSQL_ als PostgreSQL_-Datenbankadapter entwickelt, +kann aber inzwischen für beliebige Python-Anwendungen und beliebige +auf `DB-API 2`_ beruhende Python-Datenbankadapter verwendet werden. Module @@ -25,41 +27,42 @@ Module DBUtils ist als Python-Package realisiert worden, das aus zwei verschiedenen Gruppen von Modulen besteht: Einer Gruppe zur Verwendung mit beliebigen -DB-API-2-Datenbankadaptern, und einer Gruppe zur Verwendung mit dem klassischen PyGreSQL-Datenbankadapter-Modul. - -+-------------------+----------------------------------------------+ -| Allgemeine Variante für beliebige DB-API-2-Adapter | -+===================+==============================================+ -| SteadyDB.py | Gehärtete DB-API-2-Datenbankverbindungen | -+-------------------+----------------------------------------------+ -| PooledDB.py | Pooling für DB-API-2-Datenbankverbindungen | -+-------------------+----------------------------------------------+ -| PersistentDB.py | Persistente DB-API-2-Datenbankverbindungen | -+-------------------+----------------------------------------------+ -| SimplePooledDB.py | Einfaches Pooling für DB-API 2 | -+-------------------+----------------------------------------------+ - -+-------------------+----------------------------------------------+ -| Variante speziell für den klassischen PyGreSQL-Adapter | -+===================+==============================================+ -| SteadyPg.py | Gehärtete klassische PyGreSQL-Verbindungen | -+-------------------+----------------------------------------------+ -| PooledPg.py | Pooling für klassische PyGreSQL-Verbindungen | -+-------------------+----------------------------------------------+ -| PersistentPg.py | Persistente klassische PyGreSQL-Verbindungen | -+-------------------+----------------------------------------------+ -| SimplePooledPg.py | Einfaches Pooling für klassisches PyGreSQL | -+-------------------+----------------------------------------------+ +DB-API-2-Datenbankadaptern, und einer Gruppe zur Verwendung mit dem klassischen +PyGreSQL-Datenbankadapter-Modul. + ++------------------+----------------------------------------------+ +| Allgemeine Variante für beliebige DB-API-2-Adapter | ++==================+==============================================+ +| steady_db | Gehärtete DB-API-2-Datenbankverbindungen | ++------------------+----------------------------------------------+ +| pooled_db | Pooling für DB-API-2-Datenbankverbindungen | ++------------------+----------------------------------------------+ +| persistent_db | Persistente DB-API-2-Datenbankverbindungen | ++------------------+----------------------------------------------+ +| simple_pooled_db | Einfaches Pooling für DB-API 2 | ++------------------+----------------------------------------------+ + ++------------------+----------------------------------------------+ +| Variante speziell für den klassischen PyGreSQL-Adapter | ++==================+==============================================+ +| steady_pg | Gehärtete klassische PyGreSQL-Verbindungen | ++------------------+----------------------------------------------+ +| pooled_pg | Pooling für klassische PyGreSQL-Verbindungen | ++------------------+----------------------------------------------+ +| persistent_pg | Persistente klassische PyGreSQL-Verbindungen | ++------------------+----------------------------------------------+ +| simple_pooled_pg | Einfaches Pooling für klassisches PyGreSQL | ++------------------+----------------------------------------------+ Die Abhängigkeiten der Module in der Variante für beliebige DB-API-2-Adapter sind im folgenden Diagramm dargestellt: -.. image:: dbdep.png +.. image:: dependencies_db.png Die Abhängigkeiten der Module in der Variante für den klassischen PyGreSQL-Adapter sehen ähnlich aus: -.. image:: pgdep.png +.. image:: depdependencies_pg.png Download @@ -78,39 +81,24 @@ Das Source-Code-Repository befindet sich hier auf GitHub:: Installation ============ -Installation als eigenständiges Paket -------------------------------------- -Wenn Sie DBUtils für andere Anwendungen als Webware for Python verwenden -möchten, empfiehlt es sich, das Paket auf die übliche Weise zu installieren:: +Installation +------------ +Das Paket kann auf die übliche Weise installiert werden:: python setup.py install -Sie können auch `pip`_ für Download und Installation verwenden:: +Noch einfacher ist, das Paket in einem Schritt mit `pip`_ automatisch +herunterzuladen und zu installieren:: pip install DBUtils .. _pip: https://pip.pypa.io/ -Installation als Unterpaket (Plug-In) von Webware for Python ------------------------------------------------------------- -Wenn Sie DBUtils nur als Ergänzung für das Web-Framework Webware for Python -verwenden wollen, sollten Sie DBUtils als Webware-Plug-In installieren:: - - python setup.py install --install-lib=/pfad/zu/Webware - -Ersetzen Sie ``/pfad/zu/Webware`` hierbei durch den Pfad zum Wurzelverzeichnis -der Installation von Webware for Python. Sie müssen auch das Installationsskript -von Webware for Python laufen lassen, wenn dies noch nicht geschehen ist, oder -wenn Sie DBUtils in die Webware-Dokumentation integrieren wollen:: - - cd /pfad/zu/Webware - python install.py - Anforderungen ============= -DBUtils unterstützt die Python_ Versionen 2.7 und 3.5 bis 3.8. +DBUtils unterstützt die Python_ Versionen 2.7 und 3.5 bis 3.9. Die Module in der Variante für klassisches PyGreSQL benötigen PyGreSQL_ Version 4.0 oder höher, während die Module in der allgemeinen Variante @@ -124,26 +112,31 @@ Funktionalität Dieser Abschnitt verwendet nur die Bezeichnungen der DB-API-2-Variante, aber Entsprechendes gilt auch für die PyGreSQL-Variante. - -SimplePooledDB --------------- -``DBUtils.SimplePooledDB`` ist eine sehr elementare Referenz-Implementierung -eines Pools von Datenbankverbindungen. Hiermit ist ein Vorratsspeicher an -Datenbankverbindungen gemeint, aus dem sich die Python-Anwendung bedienen kann. -Diese Implementierung ist weit weniger ausgefeilt als das eigentliche -``PooledDB``-Modul und stellt insbesondere keine Ausfallsicherung zur Verfügung. -``DBUtils.SimplePooledDB`` ist im Wesentlichen identisch mit dem zu Webware for -Python gehörenden Modul ``MiscUtils.DBPool``. Es ist eher zur Verdeutlichung -des Konzepts gedacht, als zum Einsatz im produktiven Betrieb. - -SteadyDB --------- -``DBUtils.SteadyDB`` ist ein Modul, das "gehärtete" Datenbankverbindungen -bereitstellt, denen gewöhnlichen Verbindungen eines DB-API-2-Datenbankadapters -zugrunde liegen. Eine "gehärtete" Verbindung wird bei Zugriff automatisch, -ohne dass die Anwendung dies bemerkt, wieder geöffnet, wenn sie geschlossen -wurde, die Datenbankverbindung unterbrochen wurde, oder wenn sie öfter als -ein optionales Limit genutzt wurde. +DBUtils installiert sich als Paket ``dbutils``, das alle hier beschriebenen +Module enthält. Jedes dieser Modul enthält im Wesentlichen eine Klasse, die +einen analogen Namen trägt und die jeweilige Funktionalität bereitstellt. +So enthält z.B. das Modul ``dbutils.pooled_db`` die Klasse ``PooledDB``. + +SimplePooledDB (simple_pooled_db) +--------------------------------- +Die Klasse ``SimplePooledDB`` in ``dbutils.simple_pooled_db`` ist eine sehr +elementare Referenz-Implementierung eines Pools von Datenbankverbindungen. +Hiermit ist ein Vorratsspeicher an Datenbankverbindungen gemeint, aus dem sich +die Python-Anwendung bedienen kann. Diese Implementierung ist weit weniger +ausgefeilt als das eigentliche ``pooled_db``-Modul und stellt insbesondere +keine Ausfallsicherung zur Verfügung. ``dbutils.simple_pooled_db`` ist im +Wesentlichen identisch mit dem zu Webware for Python gehörenden Modul +``MiscUtils.DBPool``. Es ist eher zur Verdeutlichung des Konzepts gedacht, +als zum Einsatz im produktiven Betrieb. + +SteadyDBConnection (steady_db) +------------------------------ +Die Klasse ``SteadyDBConnection`` im Modul ``dbutils.steady_db`` stellt +"gehärtete" Datenbankverbindungen bereit, denen gewöhnlichen Verbindungen +eines DB-API-2-Datenbankadapters zugrunde liegen. Eine "gehärtete" Verbindung +wird bei Zugriff automatisch, ohne dass die Anwendung dies bemerkt, wieder +geöffnet, wenn sie geschlossen wurde, die Datenbankverbindung unterbrochen +wurde, oder wenn sie öfter als ein optionales Limit genutzt wurde. Ein typisches Beispiel wo dies benötig wird, ist, wenn die Datenbank neu gestartet wurde, während Ihre Anwendung immer noch läuft und Verbindungen @@ -151,22 +144,22 @@ zur Datenbank offen hat, oder wenn Ihre Anwendung auf eine entfernte Datenbank über ein Netzwerk zugreift, das durch eine Firewall geschützt ist, und die Firewall neu gestartet wurde und dabei ihren Verbindungsstatus verloren hat. -Normalerweise benutzen Sie das ``SteadyDB``-Modul nicht direkt; es wird aber -von den beiden nächsten Modulen benötigt, ``PersistentDB`` und ``PooledDB``. +Normalerweise benutzen Sie das ``steady_db``-Modul nicht direkt; es wird aber +von den beiden nächsten Modulen benötigt, ``persistent_db`` und ``pooled_db``. -PersistentDB ------------- -``DBUtils.PersistentDB`` stellt gehärtete, thread-affine, persistente -Datenbankverbindungen zur Verfügung, unter Benutzung eines beliebigen -DB-API-2-Datenbankadapters. Mit "thread-affin" und "persistent" ist -hierbei gemeint, dass die einzelnen Datenbankverbindungen den jeweiligen -Threads fest zugeordnet bleiben und während der Laufzeit des Threads nicht -geschlossen werden. +PersistentDB (persistent_db) +---------------------------- +Die Klasse ``PersistentDB`` im Modul ``dbutils.persistent_db`` stellt +gehärtete, thread-affine, persistente Datenbankverbindungen zur Verfügung, +unter Benutzung eines beliebigen DB-API-2-Datenbankadapters. Mit "thread-affin" +und "persistent" ist hierbei gemeint, dass die einzelnen Datenbankverbindungen +den jeweiligen Threads fest zugeordnet bleiben und während der Laufzeit des +Threads nicht geschlossen werden. Das folgende Diagramm zeigt die beteiligten Verbindungsschichten, wenn Sie -``PersistentDB``-Datenbankverbindungen einsetzen: +``persistent_db``-Datenbankverbindungen einsetzen: -.. image:: persist.png +.. image:: persistent.png Immer wenn ein Thread eine Datenbankverbindung zum ersten Mal öffnet, wird eine neue Datenbankverbindung geöffnet, die von da an immer wieder für genau @@ -176,29 +169,29 @@ gleiche Thread wieder eine Datenbankverbindung anfordert, diese gleiche bereits geöffnete Datenbankverbindung wieder verwendet werden kann. Die Verbindung wird automatisch geschlossen, wenn der Thread beendet wird. -Kurz gesagt versucht ``PersistentDB`` Datenbankverbindungen wiederzuverwerten, +Kurz gesagt versucht ``persistent_db`` Datenbankverbindungen wiederzuverwerten, um die Gesamteffizienz der Datenbankzugriffe Ihrer Multithread-Anwendungen zu steigern, aber es wird dabei sichergestellt, dass verschiedene Threads niemals die gleiche Verbindung benutzen. -Daher arbeitet ``PersistentDB`` sogar dann problemlos, wenn der zugrunde +Daher arbeitet ``persistent_db`` sogar dann problemlos, wenn der zugrunde liegende DB-API-2-Datenbankadapter nicht thread-sicher auf der Verbindungsebene ist, oder wenn parallele Threads Parameter der Datenbank-Sitzung verändern oder Transaktionen mit mehreren SQL-Befehlen durchführen. -PooledDB --------- -``DBUtils.PooledDB`` stellt, unter Benutzung eines beliebigen -DB-API-2-Datenbankadapters, einen Pool von gehärteten, thread-sicheren -Datenbankverbindungen zur Verfügung, die automatisch, ohne dass die Anwendung -dies bemerkt, wiederverwendet werden. +PooledDB (pooled_db) +-------------------- +Die Klasse ``PooledDB`` im Modul ``dbutils.pooled_db`` stellt, unter Benutzung +eines beliebigen DB-API-2-Datenbankadapters, einen Pool von gehärteten, +thread-sicheren Datenbankverbindungen zur Verfügung, die automatisch, ohne dass +die Anwendung dies bemerkt, wiederverwendet werden. Das folgende Diagramm zeigt die beteiligten Verbindungsschichten, wenn Sie -``PooledDB``-Datenbankverbindungen einsetzen: +``pooled_db``-Datenbankverbindungen einsetzen: -.. image:: pool.png +.. image:: pooled.png -Wie im Diagramm angedeutet, kann ``PooledDB`` geöffnete Datenbankverbindungen +Wie im Diagramm angedeutet, kann ``pooled_db`` geöffnete Datenbankverbindungen den verschiedenen Threads beliebig zuteilen. Dies geschieht standardmäßig, wenn Sie den Verbindungspool mit einem positiven Wert für ``maxshared`` einrichten und der zugrunde liegende DB-API-2-Datenbankadapter auf der Verbindungsebene @@ -215,24 +208,24 @@ Datenbankverbindungen zurückgegeben, damit sie wiederverwertet werden kann. Wenn der zugrunde liegende DB-API-Datenbankadapter nicht thread-sicher ist, werden Thread-Locks verwendet, um sicherzustellen, dass die -``PooledDB``-Verbindungen dennoch thread-sicher sind. Sie brauchen sich also +``pooled_db``-Verbindungen dennoch thread-sicher sind. Sie brauchen sich also hierum keine Sorgen zu machen, aber Sie sollten darauf achten, dedizierte Datenbankverbindungen zu verwenden, sobald Sie Parameter der Datenbanksitzung verändern oder Transaktionen mit mehreren SQL-Befehlen ausführen. Die Qual der Wahl ----------------- -Sowohl ``PersistentDB`` als auch ``PooledDB`` dienen dem gleichen Zweck, +Sowohl ``persistent_db`` als auch ``pooled_db`` dienen dem gleichen Zweck, nämlich die Effizienz des Datenbankzugriffs durch Wiederverwendung von Datenbankverbindungen zu steigern, und dabei gleichzeitig die Stabilität zu gewährleisten, selbst wenn die Datenbankverbindung unterbrochen wird. Welches der beiden Module sollte also verwendet werden? Nach den obigen -Erklärungen ist es klar, dass ``PersistentDB`` dann sinnvoller ist, wenn +Erklärungen ist es klar, dass ``persistent_db`` dann sinnvoller ist, wenn Ihre Anwendung eine gleich bleibende Anzahl Threads verwendet, die häufig auf die Datenbank zugreifen. In diesem Fall werden Sie ungefähr die gleiche Anzahl geöffneter Datenbankverbindungen erhalten. Wenn jedoch Ihre Anwendung -häufig Threads beendet und neu startet, dann ist ``PooledDB`` die bessere +häufig Threads beendet und neu startet, dann ist ``pooled_db`` die bessere Lösung, die auch mehr Möglichkeiten zur Feineinstellung zur Verbesserung der Effizienz erlaubt, insbesondere bei Verwendung eines thread-sicheren DB-API-2-Datenbankadapters. @@ -249,19 +242,19 @@ der Initialisierung auch einige Unterschiede, sowohl zwischen den "Pooled"- und den "Persistent"-Varianten, als auch zwischen den DB-API-2- und den PyGreSQL-Varianten. -Wir werden hier nur auf das ``PersistentDB``-Modul und das etwas kompliziertere -``PooledDB``-Modul eingehen. Einzelheiten zu den anderen Modulen finden Sie +Wir werden hier nur auf das ``persistent_db``-Modul und das etwas kompliziertere +``pooled_db``-Modul eingehen. Einzelheiten zu den anderen Modulen finden Sie in deren Docstrings. Unter Verwendung der Python-Interpreter-Konsole können Sie -sich die Dokumentation des ``PooledDB``-Moduls wie folgt anzeigen lassen (dies +sich die Dokumentation des ``pooled_db``-Moduls wie folgt anzeigen lassen (dies funktioniert entsprechend auch mit den anderen Modulen):: - help(PooledDB) + help(pooled_db) -PersistentDB ------------- -Wenn Sie das ``PersistentDB``-Modul einsetzen möchten, müssen Sie zuerst einen +PersistentDB (persistent_db) +---------------------------- +Wenn Sie das ``persistent_db``-Modul einsetzen möchten, müssen Sie zuerst einen Generator für die von Ihnen gewünschte Art von Datenbankverbindungen einrichten, -indem Sie eine Instanz der Klasse ``PersistentDB`` erzeugen, wobei Sie folgende +indem Sie eine Instanz der Klasse ``persistent_db`` erzeugen, wobei Sie folgende Parameter angeben müssen: * ``creator``: entweder eine Funktion, die neue DB-API-2-Verbindungen @@ -304,7 +297,7 @@ möchten, dass jede Verbindung Ihrer lokalen Datenbank ``meinedb`` 1000 mal wiederverwendet werden soll, sieht die Initialisierung so aus:: import pgdb # importiere das verwendete DB-API-2-Modul - from DBUtils.PersistentDB import PersistentDB + from dbutils.persistent_db import PersistentDB persist = PersistentDB(pgdb, 1000, database='meinedb') Nachdem Sie den Generator mit diesen Parametern eingerichtet haben, können @@ -314,7 +307,7 @@ Sie derartige Datenbankverbindungen von da an wie folgt anfordern:: Sie können diese Verbindungen verwenden, als wären sie gewöhnliche DB-API-2-Datenbankverbindungen. Genauer genommen erhalten Sie die -"gehärtete" ``SteadyDB``-Version der zugrunde liegenden DB-API-2-Verbindung. +"gehärtete" ``steady_db``-Version der zugrunde liegenden DB-API-2-Verbindung. Wenn Sie eine solche persistente Verbindung mit ``db.close()`` schließen, wird dies stillschweigend ignoriert, denn sie würde beim nächsten Zugriff @@ -335,11 +328,11 @@ einigen Umgebungen nicht funktionieren (es ist zum Beispiel bekannt, dass ``mod_wsgi`` hier Probleme bereitet, da es Daten, die mit ``threading.local`` gespeichert wurden, zwischen Requests löscht). -PooledDB --------- -Wenn Sie das ``PooledDB``-Modul einsetzen möchten, müssen Sie zuerst einen +PooledDB (pooled_db) +-------------------- +Wenn Sie das ``pooled_db``-Modul einsetzen möchten, müssen Sie zuerst einen Pool für die von Ihnen gewünschte Art von Datenbankverbindungen einrichten, -indem Sie eine Instanz der Klasse ``PooledDB`` erzeugen, wobei Sie folgende +indem Sie eine Instanz der Klasse ``pooled_db`` erzeugen, wobei Sie folgende Parameter angeben müssen: * ``creator``: entweder eine Funktion, die neue DB-API-2-Verbindungen @@ -406,7 +399,7 @@ und einen Pool von mindestens fünf Datenbankverbindungen zu Ihrer Datenbank ``meinedb`` verwenden möchten, dann sieht die Initialisierung so aus:: import pgdb # importiere das verwendete DB-API-2-Modul - from DBUtils.PooledDB import PooledDB + from dbutils.pooled_db import PooledDB pool = PooledDB(pgdb, 5, database='meinedb') Nachdem Sie den Pool für Datenbankverbindungen so eingerichtet haben, können @@ -416,7 +409,7 @@ Sie Verbindungen daraus wie folgt anfordern:: Sie können diese Verbindungen verwenden, als wären sie gewöhnliche DB-API-2-Datenbankverbindungen. Genauer genommen erhalten Sie die -"gehärtete" ``SteadyDB``-Version der zugrunde liegenden DB-API-2-Verbindung. +"gehärtete" ``steady_db``-Version der zugrunde liegenden DB-API-2-Verbindung. Bitte beachten Sie, dass die Verbindung von anderen Threads mitgenutzt werden kann, wenn Sie den Parameter ``maxshared`` auf einen Wert größer als Null @@ -457,32 +450,6 @@ transparente Neueröffnen von Verbindungen bis zum Ende der Transaktion ausgesetzt wird, und dass die Verbindung zurückgerollt wird, bevor sie wieder an den Verbindungspool zurückgegeben wird. -Benutzung in Webware for Python -------------------------------- -Wenn Sie DBUtils verwenden, um von Servlets des Web-Frameworks `Webware -for Python`_ auf eine Datenbank zuzugreifen, dann müssen Sie sicherstellen, -dass die Generatoren zur Erzeugung von Datenbankverbindungen nur einmal -eingerichtet werden, wenn die Anwendung startet, und nicht jedes Mal, wenn -eine Servlet-Instanz erzeugt wird. Den hierfür nötigen Code können Sie -bei der Basis-Servlet-Klasse einfügen, dort wo das Modul oder die Klasse -initialisiert wird, oder Sie können die Funktion ``contextInitialize()`` -im ``__init__.py``-Skript Ihres Anwendungskontextes verwenden. - -Das zusammen mit DButils ausgelieferte Verzeichnis ``Examples`` enthält -einen Beispielkontext für Webware for Python, der eine kleine Demo-Datenbank -verwendet, um Teilnehmer an einer Seminarreihe zu verwalten (die Idee für -dieses Beispiel wurde dem Artikel "`The Python DB-API`_" von Andrew Kuchling -entnommen). - -Der Beispielkontext kann konfiguriert werden, indem entweder eine Konfig-Datei -``Configs/Database.config`` angelegt wird, oder indem die Standard-Parameter -direkt im Beispielservlet ``Examples/DBUtilsExample.py`` geändert werden. -Auf diese Weise können Sie einen passenden Datenbanknutzer und sein Passwort -festlegen, sowie den zugrunde liegenden Datenbankadapter auswählen (das -klassische PyGreSQL-Modul oder irgendein DB-API-2-Modul). Wenn der Parameter -``maxcached`` vorhanden ist, verwendet das Beispielservlet die -``Pooled``-Variante, andernfalls die ``Persistent``-Variante. - Anmerkungen =========== @@ -495,7 +462,7 @@ ausgelagert, in der Code von DBUtils verwendet wird. Wenn Sie eine Lösung verwenden wie den Apache-Webserver mit mod_python_ oder mod_wsgi_, dann sollten Sie bedenken, dass Ihr Python-Code normalerweise im Kontext der Kindprozesse des Webservers läuft. Wenn Sie also das -``PooledDB``-Modul einsetzen, und mehrere dieser Kindprozesse laufen, dann +``pooled_db``-Modul einsetzen, und mehrere dieser Kindprozesse laufen, dann werden Sie ebensoviele Pools mit Datenbankverbindungen erhalten. Wenn diese Prozesse viele Threads laufen lassen, dann mag dies eine sinnvoller Ansatz sein, wenn aber diese Prozesse nicht mehr als einen Worker-Thread starten, @@ -511,7 +478,7 @@ Einige Ideen für zukünftige Verbesserungen: * Alternativ zur Obergrenze in der Anzahl der Nutzung einer Datenbankverbindung könnte eine maximale Lebensdauer für die Verbindung implementiert werden. -* Es könnten Module ``MonitorDB`` und ``MonitorPg`` hinzugefügt werden, die +* Es könnten Module ``monitor_db`` und ``monitor_pg`` hinzugefügt werden, die in einem separaten Thread ständig den "idle pool" und eventuell auch den "shared pool" bzw. die persistenten Verbindungen überwachen. Wenn eine unterbrochene Datenbankverbindung entdeckt wird, wird diese automatisch durch @@ -529,12 +496,12 @@ Einige Ideen für zukünftige Verbesserungen: Fehlermeldungen und Feedback ============================ -Bitte Senden Sie Fehlermeldungen, Patches und Feedback direkt an den -Autor (unter Verwendung der unten angegebenen E-Mail-Adresse). - -Probleme, die Webware betreffen, können auch in der `Webware for Python -mailing list`_ diskutiert werden. +Fehlermeldungen, Patches und Feedback können Sie als Issues_ oder +`Pull Requests`_ auf der `GitHub-Projektseite`_ von DBUtils übermitteln. +.. _GitHub-Projektseite: https://github.com/WebwareForPython/DBUtils +.. _Issues: https://github.com/WebwareForPython/DBUtils/issues +.. _Pull Requests: https://github.com/WebwareForPython/DBUtils/pulls Links ===== @@ -554,24 +521,23 @@ Einige Links zu verwandter und alternativer Software: .. _DBUtils: https://github.com/WebwareForPython/DBUtils .. _Python: https://www.python.org .. _Webware for Python: https://webwareforpython.github.io/w4py/ -.. _Webware for Python mailing list: https://lists.sourceforge.net/lists/listinfo/webware-discuss .. _DB-API 2: https://www.python.org/dev/peps/pep-0249/ .. _The Python DB-API: http://www.linuxjournal.com/article/2605 .. _PostgresQL: https://www.postgresql.org/ -.. _PyGreSQL: http://www.pygresql.org/ -.. _SQLObject: http://www.sqlobject.org/ -.. _SQLAlchemy: http://www.sqlalchemy.org -.. _Apache: http://httpd.apache.org/ +.. _PyGreSQL: https://www.pygresql.org/ +.. _SQLObject: http://sqlobject.org/ +.. _SQLAlchemy: https://www.sqlalchemy.org +.. _Apache: https://httpd.apache.org/ .. _mod_python: http://modpython.org/ .. _mod_wsgi: https://github.com/GrahamDumpleton/mod_wsgi -.. _pgpool: http://www.pgpool.net/ +.. _pgpool: https://www.pgpool.net/ .. _pgbouncer: https://pgbouncer.github.io/ Autoren ======= -:Autor: Christoph Zwerschke <cito@online.de> +:Autor: `Christoph Zwerschke`_ :Beiträge: DBUtils benutzt Code, Anmerkungen und Vorschläge von Ian Bicking, Chuck Esterbrook (Webware for Python), Dan Green (DBTools), @@ -579,11 +545,12 @@ Autoren Warren Smith (DbConnectionPool), Ezio Vernacotola, Jehiah Czebotar, Matthew Harriger, Gregory Piñero und Josef van Eenbergen. +.. _Christoph Zwerschke: https://github.com/Cito Copyright und Lizenz ==================== -Copyright © 2005-2018 Christoph Zwerschke. +Copyright © 2005-2020 Christoph Zwerschke. Alle Rechte vorbehalten. DBUtils ist freie und quelloffene Software, diff --git a/docs/main.html b/docs/main.html index 52a6944..5023149 100644 --- a/docs/main.html +++ b/docs/main.html @@ -19,47 +19,46 @@ <h1 class="title">DBUtils User's Guide</h1> <div class="contents topic" id="contents"> <p class="topic-title first">Contents</p> <ul class="simple"> -<li><p><a class="reference internal" href="#synopsis" id="id4">Synopsis</a></p></li> -<li><p><a class="reference internal" href="#modules" id="id5">Modules</a></p></li> -<li><p><a class="reference internal" href="#download" id="id6">Download</a></p></li> -<li><p><a class="reference internal" href="#installation" id="id7">Installation</a></p> +<li><p><a class="reference internal" href="#synopsis" id="id5">Synopsis</a></p></li> +<li><p><a class="reference internal" href="#modules" id="id6">Modules</a></p></li> +<li><p><a class="reference internal" href="#download" id="id7">Download</a></p></li> +<li><p><a class="reference internal" href="#installation" id="id8">Installation</a></p> <ul> -<li><p><a class="reference internal" href="#installation-as-a-standalone-top-level-package" id="id8">Installation as a standalone (top-level) package</a></p></li> -<li><p><a class="reference internal" href="#installation-as-a-webware-for-python-subpackage-plug-in" id="id9">Installation as a Webware for Python subpackage (plug-in)</a></p></li> +<li><p><a class="reference internal" href="#id1" id="id9">Installation</a></p></li> </ul> </li> <li><p><a class="reference internal" href="#requirements" id="id10">Requirements</a></p></li> <li><p><a class="reference internal" href="#functionality" id="id11">Functionality</a></p> <ul> -<li><p><a class="reference internal" href="#simplepooleddb" id="id12">SimplePooledDB</a></p></li> -<li><p><a class="reference internal" href="#steadydb" id="id13">SteadyDB</a></p></li> -<li><p><a class="reference internal" href="#persistentdb" id="id14">PersistentDB</a></p></li> -<li><p><a class="reference internal" href="#pooleddb" id="id15">PooledDB</a></p></li> +<li><p><a class="reference internal" href="#simplepooleddb-simple-pooled-db" id="id12">SimplePooledDB (simple_pooled_db)</a></p></li> +<li><p><a class="reference internal" href="#steadydbconnection-steady-db" id="id13">SteadyDBConnection (steady_db)</a></p></li> +<li><p><a class="reference internal" href="#persistentdb-persistent-db" id="id14">PersistentDB (persistent_db)</a></p></li> +<li><p><a class="reference internal" href="#pooleddb-pooled-db" id="id15">PooledDB (pooled_db)</a></p></li> <li><p><a class="reference internal" href="#which-one-to-use" id="id16">Which one to use?</a></p></li> </ul> </li> <li><p><a class="reference internal" href="#usage" id="id17">Usage</a></p> <ul> -<li><p><a class="reference internal" href="#id1" id="id18">PersistentDB</a></p></li> -<li><p><a class="reference internal" href="#id2" id="id19">PooledDB</a></p></li> -<li><p><a class="reference internal" href="#usage-in-webware-for-python" id="id20">Usage in Webware for Python</a></p></li> +<li><p><a class="reference internal" href="#id2" id="id18">PersistentDB (persistent_db)</a></p></li> +<li><p><a class="reference internal" href="#id3" id="id19">PooledDB (pooled_db)</a></p></li> </ul> </li> -<li><p><a class="reference internal" href="#notes" id="id21">Notes</a></p></li> -<li><p><a class="reference internal" href="#future" id="id22">Future</a></p></li> -<li><p><a class="reference internal" href="#bug-reports-and-feedback" id="id23">Bug reports and feedback</a></p></li> -<li><p><a class="reference internal" href="#links" id="id24">Links</a></p></li> -<li><p><a class="reference internal" href="#credits" id="id25">Credits</a></p></li> -<li><p><a class="reference internal" href="#copyright-and-license" id="id26">Copyright and License</a></p></li> +<li><p><a class="reference internal" href="#notes" id="id20">Notes</a></p></li> +<li><p><a class="reference internal" href="#future" id="id21">Future</a></p></li> +<li><p><a class="reference internal" href="#bug-reports-and-feedback" id="id22">Bug reports and feedback</a></p></li> +<li><p><a class="reference internal" href="#links" id="id23">Links</a></p></li> +<li><p><a class="reference internal" href="#credits" id="id24">Credits</a></p></li> +<li><p><a class="reference internal" href="#copyright-and-license" id="id25">Copyright and License</a></p></li> </ul> </div> <div class="section" id="synopsis"> <h1>Synopsis</h1> <p><a class="reference external" href="https://github.com/WebwareForPython/DBUtils">DBUtils</a> is a suite of Python modules allowing to connect in a safe and -efficient way between a threaded <a class="reference external" href="https://www.python.org">Python</a> application and a database. DBUtils -has been written in view of <a class="reference external" href="https://webwareforpython.github.io/w4py/">Webware for Python</a> as the application and -<a class="reference external" href="http://www.pygresql.org/">PyGreSQL</a> as the adapter to a <a class="reference external" href="https://www.postgresql.org/">PostgreSQL</a> database, but it can be used -for any other Python application and <a class="reference external" href="https://www.python.org/dev/peps/pep-0249/">DB-API 2</a> conformant database adapter.</p> +efficient way between a threaded <a class="reference external" href="https://www.python.org">Python</a> application and a database.</p> +<p>DBUtils has been originally written particularly for <a class="reference external" href="https://webwareforpython.github.io/w4py/">Webware for Python</a> as +the application and <a class="reference external" href="https://www.pygresql.org/">PyGreSQL</a> as the adapter to a <a class="reference external" href="https://www.postgresql.org/">PostgreSQL</a> database, but it +can meanwhile be used for any other Python application and <a class="reference external" href="https://www.python.org/dev/peps/pep-0249/">DB-API 2</a> +conformant database adapter.</p> </div> <div class="section" id="modules"> <h1>Modules</h1> @@ -68,58 +67,58 @@ <h1>Modules</h1> the other one for use with the classic PyGreSQL module.</p> <table> <colgroup> -<col style="width: 31%" /> -<col style="width: 69%" /> +<col style="width: 30%" /> +<col style="width: 70%" /> </colgroup> <thead> <tr><th class="head" colspan="2"><p>Universal DB-API 2 variant</p></th> </tr> </thead> <tbody> -<tr><td><p>SteadyDB.py</p></td> +<tr><td><p>steady_db</p></td> <td><p>Hardened DB-API 2 connections</p></td> </tr> -<tr><td><p>PooledDB.py</p></td> +<tr><td><p>pooled_db</p></td> <td><p>Pooling for DB-API 2 connections</p></td> </tr> -<tr><td><p>PersistentDB.py</p></td> +<tr><td><p>persistent_db</p></td> <td><p>Persistent DB-API 2 connections</p></td> </tr> -<tr><td><p>SimplePooledDB.py</p></td> +<tr><td><p>simple_pooled_db</p></td> <td><p>Simple pooling for DB-API 2</p></td> </tr> </tbody> </table> <table> <colgroup> -<col style="width: 31%" /> -<col style="width: 69%" /> +<col style="width: 30%" /> +<col style="width: 70%" /> </colgroup> <thead> <tr><th class="head" colspan="2"><p>Classic PyGreSQL variant</p></th> </tr> </thead> <tbody> -<tr><td><p>SteadyPg.py</p></td> +<tr><td><p>steady_pg</p></td> <td><p>Hardened classic PyGreSQL connections</p></td> </tr> -<tr><td><p>PooledPg.py</p></td> +<tr><td><p>pooled_pg</p></td> <td><p>Pooling for classic PyGreSQL connections</p></td> </tr> -<tr><td><p>PersistentPg.py</p></td> +<tr><td><p>persistent_pg</p></td> <td><p>Persistent classic PyGreSQL connections</p></td> </tr> -<tr><td><p>SimplePooledPg.py</p></td> +<tr><td><p>simple_pooled_pg</p></td> <td><p>Simple pooling for classic PyGreSQL</p></td> </tr> </tbody> </table> <p>The dependencies of the modules in the universal DB-API 2 variant are as indicated in the following diagram:</p> -<img alt="dbdep.png" src="dbdep.png" /> +<img alt="dependencies_db.png" src="dependencies_db.png" /> <p>The dependencies of the modules in the classic PyGreSQL variant are similar:</p> -<img alt="pgdep.png" src="pgdep.png" /> +<img alt="dependencies_pg.png" src="dependencies_pg.png" /> </div> <div class="section" id="download"> <h1>Download</h1> @@ -131,31 +130,18 @@ <h1>Download</h1> </div> <div class="section" id="installation"> <h1>Installation</h1> -<div class="section" id="installation-as-a-standalone-top-level-package"> -<h2>Installation as a standalone (top-level) package</h2> -<p>If you intend to use DBUtils from other applications than Webware for Python, -it is recommended to install the package in the usual way:</p> +<div class="section" id="id1"> +<h2>Installation</h2> +<p>The package can be installed in the usual way:</p> <pre class="literal-block">python setup.py install</pre> -<p>You can also use <a class="reference external" href="https://pip.pypa.io/">pip</a> for download and installation:</p> +<p>It is even easier to download and install the package in one go using <a class="reference external" href="https://pip.pypa.io/">pip</a>:</p> <pre class="literal-block">pip install DBUtils</pre> </div> -<div class="section" id="installation-as-a-webware-for-python-subpackage-plug-in"> -<h2>Installation as a Webware for Python subpackage (plug-in)</h2> -<p>If you want to use DBUtils as a supplement for the Webware for Python -framework only, you should install it as a Webware plug-in:</p> -<pre class="literal-block">python setup.py install --install-lib=/path/to/Webware</pre> -<p>Replace <span class="docutils literal">/path/to/Webware</span> with the path to the root directory of -your Webware for Python installation. You will also need to run the -Webware installer if this has not been done already or if you want to -integrate the DBUtils documentation into the Webware documentation:</p> -<pre class="literal-block">cd path/to/Webware -python install.py</pre> -</div> </div> <div class="section" id="requirements"> <h1>Requirements</h1> -<p>DBUtils supports <a class="reference external" href="https://www.python.org">Python</a> version 2.6 and Python versions 3.5 to 3.8.</p> -<p>The modules in the classic PyGreSQL variant need <a class="reference external" href="http://www.pygresql.org/">PyGreSQL</a> version 4.0 +<p>DBUtils supports <a class="reference external" href="https://www.python.org">Python</a> version 2.7 and Python versions 3.5 to 3.9.</p> +<p>The modules in the classic PyGreSQL variant need <a class="reference external" href="https://www.pygresql.org/">PyGreSQL</a> version 4.0 or above, while the modules in the universal DB-API 2 variant run with any Python <a class="reference external" href="https://www.python.org/dev/peps/pep-0249/">DB-API 2</a> compliant database interface module.</p> </div> @@ -163,61 +149,68 @@ <h1>Requirements</h1> <h1>Functionality</h1> <p>This section will refer to the names in the DB-API 2 variant only, but the same applies to the classic PyGreSQL variant.</p> -<div class="section" id="simplepooleddb"> -<h2>SimplePooledDB</h2> -<p><span class="docutils literal">DBUtils.SimplePooledDB</span> is a very basic reference implementation of -a pooled database connection. It is much less sophisticated than the -regular <span class="docutils literal">PooledDB</span> module and is particularly lacking the failover -functionality. <span class="docutils literal">DBUtils.SimplePooledDB</span> is essentially the same as -the <span class="docutils literal">MiscUtils.DBPool</span> module that is part of Webware for Python. +<p>DBUtils installs itself as a package <span class="docutils literal">dbutils</span> containing all the modules +that are described in this guide. Each of these modules contains essentially +one class with an analogous name that provides the corresponding functionality. +For instance, the module <span class="docutils literal">dbutils.pooled_db</span> contains the class <span class="docutils literal">PooledDB</span>.</p> +<div class="section" id="simplepooleddb-simple-pooled-db"> +<h2>SimplePooledDB (simple_pooled_db)</h2> +<p>The class <span class="docutils literal">SimplePooledDB</span> in <span class="docutils literal">dbutils.simple_pooled_db</span> is a very basic +reference implementation of a pooled database connection. It is much less +sophisticated than the regular <span class="docutils literal">pooled_db</span> module and is particularly lacking +the failover functionality. <span class="docutils literal">dbutils.simple_pooled_db</span> is essentially the +same as the <span class="docutils literal">MiscUtils.DBPool</span> module that is part of Webware for Python. You should consider it a demonstration of concept rather than something that should go into production.</p> </div> -<div class="section" id="steadydb"> -<h2>SteadyDB</h2> -<p><span class="docutils literal">DBUtils.SteadyDB</span> is a module implementing "hardened" connections -to a database, based on ordinary connections made by any DB-API 2 -database module. A "hardened" connection will transparently reopen upon -access when it has been closed or the database connection has been lost +<div class="section" id="steadydbconnection-steady-db"> +<h2>SteadyDBConnection (steady_db)</h2> +<p>The class <span class="docutils literal">SteadyDBConnection</span> in the module <span class="docutils literal">dbutils.steady_db</span> implements +"hardened" connections to a database, based on ordinary connections made by any +DB-API 2 database module. A "hardened" connection will transparently reopen +upon access when it has been closed or the database connection has been lost or when it is used more often than an optional usage limit.</p> <p>A typical example where this is needed is when the database has been restarted while your application is still running and has open connections to the database, or when your application accesses a remote database in a network that is separated by a firewall and the firewall has been restarted and lost its state.</p> -<p>Usually, you will not use the <span class="docutils literal">SteadyDB</span> module directly; it merely serves -as a basis for the next two modules, <span class="docutils literal">PersistentDB</span> and <span class="docutils literal">PooledDB</span>.</p> +<p>Usually, you will not use the <span class="docutils literal">steady_db</span> module directly; it merely serves +as a basis for the next two modules, <span class="docutils literal">persistent_db</span> and <span class="docutils literal">Pooled_db</span>.</p> </div> -<div class="section" id="persistentdb"> -<h2>PersistentDB</h2> -<p><span class="docutils literal">DBUtils.PersistentDB</span> implements steady, thread-affine, persistent -connections to a database, using any DB-API 2 database module.</p> +<div class="section" id="persistentdb-persistent-db"> +<h2>PersistentDB (persistent_db)</h2> +<p>The class <span class="docutils literal">PersistentDB</span> in the module <span class="docutils literal">dbutils.persistent_db</span> implements +steady, thread-affine, persistent connections to a database, using any DB-API 2 +database module. "Thread-affine" and "persistent" means that the individual +database connections stay assigned to the respective threads and will not be +closed during the lifetime of the threads.</p> <p>The following diagram shows the connection layers involved when you -are using <span class="docutils literal">PersistentDB</span> connections:</p> -<img alt="persist.png" src="persist.png" /> +are using <span class="docutils literal">persistent_db</span> connections:</p> +<img alt="persistent.png" src="persistent.png" /> <p>Whenever a thread opens a database connection for the first time, a new connection to the database will be opened that will be used from now on for this specific thread. When the thread closes the database connection, it will still be kept open so that the next time when a connection is requested by the same thread, this already opened connection can be used. The connection will be closed automatically when the thread dies.</p> -<p>In short: <span class="docutils literal">PersistentDB</span> tries to recycle database connections to +<p>In short: <span class="docutils literal">persistent_db</span> tries to recycle database connections to increase the overall database access performance of your threaded application, but it makes sure that connections are never shared between threads.</p> -<p>Therefore, <span class="docutils literal">PersistentDB</span> will work perfectly even if the underlying +<p>Therefore, <span class="docutils literal">persistent_db</span> will work perfectly even if the underlying DB-API module is not thread-safe at the connection level, and it will avoid problems when other threads change the database session or perform transactions spreading over more than one SQL command.</p> </div> -<div class="section" id="pooleddb"> -<h2>PooledDB</h2> -<p><span class="docutils literal">DBUtils.PooledDB</span> implements a pool of steady, thread-safe cached -connections to a database which are transparently reused, using any -DB-API 2 database module.</p> +<div class="section" id="pooleddb-pooled-db"> +<h2>PooledDB (pooled_db)</h2> +<p>The class <span class="docutils literal">PooledDB</span> in the module <span class="docutils literal">dbutils.pooled_db</span> implements a pool +of steady, thread-safe cached connections to a database which are transparently +reused, using any DB-API 2 database module.</p> <p>The following diagram shows the connection layers involved when you -are using <span class="docutils literal">PooledDB</span> connections:</p> -<img alt="pool.png" src="pool.png" /> -<p>As the diagram indicates, <span class="docutils literal">PooledDB</span> can share opened database connections +are using <span class="docutils literal">pooled_db</span> connections:</p> +<img alt="pooled.png" src="pooled.png" /> +<p>As the diagram indicates, <span class="docutils literal">pooled_db</span> can share opened database connections between different threads. This will happen by default if you set up the connection pool with a positive value of <span class="docutils literal">maxshared</span> and the underlying DB-API 2 is thread-safe at the connection level, but you can also request @@ -229,22 +222,22 @@ <h2>PooledDB</h2> connection that is not shared any more, it is returned back to the pool of idle connections so that it can be recycled again.</p> <p>If the underlying DB-API module is not thread-safe, thread locks will be -used to ensure that the <span class="docutils literal">PooledDB</span> connections are thread-safe. So you +used to ensure that the <span class="docutils literal">pooled_db</span> connections are thread-safe. So you don't need to worry about that, but you should be careful to use dedicated connections whenever you change the database session or perform transactions spreading over more than one SQL command.</p> </div> <div class="section" id="which-one-to-use"> <h2>Which one to use?</h2> -<p>Both <span class="docutils literal">PersistentDB</span> and <span class="docutils literal">PooledDB</span> serve the same purpose to improve +<p>Both <span class="docutils literal">persistent_db</span> and <span class="docutils literal">pooled_db</span> serve the same purpose to improve the database access performance by recycling database connections, while preserving stability even if database connection will be disrupted.</p> <p>So which of these two modules should you use? From the above explanations -it is clear that <span class="docutils literal">PersistentDB</span> will make more sense if your application +it is clear that <span class="docutils literal">persistent_db</span> will make more sense if your application keeps a constant number of threads which frequently use the database. In this case, you will always have the same amount of open database connections. However, if your application frequently starts and ends threads, then it -will be better to use <span class="docutils literal">PooledDB</span>. The latter will also allow more +will be better to use <span class="docutils literal">pooled_db</span>. The latter will also allow more fine-tuning, particularly if you are using a thread-safe DB-API 2 module.</p> <p>Since the interface of both modules is similar, you can easily switch from one to the other and check which one will suit better.</p> @@ -255,17 +248,17 @@ <h1>Usage</h1> <p>The usage of all the modules is similar, but there are also some differences in the initialization between the "Pooled" and "Persistent" variants and also between the universal DB-API 2 and the classic PyGreSQL variants.</p> -<p>We will cover here only the <span class="docutils literal">PersistentDB</span> module and the more complex -<span class="docutils literal">PooledDB</span> module. For the details of the other modules, have a look +<p>We will cover here only the <span class="docutils literal">persistent_db</span> module and the more complex +<span class="docutils literal">pooled_db</span> module. For the details of the other modules, have a look at their module docstrings. Using the Python interpreter console, you can -display the documentation of the <span class="docutils literal">PooledDB</span> module as follows (this +display the documentation of the <span class="docutils literal">pooled_db</span> module as follows (this works analogously for the other modules):</p> -<pre class="literal-block">help(PooledDB)</pre> -<div class="section" id="id1"> -<h2>PersistentDB</h2> -<p>In order to make use of the <span class="docutils literal">PersistentDB</span> module, you first need to set +<pre class="literal-block">help(pooled_db)</pre> +<div class="section" id="id2"> +<h2>PersistentDB (persistent_db)</h2> +<p>In order to make use of the <span class="docutils literal">persistent_db</span> module, you first need to set up a generator for your kind of database connections by creating an instance -of <span class="docutils literal">PersistentDB</span>, passing the following parameters:</p> +of <span class="docutils literal">persistent_db</span>, passing the following parameters:</p> <ul> <li><p><span class="docutils literal">creator</span>: either an arbitrary function returning new DB-API 2 connection objects or a DB-API 2 compliant database module</p></li> @@ -297,13 +290,13 @@ <h2>PersistentDB</h2> <p>For instance, if you are using <span class="docutils literal">pgdb</span> as your DB-API 2 database module and want every connection to your local database <span class="docutils literal">mydb</span> to be reused 1000 times:</p> <pre class="literal-block">import pgdb # import used DB-API 2 module -from DBUtils.PersistentDB import PersistentDB +from dbutils.persistent_db import PersistentDB persist = PersistentDB(pgdb, 1000, database='mydb')</pre> <p>Once you have set up the generator with these parameters, you can request database connections of that kind:</p> <pre class="literal-block">db = persist.connection()</pre> <p>You can use these connections just as if they were ordinary DB-API 2 -connections. Actually what you get is the hardened <span class="docutils literal">SteadyDB</span> version of +connections. Actually what you get is the hardened <span class="docutils literal">steady_db</span> version of the underlying DB-API 2 connection.</p> <p>Closing a persistent connection with <span class="docutils literal">db.close()</span> will be silently ignored since it would be reopened at the next usage anyway and @@ -319,10 +312,10 @@ <h2>PersistentDB</h2> environments (for instance, <span class="docutils literal">mod_wsgi</span> is known to cause problems since it clears the <span class="docutils literal">threading.local</span> data between requests).</p> </div> -<div class="section" id="id2"> -<h2>PooledDB</h2> -<p>In order to make use of the <span class="docutils literal">PooledDB</span> module, you first need to set up the -database connection pool by creating an instance of <span class="docutils literal">PooledDB</span>, passing the +<div class="section" id="id3"> +<h2>PooledDB (pooled_db)</h2> +<p>In order to make use of the <span class="docutils literal">pooled_db</span> module, you first need to set up the +database connection pool by creating an instance of <span class="docutils literal">pooled_db</span>, passing the following parameters:</p> <ul> <li><p><span class="docutils literal">creator</span>: either an arbitrary function returning new DB-API 2 @@ -369,13 +362,13 @@ <h2>PooledDB</h2> <p>For instance, if you are using <span class="docutils literal">pgdb</span> as your DB-API 2 database module and want a pool of at least five connections to your local database <span class="docutils literal">mydb</span>:</p> <pre class="literal-block">import pgdb # import used DB-API 2 module -from DBUtils.PooledDB import PooledDB +from dbutils.pooled_db import PooledDB pool = PooledDB(pgdb, 5, database='mydb')</pre> <p>Once you have set up the connection pool you can request database connections from that pool:</p> <pre class="literal-block">db = pool.connection()</pre> <p>You can use these connections just as if they were ordinary DB-API 2 -connections. Actually what you get is the hardened <span class="docutils literal">SteadyDB</span> version of +connections. Actually what you get is the hardened <span class="docutils literal">steady_db</span> version of the underlying DB-API 2 connection.</p> <p>Please note that the connection may be shared with other threads by default if you set a non-zero <span class="docutils literal">maxshared</span> parameter and the DB-API 2 module allows @@ -402,45 +395,23 @@ <h2>PooledDB</h2> until the end of the transaction, and that the connection will be rolled back before being given back to the connection pool.</p> </div> -<div class="section" id="usage-in-webware-for-python"> -<h2>Usage in Webware for Python</h2> -<p>If you are using DBUtils in order to access a database from <a class="reference external" href="https://webwareforpython.github.io/w4py/">Webware -for Python</a> servlets, you need to make sure that you set up your -database connection generators only once when the application starts, -and not every time a servlet instance is created. For this purpose, -you can add the necessary code to the module or class initialization -code of your base servlet class, or you can use the <span class="docutils literal">contextInitialize()</span> -function in the <span class="docutils literal">__init__.py</span> script of your application context.</p> -<p>The directory <span class="docutils literal">Examples</span> that is part of the DButils distribution -contains an example context for Webware for Python that uses a small -demo database designed to track the attendees for a series of seminars -(the idea for this example has been taken from the article -"<a class="reference external" href="http://www.linuxjournal.com/article/2605">The Python DB-API</a>" by Andrew Kuchling).</p> -<p>The example context can be configured by either creating a config file -<span class="docutils literal">Configs/Database.config</span> or by directly changing the default parameters -in the example servlet <span class="docutils literal">Examples/DBUtilsExample.py</span>. This way you can -set an appropriate database user and password, and you can choose the -underlying database module (PyGreSQL classic or any DB-API 2 module). -If the setting <span class="docutils literal">maxcached</span> is present, then the example servlet will use -the "Pooled" variant, otherwise it will use the "Persistent" variant.</p> -</div> </div> <div class="section" id="notes"> <h1>Notes</h1> <p>If you are using one of the popular object-relational mappers <a class="reference external" href="http://www.sqlobject.org/">SQLObject</a> -or <a class="reference external" href="http://www.sqlalchemy.org">SQLAlchemy</a>, you won't need DBUtils, since they come with their own +or <a class="reference external" href="https://www.sqlalchemy.org">SQLAlchemy</a>, you won't need DBUtils, since they come with their own connection pools. SQLObject 2 (SQL-API) is actually borrowing some code from DBUtils to split the pooling out into a separate layer.</p> <p>Also note that when you are using a solution like the Apache webserver with <a class="reference external" href="http://modpython.org/">mod_python</a> or <a class="reference external" href="https://github.com/GrahamDumpleton/mod_wsgi">mod_wsgi</a>, then your Python code will be usually run in the context of the webserver's child processes. So if you are using -the <span class="docutils literal">PooledDB</span> module, and several of these child processes are running, +the <span class="docutils literal">pooled_db</span> module, and several of these child processes are running, you will have as much database connection pools. If these processes are running many threads, this may still be a reasonable approach, but if these processes don't spawn more than one worker thread, as in the case of Apache's "prefork" multi-processing module, this approach does not make sense. If you're running such a configuration, you should resort to a middleware -for connection pooling that supports multi-processing, such as <a class="reference external" href="http://www.pgpool.net/">pgpool</a> +for connection pooling that supports multi-processing, such as <a class="reference external" href="https://www.pgpool.net/">pgpool</a> or <a class="reference external" href="https://pgbouncer.github.io/">pgbouncer</a> for the PostgreSQL database.</p> </div> <div class="section" id="future"> @@ -449,7 +420,7 @@ <h1>Future</h1> <ul class="simple"> <li><p>Alternatively to the maximum number of uses of a connection, implement a maximum time to live for connections.</p></li> -<li><p>Create modules <span class="docutils literal">MonitorDB</span> and <span class="docutils literal">MonitorPg</span> that will run in a separate +<li><p>Create modules <span class="docutils literal">monitor_db</span> and <span class="docutils literal">monitor_pg</span> that will run in a separate thread, monitoring the pool of the idle connections and maybe also the shared connections respectively the thread-affine connections. If a disrupted connection is detected, then it will be reestablished automatically @@ -466,10 +437,8 @@ <h1>Future</h1> </div> <div class="section" id="bug-reports-and-feedback"> <h1>Bug reports and feedback</h1> -<p>Please send bug reports, patches and feedback directly to the author -(using the email address given below).</p> -<p>If there are Webware related problems, these can also be discussed in -the <a class="reference external" href="https://lists.sourceforge.net/lists/listinfo/webware-discuss">Webware for Python mailing list</a>.</p> +<p>You can transmit bug reports, patches and feedback by creating <a class="reference external" href="https://github.com/WebwareForPython/DBUtils/issues">issues</a> or +<a class="reference external" href="https://github.com/WebwareForPython/DBUtils/pulls">pull requests</a> on the GitHub project page for DBUtils.</p> </div> <div class="section" id="links"> <h1>Links</h1> @@ -480,18 +449,18 @@ <h1>Links</h1> <li><p><a class="reference external" href="https://webwareforpython.github.io/w4py/">Webware for Python</a> framework</p></li> <li><p>Python <a class="reference external" href="https://www.python.org/dev/peps/pep-0249/">DB-API 2</a></p></li> <li><p><a class="reference external" href="https://www.postgresql.org/">PostgreSQL</a> database</p></li> -<li><p><a class="reference external" href="http://www.pygresql.org/">PyGreSQL</a> Python adapter for PostgreSQL</p></li> -<li><p><a class="reference external" href="http://www.pgpool.net/">pgpool</a> middleware for PostgreSQL connection pooling</p></li> +<li><p><a class="reference external" href="https://www.pygresql.org/">PyGreSQL</a> Python adapter for PostgreSQL</p></li> +<li><p><a class="reference external" href="https://www.pgpool.net/">pgpool</a> middleware for PostgreSQL connection pooling</p></li> <li><p><a class="reference external" href="https://pgbouncer.github.io/">pgbouncer</a> lightweight PostgreSQL connection pooling</p></li> <li><p><a class="reference external" href="http://www.sqlobject.org/">SQLObject</a> object-relational mapper</p></li> -<li><p><a class="reference external" href="http://www.sqlalchemy.org">SQLAlchemy</a> object-relational mapper</p></li> +<li><p><a class="reference external" href="https://www.sqlalchemy.org">SQLAlchemy</a> object-relational mapper</p></li> </ul> </div> <div class="section" id="credits"> <h1>Credits</h1> <dl class="field-list simple"> <dt>Author</dt> -<dd><p>Christoph Zwerschke <<a class="reference external" href="mailto:cito@online.de">cito@online.de</a>></p> +<dd><p><a class="reference external" href="https://github.com/Cito">Christoph Zwerschke</a></p> </dd> <dt>Contributions</dt> <dd><p>DBUtils uses code, input and suggestions made by @@ -504,7 +473,7 @@ <h1>Credits</h1> </div> <div class="section" id="copyright-and-license"> <h1>Copyright and License</h1> -<p>Copyright © 2005-2018 by Christoph Zwerschke. +<p>Copyright © 2005-2020 by Christoph Zwerschke. All Rights Reserved.</p> <p>DBUtils is free and open source software, licensed under the <a class="reference external" href="https://opensource.org/licenses/MIT">MIT license</a>.</p> diff --git a/docs/main.rst b/docs/main.rst index 59b0b70..96f9804 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -13,10 +13,12 @@ Synopsis ======== DBUtils_ is a suite of Python modules allowing to connect in a safe and -efficient way between a threaded Python_ application and a database. DBUtils -has been written in view of `Webware for Python`_ as the application and -PyGreSQL_ as the adapter to a PostgreSQL_ database, but it can be used -for any other Python application and `DB-API 2`_ conformant database adapter. +efficient way between a threaded Python_ application and a database. + +DBUtils has been originally written particularly for `Webware for Python`_ as +the application and PyGreSQL_ as the adapter to a PostgreSQL_ database, but it +can meanwhile be used for any other Python application and `DB-API 2`_ +conformant database adapter. Modules @@ -26,39 +28,39 @@ The DBUtils suite is realized as a Python package containing two subsets of modules, one for use with arbitrary DB-API 2 modules, the other one for use with the classic PyGreSQL module. -+-------------------+------------------------------------------+ -| Universal DB-API 2 variant | -+===================+==========================================+ -| SteadyDB.py | Hardened DB-API 2 connections | -+-------------------+------------------------------------------+ -| PooledDB.py | Pooling for DB-API 2 connections | -+-------------------+------------------------------------------+ -| PersistentDB.py | Persistent DB-API 2 connections | -+-------------------+------------------------------------------+ -| SimplePooledDB.py | Simple pooling for DB-API 2 | -+-------------------+------------------------------------------+ - -+-------------------+------------------------------------------+ -| Classic PyGreSQL variant | -+===================+==========================================+ -| SteadyPg.py | Hardened classic PyGreSQL connections | -+-------------------+------------------------------------------+ -| PooledPg.py | Pooling for classic PyGreSQL connections | -+-------------------+------------------------------------------+ -| PersistentPg.py | Persistent classic PyGreSQL connections | -+-------------------+------------------------------------------+ -| SimplePooledPg.py | Simple pooling for classic PyGreSQL | -+-------------------+------------------------------------------+ ++------------------+------------------------------------------+ +| Universal DB-API 2 variant | ++==================+==========================================+ +| steady_db | Hardened DB-API 2 connections | ++------------------+------------------------------------------+ +| pooled_db | Pooling for DB-API 2 connections | ++------------------+------------------------------------------+ +| persistent_db | Persistent DB-API 2 connections | ++------------------+------------------------------------------+ +| simple_pooled_db | Simple pooling for DB-API 2 | ++------------------+------------------------------------------+ + ++------------------+------------------------------------------+ +| Classic PyGreSQL variant | ++==================+==========================================+ +| steady_pg | Hardened classic PyGreSQL connections | ++------------------+------------------------------------------+ +| pooled_pg | Pooling for classic PyGreSQL connections | ++------------------+------------------------------------------+ +| persistent_pg | Persistent classic PyGreSQL connections | ++------------------+------------------------------------------+ +| simple_pooled_pg | Simple pooling for classic PyGreSQL | ++------------------+------------------------------------------+ The dependencies of the modules in the universal DB-API 2 variant are as indicated in the following diagram: -.. image:: dbdep.png +.. image:: dependencies_db.png The dependencies of the modules in the classic PyGreSQL variant are similar: -.. image:: pgdep.png +.. image:: dependencies_pg.png Download @@ -77,39 +79,23 @@ The source code repository can be found here on GitHub:: Installation ============ -Installation as a standalone (top-level) package ------------------------------------------------- -If you intend to use DBUtils from other applications than Webware for Python, -it is recommended to install the package in the usual way:: +Installation +------------ +The package can be installed in the usual way:: python setup.py install -You can also use `pip`_ for download and installation:: +It is even easier to download and install the package in one go using `pip`_:: pip install DBUtils .. _pip: https://pip.pypa.io/ -Installation as a Webware for Python subpackage (plug-in) ---------------------------------------------------------- -If you want to use DBUtils as a supplement for the Webware for Python -framework only, you should install it as a Webware plug-in:: - - python setup.py install --install-lib=/path/to/Webware - -Replace ``/path/to/Webware`` with the path to the root directory of -your Webware for Python installation. You will also need to run the -Webware installer if this has not been done already or if you want to -integrate the DBUtils documentation into the Webware documentation:: - - cd path/to/Webware - python install.py - Requirements ============ -DBUtils supports Python_ version 2.6 and Python versions 3.5 to 3.8. +DBUtils supports Python_ version 2.7 and Python versions 3.5 to 3.9. The modules in the classic PyGreSQL variant need PyGreSQL_ version 4.0 or above, while the modules in the universal DB-API 2 variant run with @@ -122,22 +108,27 @@ Functionality This section will refer to the names in the DB-API 2 variant only, but the same applies to the classic PyGreSQL variant. -SimplePooledDB --------------- -``DBUtils.SimplePooledDB`` is a very basic reference implementation of -a pooled database connection. It is much less sophisticated than the -regular ``PooledDB`` module and is particularly lacking the failover -functionality. ``DBUtils.SimplePooledDB`` is essentially the same as -the ``MiscUtils.DBPool`` module that is part of Webware for Python. +DBUtils installs itself as a package ``dbutils`` containing all the modules +that are described in this guide. Each of these modules contains essentially +one class with an analogous name that provides the corresponding functionality. +For instance, the module ``dbutils.pooled_db`` contains the class ``PooledDB``. + +SimplePooledDB (simple_pooled_db) +--------------------------------- +The class ``SimplePooledDB`` in ``dbutils.simple_pooled_db`` is a very basic +reference implementation of a pooled database connection. It is much less +sophisticated than the regular ``pooled_db`` module and is particularly lacking +the failover functionality. ``dbutils.simple_pooled_db`` is essentially the +same as the ``MiscUtils.DBPool`` module that is part of Webware for Python. You should consider it a demonstration of concept rather than something that should go into production. -SteadyDB --------- -``DBUtils.SteadyDB`` is a module implementing "hardened" connections -to a database, based on ordinary connections made by any DB-API 2 -database module. A "hardened" connection will transparently reopen upon -access when it has been closed or the database connection has been lost +SteadyDBConnection (steady_db) +------------------------------ +The class ``SteadyDBConnection`` in the module ``dbutils.steady_db`` implements +"hardened" connections to a database, based on ordinary connections made by any +DB-API 2 database module. A "hardened" connection will transparently reopen +upon access when it has been closed or the database connection has been lost or when it is used more often than an optional usage limit. A typical example where this is needed is when the database has been @@ -146,18 +137,21 @@ to the database, or when your application accesses a remote database in a network that is separated by a firewall and the firewall has been restarted and lost its state. -Usually, you will not use the ``SteadyDB`` module directly; it merely serves -as a basis for the next two modules, ``PersistentDB`` and ``PooledDB``. +Usually, you will not use the ``steady_db`` module directly; it merely serves +as a basis for the next two modules, ``persistent_db`` and ``Pooled_db``. -PersistentDB ------------- -``DBUtils.PersistentDB`` implements steady, thread-affine, persistent -connections to a database, using any DB-API 2 database module. +PersistentDB (persistent_db) +---------------------------- +The class ``PersistentDB`` in the module ``dbutils.persistent_db`` implements +steady, thread-affine, persistent connections to a database, using any DB-API 2 +database module. "Thread-affine" and "persistent" means that the individual +database connections stay assigned to the respective threads and will not be +closed during the lifetime of the threads. The following diagram shows the connection layers involved when you -are using ``PersistentDB`` connections: +are using ``persistent_db`` connections: -.. image:: persist.png +.. image:: persistent.png Whenever a thread opens a database connection for the first time, a new connection to the database will be opened that will be used from now on @@ -166,27 +160,27 @@ it will still be kept open so that the next time when a connection is requested by the same thread, this already opened connection can be used. The connection will be closed automatically when the thread dies. -In short: ``PersistentDB`` tries to recycle database connections to +In short: ``persistent_db`` tries to recycle database connections to increase the overall database access performance of your threaded application, but it makes sure that connections are never shared between threads. -Therefore, ``PersistentDB`` will work perfectly even if the underlying +Therefore, ``persistent_db`` will work perfectly even if the underlying DB-API module is not thread-safe at the connection level, and it will avoid problems when other threads change the database session or perform transactions spreading over more than one SQL command. -PooledDB --------- -``DBUtils.PooledDB`` implements a pool of steady, thread-safe cached -connections to a database which are transparently reused, using any -DB-API 2 database module. +PooledDB (pooled_db) +-------------------- +The class ``PooledDB`` in the module ``dbutils.pooled_db`` implements a pool +of steady, thread-safe cached connections to a database which are transparently +reused, using any DB-API 2 database module. The following diagram shows the connection layers involved when you -are using ``PooledDB`` connections: +are using ``pooled_db`` connections: -.. image:: pool.png +.. image:: pooled.png -As the diagram indicates, ``PooledDB`` can share opened database connections +As the diagram indicates, ``pooled_db`` can share opened database connections between different threads. This will happen by default if you set up the connection pool with a positive value of ``maxshared`` and the underlying DB-API 2 is thread-safe at the connection level, but you can also request @@ -199,23 +193,23 @@ connection that is not shared any more, it is returned back to the pool of idle connections so that it can be recycled again. If the underlying DB-API module is not thread-safe, thread locks will be -used to ensure that the ``PooledDB`` connections are thread-safe. So you +used to ensure that the ``pooled_db`` connections are thread-safe. So you don't need to worry about that, but you should be careful to use dedicated connections whenever you change the database session or perform transactions spreading over more than one SQL command. Which one to use? ----------------- -Both ``PersistentDB`` and ``PooledDB`` serve the same purpose to improve +Both ``persistent_db`` and ``pooled_db`` serve the same purpose to improve the database access performance by recycling database connections, while preserving stability even if database connection will be disrupted. So which of these two modules should you use? From the above explanations -it is clear that ``PersistentDB`` will make more sense if your application +it is clear that ``persistent_db`` will make more sense if your application keeps a constant number of threads which frequently use the database. In this case, you will always have the same amount of open database connections. However, if your application frequently starts and ends threads, then it -will be better to use ``PooledDB``. The latter will also allow more +will be better to use ``pooled_db``. The latter will also allow more fine-tuning, particularly if you are using a thread-safe DB-API 2 module. Since the interface of both modules is similar, you can easily switch from @@ -229,19 +223,19 @@ The usage of all the modules is similar, but there are also some differences in the initialization between the "Pooled" and "Persistent" variants and also between the universal DB-API 2 and the classic PyGreSQL variants. -We will cover here only the ``PersistentDB`` module and the more complex -``PooledDB`` module. For the details of the other modules, have a look +We will cover here only the ``persistent_db`` module and the more complex +``pooled_db`` module. For the details of the other modules, have a look at their module docstrings. Using the Python interpreter console, you can -display the documentation of the ``PooledDB`` module as follows (this +display the documentation of the ``pooled_db`` module as follows (this works analogously for the other modules):: - help(PooledDB) + help(pooled_db) -PersistentDB ------------- -In order to make use of the ``PersistentDB`` module, you first need to set +PersistentDB (persistent_db) +---------------------------- +In order to make use of the ``persistent_db`` module, you first need to set up a generator for your kind of database connections by creating an instance -of ``PersistentDB``, passing the following parameters: +of ``persistent_db``, passing the following parameters: * ``creator``: either an arbitrary function returning new DB-API 2 connection objects or a DB-API 2 compliant database module @@ -281,7 +275,7 @@ For instance, if you are using ``pgdb`` as your DB-API 2 database module and want every connection to your local database ``mydb`` to be reused 1000 times:: import pgdb # import used DB-API 2 module - from DBUtils.PersistentDB import PersistentDB + from dbutils.persistent_db import PersistentDB persist = PersistentDB(pgdb, 1000, database='mydb') Once you have set up the generator with these parameters, you can request @@ -290,7 +284,7 @@ database connections of that kind:: db = persist.connection() You can use these connections just as if they were ordinary DB-API 2 -connections. Actually what you get is the hardened ``SteadyDB`` version of +connections. Actually what you get is the hardened ``steady_db`` version of the underlying DB-API 2 connection. Closing a persistent connection with ``db.close()`` will be silently @@ -309,10 +303,10 @@ connections may become a bit faster, but this may not work in all environments (for instance, ``mod_wsgi`` is known to cause problems since it clears the ``threading.local`` data between requests). -PooledDB --------- -In order to make use of the ``PooledDB`` module, you first need to set up the -database connection pool by creating an instance of ``PooledDB``, passing the +PooledDB (pooled_db) +-------------------- +In order to make use of the ``pooled_db`` module, you first need to set up the +database connection pool by creating an instance of ``pooled_db``, passing the following parameters: * ``creator``: either an arbitrary function returning new DB-API 2 @@ -371,7 +365,7 @@ For instance, if you are using ``pgdb`` as your DB-API 2 database module and want a pool of at least five connections to your local database ``mydb``:: import pgdb # import used DB-API 2 module - from DBUtils.PooledDB import PooledDB + from dbutils.pooled_db import PooledDB pool = PooledDB(pgdb, 5, database='mydb') Once you have set up the connection pool you can request database connections @@ -380,7 +374,7 @@ from that pool:: db = pool.connection() You can use these connections just as if they were ordinary DB-API 2 -connections. Actually what you get is the hardened ``SteadyDB`` version of +connections. Actually what you get is the hardened ``steady_db`` version of the underlying DB-API 2 connection. Please note that the connection may be shared with other threads by default @@ -417,30 +411,6 @@ with other threads, that the transparent reopening will be suspended until the end of the transaction, and that the connection will be rolled back before being given back to the connection pool. -Usage in Webware for Python ---------------------------- -If you are using DBUtils in order to access a database from `Webware -for Python`_ servlets, you need to make sure that you set up your -database connection generators only once when the application starts, -and not every time a servlet instance is created. For this purpose, -you can add the necessary code to the module or class initialization -code of your base servlet class, or you can use the ``contextInitialize()`` -function in the ``__init__.py`` script of your application context. - -The directory ``Examples`` that is part of the DButils distribution -contains an example context for Webware for Python that uses a small -demo database designed to track the attendees for a series of seminars -(the idea for this example has been taken from the article -"`The Python DB-API`_" by Andrew Kuchling). - -The example context can be configured by either creating a config file -``Configs/Database.config`` or by directly changing the default parameters -in the example servlet ``Examples/DBUtilsExample.py``. This way you can -set an appropriate database user and password, and you can choose the -underlying database module (PyGreSQL classic or any DB-API 2 module). -If the setting ``maxcached`` is present, then the example servlet will use -the "Pooled" variant, otherwise it will use the "Persistent" variant. - Notes ===== @@ -452,7 +422,7 @@ from DBUtils to split the pooling out into a separate layer. Also note that when you are using a solution like the Apache webserver with mod_python_ or mod_wsgi_, then your Python code will be usually run in the context of the webserver's child processes. So if you are using -the ``PooledDB`` module, and several of these child processes are running, +the ``pooled_db`` module, and several of these child processes are running, you will have as much database connection pools. If these processes are running many threads, this may still be a reasonable approach, but if these processes don't spawn more than one worker thread, as in the case of Apache's @@ -468,7 +438,7 @@ Some ideas for future improvements: * Alternatively to the maximum number of uses of a connection, implement a maximum time to live for connections. -* Create modules ``MonitorDB`` and ``MonitorPg`` that will run in a separate +* Create modules ``monitor_db`` and ``monitor_pg`` that will run in a separate thread, monitoring the pool of the idle connections and maybe also the shared connections respectively the thread-affine connections. If a disrupted connection is detected, then it will be reestablished automatically @@ -485,11 +455,12 @@ Some ideas for future improvements: Bug reports and feedback ======================== -Please send bug reports, patches and feedback directly to the author -(using the email address given below). +You can transmit bug reports, patches and feedback by creating issues_ or +`pull requests`_ on the GitHub project page for DBUtils. -If there are Webware related problems, these can also be discussed in -the `Webware for Python mailing list`_. +.. _GitHub-Projektseite: https://github.com/WebwareForPython/DBUtils +.. _Issues: https://github.com/WebwareForPython/DBUtils/issues +.. _Pull Requests: https://github.com/WebwareForPython/DBUtils/pulls Links @@ -514,20 +485,20 @@ Some links to related and alternative software: .. _DB-API 2: https://www.python.org/dev/peps/pep-0249/ .. _The Python DB-API: http://www.linuxjournal.com/article/2605 .. _PostgresQL: https://www.postgresql.org/ -.. _PyGreSQL: http://www.pygresql.org/ +.. _PyGreSQL: https://www.pygresql.org/ .. _SQLObject: http://www.sqlobject.org/ -.. _SQLAlchemy: http://www.sqlalchemy.org -.. _Apache: http://httpd.apache.org/ +.. _SQLAlchemy: https://www.sqlalchemy.org +.. _Apache: https://httpd.apache.org/ .. _mod_python: http://modpython.org/ .. _mod_wsgi: https://github.com/GrahamDumpleton/mod_wsgi -.. _pgpool: http://www.pgpool.net/ +.. _pgpool: https://www.pgpool.net/ .. _pgbouncer: https://pgbouncer.github.io/ Credits ======= -:Author: Christoph Zwerschke <cito@online.de> +:Author: `Christoph Zwerschke`_ :Contributions: DBUtils uses code, input and suggestions made by Ian Bicking, Chuck Esterbrook (Webware for Python), Dan Green (DBTools), @@ -535,11 +506,13 @@ Credits Warren Smith (DbConnectionPool), Ezio Vernacotola, Jehiah Czebotar, Matthew Harriger, Gregory Piñero and Josef van Eenbergen. +.. _Christoph Zwerschke: https://github.com/Cito + Copyright and License ===================== -Copyright © 2005-2018 by Christoph Zwerschke. +Copyright © 2005-2020 by Christoph Zwerschke. All Rights Reserved. DBUtils is free and open source software, diff --git a/docs/persist.png b/docs/persist.png deleted file mode 100644 index e76856d7be4aaee2d008839e503b5c7d513f279d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7063 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 diff --git a/docs/persistent.png b/docs/persistent.png new file mode 100644 index 0000000000000000000000000000000000000000..3eb2782a8e463375382dc482d77927a1e3366390 GIT binary patch literal 7038 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 literal 0 HcmV?d00001 diff --git a/docs/pgdep.png b/docs/pgdep.png deleted file mode 100644 index f235e436be4d796378650186b8fbd7fa1bc94e84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3129 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 TVXvr+00000NkvXXu0mjfk(cn* diff --git a/docs/pool.png b/docs/pool.png deleted file mode 100644 index c6677b04881669f261033623168bbe5f49118b04..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11963 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 E2RKJmU;qFB diff --git a/docs/pooled.png b/docs/pooled.png new file mode 100644 index 0000000000000000000000000000000000000000..dc7ea83afdf91caff6ed0a06912f338aa980549a GIT binary patch literal 12053 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 LdK$%dpTGHkm?OQ0 literal 0 HcmV?d00001 From 9bf992981d943ebfb31b719685189ecb9d31fc69 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 26 Sep 2020 20:39:47 +0200 Subject: [PATCH 08/84] Update README --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3697d59..b35abf9 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,11 @@ DBUtils ======= DBUtils is a suite of tools providing solid, persistent and pooled connections -to a database that can be used in all kinds of multi-threaded environments -like Webware for Python or other web application servers. The suite supports -DB-API 2 compliant database interfaces and the classic PyGreSQL interface. +to a database that can be used in all kinds of multi-threaded environments. -The current version 2.0 of DBUtils supports Python versions 2.7 and 3.5 - 3.8. +The suite supports DB-API 2 compliant database interfaces +and the classic PyGreSQL interface. -The DBUtils home page can be found here: https://webwareforpython.github.io/DBUtils/ +The current version 2.0 of DBUtils supports Python versions 2.7 and 3.5 to 3.9. + +The DBUtils home page can be found at https://webwareforpython.github.io/DBUtils/ From 25e64c4074ea3e4f699bed5dea271a62e88dd4be Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 26 Sep 2020 20:40:21 +0200 Subject: [PATCH 09/84] Remove outdated "creating a release" notes --- release.md | 58 ------------------------------------------------------ 1 file changed, 58 deletions(-) delete mode 100644 release.md diff --git a/release.md b/release.md deleted file mode 100644 index aec81f3..0000000 --- a/release.md +++ /dev/null @@ -1,58 +0,0 @@ -Create a new DBUtils release: -============================= - -* Check the documentation. If possible, update all translations. - (Chinese translation was too old and has been removed for the time being.) - -* Use tox to run all tests in DBUtils/Tests with all supported Python versions - and to run flake8 in order to check the code style and quality. - -* Check the examples in DBUtils/Examples with the current Webware version. - -* Update and check the Release Notes and copyright information. - -* Set version number and release date with `setversion.py`. - -* Revert to old version number for translations that have not been updated. - -* Build html pages using `buildhtml.py`. - -* Create a tag in the Git repository. - -* Create a source tarball with: - - python setup.py sdist - - You will find the tarball in the "dist" folder. - - Generally, it is better to create the release under Unix to avoid - problems with DOS line feeds and wrong file permission. - -* Upload to the Python Package Index: - - Create a .pypirc file in your home directory as follows: - - echo "[pypi] - repository: https://upload.pypi.org/legacy/ - username: your username - password: your password - - [pypitest] - repository: https://test.pypi.org/legacy/ - username: your username - password: your password - " > ~.pypirc - - -* Upload the source package to the test PyPI with: - - twine upload -r pypitest dist/*.tar.gz - -* Register and upload the project to the real PyPI with: - - twine upload -r pypi dist/*.tar.gz - -* Don't forget to update the home page: - - * https://webwareforpython.github.io/DBUtils/ - * https://webwareforpython.github.io/w4py/ From 765a0fed6564acbe0ca0be7a1d15ac7bfafa1290 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 26 Sep 2020 20:40:44 +0200 Subject: [PATCH 10/84] Check manifest (create proper source distribution) --- .pylintrc | 317 ---------------------------------------------------- MANIFEST.in | 7 +- tox.ini | 19 ++-- 3 files changed, 16 insertions(+), 327 deletions(-) delete mode 100644 .pylintrc diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 4c3d6ec..0000000 --- a/.pylintrc +++ /dev/null @@ -1,317 +0,0 @@ -# lint Python modules using external checkers. -# -# This is the main checker controling the other ones and the reports -# generation. It is itself both a raw checker and an astng checker in order -# to: -# * handle message activation / deactivation at the module level -# * handle some basic but necessary stats'data (number of classes, methods...) - -# -[MASTER] - -# Specify a configuration file. -#rcfile= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Profiled execution. -profile=no - -# Add <file or directory> to the black list. It should be a base name, not a -# path. You may set this option multiple times. -ignore=CVS - -# Pickle collected data for later comparisons. -persistent=yes - -# Set the cache size for astng objects. -cache-size=500 - -# List of plugins (as comma separated values of python modules names) to load, - -# usually to register additional checkers. -load-plugins= - - -[MESSAGES CONTROL] - -# Enable only checker(s) with the given id(s). This option conflict with the -# disable-checker option -#enable-checker= - -# Enable all checker(s) except those with the given id(s). This option conflict -# with the disable-checker option -#disable-checker= - -# Enable all messages in the listed categories. -#enable-msg-cat= - -# Disable all messages in the listed categories. -#disable-msg-cat= - -# Enable the message(s) with the given id(s). -#enable-msg= - -# Disable the message(s) with the given id(s). -#disable-msg= - - -[REPORTS] - -# set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html -output-format=text - -# Include message's id in output -include-ids=no - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". -files-output=no - -# Tells wether to display a full report or only the messages -reports=yes - -# Python expression which should return a note less than 10 (10 is the highest -# note).You have access to the variables errors warning, statement which -# respectivly contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (R0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Add a comment according to your evaluation note. This is used by the global -# evaluation report (R0004). -comment=no - -# Enable the report(s) with the given id(s). -#enable-report= - -# Disable the report(s) with the given id(s). -#disable-report= - - -# checks for : - -# * doc strings -# * modules / classes / functions / methods / arguments / variables name -# * number of arguments, local variables, branchs, returns and statements in -# functions, methods -# * required module attributes -# * dangerous default values as arguments -# * redefinition of function / method / class -# * uses of the global statement -# -[BASIC] - -# Required attributes for module, separated by a comma -required-attributes= - -# Regular expression which should only match functions or classes name which do - -# not require a docstring -no-docstring-rgx=__.*__ - -# Regular expression which should only match correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression which should only match correct module level names -const-rgx=(([A-Z_][A-Z1-9_]*)|(__.*__))$ - -# Regular expression which should only match correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ - -# Regular expression which should only match correct function names -function-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct method names -method-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct instance attribute names -attr-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct argument names -argument-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct variable names -variable-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct list comprehension / - -# generator expression variable names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,ex,Run,_ - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata - -# List of builtins function names that should not be used, separated by a comma -bad-functions=map,filter,apply,input - - -# try to find bugs in the code using type inference -# -[TYPECHECK] - -# Tells wether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# When zope mode is activated, consider the acquired-members option to ignore -# access to some undefined attributes. -zope=no - -# List of members which are usually get through zope's acquisition mecanism and -# so shouldn't trigger E0201 when accessed (need zope=yes to be considered). -acquired-members=REQUEST,acl_users,aq_parent - - -# checks for -# * unused variables / imports -# * undefined variables -# * redefinition of variable from builtins or from an outer scope -# * use of variable before assigment -# -[VARIABLES] - -# Tells wether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching names used for dummy variables (i.e. not used). -dummy-variables-rgx=_|dummy - -# List of additional names supposed to be defined in builtins. Remember that - -# you should avoid to define new builtins when possible. -additional-builtins= - - -# checks for : -# * methods without self as first argument -# * overridden methods signature -# * access only to existant members via self -# * attributes not defined in the __init__ method -# * supported interfaces implementation -# * unreachable code -# -[CLASSES] - -# List of interface methods to ignore, separated by a comma. This is used for -# instance to not check methods defines in Zope's Interface base class. -ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setUp - - -# checks for sign of poor/misdesign: -# * number of methods, attributes, local variables... -# * size, complexity of functions, methods -# -[DESIGN] - -# Maximum number of arguments for function / method -#max-args=5 -max-args=15 - -# Maximum number of locals for function / method body -max-locals=15 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of branch for function / method body -#max-branchs=12 -max-branchs=25 - -# Maximum number of statements in function / method body -#max-statements=50 -max-statements=75 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of attributes for a class (see R0902). -#max-attributes=7 -max-attributes=20 - -# Minimum number of public methods for a class (see R0903). -#min-public-methods=2 -min-public-methods=1 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - - -# checks for -# * external modules dependencies -# * relative / wildcard imports -# * cyclic imports -# * uses of deprecated modules -# -[IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub,string,TERMIOS,Bastion,rexec - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report R0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report R0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report R0402 must -# not be disabled) -int-import-graph= - - -# checks for : -# * unauthorized constructions -# * strict indentation -# * line length -# * use of <> instead of != -# -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=80 - -# Maximum number of lines in a module -max-module-lines=1000 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - - -# checks for: -# * warning notes in the code like FIXME, XXX -# * PEP 263: source code with non ascii character but no encoding declaration -# -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME,XXX,TODO - - -# checks for similarities and duplicated code. This computation may be -# memory / CPU intensive, so you should disable it if you experiments some -# problems. - -# -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=4 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes diff --git a/MANIFEST.in b/MANIFEST.in index 9896216..2d8e122 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,9 +3,12 @@ include MANIFEST.in include LICENSE include README.md -recursive-include docs *.rst make.py *.html *.css *.png +include .bumpversion.cfg +include tox.ini + +recursive-include tests *.py +recursive-include docs *.rst make.py *.html *.css *.png prune docs/_build -exclude release.md setversion.py global-exclude *.py[co] __pycache__ diff --git a/tox.ini b/tox.ini index 0e56291..c0155ef 100644 --- a/tox.ini +++ b/tox.ini @@ -1,18 +1,21 @@ [tox] -envlist = py{27,35,36,37,38,39}, flake8 +envlist = py{27,35,36,37,38,39}, flake8, manifest [testenv] setenv = PYTHONPATH = {toxinidir} -deps = - pytest +deps = pytest commands = - pytest + pytest {posargs} [testenv:flake8] -basepython = - python3.8 -deps = - flake8 +basepython = python3.8 +deps = flake8 commands = flake8 dbutils tests docs setup.py + +[testenv:manifest] +basepython = python3.8 +deps = check-manifest +commands = + check-manifest -v From f136802f907ac35b431089e641ab4e0ba1fd0fd8 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 26 Sep 2020 22:40:31 +0200 Subject: [PATCH 11/84] Test that docs can be built properly --- .travis.yml | 39 +++++++++++++++++++++++++++------------ docs/changelog.html | 4 ++-- docs/main.de.html | 4 ++-- docs/main.html | 4 ++-- docs/make.py | 18 +++++++++--------- tox.ini | 9 ++++++++- 6 files changed, 50 insertions(+), 28 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5da2af9..e705d3c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,16 +2,31 @@ # see https://docs.travis-ci.com/user/languages/python language: python - -python: - - "2.7" - - "3.5" - - "3.6" - - "3.7" - - "3.8" - +matrix: + include: + - name: Code quality tests + env: TOXENV=flake8,manifest,docs + python: 3.8 + - name: Unit tests with Python 3.8 + env: TOXENV=py38 + python: 3.8 + - name: Unit tests with Python 3.7 + env: TOXENV=py37 + python: 3.7 + - name: Unit tests with Python 3.6 + env: TOXENV=py36 + python: 3.6 + - name: Unit tests with Python 3.5 + env: TOXENV=py35 + python: 3.5 + - name: Unit tests with Python 2.7 + env: TOXENV=py27 + python: 2.7 +cache: + directories: + - "$HOME/.cache/pip" + - "$TRAVIS_BUILD_DIR/.tox" install: - - pip install pytest - - pip install . - -script: pytest DBUtils/Tests/Test*.py + - python setup.py install +script: + - tox -e $TOXENV diff --git a/docs/changelog.html b/docs/changelog.html index fc37f48..946cda8 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -2,7 +2,7 @@ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta charset="utf-8"/> -<meta name="generator" content="Docutils 0.15.2: http://docutils.sourceforge.net/" /> +<meta name="generator" content="Docutils 0.16: http://docutils.sourceforge.net/" /> <title>Changelog for DBUtils</title> <link rel="stylesheet" href="Doc.css" type="text/css" /> </head> @@ -11,7 +11,7 @@ <h1 class="title">Changelog for DBUtils</h1> <div class="contents topic" id="contents"> -<p class="topic-title first">Contents</p> +<p class="topic-title">Contents</p> <ul class="simple"> <li><p><a class="reference internal" href="#id1" id="id21">2.0</a></p> <ul> diff --git a/docs/main.de.html b/docs/main.de.html index b744c1f..3d63022 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -2,7 +2,7 @@ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="de" lang="de"> <head> <meta charset="utf-8"/> -<meta name="generator" content="Docutils 0.15.2: http://docutils.sourceforge.net/" /> +<meta name="generator" content="Docutils 0.16: http://docutils.sourceforge.net/" /> <title>Benutzeranleitung für DBUtils</title> <link rel="stylesheet" href="Doc.css" type="text/css" /> </head> @@ -17,7 +17,7 @@ <h1 class="title">Benutzeranleitung für DBUtils</h1> </dd> </dl> <div class="contents topic" id="inhalt"> -<p class="topic-title first">Inhalt</p> +<p class="topic-title">Inhalt</p> <ul class="simple"> <li><p><a class="reference internal" href="#zusammenfassung" id="id5">Zusammenfassung</a></p></li> <li><p><a class="reference internal" href="#module" id="id6">Module</a></p></li> diff --git a/docs/main.html b/docs/main.html index 5023149..22a6ea9 100644 --- a/docs/main.html +++ b/docs/main.html @@ -2,7 +2,7 @@ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta charset="utf-8"/> -<meta name="generator" content="Docutils 0.15.2: http://docutils.sourceforge.net/" /> +<meta name="generator" content="Docutils 0.16: http://docutils.sourceforge.net/" /> <title>DBUtils User's Guide</title> <link rel="stylesheet" href="Doc.css" type="text/css" /> </head> @@ -17,7 +17,7 @@ <h1 class="title">DBUtils User's Guide</h1> </dd> </dl> <div class="contents topic" id="contents"> -<p class="topic-title first">Contents</p> +<p class="topic-title">Contents</p> <ul class="simple"> <li><p><a class="reference internal" href="#synopsis" id="id5">Synopsis</a></p></li> <li><p><a class="reference internal" href="#modules" id="id6">Modules</a></p></li> diff --git a/docs/make.py b/docs/make.py index f2153df..d81cfab 100755 --- a/docs/make.py +++ b/docs/make.py @@ -2,8 +2,6 @@ """Build HMTL from reST files.""" -from __future__ import print_function - from glob import glob from os.path import splitext from docutils.core import publish_file @@ -24,12 +22,14 @@ with open(rst_file, encoding='utf-8-sig') as source: with open(html_file, 'w', encoding='utf-8') as destination: - publish_file(writer_name='html5', - source=source, destination=destination, - settings_overrides=dict( - stylesheet_path='Doc.css', - embed_stylesheet=False, - toc_backlinks=False, - language_code=lang)) + output = publish_file( + writer_name='html5', source=source, destination=destination, + enable_exit_status=True, + settings_overrides=dict( + stylesheet_path='Doc.css', + embed_stylesheet=False, + toc_backlinks=False, + language_code=lang, + exit_status_level=2)) print("Done.") diff --git a/tox.ini b/tox.ini index c0155ef..2de6636 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{27,35,36,37,38,39}, flake8, manifest +envlist = py{27,35,36,37,38,39}, flake8, manifest, docs [testenv] setenv = @@ -19,3 +19,10 @@ basepython = python3.8 deps = check-manifest commands = check-manifest -v + +[testenv:docs] +basepython = python3.8 +deps = docutils +changedir = docs +commands = + python make.py From 78f7b23b91a420d8aed31c0e9a0cbe570d1f5417 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 26 Sep 2020 23:02:34 +0200 Subject: [PATCH 12/84] Add project URLs to setup.py --- docs/changelog.html | 132 +++++++++----------------------------------- docs/changelog.rst | 27 +++------ setup.py | 14 ++++- 3 files changed, 44 insertions(+), 129 deletions(-) diff --git a/docs/changelog.html b/docs/changelog.html index 946cda8..d473594 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -10,132 +10,65 @@ <div class="document" id="changelog-for-dbutils"> <h1 class="title">Changelog for DBUtils</h1> -<div class="contents topic" id="contents"> -<p class="topic-title">Contents</p> -<ul class="simple"> -<li><p><a class="reference internal" href="#id1" id="id21">2.0</a></p> -<ul> -<li><p><a class="reference internal" href="#changes" id="id22">Changes:</a></p></li> -</ul> -</li> -<li><p><a class="reference internal" href="#id2" id="id23">1.4</a></p> -<ul> -<li><p><a class="reference internal" href="#improvements" id="id24">Improvements:</a></p></li> -</ul> -</li> -<li><p><a class="reference internal" href="#id3" id="id25">1.3</a></p> -<ul> -<li><p><a class="reference internal" href="#id4" id="id26">Improvements:</a></p></li> -</ul> -</li> -<li><p><a class="reference internal" href="#id5" id="id27">1.2</a></p></li> -<li><p><a class="reference internal" href="#id6" id="id28">1.1.1</a></p> -<ul> -<li><p><a class="reference internal" href="#id7" id="id29">Improvements:</a></p></li> -<li><p><a class="reference internal" href="#bugfixes" id="id30">Bugfixes:</a></p></li> -</ul> -</li> -<li><p><a class="reference internal" href="#id8" id="id31">1.1</a></p> -<ul> -<li><p><a class="reference internal" href="#id9" id="id32">Improvements:</a></p></li> -<li><p><a class="reference internal" href="#id10" id="id33">Bugfixes:</a></p></li> -</ul> -</li> -<li><p><a class="reference internal" href="#id11" id="id34">1.0</a></p> -<ul> -<li><p><a class="reference internal" href="#id12" id="id35">Changes:</a></p></li> -<li><p><a class="reference internal" href="#bugfixes-and-improvements" id="id36">Bugfixes and Improvements:</a></p></li> -</ul> -</li> -<li><p><a class="reference internal" href="#id13" id="id37">0.9.4</a></p></li> -<li><p><a class="reference internal" href="#id14" id="id38">0.9.3</a></p> -<ul> -<li><p><a class="reference internal" href="#id15" id="id39">Changes:</a></p></li> -</ul> -</li> -<li><p><a class="reference internal" href="#id16" id="id40">0.9.2</a></p> -<ul> -<li><p><a class="reference internal" href="#id17" id="id41">Changes:</a></p></li> -</ul> -</li> -<li><p><a class="reference internal" href="#id18" id="id42">0.9.1</a></p> -<ul> -<li><p><a class="reference internal" href="#id19" id="id43">Changes:</a></p></li> -</ul> -</li> -<li><p><a class="reference internal" href="#id20" id="id44">0.8.1 - 2005-09-13</a></p></li> -</ul> -</div> <div class="section" id="id1"> <h1>2.0</h1> <p>DBUtils 2.0 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> -<div class="section" id="changes"> -<h2>Changes:</h2> +<p>Changes:</p> <ul class="simple"> <li><p>DBUtils does not act as a Webware plugin anymore, it is now just an ordinary Python package (of course it could be used as such also before).</p></li> -<li><p>The top-level packages and folders have been renamed to lower-case. -Particularly, you need to import <span class="docutils literal">dbutils</span> instead of <span class="docutils literal">DBUtils</span> now.</p></li> <li><p>The Webware <span class="docutils literal">Examples</span> folder has been removed.</p></li> -<li><p>The internal naming conventions have been changed to comply with PEP8.</p></li> +<li><p>Folders, packages and modules have been renamed to lower-case. +Particularly, you need to import <span class="docutils literal">dbutils</span> instead of <span class="docutils literal">DBUtils</span> now.</p></li> +<li><p>The internal naming conventions have also been changed to comply with PEP8.</p></li> <li><p>The documentation has been adapted to reflect the changes in this version.</p></li> -<li><p>This changelog file has been created from the former release notes.</p></li> +<li><p>This changelog has been compiled from the former release notes.</p></li> </ul> </div> -</div> <div class="section" id="id2"> <h1>1.4</h1> -<p>DBUtils 1.4 was released on August 26, 2020.</p> +<p>DBUtils 1.4 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> -<div class="section" id="improvements"> -<h2>Improvements:</h2> +<p>Improvements:</p> <ul class="simple"> <li><p>The <span class="docutils literal">SteadyDB</span> and <span class="docutils literal">SteadyPg</span> classes only reconnect after the <span class="docutils literal">maxusage</span> limit has been reached when the connection is not currently inside a transaction.</p></li> </ul> </div> -</div> <div class="section" id="id3"> <h1>1.3</h1> <p>DBUtils 1.3 was released on March 3, 2018.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.4 to 3.7.</p> -<div class="section" id="id4"> -<h2>Improvements:</h2> +<p>Improvements:</p> <ul class="simple"> <li><p>This version now supports context handlers for connections and cursors.</p></li> </ul> </div> -</div> -<div class="section" id="id5"> +<div class="section" id="id4"> <h1>1.2</h1> <p>DBUtils 1.2 was released on February 5, 2017.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.0 to 3.6.</p> </div> -<div class="section" id="id6"> +<div class="section" id="id5"> <h1>1.1.1</h1> <p>DBUtils 1.1.1 was released on February 4, 2017.</p> <p>It is intended to be used with Python versions 2.3 to 2.7.</p> -<div class="section" id="id7"> -<h2>Improvements:</h2> +<p>Improvements:</p> <ul class="simple"> <li><p>Reopen <span class="docutils literal">SteadyDB</span> connections when commit or rollback fails (suggested by Ben Hoyt).</p></li> </ul> -</div> -<div class="section" id="bugfixes"> -<h2>Bugfixes:</h2> +<p>Bugfixes:</p> <ul class="simple"> <li><p>Fixed a problem when running under Jython (reported by Vitaly Kruglikov).</p></li> </ul> </div> -</div> -<div class="section" id="id8"> +<div class="section" id="id6"> <h1>1.1</h1> <p>DBUtils 1.1 was released on August 14, 2011.</p> -<div class="section" id="id9"> -<h2>Improvements:</h2> +<p>Improvements:</p> <ul class="simple"> <li><p>The transparent reopening of connections is actually an undesired behavior if it happens during database transactions. In these cases, the transaction @@ -154,22 +87,18 @@ <h2>Improvements:</h2> <li><p><span class="docutils literal">PooledDB</span> has got another new parameter <span class="docutils literal">reset</span> for controlling how connections are reset before being put back into the pool.</p></li> </ul> -</div> -<div class="section" id="id10"> -<h2>Bugfixes:</h2> +<p>Bugfixes:</p> <ul class="simple"> <li><p>Fixed propagation of error messages when the connection was lost.</p></li> <li><p>Fixed an issue with the <span class="docutils literal">setoutputsize()</span> cursor method.</p></li> <li><p>Fixed some minor issues with the <span class="docutils literal">DBUtilsExample</span> for Webware.</p></li> </ul> </div> -</div> -<div class="section" id="id11"> +<div class="section" id="id7"> <h1>1.0</h1> <p>DBUtils 1.0 was released on November 29, 2008.</p> <p>It is intended to be used with Python versions 2.2 to 2.6.</p> -<div class="section" id="id12"> -<h2>Changes:</h2> +<p>Changes:</p> <ul class="simple"> <li><p>Added a <span class="docutils literal">failures</span> parameter for configuring the exception classes for which the failover mechanisms is applied (as suggested by Matthew Harriger).</p></li> @@ -188,9 +117,7 @@ <h2>Changes:</h2> A new parameter <span class="docutils literal">threadlocal</span> allows you to pass an arbitrary class such as <span class="docutils literal">threading.local</span> if you know it works in your environment.</p></li> </ul> -</div> -<div class="section" id="bugfixes-and-improvements"> -<h2>Bugfixes and Improvements:</h2> +<p>Bugfixes and improvements:</p> <ul class="simple"> <li><p>In some cases, when instance initialization failed or referenced objects were already destroyed, finalizers could throw exceptions or create infinite @@ -200,8 +127,7 @@ <h2>Bugfixes and Improvements:</h2> the MySQLdb module (problem reported by Gregory Pinero).</p></li> </ul> </div> -</div> -<div class="section" id="id13"> +<div class="section" id="id8"> <h1>0.9.4</h1> <p>DBUtils 0.9.4 was released on July 7, 2007.</p> <p>This release fixes a problem in the destructor code and has been supplemented @@ -210,11 +136,10 @@ <h1>0.9.4</h1> in the last release, since you can now pass custom creator functions for database connections instead of DB-API 2 modules.</p> </div> -<div class="section" id="id14"> +<div class="section" id="id9"> <h1>0.9.3</h1> <p>DBUtils 0.9.3 was released on May 21, 2007.</p> -<div class="section" id="id15"> -<h2>Changes:</h2> +<p>Changes:</p> <ul class="simple"> <li><p>Support custom creator functions for database connections. These can now be used as the first parameter instead of an DB-API module @@ -226,25 +151,21 @@ <h2>Changes:</h2> Added Chinese translation of the User's Guide, kindly contributed by gashero.</p></li> </ul> </div> -</div> -<div class="section" id="id16"> +<div class="section" id="id10"> <h1>0.9.2</h1> <p>DBUtils 0.9.2 was released on September 22, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.5.</p> -<div class="section" id="id17"> -<h2>Changes:</h2> +<p>Changes:</p> <ul class="simple"> <li><p>Renamed <span class="docutils literal">SolidDB</span> to <span class="docutils literal">SteadyDB</span> to avoid confusion with the "solidDB" storage engine. Accordingly, renamed <span class="docutils literal">SolidPg</span> to <span class="docutils literal">SteadyPg</span>.</p></li> </ul> </div> -</div> -<div class="section" id="id18"> +<div class="section" id="id11"> <h1>0.9.1</h1> <p>DBUtils 0.9.1 was released on May 8, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.4.</p> -<div class="section" id="id19"> -<h2>Changes:</h2> +<p>Changes:</p> <ul class="simple"> <li><p>Added <span class="docutils literal">_closeable</span> attribute and made persistent connections not closeable by default. This allows <span class="docutils literal">PersistentDB</span> to be used in the same way as you @@ -254,8 +175,7 @@ <h2>Changes:</h2> <li><p>Improved the documentation and added a User's Guide.</p></li> </ul> </div> -</div> -<div class="section" id="id20"> +<div class="section" id="id12"> <h1>0.8.1 - 2005-09-13</h1> <p>DBUtils 0.8.1 was released on September 13, 2005.</p> <p>It is intended to be used with Python versions 2.0 to 2.4.</p> diff --git a/docs/changelog.rst b/docs/changelog.rst index 25b7d88..fad22c0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,8 +1,6 @@ Changelog for DBUtils +++++++++++++++++++++ -.. contents:: Contents - 2.0 === @@ -11,26 +9,24 @@ DBUtils 2.0 was released on September 26, 2020. It is intended to be used with Python versions 2.7 and 3.5 to 3.9. Changes: --------- * DBUtils does not act as a Webware plugin anymore, it is now just an ordinary Python package (of course it could be used as such also before). -* The top-level packages and folders have been renamed to lower-case. - Particularly, you need to import ``dbutils`` instead of ``DBUtils`` now. * The Webware ``Examples`` folder has been removed. -* The internal naming conventions have been changed to comply with PEP8. +* Folders, packages and modules have been renamed to lower-case. + Particularly, you need to import ``dbutils`` instead of ``DBUtils`` now. +* The internal naming conventions have also been changed to comply with PEP8. * The documentation has been adapted to reflect the changes in this version. -* This changelog file has been created from the former release notes. +* This changelog has been compiled from the former release notes. 1.4 === -DBUtils 1.4 was released on August 26, 2020. +DBUtils 1.4 was released on September 26, 2020. It is intended to be used with Python versions 2.7 and 3.5 to 3.9. Improvements: -------------- * The ``SteadyDB`` and ``SteadyPg`` classes only reconnect after the ``maxusage`` limit has been reached when the connection is not currently @@ -44,7 +40,6 @@ DBUtils 1.3 was released on March 3, 2018. It is intended to be used with Python versions 2.6, 2.7 and 3.4 to 3.7. Improvements: -------------- * This version now supports context handlers for connections and cursors. @@ -63,13 +58,11 @@ DBUtils 1.1.1 was released on February 4, 2017. It is intended to be used with Python versions 2.3 to 2.7. Improvements: -------------- * Reopen ``SteadyDB`` connections when commit or rollback fails (suggested by Ben Hoyt). Bugfixes: ---------- * Fixed a problem when running under Jython (reported by Vitaly Kruglikov). @@ -79,7 +72,6 @@ Bugfixes: DBUtils 1.1 was released on August 14, 2011. Improvements: -------------- * The transparent reopening of connections is actually an undesired behavior if it happens during database transactions. In these cases, the transaction @@ -99,7 +91,6 @@ Improvements: connections are reset before being put back into the pool. Bugfixes: ---------- * Fixed propagation of error messages when the connection was lost. * Fixed an issue with the ``setoutputsize()`` cursor method. @@ -114,7 +105,6 @@ DBUtils 1.0 was released on November 29, 2008. It is intended to be used with Python versions 2.2 to 2.6. Changes: --------- * Added a ``failures`` parameter for configuring the exception classes for which the failover mechanisms is applied (as suggested by Matthew Harriger). @@ -133,8 +123,7 @@ Changes: A new parameter ``threadlocal`` allows you to pass an arbitrary class such as ``threading.local`` if you know it works in your environment. -Bugfixes and Improvements: --------------------------- +Bugfixes and improvements: * In some cases, when instance initialization failed or referenced objects were already destroyed, finalizers could throw exceptions or create infinite @@ -161,7 +150,6 @@ for database connections instead of DB-API 2 modules. DBUtils 0.9.3 was released on May 21, 2007. Changes: --------- * Support custom creator functions for database connections. These can now be used as the first parameter instead of an DB-API module @@ -182,7 +170,6 @@ DBUtils 0.9.2 was released on September 22, 2006. It is intended to be used with Python versions 2.2 to 2.5. Changes: --------- * Renamed ``SolidDB`` to ``SteadyDB`` to avoid confusion with the "solidDB" storage engine. Accordingly, renamed ``SolidPg`` to ``SteadyPg``. @@ -195,7 +182,7 @@ DBUtils 0.9.1 was released on May 8, 2006. It is intended to be used with Python versions 2.2 to 2.4. Changes: --------- + * Added ``_closeable`` attribute and made persistent connections not closeable by default. This allows ``PersistentDB`` to be used in the same way as you would use ``PooledDB``. diff --git a/setup.py b/setup.py index 5f54dfe..82f6bba 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,10 @@ """Setup Script for DBUtils""" import warnings -from distutils.core import setup +try: + from setuptools import setup +except ImportError: + from distutils.core import setup from sys import version_info py_version = version_info[:2] @@ -41,8 +44,13 @@ author='Christoph Zwerschke', author_email='cito@online.de', url='https://webwareforpython.github.io/DBUtils/', + download_url="https://pypi.org/project/DBUtils/", + project_urls={ + "Documentation": "https://webwareforpython.github.io/DBUtils/main.html", + "Changelog": "https://webwareforpython.github.io/DBUtils/changelog.html", + "Issue Tracker": "https://github.com/WebwareForPython/DBUtils/issues", + "Source Code": "https://github.com/WebwareForPython/DBUtils"}, platforms=['any'], license='MIT License', - packages=['dbutils'], - zip_safe=False, + packages=['dbutils'] ) From 03181b0a37d1e0e8ca269b99acd16930c157c42b Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 26 Sep 2020 23:09:31 +0200 Subject: [PATCH 13/84] Remove blank lines in docstrings --- dbutils/persistent_db.py | 4 ---- dbutils/persistent_pg.py | 3 --- dbutils/pooled_db.py | 7 ------- dbutils/pooled_pg.py | 4 ---- dbutils/simple_pooled_db.py | 6 ------ dbutils/simple_pooled_pg.py | 4 ---- dbutils/steady_db.py | 10 ---------- dbutils/steady_pg.py | 9 --------- tests/test_persistent_db.py | 1 - tests/test_persistent_pg.py | 1 - tests/test_pooled_db.py | 1 - tests/test_pooled_pg.py | 1 - tests/test_simple_pooled_db.py | 1 - tests/test_simple_pooled_pg.py | 1 - tests/test_steady_db.py | 1 - tests/test_steady_pg.py | 1 - 16 files changed, 55 deletions(-) diff --git a/dbutils/persistent_db.py b/dbutils/persistent_db.py index ac26c11..cc93720 100644 --- a/dbutils/persistent_db.py +++ b/dbutils/persistent_db.py @@ -108,7 +108,6 @@ by Geoffrey Talvola in July 2005 Licensed under the MIT license. - """ from . import __version__ @@ -137,7 +136,6 @@ class PersistentDB: After you have created the connection pool, you can use connection() to get thread-affine, steady DB-API 2 connections. - """ version = __version__ @@ -169,7 +167,6 @@ def __init__( (threading.local is faster, but cannot be used in all cases) args, kwargs: the parameters that shall be passed to the creator function or the connection constructor of the DB-API 2 module - """ try: threadsafety = creator.threadsafety @@ -205,7 +202,6 @@ def connection(self, shareable=False): The shareable parameter exists only for compatibility with the PooledDB connection method. In reality, persistent connections are of course never shared with other threads. - """ try: con = self.thread.connection diff --git a/dbutils/persistent_pg.py b/dbutils/persistent_pg.py index b14960e..eb53e22 100644 --- a/dbutils/persistent_pg.py +++ b/dbutils/persistent_pg.py @@ -99,7 +99,6 @@ by Geoffrey Talvola in July 2005 Licensed under the MIT license. - """ from . import __version__ @@ -120,7 +119,6 @@ class PersistentPg: After you have created the connection pool, you can use connection() to get thread-affine, steady PostgreSQL connections. - """ version = __version__ @@ -143,7 +141,6 @@ def __init__( (threading.local is faster, but cannot be used in all cases) args, kwargs: the parameters that shall be used to establish the PostgreSQL connections using class PyGreSQL pg.DB() - """ self._maxusage = maxusage self._setsession = setsession diff --git a/dbutils/pooled_db.py b/dbutils/pooled_db.py index 0b13c96..04b4f20 100644 --- a/dbutils/pooled_db.py +++ b/dbutils/pooled_db.py @@ -136,7 +136,6 @@ by Dan Green in December 2000 Licensed under the MIT license. - """ from threading import Condition @@ -166,7 +165,6 @@ class PooledDB: After you have created the connection pool, you can use connection() to get pooled, steady DB-API 2 connections. - """ version = __version__ @@ -212,7 +210,6 @@ def __init__( 7 = always, and all other bit combinations of these values) args, kwargs: the parameters that shall be passed to the creator function or the connection constructor of the DB-API 2 module - """ try: threadsafety = creator.threadsafety @@ -278,7 +275,6 @@ def connection(self, shareable=True): If shareable is set and the underlying DB-API 2 allows it, then the connection may be shared with other threads. - """ if shareable and self._maxshared: self._lock.acquire() @@ -413,7 +409,6 @@ def __init__(self, pool, con): pool: the corresponding PooledDB instance con: the underlying SteadyDB connection - """ # basic initialization to make finalizer work self._con = None @@ -453,7 +448,6 @@ def __init__(self, con): """Create a shared connection. con: the underlying SteadyDB connection - """ self.con = con self.shared = 1 @@ -500,7 +494,6 @@ def __init__(self, pool, shared_con): pool: the corresponding PooledDB instance con: the underlying SharedDBConnection - """ # basic initialization to make finalizer work self._con = None diff --git a/dbutils/pooled_pg.py b/dbutils/pooled_pg.py index 2f8b937..a14cf6e 100644 --- a/dbutils/pooled_pg.py +++ b/dbutils/pooled_pg.py @@ -105,7 +105,6 @@ by Dan Green in December 2000 Licensed under the MIT license. - """ try: @@ -134,7 +133,6 @@ class PooledPg: After you have created the connection pool, you can use connection() to get pooled, steady PostgreSQL connections. - """ version = __version__ @@ -166,7 +164,6 @@ def __init__( 1 to always issue a rollback, 2 for a complete reset) args, kwargs: the parameters that shall be used to establish the PostgreSQL connections using class PyGreSQL pg.DB() - """ self._args, self._kwargs = args, kwargs self._maxusage = maxusage @@ -261,7 +258,6 @@ def __init__(self, pool, con): pool: the corresponding PooledPg instance con: the underlying SteadyPg connection - """ self._pool = pool self._con = con diff --git a/dbutils/simple_pooled_db.py b/dbutils/simple_pooled_db.py index 3e1b406..cfd2c8b 100644 --- a/dbutils/simple_pooled_db.py +++ b/dbutils/simple_pooled_db.py @@ -70,7 +70,6 @@ by Christoph Zwerschke in September 2005 Licensed under the MIT license. - """ from . import __version__ @@ -89,7 +88,6 @@ class PooledDBConnection: You don't normally deal with this class directly, but use PooledDB to get new connections. - """ def __init__(self, pool, con): @@ -117,7 +115,6 @@ class PooledDB: After you have created the connection pool, you can get connections using getConnection(). - """ version = __version__ @@ -129,7 +126,6 @@ def __init__(self, dbapi, maxconnections, *args, **kwargs): maxconnections: the number of connections cached in the pool args, kwargs: the parameters that shall be used to establish the database connections using connect() - """ try: threadsafety = dbapi.threadsafety @@ -190,7 +186,6 @@ def _unthreadsafe_return_connection(self, con): back into the queue after they have been used. This is done automatically when the connection is closed and should never be called explicitly outside of this module. - """ self._unthreadsafe_add_connection(con) @@ -222,6 +217,5 @@ def _threadsafe_return_connection(self, con): In this case, the connections always stay in the pool, so there is no need to do anything here. - """ pass diff --git a/dbutils/simple_pooled_pg.py b/dbutils/simple_pooled_pg.py index 4927a35..844fdd0 100644 --- a/dbutils/simple_pooled_pg.py +++ b/dbutils/simple_pooled_pg.py @@ -65,7 +65,6 @@ by Dan Green in December 2000 Licensed under the MIT license. - """ from pg import DB as PgConnection @@ -78,7 +77,6 @@ class PooledPgConnection: You don't normally deal with this class directly, but use PooledPg to get new connections. - """ def __init__(self, pool, con): @@ -106,7 +104,6 @@ class PooledPg: After you have created the connection pool, you can get connections using getConnection(). - """ version = __version__ @@ -117,7 +114,6 @@ def __init__(self, maxconnections, *args, **kwargs): maxconnections: the number of connections cached in the pool args, kwargs: the parameters that shall be used to establish the PostgreSQL connections using pg.connect() - """ # Since there is no connection level safety, we # build the pool using the synchronized queue class diff --git a/dbutils/steady_db.py b/dbutils/steady_db.py index 13918d6..6af877c 100644 --- a/dbutils/steady_db.py +++ b/dbutils/steady_db.py @@ -87,7 +87,6 @@ suggested by Ezio Vernacotola in December 2006 Licensed under the MIT license. - """ import sys @@ -132,7 +131,6 @@ def connect( be silently ignored, but by default the connection can be closed args, kwargs: the parameters that shall be passed to the creator function or the connection constructor of the DB-API 2 module - """ return SteadyDBConnection( creator, maxusage, setsession, @@ -199,7 +197,6 @@ def __exit__(self, *exc): """Exit the runtime context for the connection object. This does not close the connection, but it ends a transaction. - """ if exc[0] is None and exc[1] is None and exc[2] is None: self.commit() @@ -317,7 +314,6 @@ def _close(self): You can always close a tough connection with this method and it will not complain if you close it more than once. - """ if not self._closed: try: @@ -331,7 +327,6 @@ def _reset(self, force=False): """Reset a tough connection. Rollback if forced or the connection was in a transaction. - """ if not self._closed and (force or self._transaction): try: @@ -345,7 +340,6 @@ def _ping_check(self, ping=1, reconnect=True): If the the underlying connection is not active and the ping parameter is set accordingly, the connection will be recreated unless the connection is currently inside a transaction. - """ if ping & self._ping: try: # if possible, ping the connection @@ -402,7 +396,6 @@ def close(self): You can disallow closing connections by setting the closeable parameter to something false. In this case, closing tough connections will be silently ignored. - """ if self._closeable: self._close() @@ -417,7 +410,6 @@ def begin(self, *args, **kwargs): If the underlying driver supports this method, it will be called with the given parameters (e.g. for distributed transactions). - """ self._transaction = True try: @@ -461,7 +453,6 @@ def cancel(self): """Cancel a long-running transaction. If the underlying driver supports this method, it will be called. - """ self._transaction = False try: @@ -580,7 +571,6 @@ def close(self): """Close the tough cursor. It will not complain if you close it more than once. - """ if not self._closed: try: diff --git a/dbutils/steady_pg.py b/dbutils/steady_pg.py index a75b025..f7e4d4c 100644 --- a/dbutils/steady_pg.py +++ b/dbutils/steady_pg.py @@ -67,7 +67,6 @@ by Christoph Zwerschke in September 2005 Licensed under the MIT license. - """ from pg import DB as PgConnection @@ -98,7 +97,6 @@ class SteadyPgConnection: If you want the connection to be persistent in a threaded environment, then you should not deal with this class directly, but use either the PooledPg module or the PersistentPg module to get the connections. - """ version = __version__ @@ -117,7 +115,6 @@ def __init__( be silently ignored, but by default the connection can be closed args, kwargs: the parameters that shall be used to establish the PostgreSQL connections with PyGreSQL using pg.DB() - """ # basic initialization to make finalizer work self._con = None @@ -159,7 +156,6 @@ def _close(self): You can always close a tough connection with this method and it will not complain if you close it more than once. - """ if not self._closed: try: @@ -178,7 +174,6 @@ def close(self): You can disallow closing connections by setting the closeable parameter to something false. In this case, closing tough connections will be silently ignored. - """ if self._closeable: self._close() @@ -189,7 +184,6 @@ def reopen(self): """Reopen the tough connection. It will not complain if the connection cannot be reopened. - """ try: self._con.reopen() @@ -211,7 +205,6 @@ def reset(self): If a reset is not possible, tries to reopen the connection. It will not complain if the connection is already closed. - """ try: self._con.reset() @@ -286,7 +279,6 @@ def _get_tough_method(self, method): The tough version checks whether the connection is bad (lost) and automatically and transparently tries to reset the connection if this is the case (for instance, the database has been restarted). - """ def tough_method(*args, **kwargs): transaction = self._transaction @@ -318,7 +310,6 @@ def __getattr__(self, name): """Inherit the members of the standard connection class. Some methods are made "tougher" than in the standard version. - """ if self._con: attr = getattr(self._con, name) diff --git a/tests/test_persistent_db.py b/tests/test_persistent_db.py index bf1f8c6..7cccb30 100644 --- a/tests/test_persistent_db.py +++ b/tests/test_persistent_db.py @@ -8,7 +8,6 @@ Copyright and credit info: * This test was contributed by Christoph Zwerschke - """ import unittest diff --git a/tests/test_persistent_pg.py b/tests/test_persistent_pg.py index bed067b..5053316 100644 --- a/tests/test_persistent_pg.py +++ b/tests/test_persistent_pg.py @@ -8,7 +8,6 @@ Copyright and credit info: * This test was contributed by Christoph Zwerschke - """ import unittest diff --git a/tests/test_pooled_db.py b/tests/test_pooled_db.py index 860b7f5..bd5ccd0 100644 --- a/tests/test_pooled_db.py +++ b/tests/test_pooled_db.py @@ -8,7 +8,6 @@ Copyright and credit info: * This test was contributed by Christoph Zwerschke - """ import unittest diff --git a/tests/test_pooled_pg.py b/tests/test_pooled_pg.py index 0379693..1265888 100644 --- a/tests/test_pooled_pg.py +++ b/tests/test_pooled_pg.py @@ -8,7 +8,6 @@ Copyright and credit info: * This test was contributed by Christoph Zwerschke - """ import unittest diff --git a/tests/test_simple_pooled_db.py b/tests/test_simple_pooled_db.py index 3c6ede1..564fda3 100644 --- a/tests/test_simple_pooled_db.py +++ b/tests/test_simple_pooled_db.py @@ -9,7 +9,6 @@ Copyright and credit info: * This test was contributed by Christoph Zwerschke - """ import unittest diff --git a/tests/test_simple_pooled_pg.py b/tests/test_simple_pooled_pg.py index 251fd72..7065d58 100644 --- a/tests/test_simple_pooled_pg.py +++ b/tests/test_simple_pooled_pg.py @@ -8,7 +8,6 @@ Copyright and credit info: * This test was contributed by Christoph Zwerschke - """ import unittest diff --git a/tests/test_steady_db.py b/tests/test_steady_db.py index 79342a6..08aacfe 100644 --- a/tests/test_steady_db.py +++ b/tests/test_steady_db.py @@ -7,7 +7,6 @@ Copyright and credit info: * This test was contributed by Christoph Zwerschke - """ import unittest diff --git a/tests/test_steady_pg.py b/tests/test_steady_pg.py index df88b77..f798f43 100644 --- a/tests/test_steady_pg.py +++ b/tests/test_steady_pg.py @@ -9,7 +9,6 @@ Copyright and credit info: * This test was contributed by Christoph Zwerschke - """ import unittest From 8d61b6fae9effb6d9d93616c23bf545c14be30ed Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 26 Sep 2020 23:12:42 +0200 Subject: [PATCH 14/84] Fix PEP8 issues in setup.py --- setup.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 82f6bba..861ed92 100755 --- a/setup.py +++ b/setup.py @@ -46,10 +46,14 @@ url='https://webwareforpython.github.io/DBUtils/', download_url="https://pypi.org/project/DBUtils/", project_urls={ - "Documentation": "https://webwareforpython.github.io/DBUtils/main.html", - "Changelog": "https://webwareforpython.github.io/DBUtils/changelog.html", - "Issue Tracker": "https://github.com/WebwareForPython/DBUtils/issues", - "Source Code": "https://github.com/WebwareForPython/DBUtils"}, + 'Documentation': + 'https://webwareforpython.github.io/DBUtils/main.html', + 'Changelog': + 'https://webwareforpython.github.io/DBUtils/changelog.html', + 'Issue Tracker': + 'https://github.com/WebwareForPython/DBUtils/issues', + 'Source Code': + 'https://github.com/WebwareForPython/DBUtils'}, platforms=['any'], license='MIT License', packages=['dbutils'] From c5f7db2ac3829877e0cd1452259a4f7b76bac558 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 26 Sep 2020 23:38:30 +0200 Subject: [PATCH 15/84] Install tox in Travis configuration --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e705d3c..d67b1e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,6 +27,6 @@ cache: - "$HOME/.cache/pip" - "$TRAVIS_BUILD_DIR/.tox" install: - - python setup.py install + - pip install tox script: - tox -e $TOXENV From 4a60187187e1187f9c554bcb991b3bbd5f159594 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sun, 27 Sep 2020 00:00:58 +0200 Subject: [PATCH 16/84] Forgot to change CSS path to lower-case --- docs/changelog.html | 2 +- docs/main.de.html | 2 +- docs/main.html | 2 +- docs/make.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/changelog.html b/docs/changelog.html index d473594..f85ecbb 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -4,7 +4,7 @@ <meta charset="utf-8"/> <meta name="generator" content="Docutils 0.16: http://docutils.sourceforge.net/" /> <title>Changelog for DBUtils</title> -<link rel="stylesheet" href="Doc.css" type="text/css" /> +<link rel="stylesheet" href="doc.css" type="text/css" /> </head> <body> <div class="document" id="changelog-for-dbutils"> diff --git a/docs/main.de.html b/docs/main.de.html index 3d63022..0720d75 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -4,7 +4,7 @@ <meta charset="utf-8"/> <meta name="generator" content="Docutils 0.16: http://docutils.sourceforge.net/" /> <title>Benutzeranleitung für DBUtils</title> -<link rel="stylesheet" href="Doc.css" type="text/css" /> +<link rel="stylesheet" href="doc.css" type="text/css" /> </head> <body> <div class="document" id="benutzeranleitung-fur-dbutils"> diff --git a/docs/main.html b/docs/main.html index 22a6ea9..e6bc741 100644 --- a/docs/main.html +++ b/docs/main.html @@ -4,7 +4,7 @@ <meta charset="utf-8"/> <meta name="generator" content="Docutils 0.16: http://docutils.sourceforge.net/" /> <title>DBUtils User's Guide</title> -<link rel="stylesheet" href="Doc.css" type="text/css" /> +<link rel="stylesheet" href="doc.css" type="text/css" /> </head> <body> <div class="document" id="dbutils-user-s-guide"> diff --git a/docs/make.py b/docs/make.py index d81cfab..c2a0494 100755 --- a/docs/make.py +++ b/docs/make.py @@ -26,7 +26,7 @@ writer_name='html5', source=source, destination=destination, enable_exit_status=True, settings_overrides=dict( - stylesheet_path='Doc.css', + stylesheet_path='doc.css', embed_stylesheet=False, toc_backlinks=False, language_code=lang, From 19a8a33f71bb4295c8b5de364acbb494bd59fa8c Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 17 Oct 2020 10:32:16 +0200 Subject: [PATCH 17/84] Add note about breaking changes to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b35abf9..9debced 100644 --- a/README.md +++ b/README.md @@ -9,4 +9,6 @@ and the classic PyGreSQL interface. The current version 2.0 of DBUtils supports Python versions 2.7 and 3.5 to 3.9. +**Please have a look at the [changelog](https://webwareforpython.github.io/DBUtils/changelog.html), because there are some breaking changes in version 2.0.** + The DBUtils home page can be found at https://webwareforpython.github.io/DBUtils/ From a703b00be8315767b4d2dc1bb8e751409583704d Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 3 Apr 2021 22:02:21 +0200 Subject: [PATCH 18/84] Avoid "name Exception is not defined" when exiting At interpreter shutdown, the module's global variables are set to None before the module itself is released. __del__ methods may be called in those precarious circumstances, and should not rely on any global state. --- dbutils/pooled_db.py | 6 +++--- dbutils/pooled_pg.py | 4 ++-- dbutils/steady_db.py | 4 ++-- dbutils/steady_pg.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dbutils/pooled_db.py b/dbutils/pooled_db.py index 04b4f20..2de9b55 100644 --- a/dbutils/pooled_db.py +++ b/dbutils/pooled_db.py @@ -389,7 +389,7 @@ def __del__(self): """Delete the pool.""" try: self.close() - except Exception: + except: # builtin Exceptions might not exist any more pass def _wait_lock(self): @@ -437,7 +437,7 @@ def __del__(self): """Delete the pooled connection.""" try: self.close() - except Exception: + except: # builtin Exceptions might not exist any more pass @@ -524,5 +524,5 @@ def __del__(self): """Delete the pooled connection.""" try: self.close() - except Exception: + except: # builtin Exceptions might not exist any more pass diff --git a/dbutils/pooled_pg.py b/dbutils/pooled_pg.py index a14cf6e..0e0a3ae 100644 --- a/dbutils/pooled_pg.py +++ b/dbutils/pooled_pg.py @@ -244,7 +244,7 @@ def __del__(self): """Delete the pool.""" try: self.close() - except Exception: + except: # builtin Exceptions might not exist any more pass @@ -291,5 +291,5 @@ def __del__(self): """Delete the pooled connection.""" try: self.close() - except Exception: + except: # builtin Exceptions might not exist any more pass diff --git a/dbutils/steady_db.py b/dbutils/steady_db.py index 6af877c..e5a6b03 100644 --- a/dbutils/steady_db.py +++ b/dbutils/steady_db.py @@ -512,7 +512,7 @@ def __del__(self): """Delete the steady connection.""" try: self._close() # make sure the connection is closed - except Exception: + except: # builtin Exceptions might not exist any more pass @@ -697,5 +697,5 @@ def __del__(self): """Delete the steady cursor.""" try: self.close() # make sure the cursor is closed - except Exception: + except: # builtin Exceptions might not exist any more pass diff --git a/dbutils/steady_pg.py b/dbutils/steady_pg.py index f7e4d4c..3a500ee 100644 --- a/dbutils/steady_pg.py +++ b/dbutils/steady_pg.py @@ -324,5 +324,5 @@ def __del__(self): """Delete the steady connection.""" try: self._close() # make sure the connection is closed - except Exception: + except: # builtin Exceptions might not exist any more pass From 4085abb0bfc97c73306b3c4bc8c4e12c5b3ada83 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 3 Apr 2021 22:17:53 +0200 Subject: [PATCH 19/84] Make Flake8 happy again --- .flake8 | 4 ++++ MANIFEST.in | 1 + 2 files changed, 5 insertions(+) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..578558b --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +ignore = E722,W503 +exclude = .git,.pytest_cache,.tox,.venv,.idea,__pycache__,build,dist,docs +max-line-length = 79 diff --git a/MANIFEST.in b/MANIFEST.in index 2d8e122..82221db 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,6 +4,7 @@ include LICENSE include README.md include .bumpversion.cfg +include .flake8 include tox.ini recursive-include tests *.py From a1c72a3d0b1e5fc0508a7ea8db5419f7972e7075 Mon Sep 17 00:00:00 2001 From: Christian Clauss <cclauss@me.com> Date: Thu, 8 Apr 2021 17:27:02 +0200 Subject: [PATCH 20/84] GitHub Action to test with tox (#31) --- .github/workflows/test_with_tox.yml | 20 ++++++++++++++++++ .travis.yml | 32 ----------------------------- dbutils/simple_pooled_db.py | 2 +- tox.ini | 14 +++++++++---- 4 files changed, 31 insertions(+), 37 deletions(-) create mode 100644 .github/workflows/test_with_tox.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/test_with_tox.yml b/.github/workflows/test_with_tox.yml new file mode 100644 index 0000000..e19f860 --- /dev/null +++ b/.github/workflows/test_with_tox.yml @@ -0,0 +1,20 @@ +name: Python package + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python: [2.7, 3.6, 3.7, 3.8, 3.9] + steps: + - uses: actions/checkout@v2 + - name: Setup Python ${{ matrix.python }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - run: pip install tox + - run: tox -e py + - if: matrix.python == 3.9 + run: TOXENV=codespell,flake8,manifest,docs tox diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d67b1e0..0000000 --- a/.travis.yml +++ /dev/null @@ -1,32 +0,0 @@ -# Travis CI configuration -# see https://docs.travis-ci.com/user/languages/python - -language: python -matrix: - include: - - name: Code quality tests - env: TOXENV=flake8,manifest,docs - python: 3.8 - - name: Unit tests with Python 3.8 - env: TOXENV=py38 - python: 3.8 - - name: Unit tests with Python 3.7 - env: TOXENV=py37 - python: 3.7 - - name: Unit tests with Python 3.6 - env: TOXENV=py36 - python: 3.6 - - name: Unit tests with Python 3.5 - env: TOXENV=py35 - python: 3.5 - - name: Unit tests with Python 2.7 - env: TOXENV=py27 - python: 2.7 -cache: - directories: - - "$HOME/.cache/pip" - - "$TRAVIS_BUILD_DIR/.tox" -install: - - pip install tox -script: - - tox -e $TOXENV diff --git a/dbutils/simple_pooled_db.py b/dbutils/simple_pooled_db.py index cfd2c8b..5e31b29 100644 --- a/dbutils/simple_pooled_db.py +++ b/dbutils/simple_pooled_db.py @@ -47,7 +47,7 @@ * Connections should have some sort of maximum usage limit after which they should be automatically closed and reopened. * Prefer or enforce thread-affinity for the connections, -allowing for both sharable and non-sharable connections. +allowing for both shareable and non-shareable connections. Please note that these and other ideas have been already implemented in in PooledDB, a more sophisticated version diff --git a/tox.ini b/tox.ini index 2de6636..9700b46 100644 --- a/tox.ini +++ b/tox.ini @@ -8,20 +8,26 @@ deps = pytest commands = pytest {posargs} +[testenv:codespell] +basepython = python3.9 +deps = codespell +commands = + codespell --ignore-words-list="dont'" --quiet-level=2 --skip="./.*,./docs/main.de.*" + [testenv:flake8] -basepython = python3.8 +basepython = python3.9 deps = flake8 commands = - flake8 dbutils tests docs setup.py + flake8 . [testenv:manifest] -basepython = python3.8 +basepython = python3.9 deps = check-manifest commands = check-manifest -v [testenv:docs] -basepython = python3.8 +basepython = python3.9 deps = docutils changedir = docs commands = From afcecd47592435bc36ef6c3b91ad06d852ba4488 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Thu, 8 Apr 2021 17:43:07 +0200 Subject: [PATCH 21/84] Change name of GitHub workflow --- .github/workflows/test_with_tox.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_with_tox.yml b/.github/workflows/test_with_tox.yml index e19f860..509af11 100644 --- a/.github/workflows/test_with_tox.yml +++ b/.github/workflows/test_with_tox.yml @@ -1,4 +1,4 @@ -name: Python package +name: Test DBUtils using tox on: [push, pull_request] From 6d5bd16140869032b4bcf45e8565cf2d0b3b7c4f Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Thu, 8 Apr 2021 18:12:18 +0200 Subject: [PATCH 22/84] Use config file for codespell This allows running it without tox. --- .codespellrc | 3 +++ .github/workflows/test_with_tox.yml | 2 +- tests/test_steady_db.py | 6 +++--- tox.ini | 6 +++--- 4 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 .codespellrc diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 0000000..15761d2 --- /dev/null +++ b/.codespellrc @@ -0,0 +1,3 @@ +[codespell] +skip = .tox,.venv,*.de.html,*.de.rst +quiet-level = 2 diff --git a/.github/workflows/test_with_tox.yml b/.github/workflows/test_with_tox.yml index 509af11..fe6e356 100644 --- a/.github/workflows/test_with_tox.yml +++ b/.github/workflows/test_with_tox.yml @@ -17,4 +17,4 @@ jobs: - run: pip install tox - run: tox -e py - if: matrix.python == 3.9 - run: TOXENV=codespell,flake8,manifest,docs tox + run: TOXENV=flake8,manifest,docs,spell tox diff --git a/tests/test_steady_db.py b/tests/test_steady_db.py index 08aacfe..e32a711 100644 --- a/tests/test_steady_db.py +++ b/tests/test_steady_db.py @@ -237,12 +237,12 @@ def test_connection(self): self.assertTrue(cursor.valid) self.assertEqual(db._usage, 1) self.assertEqual(db._con.num_uses, 1) - cursor.execute('set doit') + cursor.execute('set this') db.commit() - cursor.execute('set dont') + cursor.execute('set that') db.rollback() self.assertEqual( - db._con.session, ['doit', 'commit', 'dont', 'rollback']) + db._con.session, ['this', 'commit', 'that', 'rollback']) def test_connection_context_handler(self): db = SteadyDBconnect( diff --git a/tox.ini b/tox.ini index 9700b46..a8b505b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{27,35,36,37,38,39}, flake8, manifest, docs +envlist = py{27,35,36,37,38,39}, flake8, manifest, docs, spell [testenv] setenv = @@ -8,11 +8,11 @@ deps = pytest commands = pytest {posargs} -[testenv:codespell] +[testenv:spell] basepython = python3.9 deps = codespell commands = - codespell --ignore-words-list="dont'" --quiet-level=2 --skip="./.*,./docs/main.de.*" + codespell . [testenv:flake8] basepython = python3.9 From ace813f963329dfc0e43b57a611e031aafe8082e Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Thu, 8 Apr 2021 18:12:58 +0200 Subject: [PATCH 23/84] Recreate html with latest docutils --- docs/changelog.html | 79 +++++++++++++------------- docs/main.de.html | 133 ++++++++++++++++++++++---------------------- docs/main.html | 133 ++++++++++++++++++++++---------------------- 3 files changed, 174 insertions(+), 171 deletions(-) diff --git a/docs/changelog.html b/docs/changelog.html index f85ecbb..7853af5 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -2,16 +2,17 @@ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta charset="utf-8"/> -<meta name="generator" content="Docutils 0.16: http://docutils.sourceforge.net/" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<meta name="generator" content="Docutils 0.17: http://docutils.sourceforge.net/" /> <title>Changelog for DBUtils</title> <link rel="stylesheet" href="doc.css" type="text/css" /> </head> <body> -<div class="document" id="changelog-for-dbutils"> +<main id="changelog-for-dbutils"> <h1 class="title">Changelog for DBUtils</h1> -<div class="section" id="id1"> -<h1>2.0</h1> +<section id="id1"> +<h2>2.0</h2> <p>DBUtils 2.0 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> <p>Changes:</p> @@ -25,9 +26,9 @@ <h1>2.0</h1> <li><p>The documentation has been adapted to reflect the changes in this version.</p></li> <li><p>This changelog has been compiled from the former release notes.</p></li> </ul> -</div> -<div class="section" id="id2"> -<h1>1.4</h1> +</section> +<section id="id2"> +<h2>1.4</h2> <p>DBUtils 1.4 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> <p>Improvements:</p> @@ -36,23 +37,23 @@ <h1>1.4</h1> <span class="docutils literal">maxusage</span> limit has been reached when the connection is not currently inside a transaction.</p></li> </ul> -</div> -<div class="section" id="id3"> -<h1>1.3</h1> +</section> +<section id="id3"> +<h2>1.3</h2> <p>DBUtils 1.3 was released on March 3, 2018.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.4 to 3.7.</p> <p>Improvements:</p> <ul class="simple"> <li><p>This version now supports context handlers for connections and cursors.</p></li> </ul> -</div> -<div class="section" id="id4"> -<h1>1.2</h1> +</section> +<section id="id4"> +<h2>1.2</h2> <p>DBUtils 1.2 was released on February 5, 2017.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.0 to 3.6.</p> -</div> -<div class="section" id="id5"> -<h1>1.1.1</h1> +</section> +<section id="id5"> +<h2>1.1.1</h2> <p>DBUtils 1.1.1 was released on February 4, 2017.</p> <p>It is intended to be used with Python versions 2.3 to 2.7.</p> <p>Improvements:</p> @@ -64,9 +65,9 @@ <h1>1.1.1</h1> <ul class="simple"> <li><p>Fixed a problem when running under Jython (reported by Vitaly Kruglikov).</p></li> </ul> -</div> -<div class="section" id="id6"> -<h1>1.1</h1> +</section> +<section id="id6"> +<h2>1.1</h2> <p>DBUtils 1.1 was released on August 14, 2011.</p> <p>Improvements:</p> <ul class="simple"> @@ -93,9 +94,9 @@ <h1>1.1</h1> <li><p>Fixed an issue with the <span class="docutils literal">setoutputsize()</span> cursor method.</p></li> <li><p>Fixed some minor issues with the <span class="docutils literal">DBUtilsExample</span> for Webware.</p></li> </ul> -</div> -<div class="section" id="id7"> -<h1>1.0</h1> +</section> +<section id="id7"> +<h2>1.0</h2> <p>DBUtils 1.0 was released on November 29, 2008.</p> <p>It is intended to be used with Python versions 2.2 to 2.6.</p> <p>Changes:</p> @@ -126,18 +127,18 @@ <h1>1.0</h1> connection creator function is specified. This had not worked before with the MySQLdb module (problem reported by Gregory Pinero).</p></li> </ul> -</div> -<div class="section" id="id8"> -<h1>0.9.4</h1> +</section> +<section id="id8"> +<h2>0.9.4</h2> <p>DBUtils 0.9.4 was released on July 7, 2007.</p> <p>This release fixes a problem in the destructor code and has been supplemented with a German User's Guide.</p> <p>Again, please note that the <span class="docutils literal">dbapi</span> parameter has been renamed to <span class="docutils literal">creator</span> in the last release, since you can now pass custom creator functions for database connections instead of DB-API 2 modules.</p> -</div> -<div class="section" id="id9"> -<h1>0.9.3</h1> +</section> +<section id="id9"> +<h2>0.9.3</h2> <p>DBUtils 0.9.3 was released on May 21, 2007.</p> <p>Changes:</p> <ul class="simple"> @@ -150,9 +151,9 @@ <h1>0.9.3</h1> <li><p>Some fixes in the documentation. Added Chinese translation of the User's Guide, kindly contributed by gashero.</p></li> </ul> -</div> -<div class="section" id="id10"> -<h1>0.9.2</h1> +</section> +<section id="id10"> +<h2>0.9.2</h2> <p>DBUtils 0.9.2 was released on September 22, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.5.</p> <p>Changes:</p> @@ -160,9 +161,9 @@ <h1>0.9.2</h1> <li><p>Renamed <span class="docutils literal">SolidDB</span> to <span class="docutils literal">SteadyDB</span> to avoid confusion with the "solidDB" storage engine. Accordingly, renamed <span class="docutils literal">SolidPg</span> to <span class="docutils literal">SteadyPg</span>.</p></li> </ul> -</div> -<div class="section" id="id11"> -<h1>0.9.1</h1> +</section> +<section id="id11"> +<h2>0.9.1</h2> <p>DBUtils 0.9.1 was released on May 8, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.4.</p> <p>Changes:</p> @@ -174,13 +175,13 @@ <h1>0.9.1</h1> to specify cursor classes. (Suggested by Michael Palmer.)</p></li> <li><p>Improved the documentation and added a User's Guide.</p></li> </ul> -</div> -<div class="section" id="id12"> -<h1>0.8.1 - 2005-09-13</h1> +</section> +<section id="id12"> +<h2>0.8.1 - 2005-09-13</h2> <p>DBUtils 0.8.1 was released on September 13, 2005.</p> <p>It is intended to be used with Python versions 2.0 to 2.4.</p> <p>This is the first public release of DBUtils.</p> -</div> -</div> +</section> +</main> </body> </html> diff --git a/docs/main.de.html b/docs/main.de.html index 0720d75..1e6ae7e 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -2,12 +2,13 @@ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="de" lang="de"> <head> <meta charset="utf-8"/> -<meta name="generator" content="Docutils 0.16: http://docutils.sourceforge.net/" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<meta name="generator" content="Docutils 0.17: http://docutils.sourceforge.net/" /> <title>Benutzeranleitung für DBUtils</title> <link rel="stylesheet" href="doc.css" type="text/css" /> </head> -<body> -<div class="document" id="benutzeranleitung-fur-dbutils"> +<body class="with-toc"> +<main id="benutzeranleitung-fur-dbutils"> <h1 class="title">Benutzeranleitung für DBUtils</h1> <dl class="docinfo simple"> <dt class="version">Version</dt> @@ -51,8 +52,8 @@ <h1 class="title">Benutzeranleitung für DBUtils</h1> <li><p><a class="reference internal" href="#copyright-und-lizenz" id="id25">Copyright und Lizenz</a></p></li> </ul> </div> -<div class="section" id="zusammenfassung"> -<h1>Zusammenfassung</h1> +<section id="zusammenfassung"> +<h2>Zusammenfassung</h2> <p><a class="reference external" href="https://github.com/WebwareForPython/DBUtils">DBUtils</a> ist eine Sammlung von Python-Modulen, mit deren Hilfe man in <a class="reference external" href="https://www.python.org">Python</a> geschriebene Multithread-Anwendungen auf sichere und effiziente Weise an Datenbanken anbinden kann.</p> @@ -60,9 +61,9 @@ <h1>Zusammenfassung</h1> und <a class="reference external" href="https://www.pygresql.org/">PyGreSQL</a> als <a class="reference external" href="https://www.postgresql.org/">PostgreSQL</a>-Datenbankadapter entwickelt, kann aber inzwischen für beliebige Python-Anwendungen und beliebige auf <a class="reference external" href="https://www.python.org/dev/peps/pep-0249/">DB-API 2</a> beruhende Python-Datenbankadapter verwendet werden.</p> -</div> -<div class="section" id="module"> -<h1>Module</h1> +</section> +<section id="module"> +<h2>Module</h2> <p>DBUtils ist als Python-Package realisiert worden, das aus zwei verschiedenen Gruppen von Modulen besteht: Einer Gruppe zur Verwendung mit beliebigen DB-API-2-Datenbankadaptern, und einer Gruppe zur Verwendung mit dem klassischen @@ -121,44 +122,44 @@ <h1>Module</h1> <p>Die Abhängigkeiten der Module in der Variante für den klassischen PyGreSQL-Adapter sehen ähnlich aus:</p> <img alt="depdependencies_pg.png" src="depdependencies_pg.png" /> -</div> -<div class="section" id="download"> -<h1>Download</h1> +</section> +<section id="download"> +<h2>Download</h2> <p>Die aktuelle Version von DBUtils kann vom Python Package Index heruntergeladen werden:</p> <pre class="literal-block">https://pypi.python.org/pypi/DBUtils</pre> <p>Das Source-Code-Repository befindet sich hier auf GitHub:</p> <pre class="literal-block">https://github.com/WebwareForPython/DBUtils</pre> -</div> -<div class="section" id="installation"> -<h1>Installation</h1> -<div class="section" id="id1"> +</section> +<section id="installation"> <h2>Installation</h2> +<section id="id1"> +<h3>Installation</h3> <p>Das Paket kann auf die übliche Weise installiert werden:</p> <pre class="literal-block">python setup.py install</pre> <p>Noch einfacher ist, das Paket in einem Schritt mit <a class="reference external" href="https://pip.pypa.io/">pip</a> automatisch herunterzuladen und zu installieren:</p> <pre class="literal-block">pip install DBUtils</pre> -</div> -</div> -<div class="section" id="anforderungen"> -<h1>Anforderungen</h1> +</section> +</section> +<section id="anforderungen"> +<h2>Anforderungen</h2> <p>DBUtils unterstützt die <a class="reference external" href="https://www.python.org">Python</a> Versionen 2.7 und 3.5 bis 3.9.</p> <p>Die Module in der Variante für klassisches PyGreSQL benötigen <a class="reference external" href="https://www.pygresql.org/">PyGreSQL</a> Version 4.0 oder höher, während die Module in der allgemeinen Variante für DB-API 2 mit jedem beliebigen Python-Datenbankadapter-Modul zusammenarbeiten, das auf <a class="reference external" href="https://www.python.org/dev/peps/pep-0249/">DB-API 2</a> basiert.</p> -</div> -<div class="section" id="funktionalitat"> -<h1>Funktionalität</h1> +</section> +<section id="funktionalitat"> +<h2>Funktionalität</h2> <p>Dieser Abschnitt verwendet nur die Bezeichnungen der DB-API-2-Variante, aber Entsprechendes gilt auch für die PyGreSQL-Variante.</p> <p>DBUtils installiert sich als Paket <span class="docutils literal">dbutils</span>, das alle hier beschriebenen Module enthält. Jedes dieser Modul enthält im Wesentlichen eine Klasse, die einen analogen Namen trägt und die jeweilige Funktionalität bereitstellt. So enthält z.B. das Modul <span class="docutils literal">dbutils.pooled_db</span> die Klasse <span class="docutils literal">PooledDB</span>.</p> -<div class="section" id="simplepooleddb-simple-pooled-db"> -<h2>SimplePooledDB (simple_pooled_db)</h2> +<section id="simplepooleddb-simple-pooled-db"> +<h3>SimplePooledDB (simple_pooled_db)</h3> <p>Die Klasse <span class="docutils literal">SimplePooledDB</span> in <span class="docutils literal">dbutils.simple_pooled_db</span> ist eine sehr elementare Referenz-Implementierung eines Pools von Datenbankverbindungen. Hiermit ist ein Vorratsspeicher an Datenbankverbindungen gemeint, aus dem sich @@ -168,9 +169,9 @@ <h2>SimplePooledDB (simple_pooled_db)</h2> Wesentlichen identisch mit dem zu Webware for Python gehörenden Modul <span class="docutils literal">MiscUtils.DBPool</span>. Es ist eher zur Verdeutlichung des Konzepts gedacht, als zum Einsatz im produktiven Betrieb.</p> -</div> -<div class="section" id="steadydbconnection-steady-db"> -<h2>SteadyDBConnection (steady_db)</h2> +</section> +<section id="steadydbconnection-steady-db"> +<h3>SteadyDBConnection (steady_db)</h3> <p>Die Klasse <span class="docutils literal">SteadyDBConnection</span> im Modul <span class="docutils literal">dbutils.steady_db</span> stellt "gehärtete" Datenbankverbindungen bereit, denen gewöhnlichen Verbindungen eines DB-API-2-Datenbankadapters zugrunde liegen. Eine "gehärtete" Verbindung @@ -184,9 +185,9 @@ <h2>SteadyDBConnection (steady_db)</h2> Firewall neu gestartet wurde und dabei ihren Verbindungsstatus verloren hat.</p> <p>Normalerweise benutzen Sie das <span class="docutils literal">steady_db</span>-Modul nicht direkt; es wird aber von den beiden nächsten Modulen benötigt, <span class="docutils literal">persistent_db</span> und <span class="docutils literal">pooled_db</span>.</p> -</div> -<div class="section" id="persistentdb-persistent-db"> -<h2>PersistentDB (persistent_db)</h2> +</section> +<section id="persistentdb-persistent-db"> +<h3>PersistentDB (persistent_db)</h3> <p>Die Klasse <span class="docutils literal">PersistentDB</span> im Modul <span class="docutils literal">dbutils.persistent_db</span> stellt gehärtete, thread-affine, persistente Datenbankverbindungen zur Verfügung, unter Benutzung eines beliebigen DB-API-2-Datenbankadapters. Mit "thread-affin" @@ -211,9 +212,9 @@ <h2>PersistentDB (persistent_db)</h2> liegende DB-API-2-Datenbankadapter nicht thread-sicher auf der Verbindungsebene ist, oder wenn parallele Threads Parameter der Datenbank-Sitzung verändern oder Transaktionen mit mehreren SQL-Befehlen durchführen.</p> -</div> -<div class="section" id="pooleddb-pooled-db"> -<h2>PooledDB (pooled_db)</h2> +</section> +<section id="pooleddb-pooled-db"> +<h3>PooledDB (pooled_db)</h3> <p>Die Klasse <span class="docutils literal">PooledDB</span> im Modul <span class="docutils literal">dbutils.pooled_db</span> stellt, unter Benutzung eines beliebigen DB-API-2-Datenbankadapters, einen Pool von gehärteten, thread-sicheren Datenbankverbindungen zur Verfügung, die automatisch, ohne dass @@ -241,9 +242,9 @@ <h2>PooledDB (pooled_db)</h2> hierum keine Sorgen zu machen, aber Sie sollten darauf achten, dedizierte Datenbankverbindungen zu verwenden, sobald Sie Parameter der Datenbanksitzung verändern oder Transaktionen mit mehreren SQL-Befehlen ausführen.</p> -</div> -<div class="section" id="die-qual-der-wahl"> -<h2>Die Qual der Wahl</h2> +</section> +<section id="die-qual-der-wahl"> +<h3>Die Qual der Wahl</h3> <p>Sowohl <span class="docutils literal">persistent_db</span> als auch <span class="docutils literal">pooled_db</span> dienen dem gleichen Zweck, nämlich die Effizienz des Datenbankzugriffs durch Wiederverwendung von Datenbankverbindungen zu steigern, und dabei gleichzeitig die Stabilität @@ -259,10 +260,10 @@ <h2>Die Qual der Wahl</h2> DB-API-2-Datenbankadapters.</p> <p>Da die Schnittstellen beider Module sehr ähnlich sind, können Sie recht einfach von einem Modul zum anderen wechseln und austesten, welches geeigneter ist.</p> -</div> -</div> -<div class="section" id="benutzung"> -<h1>Benutzung</h1> +</section> +</section> +<section id="benutzung"> +<h2>Benutzung</h2> <p>Die Benutzung aller Module ist zwar recht ähnlich, aber es gibt vor allem bei der Initialisierung auch einige Unterschiede, sowohl zwischen den "Pooled"- und den "Persistent"-Varianten, als auch zwischen den DB-API-2- und den @@ -273,8 +274,8 @@ <h1>Benutzung</h1> sich die Dokumentation des <span class="docutils literal">pooled_db</span>-Moduls wie folgt anzeigen lassen (dies funktioniert entsprechend auch mit den anderen Modulen):</p> <pre class="literal-block">help(pooled_db)</pre> -<div class="section" id="id2"> -<h2>PersistentDB (persistent_db)</h2> +<section id="id2"> +<h3>PersistentDB (persistent_db)</h3> <p>Wenn Sie das <span class="docutils literal">persistent_db</span>-Modul einsetzen möchten, müssen Sie zuerst einen Generator für die von Ihnen gewünschte Art von Datenbankverbindungen einrichten, indem Sie eine Instanz der Klasse <span class="docutils literal">persistent_db</span> erzeugen, wobei Sie folgende @@ -336,9 +337,9 @@ <h2>PersistentDB (persistent_db)</h2> einigen Umgebungen nicht funktionieren (es ist zum Beispiel bekannt, dass <span class="docutils literal">mod_wsgi</span> hier Probleme bereitet, da es Daten, die mit <span class="docutils literal">threading.local</span> gespeichert wurden, zwischen Requests löscht).</p> -</div> -<div class="section" id="id3"> -<h2>PooledDB (pooled_db)</h2> +</section> +<section id="id3"> +<h3>PooledDB (pooled_db)</h3> <p>Wenn Sie das <span class="docutils literal">pooled_db</span>-Modul einsetzen möchten, müssen Sie zuerst einen Pool für die von Ihnen gewünschte Art von Datenbankverbindungen einrichten, indem Sie eine Instanz der Klasse <span class="docutils literal">pooled_db</span> erzeugen, wobei Sie folgende @@ -433,10 +434,10 @@ <h2>PooledDB (pooled_db)</h2> transparente Neueröffnen von Verbindungen bis zum Ende der Transaktion ausgesetzt wird, und dass die Verbindung zurückgerollt wird, bevor sie wieder an den Verbindungspool zurückgegeben wird.</p> -</div> -</div> -<div class="section" id="anmerkungen"> -<h1>Anmerkungen</h1> +</section> +</section> +<section id="anmerkungen"> +<h2>Anmerkungen</h2> <p>Wenn Sie einen der bekannten "Object-Relational Mapper" <a class="reference external" href="http://sqlobject.org/">SQLObject</a> oder <a class="reference external" href="https://www.sqlalchemy.org">SQLAlchemy</a> verwenden, dann benötigen Sie DBUtils nicht, denn diese haben ihre eigenen Mechanismen zum Pooling von Datenbankverbindungen eingebaut. @@ -453,9 +454,9 @@ <h1>Anmerkungen</h1> dann sollten Sie auf eine Middleware für das Connection-Pooling zurückgreifen, die Multi-Processing unterstützt, wie zum Beispiel <a class="reference external" href="https://www.pgpool.net/">pgpool</a> oder <a class="reference external" href="https://pgbouncer.github.io/">pgbouncer</a> für die PostgreSQL-Datenbank.</p> -</div> -<div class="section" id="zukunft"> -<h1>Zukunft</h1> +</section> +<section id="zukunft"> +<h2>Zukunft</h2> <p>Einige Ideen für zukünftige Verbesserungen:</p> <ul class="simple"> <li><p>Alternativ zur Obergrenze in der Anzahl der Nutzung einer Datenbankverbindung @@ -475,14 +476,14 @@ <h1>Zukunft</h1> <li><p>Optional sollten Benutzung, schlechte Verbindungen und Überschreitung von Obergrenzen in Logs gespeichert werden können.</p></li> </ul> -</div> -<div class="section" id="fehlermeldungen-und-feedback"> -<h1>Fehlermeldungen und Feedback</h1> +</section> +<section id="fehlermeldungen-und-feedback"> +<h2>Fehlermeldungen und Feedback</h2> <p>Fehlermeldungen, Patches und Feedback können Sie als <a class="reference external" href="https://github.com/WebwareForPython/DBUtils/issues">Issues</a> oder <a class="reference external" href="https://github.com/WebwareForPython/DBUtils/pulls">Pull Requests</a> auf der <a class="reference external" href="https://github.com/WebwareForPython/DBUtils">GitHub-Projektseite</a> von DBUtils übermitteln.</p> -</div> -<div class="section" id="links"> -<h1>Links</h1> +</section> +<section id="links"> +<h2>Links</h2> <p>Einige Links zu verwandter und alternativer Software:</p> <ul class="simple"> <li><p><a class="reference external" href="https://github.com/WebwareForPython/DBUtils">DBUtils</a></p></li> @@ -496,9 +497,9 @@ <h1>Links</h1> <li><p><a class="reference external" href="http://sqlobject.org/">SQLObject</a> Objekt-relationaler Mapper</p></li> <li><p><a class="reference external" href="https://www.sqlalchemy.org">SQLAlchemy</a> Objekt-relationaler Mapper</p></li> </ul> -</div> -<div class="section" id="autoren"> -<h1>Autoren</h1> +</section> +<section id="autoren"> +<h2>Autoren</h2> <dl class="field-list simple"> <dt>Autor</dt> <dd><p><a class="reference external" href="https://github.com/Cito">Christoph Zwerschke</a></p> @@ -511,14 +512,14 @@ <h1>Autoren</h1> Matthew Harriger, Gregory Piñero und Josef van Eenbergen.</p> </dd> </dl> -</div> -<div class="section" id="copyright-und-lizenz"> -<h1>Copyright und Lizenz</h1> +</section> +<section id="copyright-und-lizenz"> +<h2>Copyright und Lizenz</h2> <p>Copyright © 2005-2020 Christoph Zwerschke. Alle Rechte vorbehalten.</p> <p>DBUtils ist freie und quelloffene Software, lizenziert unter der <a class="reference external" href="https://opensource.org/licenses/MIT">MIT-Lizenz</a>.</p> -</div> -</div> +</section> +</main> </body> </html> diff --git a/docs/main.html b/docs/main.html index e6bc741..49c5cc5 100644 --- a/docs/main.html +++ b/docs/main.html @@ -2,12 +2,13 @@ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta charset="utf-8"/> -<meta name="generator" content="Docutils 0.16: http://docutils.sourceforge.net/" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<meta name="generator" content="Docutils 0.17: http://docutils.sourceforge.net/" /> <title>DBUtils User's Guide</title> <link rel="stylesheet" href="doc.css" type="text/css" /> </head> -<body> -<div class="document" id="dbutils-user-s-guide"> +<body class="with-toc"> +<main id="dbutils-user-s-guide"> <h1 class="title">DBUtils User's Guide</h1> <dl class="docinfo simple"> <dt class="version">Version</dt> @@ -51,17 +52,17 @@ <h1 class="title">DBUtils User's Guide</h1> <li><p><a class="reference internal" href="#copyright-and-license" id="id25">Copyright and License</a></p></li> </ul> </div> -<div class="section" id="synopsis"> -<h1>Synopsis</h1> +<section id="synopsis"> +<h2>Synopsis</h2> <p><a class="reference external" href="https://github.com/WebwareForPython/DBUtils">DBUtils</a> is a suite of Python modules allowing to connect in a safe and efficient way between a threaded <a class="reference external" href="https://www.python.org">Python</a> application and a database.</p> <p>DBUtils has been originally written particularly for <a class="reference external" href="https://webwareforpython.github.io/w4py/">Webware for Python</a> as the application and <a class="reference external" href="https://www.pygresql.org/">PyGreSQL</a> as the adapter to a <a class="reference external" href="https://www.postgresql.org/">PostgreSQL</a> database, but it can meanwhile be used for any other Python application and <a class="reference external" href="https://www.python.org/dev/peps/pep-0249/">DB-API 2</a> conformant database adapter.</p> -</div> -<div class="section" id="modules"> -<h1>Modules</h1> +</section> +<section id="modules"> +<h2>Modules</h2> <p>The DBUtils suite is realized as a Python package containing two subsets of modules, one for use with arbitrary DB-API 2 modules, the other one for use with the classic PyGreSQL module.</p> @@ -119,42 +120,42 @@ <h1>Modules</h1> <p>The dependencies of the modules in the classic PyGreSQL variant are similar:</p> <img alt="dependencies_pg.png" src="dependencies_pg.png" /> -</div> -<div class="section" id="download"> -<h1>Download</h1> +</section> +<section id="download"> +<h2>Download</h2> <p>You can download the actual version of DBUtils from the Python Package Index at:</p> <pre class="literal-block">https://pypi.python.org/pypi/DBUtils</pre> <p>The source code repository can be found here on GitHub:</p> <pre class="literal-block">https://github.com/WebwareForPython/DBUtils</pre> -</div> -<div class="section" id="installation"> -<h1>Installation</h1> -<div class="section" id="id1"> +</section> +<section id="installation"> <h2>Installation</h2> +<section id="id1"> +<h3>Installation</h3> <p>The package can be installed in the usual way:</p> <pre class="literal-block">python setup.py install</pre> <p>It is even easier to download and install the package in one go using <a class="reference external" href="https://pip.pypa.io/">pip</a>:</p> <pre class="literal-block">pip install DBUtils</pre> -</div> -</div> -<div class="section" id="requirements"> -<h1>Requirements</h1> +</section> +</section> +<section id="requirements"> +<h2>Requirements</h2> <p>DBUtils supports <a class="reference external" href="https://www.python.org">Python</a> version 2.7 and Python versions 3.5 to 3.9.</p> <p>The modules in the classic PyGreSQL variant need <a class="reference external" href="https://www.pygresql.org/">PyGreSQL</a> version 4.0 or above, while the modules in the universal DB-API 2 variant run with any Python <a class="reference external" href="https://www.python.org/dev/peps/pep-0249/">DB-API 2</a> compliant database interface module.</p> -</div> -<div class="section" id="functionality"> -<h1>Functionality</h1> +</section> +<section id="functionality"> +<h2>Functionality</h2> <p>This section will refer to the names in the DB-API 2 variant only, but the same applies to the classic PyGreSQL variant.</p> <p>DBUtils installs itself as a package <span class="docutils literal">dbutils</span> containing all the modules that are described in this guide. Each of these modules contains essentially one class with an analogous name that provides the corresponding functionality. For instance, the module <span class="docutils literal">dbutils.pooled_db</span> contains the class <span class="docutils literal">PooledDB</span>.</p> -<div class="section" id="simplepooleddb-simple-pooled-db"> -<h2>SimplePooledDB (simple_pooled_db)</h2> +<section id="simplepooleddb-simple-pooled-db"> +<h3>SimplePooledDB (simple_pooled_db)</h3> <p>The class <span class="docutils literal">SimplePooledDB</span> in <span class="docutils literal">dbutils.simple_pooled_db</span> is a very basic reference implementation of a pooled database connection. It is much less sophisticated than the regular <span class="docutils literal">pooled_db</span> module and is particularly lacking @@ -162,9 +163,9 @@ <h2>SimplePooledDB (simple_pooled_db)</h2> same as the <span class="docutils literal">MiscUtils.DBPool</span> module that is part of Webware for Python. You should consider it a demonstration of concept rather than something that should go into production.</p> -</div> -<div class="section" id="steadydbconnection-steady-db"> -<h2>SteadyDBConnection (steady_db)</h2> +</section> +<section id="steadydbconnection-steady-db"> +<h3>SteadyDBConnection (steady_db)</h3> <p>The class <span class="docutils literal">SteadyDBConnection</span> in the module <span class="docutils literal">dbutils.steady_db</span> implements "hardened" connections to a database, based on ordinary connections made by any DB-API 2 database module. A "hardened" connection will transparently reopen @@ -177,9 +178,9 @@ <h2>SteadyDBConnection (steady_db)</h2> restarted and lost its state.</p> <p>Usually, you will not use the <span class="docutils literal">steady_db</span> module directly; it merely serves as a basis for the next two modules, <span class="docutils literal">persistent_db</span> and <span class="docutils literal">Pooled_db</span>.</p> -</div> -<div class="section" id="persistentdb-persistent-db"> -<h2>PersistentDB (persistent_db)</h2> +</section> +<section id="persistentdb-persistent-db"> +<h3>PersistentDB (persistent_db)</h3> <p>The class <span class="docutils literal">PersistentDB</span> in the module <span class="docutils literal">dbutils.persistent_db</span> implements steady, thread-affine, persistent connections to a database, using any DB-API 2 database module. "Thread-affine" and "persistent" means that the individual @@ -201,9 +202,9 @@ <h2>PersistentDB (persistent_db)</h2> DB-API module is not thread-safe at the connection level, and it will avoid problems when other threads change the database session or perform transactions spreading over more than one SQL command.</p> -</div> -<div class="section" id="pooleddb-pooled-db"> -<h2>PooledDB (pooled_db)</h2> +</section> +<section id="pooleddb-pooled-db"> +<h3>PooledDB (pooled_db)</h3> <p>The class <span class="docutils literal">PooledDB</span> in the module <span class="docutils literal">dbutils.pooled_db</span> implements a pool of steady, thread-safe cached connections to a database which are transparently reused, using any DB-API 2 database module.</p> @@ -226,9 +227,9 @@ <h2>PooledDB (pooled_db)</h2> don't need to worry about that, but you should be careful to use dedicated connections whenever you change the database session or perform transactions spreading over more than one SQL command.</p> -</div> -<div class="section" id="which-one-to-use"> -<h2>Which one to use?</h2> +</section> +<section id="which-one-to-use"> +<h3>Which one to use?</h3> <p>Both <span class="docutils literal">persistent_db</span> and <span class="docutils literal">pooled_db</span> serve the same purpose to improve the database access performance by recycling database connections, while preserving stability even if database connection will be disrupted.</p> @@ -241,10 +242,10 @@ <h2>Which one to use?</h2> fine-tuning, particularly if you are using a thread-safe DB-API 2 module.</p> <p>Since the interface of both modules is similar, you can easily switch from one to the other and check which one will suit better.</p> -</div> -</div> -<div class="section" id="usage"> -<h1>Usage</h1> +</section> +</section> +<section id="usage"> +<h2>Usage</h2> <p>The usage of all the modules is similar, but there are also some differences in the initialization between the "Pooled" and "Persistent" variants and also between the universal DB-API 2 and the classic PyGreSQL variants.</p> @@ -254,8 +255,8 @@ <h1>Usage</h1> display the documentation of the <span class="docutils literal">pooled_db</span> module as follows (this works analogously for the other modules):</p> <pre class="literal-block">help(pooled_db)</pre> -<div class="section" id="id2"> -<h2>PersistentDB (persistent_db)</h2> +<section id="id2"> +<h3>PersistentDB (persistent_db)</h3> <p>In order to make use of the <span class="docutils literal">persistent_db</span> module, you first need to set up a generator for your kind of database connections by creating an instance of <span class="docutils literal">persistent_db</span>, passing the following parameters:</p> @@ -311,9 +312,9 @@ <h2>PersistentDB (persistent_db)</h2> connections may become a bit faster, but this may not work in all environments (for instance, <span class="docutils literal">mod_wsgi</span> is known to cause problems since it clears the <span class="docutils literal">threading.local</span> data between requests).</p> -</div> -<div class="section" id="id3"> -<h2>PooledDB (pooled_db)</h2> +</section> +<section id="id3"> +<h3>PooledDB (pooled_db)</h3> <p>In order to make use of the <span class="docutils literal">pooled_db</span> module, you first need to set up the database connection pool by creating an instance of <span class="docutils literal">pooled_db</span>, passing the following parameters:</p> @@ -394,10 +395,10 @@ <h2>PooledDB (pooled_db)</h2> with other threads, that the transparent reopening will be suspended until the end of the transaction, and that the connection will be rolled back before being given back to the connection pool.</p> -</div> -</div> -<div class="section" id="notes"> -<h1>Notes</h1> +</section> +</section> +<section id="notes"> +<h2>Notes</h2> <p>If you are using one of the popular object-relational mappers <a class="reference external" href="http://www.sqlobject.org/">SQLObject</a> or <a class="reference external" href="https://www.sqlalchemy.org">SQLAlchemy</a>, you won't need DBUtils, since they come with their own connection pools. SQLObject 2 (SQL-API) is actually borrowing some code @@ -413,9 +414,9 @@ <h1>Notes</h1> If you're running such a configuration, you should resort to a middleware for connection pooling that supports multi-processing, such as <a class="reference external" href="https://www.pgpool.net/">pgpool</a> or <a class="reference external" href="https://pgbouncer.github.io/">pgbouncer</a> for the PostgreSQL database.</p> -</div> -<div class="section" id="future"> -<h1>Future</h1> +</section> +<section id="future"> +<h2>Future</h2> <p>Some ideas for future improvements:</p> <ul class="simple"> <li><p>Alternatively to the maximum number of uses of a connection, @@ -434,14 +435,14 @@ <h1>Future</h1> the connection pool every day shortly before the users arrive.</p></li> <li><p>Optionally log usage, bad connections and exceeding of limits.</p></li> </ul> -</div> -<div class="section" id="bug-reports-and-feedback"> -<h1>Bug reports and feedback</h1> +</section> +<section id="bug-reports-and-feedback"> +<h2>Bug reports and feedback</h2> <p>You can transmit bug reports, patches and feedback by creating <a class="reference external" href="https://github.com/WebwareForPython/DBUtils/issues">issues</a> or <a class="reference external" href="https://github.com/WebwareForPython/DBUtils/pulls">pull requests</a> on the GitHub project page for DBUtils.</p> -</div> -<div class="section" id="links"> -<h1>Links</h1> +</section> +<section id="links"> +<h2>Links</h2> <p>Some links to related and alternative software:</p> <ul class="simple"> <li><p><a class="reference external" href="https://github.com/WebwareForPython/DBUtils">DBUtils</a></p></li> @@ -455,9 +456,9 @@ <h1>Links</h1> <li><p><a class="reference external" href="http://www.sqlobject.org/">SQLObject</a> object-relational mapper</p></li> <li><p><a class="reference external" href="https://www.sqlalchemy.org">SQLAlchemy</a> object-relational mapper</p></li> </ul> -</div> -<div class="section" id="credits"> -<h1>Credits</h1> +</section> +<section id="credits"> +<h2>Credits</h2> <dl class="field-list simple"> <dt>Author</dt> <dd><p><a class="reference external" href="https://github.com/Cito">Christoph Zwerschke</a></p> @@ -470,14 +471,14 @@ <h1>Credits</h1> Matthew Harriger, Gregory Piñero and Josef van Eenbergen.</p> </dd> </dl> -</div> -<div class="section" id="copyright-and-license"> -<h1>Copyright and License</h1> +</section> +<section id="copyright-and-license"> +<h2>Copyright and License</h2> <p>Copyright © 2005-2020 by Christoph Zwerschke. All Rights Reserved.</p> <p>DBUtils is free and open source software, licensed under the <a class="reference external" href="https://opensource.org/licenses/MIT">MIT license</a>.</p> -</div> -</div> +</section> +</main> </body> </html> From 844c31b06b9a2ceeed445407a0d12f3cbf4103c4 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Thu, 8 Apr 2021 18:25:37 +0200 Subject: [PATCH 24/84] Add GitHub workflow for publishing releases on PyPI --- .github/workflows/publish_on_pypi.yml | 33 +++++++++++++++++++++++++++ .github/workflows/test_with_tox.yml | 5 ++++ 2 files changed, 38 insertions(+) create mode 100644 .github/workflows/publish_on_pypi.yml diff --git a/.github/workflows/publish_on_pypi.yml b/.github/workflows/publish_on_pypi.yml new file mode 100644 index 0000000..fc22580 --- /dev/null +++ b/.github/workflows/publish_on_pypi.yml @@ -0,0 +1,33 @@ +name: Publish DBUtils on PyPI + +on: + push: + tags: + - 'Release-*' + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python: [2.7, 3.9] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + + - name: Build wheel and source tarball + run: | + pip install wheel + python setup.py sdist bdist_wheel + + - name: Publish a Python distribution to PyPI + if: matrix.python == 3.9 + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/test_with_tox.yml b/.github/workflows/test_with_tox.yml index fe6e356..4ade996 100644 --- a/.github/workflows/test_with_tox.yml +++ b/.github/workflows/test_with_tox.yml @@ -8,13 +8,18 @@ jobs: strategy: matrix: python: [2.7, 3.6, 3.7, 3.8, 3.9] + steps: - uses: actions/checkout@v2 + - name: Setup Python ${{ matrix.python }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} + - run: pip install tox + - run: tox -e py + - if: matrix.python == 3.9 run: TOXENV=flake8,manifest,docs,spell tox From 0ba650bf5fa4776738f3627829c2062b1a5d7ebc Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Thu, 8 Apr 2021 19:20:07 +0200 Subject: [PATCH 25/84] Prepare patch release --- .bumpversion.cfg | 2 +- LICENSE | 2 +- MANIFEST.in | 1 + README.md | 2 +- dbutils/__init__.py | 2 +- docs/changelog.html | 30 +++++++++++++++++++----------- docs/changelog.rst | 9 +++++++++ docs/main.de.html | 4 ++-- docs/main.de.rst | 4 ++-- docs/main.html | 4 ++-- docs/main.rst | 4 ++-- setup.py | 2 +- 12 files changed, 42 insertions(+), 24 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 9eab400..456e479 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.0 +current_version = 2.0.1 [bumpversion:file:setup.py] search = __version__ = '{current_version}' diff --git a/LICENSE b/LICENSE index 9ecd470..641db5d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2018 Christoph Zwerschke +Copyright (c) 2021 Christoph Zwerschke Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MANIFEST.in b/MANIFEST.in index 82221db..be66fba 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,6 +4,7 @@ include LICENSE include README.md include .bumpversion.cfg +include .codespellrc include .flake8 include tox.ini diff --git a/README.md b/README.md index 9debced..ffd3afd 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ to a database that can be used in all kinds of multi-threaded environments. The suite supports DB-API 2 compliant database interfaces and the classic PyGreSQL interface. -The current version 2.0 of DBUtils supports Python versions 2.7 and 3.5 to 3.9. +The current version 2.0.1 of DBUtils supports Python versions 2.7 and 3.5 to 3.9. **Please have a look at the [changelog](https://webwareforpython.github.io/DBUtils/changelog.html), because there are some breaking changes in version 2.0.** diff --git a/dbutils/__init__.py b/dbutils/__init__.py index adfdbf7..23e2f58 100644 --- a/dbutils/__init__.py +++ b/dbutils/__init__.py @@ -5,4 +5,4 @@ 'simple_pooled_pg', 'steady_pg', 'pooled_pg', 'persistent_pg', 'simple_pooled_db', 'steady_db', 'pooled_db', 'persistent_db'] -__version__ = '2.0' +__version__ = '2.0.1' diff --git a/docs/changelog.html b/docs/changelog.html index 7853af5..36833ea 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -12,6 +12,14 @@ <h1 class="title">Changelog for DBUtils</h1> <section id="id1"> +<h2>2.0.1</h2> +<p>DBUtils 2.0.1 was released on April 8, 2021.</p> +<p>Changes:</p> +<ul class="simple"> +<li><p>Avoid "name Exception is not defined" when exiting</p></li> +</ul> +</section> +<section id="id2"> <h2>2.0</h2> <p>DBUtils 2.0 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> @@ -27,7 +35,7 @@ <h2>2.0</h2> <li><p>This changelog has been compiled from the former release notes.</p></li> </ul> </section> -<section id="id2"> +<section id="id3"> <h2>1.4</h2> <p>DBUtils 1.4 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> @@ -38,7 +46,7 @@ <h2>1.4</h2> inside a transaction.</p></li> </ul> </section> -<section id="id3"> +<section id="id4"> <h2>1.3</h2> <p>DBUtils 1.3 was released on March 3, 2018.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.4 to 3.7.</p> @@ -47,12 +55,12 @@ <h2>1.3</h2> <li><p>This version now supports context handlers for connections and cursors.</p></li> </ul> </section> -<section id="id4"> +<section id="id5"> <h2>1.2</h2> <p>DBUtils 1.2 was released on February 5, 2017.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.0 to 3.6.</p> </section> -<section id="id5"> +<section id="id6"> <h2>1.1.1</h2> <p>DBUtils 1.1.1 was released on February 4, 2017.</p> <p>It is intended to be used with Python versions 2.3 to 2.7.</p> @@ -66,7 +74,7 @@ <h2>1.1.1</h2> <li><p>Fixed a problem when running under Jython (reported by Vitaly Kruglikov).</p></li> </ul> </section> -<section id="id6"> +<section id="id7"> <h2>1.1</h2> <p>DBUtils 1.1 was released on August 14, 2011.</p> <p>Improvements:</p> @@ -95,7 +103,7 @@ <h2>1.1</h2> <li><p>Fixed some minor issues with the <span class="docutils literal">DBUtilsExample</span> for Webware.</p></li> </ul> </section> -<section id="id7"> +<section id="id8"> <h2>1.0</h2> <p>DBUtils 1.0 was released on November 29, 2008.</p> <p>It is intended to be used with Python versions 2.2 to 2.6.</p> @@ -128,7 +136,7 @@ <h2>1.0</h2> the MySQLdb module (problem reported by Gregory Pinero).</p></li> </ul> </section> -<section id="id8"> +<section id="id9"> <h2>0.9.4</h2> <p>DBUtils 0.9.4 was released on July 7, 2007.</p> <p>This release fixes a problem in the destructor code and has been supplemented @@ -137,7 +145,7 @@ <h2>0.9.4</h2> in the last release, since you can now pass custom creator functions for database connections instead of DB-API 2 modules.</p> </section> -<section id="id9"> +<section id="id10"> <h2>0.9.3</h2> <p>DBUtils 0.9.3 was released on May 21, 2007.</p> <p>Changes:</p> @@ -152,7 +160,7 @@ <h2>0.9.3</h2> Added Chinese translation of the User's Guide, kindly contributed by gashero.</p></li> </ul> </section> -<section id="id10"> +<section id="id11"> <h2>0.9.2</h2> <p>DBUtils 0.9.2 was released on September 22, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.5.</p> @@ -162,7 +170,7 @@ <h2>0.9.2</h2> storage engine. Accordingly, renamed <span class="docutils literal">SolidPg</span> to <span class="docutils literal">SteadyPg</span>.</p></li> </ul> </section> -<section id="id11"> +<section id="id12"> <h2>0.9.1</h2> <p>DBUtils 0.9.1 was released on May 8, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.4.</p> @@ -176,7 +184,7 @@ <h2>0.9.1</h2> <li><p>Improved the documentation and added a User's Guide.</p></li> </ul> </section> -<section id="id12"> +<section id="id13"> <h2>0.8.1 - 2005-09-13</h2> <p>DBUtils 0.8.1 was released on September 13, 2005.</p> <p>It is intended to be used with Python versions 2.0 to 2.4.</p> diff --git a/docs/changelog.rst b/docs/changelog.rst index fad22c0..36b300a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,15 @@ Changelog for DBUtils +++++++++++++++++++++ +2.0.1 +===== + +DBUtils 2.0.1 was released on April 8, 2021. + +Changes: + +* Avoid "name Exception is not defined" when exiting + 2.0 === diff --git a/docs/main.de.html b/docs/main.de.html index 1e6ae7e..833ac1e 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -12,7 +12,7 @@ <h1 class="title">Benutzeranleitung für DBUtils</h1> <dl class="docinfo simple"> <dt class="version">Version</dt> -<dd class="version">2.0</dd> +<dd class="version">2.0.1</dd> <dt class="translations">Translations</dt> <dd class="translations"><p><a class="reference external" href="main.html">English</a> | German</p> </dd> @@ -515,7 +515,7 @@ <h2>Autoren</h2> </section> <section id="copyright-und-lizenz"> <h2>Copyright und Lizenz</h2> -<p>Copyright © 2005-2020 Christoph Zwerschke. +<p>Copyright © 2005-2021 Christoph Zwerschke. Alle Rechte vorbehalten.</p> <p>DBUtils ist freie und quelloffene Software, lizenziert unter der <a class="reference external" href="https://opensource.org/licenses/MIT">MIT-Lizenz</a>.</p> diff --git a/docs/main.de.rst b/docs/main.de.rst index 3742b5b..561d3e2 100644 --- a/docs/main.de.rst +++ b/docs/main.de.rst @@ -1,7 +1,7 @@ Benutzeranleitung für DBUtils +++++++++++++++++++++++++++++ -:Version: 2.0 +:Version: 2.0.1 :Translations: English_ | German .. _English: main.html @@ -550,7 +550,7 @@ Autoren Copyright und Lizenz ==================== -Copyright © 2005-2020 Christoph Zwerschke. +Copyright © 2005-2021 Christoph Zwerschke. Alle Rechte vorbehalten. DBUtils ist freie und quelloffene Software, diff --git a/docs/main.html b/docs/main.html index 49c5cc5..c666367 100644 --- a/docs/main.html +++ b/docs/main.html @@ -12,7 +12,7 @@ <h1 class="title">DBUtils User's Guide</h1> <dl class="docinfo simple"> <dt class="version">Version</dt> -<dd class="version">2.0</dd> +<dd class="version">2.0.1</dd> <dt class="translations">Translations</dt> <dd class="translations"><p>English | <a class="reference external" href="main.de.html">German</a></p> </dd> @@ -474,7 +474,7 @@ <h2>Credits</h2> </section> <section id="copyright-and-license"> <h2>Copyright and License</h2> -<p>Copyright © 2005-2020 by Christoph Zwerschke. +<p>Copyright © 2005-2021 by Christoph Zwerschke. All Rights Reserved.</p> <p>DBUtils is free and open source software, licensed under the <a class="reference external" href="https://opensource.org/licenses/MIT">MIT license</a>.</p> diff --git a/docs/main.rst b/docs/main.rst index 96f9804..8294edf 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -1,7 +1,7 @@ DBUtils User's Guide ++++++++++++++++++++ -:Version: 2.0 +:Version: 2.0.1 :Translations: English | German_ .. _German: main.de.html @@ -512,7 +512,7 @@ Credits Copyright and License ===================== -Copyright © 2005-2020 by Christoph Zwerschke. +Copyright © 2005-2021 by Christoph Zwerschke. All Rights Reserved. DBUtils is free and open source software, diff --git a/setup.py b/setup.py index 861ed92..c355a37 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ warnings.filterwarnings('ignore', 'Unknown distribution option') -__version__ = '2.0' +__version__ = '2.0.1' readme = open('README.md').read() From 1e888ba57dc021fee7582c4f697e246c772e8be6 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Thu, 8 Apr 2021 19:37:55 +0200 Subject: [PATCH 26/84] Improve workflow for publishing on PyPI We want to publish wheels for both Python 2 and 3 --- .github/workflows/publish_on_pypi.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish_on_pypi.yml b/.github/workflows/publish_on_pypi.yml index fc22580..92f72db 100644 --- a/.github/workflows/publish_on_pypi.yml +++ b/.github/workflows/publish_on_pypi.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: [2.7, 3.9] + python: [3.9, 2.7] steps: - uses: actions/checkout@v2 @@ -20,13 +20,16 @@ jobs: with: python-version: ${{ matrix.python }} - - name: Build wheel and source tarball + - name: Build source tarball + if: matrix.python == 3.9 + run: python setup.py sdist + + - name: Build wheel run: | pip install wheel - python setup.py sdist bdist_wheel + python setup.py bdist_wheel - - name: Publish a Python distribution to PyPI - if: matrix.python == 3.9 + - name: Publish distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ From c35a36d7cb7e71d5ba17982f8139dc1a7cc1e882 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Thu, 8 Apr 2021 20:07:29 +0200 Subject: [PATCH 27/84] Update Docutils CSS --- docs/docutils.css | 103 +--------------------------------------------- 1 file changed, 2 insertions(+), 101 deletions(-) diff --git a/docs/docutils.css b/docs/docutils.css index 5219d8e..bebeb07 100644 --- a/docs/docutils.css +++ b/docs/docutils.css @@ -1,101 +1,2 @@ -/* CSS 3.1 style sheet for the output of Docutils 0.13 HTML5 writer. */ -.align-left{text-align:left} -.align-right{text-align:right} -.align-center{clear:both;text-align:center} -.align-top{vertical-align:top} -.align-middle{vertical-align:middle} -.align-bottom{vertical-align:bottom} -h1.title,p.subtitle{text-align:center} -p.admonition-title,p.topic-title,p.sidebar-title,p.rubric,p.system-message-title{font-weight:700} -h1 + p.subtitle,h1 + p.section-subtitle{font-size:1.6em} -h2 + p.section-subtitle{font-size:1.28em} -p.subtitle,p.section-subtitle,p.sidebar-subtitle{font-weight:700;margin-top:-.5em} -p.sidebar-title,p.rubric{font-size:larger} -p.rubric{color:maroon} -a.toc-backref{color:#000;text-decoration:none} -div.caution p.admonition-title,div.attention p.admonition-title,div.danger p.admonition-title,div.error p.admonition-title,div.warning p.admonition-title,div.system-messages h1,div.error,span.problematic,p.system-message-title{color:red} -span.docutils.literal{font-family:monospace;white-space:pre-wrap} -.literal > span.pre{white-space:nowrap} -.simple li,.compact li,.simple ul,.compact ul,.simple ol,.compact ol,.simple > li p,.compact > li p,dl.simple > dd,dl.compact > dd{margin-top:0;margin-bottom:0} -div.topic.contents{margin:0} -ul.auto-toc{list-style-type:none;padding-left:1.5em} -ol.arabic{list-style:decimal} -ol.loweralpha{list-style:lower-alpha} -ol.upperalpha{list-style:upper-alpha} -ol.lowerroman{list-style:lower-roman} -ol.upperroman{list-style:upper-roman} -dt span.classifier{font-style:italic} -dt span.classifier:before{font-style:normal;margin:.5em;content:":"} -dl.field-list > dt,dl.option-list > dt,dl.docinfo > dt,dl.footnote > dt,dl.citation > dt{font-weight:700;clear:left;float:left;margin:0;padding:0;padding-right:.5em} -dl.field-list > dd,dl.option-list > dd,dl.docinfo > dd{margin-left:9em} -dl.field-list > dd > :first-child,dl.option-list > dd > :first-child{display:inline-block;width:100%;margin:0} -dl.field-list > dt:after,dl.docinfo > dt:after{content:":"} -pre.address{font:inherit} -dd.authors > p{margin:0} -dl.option-list{margin-left:40px} -dl.option-list > dt{font-weight:400} -span.option{white-space:nowrap} -dl.footnote.superscript > dd{margin-left:1em} -dl.footnote.brackets > dd{margin-left:2em} -dl > dt.label{font-weight:400} -a.footnote-reference.brackets:before,dt.label > span.brackets:before{content:"["} -a.footnote-reference.brackets:after,dt.label > span.brackets:after{content:"]"} -a.footnote-reference.superscript,dl.footnote.superscript > dt.label{vertical-align:super;font-size:smaller} -dt.label > span.fn-backref{margin-left:.2em} -dt.label > span.fn-backref > a{font-style:italic} -div.line-block{display:block} -div.line-block div.line-block{margin-top:0;margin-bottom:0;margin-left:40px} -.figure.align-left,img.align-left,object.align-left,table.align-left{margin-right:auto} -.figure.align-center,img.align-center,object.align-center{margin-left:auto;margin-right:auto;display:block} -table.align-center{margin-left:auto;margin-right:auto} -.figure.align-right,img.align-right,object.align-right,table.align-right{margin-left:auto} -div.align-left,div.align-center,div.align-right,table.align-left,table.align-center,table.align-right{text-align:inherit} -div.admonition,div.system-message,div.sidebar{margin:40px;border:medium outset;padding-right:1em;padding-left:1em} -div.sidebar{width:30%;max-width:26em;float:right;clear:right} -div.topic,pre.literal-block,pre.doctest-block,pre.math,pre.code{margin-right:40px;margin-left:40px} -pre.code .ln{color:gray} -table{border-collapse:collapse} -td,th{border-style:solid;border-color:silver;padding:0 1ex;border-width:thin} -td > p:first-child,th > p:first-child{margin-top:0} -td > p,th > p{margin-bottom:0} -table > caption{text-align:left;margin-bottom:.25em} -table.borderless td,table.borderless th{border:0;padding:0;padding-right:.5em} -body{padding:0 5%;margin:8px 0} -div.document{line-height:1.3;counter-reset:table;max-width:50em;margin:auto} -hr.docutils{width:80%;margin-top:1em;margin-bottom:1em;clear:both} -p,ol,ul,dl,div.line-block,table{margin-top:.5em;margin-bottom:.5em} -h1,h2,h3,h4,h5,h6,dl > dd{margin-bottom:.5em} -dl > dd p:first-child{margin-top:0} -dd > ul:only-child,dd > ol:only-child{padding-left:1em} -dl.description > dt{font-weight:700;clear:left;float:left;margin:0;padding:0;padding-right:.5em} -dl.field-list.narrow > dd{margin-left:5em} -dl.field-list.run-in > dd p{display:block} -div.abstract p.topic-title{text-align:center} -div.dedication{margin:2em 5em;text-align:center;font-style:italic} -div.dedication p.topic-title{font-style:normal} -dl.citation dt.label{font-weight:700} -span.fn-backref{font-weight:400} -pre.literal-block,pre.doctest-block,pre.math,pre.code{margin-left:1.5em;margin-right:1.5em} -blockquote,div.topic{margin-left:1.5em;margin-right:1.5em} -blockquote > table,div.topic > table{margin-top:0;margin-bottom:0} -blockquote p.attribution,div.topic p.attribution{text-align:right;margin-left:20%} -table tr{text-align:left} -table.booktabs{border:0;border-top:2px solid;border-bottom:2px solid;border-collapse:collapse} -table.booktabs *{border:0} -table.booktabs th{border-bottom:thin solid} -table.numbered > caption:before{counter-increment:table;content:"Table " counter(table) ": ";font-weight:700} -dl.footnote{padding-left:1ex;border-left:solid;border-left-width:thin} -.figure.align-left,img.align-left,object.align-left{display:block;clear:left;float:left;margin-right:1em} -.figure.align-right,img.align-right,object.align-right{display:block;clear:right;float:right;margin-left:1em} -h1,h2,h3{clear:both} -div.sidebar{width:30%;max-width:26em;margin-left:1em;margin-right:-5.5%;background-color:#ffe} -pre.code,code{background-color:#eee} -pre.code .ln{color:gray} -pre.code .comment,code .comment{color:#5C6576} -pre.code .keyword,code .keyword{color:#3B0D06;font-weight:700} -pre.code .literal.string,code .literal.string{color:#0C5404} -pre.code .name.builtin,code .name.builtin{color:#352B84} -pre.code .deleted,code .deleted{background-color:#DEB0A1} -pre.code .inserted,code .inserted{background-color:#A3D289} -div.footer,div.header{clear:both;font-size:smaller} -a{text-decoration:none} \ No newline at end of file +/* CSS 3.1 style sheet for the output of Docutils 0.17 HTML writer. */ +body{margin:0;background-color:#dbdbdb}main,footer,header{line-height:1.3;max-width:50rem;padding:1px 2%;margin:auto}main{counter-reset:table figure;background-color:white}footer,header{font-size:smaller;padding:.5em 2%;border:0}hr.docutils{width:80%;margin-top:1em;margin-bottom:1em;clear:both}p,ol,ul,dl,li,dd,div.line-block,div.topic,table{margin-top:.5em;margin-bottom:.5em}p:first-child{margin-top:0}p:last-child{margin-bottom:0}h1,h2,h3,h4,h5,h6,dl>dd{margin-bottom:.5em}dl>dd,ol>li,dd>ul:only-child,dd>ol:only-child{padding-left:1em}dl.description>dt{font-weight:bold;clear:left;float:left;margin:0;padding:0;padding-right:.5em}dl.field-list.narrow>dd{margin-left:5em}dl.field-list.run-in>dd p{display:block}div.abstract p.topic-title{text-align:center}div.dedication{margin:2em 5em;text-align:center;font-style:italic}div.dedication p.topic-title{font-style:normal}pre.literal-block,pre.doctest-block,pre.math,pre.code{font-family:monospace}blockquote>table,div.topic>table{margin-top:0;margin-bottom:0}blockquote p.attribution,div.topic p.attribution{text-align:right;margin-left:20%}table tr{text-align:left}table.booktabs{border:0;border-top:2px solid;border-bottom:2px solid;border-collapse:collapse}table.booktabs *{border:0}table.booktabs th{border-bottom:thin solid}table.numbered>caption:before{counter-increment:table;content:"Table " counter(table) ": ";font-weight:bold}dl.footnote{padding-left:1ex;border-left:solid;border-left-width:thin}figure.align-left,img.align-left,video.align-left,object.align-left{clear:left;float:left;margin-right:1em}figure.align-right,img.align-right,video.align-right,object.align-right{clear:right;float:right;margin-left:1em}h1,h2,h3,h4,footer,header{clear:both}figure.numbered>figcaption>p:before{counter-increment:figure;content:"Figure " counter(figure) ": ";font-weight:bold}.caution p.admonition-title,.attention p.admonition-title,.danger p.admonition-title,.error p.admonition-title,.warning p.admonition-title,div.error{color:red}aside.sidebar{width:30%;max-width:26em;margin-left:1em;margin-right:-2%;background-color:#ffe}pre.code{padding:.7ex}pre.code,code{background-color:#eee}pre.code .comment,code .comment{color:#5c6576}pre.code .keyword,code .keyword{color:#3b0d06;font-weight:bold}pre.code .literal.string,code .literal.string{color:#0c5404}pre.code .name.builtin,code .name.builtin{color:#352b84}pre.code .deleted,code .deleted{background-color:#deb0a1}pre.code .inserted,code .inserted{background-color:#a3d289}a{text-decoration:none} \ No newline at end of file From 54da25ea700a4128b1f5291dbb053e32847cdb01 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Fri, 14 May 2021 22:37:13 +0200 Subject: [PATCH 28/84] Support context managers (fixes #33) --- dbutils/pooled_db.py | 23 +++++++++++++++++++++++ dbutils/pooled_pg.py | 13 +++++++++++++ docs/changelog.html | 34 +++++++++++++++++++++------------- docs/changelog.rst | 12 +++++++++++- docs/main.de.html | 5 +++++ docs/main.de.rst | 8 ++++++++ docs/main.html | 5 +++++ docs/main.rst | 7 +++++++ tests/test_persistent_db.py | 8 ++++++++ tests/test_persistent_pg.py | 6 ++++++ tests/test_pooled_db.py | 27 +++++++++++++++++++++++++++ tests/test_pooled_pg.py | 15 ++++++++++++++- 12 files changed, 148 insertions(+), 15 deletions(-) diff --git a/dbutils/pooled_db.py b/dbutils/pooled_db.py index 2de9b55..e8367b0 100644 --- a/dbutils/pooled_db.py +++ b/dbutils/pooled_db.py @@ -114,6 +114,13 @@ cur.close() # or del cur db.close() # or del db +You can also use context managers for simpler code: + + with pool.connection() as db: + with db.cursor as cur: + cur.execute(...) + res = cur.fetchone() + Note that you need to explicitly start transactions by calling the begin() method. This ensures that the connection will not be shared with other threads, that the transparent reopening will be suspended @@ -440,6 +447,14 @@ def __del__(self): except: # builtin Exceptions might not exist any more pass + def __enter__(self): + """Enter a runtime context for the connection.""" + return self + + def __exit__(self, *exc): + """Exit a runtime context for the connection.""" + self.close() + class SharedDBConnection: """Auxiliary class for shared connections.""" @@ -526,3 +541,11 @@ def __del__(self): self.close() except: # builtin Exceptions might not exist any more pass + + def __enter__(self): + """Enter a runtime context for the connection.""" + return self + + def __exit__(self, *exc): + """Exit a runtime context for the connection.""" + self.close() diff --git a/dbutils/pooled_pg.py b/dbutils/pooled_pg.py index 0e0a3ae..86c379d 100644 --- a/dbutils/pooled_pg.py +++ b/dbutils/pooled_pg.py @@ -83,6 +83,11 @@ res = db.query(...).getresult() db.close() # or del db +You can also a context manager for simpler code: + + with pool.connection() as db: + res = db.query(...).getresult() + Note that you need to explicitly start transactions by calling the begin() method. This ensures that the transparent reopening will be suspended until the end of the transaction, and that the connection will @@ -293,3 +298,11 @@ def __del__(self): self.close() except: # builtin Exceptions might not exist any more pass + + def __enter__(self): + """Enter a runtime context for the connection.""" + return self + + def __exit__(self, *exc): + """Exit a runtime context for the connection.""" + self.close() diff --git a/docs/changelog.html b/docs/changelog.html index 36833ea..c8eddb0 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -12,14 +12,22 @@ <h1 class="title">Changelog for DBUtils</h1> <section id="id1"> +<h2>2.0.2</h2> +<p>DBUtils 2.0.2 was released on ...</p> +<p>Changes:</p> +<ul class="simple"> +<li><p>Allow using context managers for pooled connections.</p></li> +</ul> +</section> +<section id="id2"> <h2>2.0.1</h2> <p>DBUtils 2.0.1 was released on April 8, 2021.</p> <p>Changes:</p> <ul class="simple"> -<li><p>Avoid "name Exception is not defined" when exiting</p></li> +<li><p>Avoid "name Exception is not defined" when exiting.</p></li> </ul> </section> -<section id="id2"> +<section id="id3"> <h2>2.0</h2> <p>DBUtils 2.0 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> @@ -35,7 +43,7 @@ <h2>2.0</h2> <li><p>This changelog has been compiled from the former release notes.</p></li> </ul> </section> -<section id="id3"> +<section id="id4"> <h2>1.4</h2> <p>DBUtils 1.4 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> @@ -46,7 +54,7 @@ <h2>1.4</h2> inside a transaction.</p></li> </ul> </section> -<section id="id4"> +<section id="id5"> <h2>1.3</h2> <p>DBUtils 1.3 was released on March 3, 2018.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.4 to 3.7.</p> @@ -55,12 +63,12 @@ <h2>1.3</h2> <li><p>This version now supports context handlers for connections and cursors.</p></li> </ul> </section> -<section id="id5"> +<section id="id6"> <h2>1.2</h2> <p>DBUtils 1.2 was released on February 5, 2017.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.0 to 3.6.</p> </section> -<section id="id6"> +<section id="id7"> <h2>1.1.1</h2> <p>DBUtils 1.1.1 was released on February 4, 2017.</p> <p>It is intended to be used with Python versions 2.3 to 2.7.</p> @@ -74,7 +82,7 @@ <h2>1.1.1</h2> <li><p>Fixed a problem when running under Jython (reported by Vitaly Kruglikov).</p></li> </ul> </section> -<section id="id7"> +<section id="id8"> <h2>1.1</h2> <p>DBUtils 1.1 was released on August 14, 2011.</p> <p>Improvements:</p> @@ -103,7 +111,7 @@ <h2>1.1</h2> <li><p>Fixed some minor issues with the <span class="docutils literal">DBUtilsExample</span> for Webware.</p></li> </ul> </section> -<section id="id8"> +<section id="id9"> <h2>1.0</h2> <p>DBUtils 1.0 was released on November 29, 2008.</p> <p>It is intended to be used with Python versions 2.2 to 2.6.</p> @@ -136,7 +144,7 @@ <h2>1.0</h2> the MySQLdb module (problem reported by Gregory Pinero).</p></li> </ul> </section> -<section id="id9"> +<section id="id10"> <h2>0.9.4</h2> <p>DBUtils 0.9.4 was released on July 7, 2007.</p> <p>This release fixes a problem in the destructor code and has been supplemented @@ -145,7 +153,7 @@ <h2>0.9.4</h2> in the last release, since you can now pass custom creator functions for database connections instead of DB-API 2 modules.</p> </section> -<section id="id10"> +<section id="id11"> <h2>0.9.3</h2> <p>DBUtils 0.9.3 was released on May 21, 2007.</p> <p>Changes:</p> @@ -160,7 +168,7 @@ <h2>0.9.3</h2> Added Chinese translation of the User's Guide, kindly contributed by gashero.</p></li> </ul> </section> -<section id="id11"> +<section id="id12"> <h2>0.9.2</h2> <p>DBUtils 0.9.2 was released on September 22, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.5.</p> @@ -170,7 +178,7 @@ <h2>0.9.2</h2> storage engine. Accordingly, renamed <span class="docutils literal">SolidPg</span> to <span class="docutils literal">SteadyPg</span>.</p></li> </ul> </section> -<section id="id12"> +<section id="id13"> <h2>0.9.1</h2> <p>DBUtils 0.9.1 was released on May 8, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.4.</p> @@ -184,7 +192,7 @@ <h2>0.9.1</h2> <li><p>Improved the documentation and added a User's Guide.</p></li> </ul> </section> -<section id="id13"> +<section id="id14"> <h2>0.8.1 - 2005-09-13</h2> <p>DBUtils 0.8.1 was released on September 13, 2005.</p> <p>It is intended to be used with Python versions 2.0 to 2.4.</p> diff --git a/docs/changelog.rst b/docs/changelog.rst index 36b300a..5fd6765 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,16 @@ Changelog for DBUtils +++++++++++++++++++++ +2.0.2 +===== + +DBUtils 2.0.2 was released on ... + +Changes: + +* Allow using context managers for pooled connections. + + 2.0.1 ===== @@ -8,7 +18,7 @@ DBUtils 2.0.1 was released on April 8, 2021. Changes: -* Avoid "name Exception is not defined" when exiting +* Avoid "name Exception is not defined" when exiting. 2.0 === diff --git a/docs/main.de.html b/docs/main.de.html index 833ac1e..a41f502 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -428,6 +428,11 @@ <h3>PooledDB (pooled_db)</h3> res = cur.fetchone() cur.close() # oder del cur db.close() # oder del db</pre> +<p>Sie können dies auch durch Verwendung von Kontext-Managern vereinfachen:</p> +<pre class="literal-block">with pool.connection() as db: + with db.cursor as cur: + cur.execute(...) + res = cur.fetchone()</pre> <p>Bitte beachten Sie, dass Transaktionen explizit durch Aufruf der Methode <span class="docutils literal">begin()</span> eingeleiten werden müssen. Hierdurch wird sichergestellt, dass die Verbindung nicht mehr mit anderen Threads geteilt wird, dass das diff --git a/docs/main.de.rst b/docs/main.de.rst index 561d3e2..dc3f315 100644 --- a/docs/main.de.rst +++ b/docs/main.de.rst @@ -443,6 +443,14 @@ sie gebraucht werden, etwa so:: cur.close() # oder del cur db.close() # oder del db +Sie können dies auch durch Verwendung von Kontext-Managern vereinfachen:: + + with pool.connection() as db: + with db.cursor as cur: + cur.execute(...) + res = cur.fetchone() + + Bitte beachten Sie, dass Transaktionen explizit durch Aufruf der Methode ``begin()`` eingeleiten werden müssen. Hierdurch wird sichergestellt, dass die Verbindung nicht mehr mit anderen Threads geteilt wird, dass das diff --git a/docs/main.html b/docs/main.html index c666367..2f81912 100644 --- a/docs/main.html +++ b/docs/main.html @@ -390,6 +390,11 @@ <h3>PooledDB (pooled_db)</h3> res = cur.fetchone() cur.close() # or del cur db.close() # or del db</pre> +<p>You can also use context managers for simpler code:</p> +<pre class="literal-block">with pool.connection() as db: + with db.cursor as cur: + cur.execute(...) + res = cur.fetchone()</pre> <p>Note that you need to explicitly start transactions by calling the <span class="docutils literal">begin()</span> method. This ensures that the connection will not be shared with other threads, that the transparent reopening will be suspended diff --git a/docs/main.rst b/docs/main.rst index 8294edf..426a298 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -405,6 +405,13 @@ object stays alive as long as you are using it, like that:: cur.close() # or del cur db.close() # or del db +You can also use context managers for simpler code:: + + with pool.connection() as db: + with db.cursor as cur: + cur.execute(...) + res = cur.fetchone() + Note that you need to explicitly start transactions by calling the ``begin()`` method. This ensures that the connection will not be shared with other threads, that the transparent reopening will be suspended diff --git a/tests/test_persistent_db.py b/tests/test_persistent_db.py index 7cccb30..ed5bce6 100644 --- a/tests/test_persistent_db.py +++ b/tests/test_persistent_db.py @@ -286,6 +286,14 @@ def test_failed_transaction(self): db._con.close() cursor.execute('select test') + def test_context_manager(self): + persist = PersistentDB(dbapi) + with persist.connection() as db: + with db.cursor() as cursor: + cursor.execute('select test') + r = cursor.fetchone() + self.assertEqual(r, 'test') + if __name__ == '__main__': unittest.main() diff --git a/tests/test_persistent_pg.py b/tests/test_persistent_pg.py index 5053316..78c85ca 100644 --- a/tests/test_persistent_pg.py +++ b/tests/test_persistent_pg.py @@ -189,6 +189,12 @@ def test_failed_transaction(self): db._con.close() self.assertEqual(db.query('select test'), 'test') + def test_context_manager(self): + persist = PersistentPg() + with persist.connection() as db: + db.query('select test') + self.assertEqual(db.num_queries, 1) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_pooled_db.py b/tests/test_pooled_db.py index bd5ccd0..80a8b45 100644 --- a/tests/test_pooled_db.py +++ b/tests/test_pooled_db.py @@ -1181,6 +1181,33 @@ def test_reset_transaction(self): self.assertFalse(con._transaction) self.assertEqual(con._con.session, ['rollback']) + def test_context_manager(self): + pool = PooledDB(dbapi, 1, 1, 1) + con = pool._idle_cache[0]._con + with pool.connection() as db: + self.assertTrue(hasattr(db, '_shared_con')) + self.assertFalse(pool._idle_cache) + self.assertTrue(con.valid) + with db.cursor() as cursor: + self.assertEqual(con.open_cursors, 1) + cursor.execute('select test') + r = cursor.fetchone() + self.assertEqual(con.open_cursors, 0) + self.assertEqual(r, 'test') + self.assertEqual(con.num_queries, 1) + self.assertTrue(pool._idle_cache) + with pool.dedicated_connection() as db: + self.assertFalse(hasattr(db, '_shared_con')) + self.assertFalse(pool._idle_cache) + with db.cursor() as cursor: + self.assertEqual(con.open_cursors, 1) + cursor.execute('select test') + r = cursor.fetchone() + self.assertEqual(con.open_cursors, 0) + self.assertEqual(r, 'test') + self.assertEqual(con.num_queries, 2) + self.assertTrue(pool._idle_cache) + class TestSharedDBConnection(unittest.TestCase): diff --git a/tests/test_pooled_pg.py b/tests/test_pooled_pg.py index 1265888..bf0e314 100644 --- a/tests/test_pooled_pg.py +++ b/tests/test_pooled_pg.py @@ -14,7 +14,7 @@ from . import mock_pg # noqa -from dbutils.pooled_pg import PooledPg, InvalidConnection +from dbutils.pooled_pg import PooledPg, InvalidConnection, TooManyConnections class TestPooledPg(unittest.TestCase): @@ -304,6 +304,19 @@ def test_reset_transaction(self): self.assertEqual(con.session, []) self.assertEqual(con.num_queries, 0) + def test_context_manager(self): + pool = PooledPg(1, 1, 1) + with pool.connection() as db: + db_con = db._con._con + db.query('select test') + self.assertEqual(db_con.num_queries, 1) + self.assertRaises(TooManyConnections, pool.connection) + with pool.connection() as db: + db_con = db._con._con + db.query('select test') + self.assertEqual(db_con.num_queries, 2) + self.assertRaises(TooManyConnections, pool.connection) + if __name__ == '__main__': unittest.main() From 2a43fda98ec58f35c9f50ed91160e119ceadc06d Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Fri, 14 May 2021 22:41:24 +0200 Subject: [PATCH 29/84] Fix some typos --- dbutils/persistent_db.py | 2 +- dbutils/persistent_pg.py | 2 +- docs/changelog.html | 2 +- docs/main.de.html | 8 ++++---- docs/main.de.rst | 6 +++--- docs/main.html | 4 ++-- docs/main.rst | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/dbutils/persistent_db.py b/dbutils/persistent_db.py index cc93720..8ebf43d 100644 --- a/dbutils/persistent_db.py +++ b/dbutils/persistent_db.py @@ -80,7 +80,7 @@ ignored since it would be reopened at the next usage anyway and contrary to the intent of having persistent connections. Instead, the connection will be automatically closed when the thread dies. -You can change this behavior be setting the closeable parameter. +You can change this behavior by setting the closeable parameter. Note that you need to explicitly start transactions by calling the begin() method. This ensures that the transparent reopening will be diff --git a/dbutils/persistent_pg.py b/dbutils/persistent_pg.py index eb53e22..16b1338 100644 --- a/dbutils/persistent_pg.py +++ b/dbutils/persistent_pg.py @@ -70,7 +70,7 @@ ignored since it would be reopened at the next usage anyway and contrary to the intent of having persistent connections. Instead, the connection will be automatically closed when the thread dies. -You can change this behavior be setting the closeable parameter. +You can change this behavior by setting the closeable parameter. Note that you need to explicitly start transactions by calling the begin() method. This ensures that the transparent reopening will be diff --git a/docs/changelog.html b/docs/changelog.html index c8eddb0..e38ae50 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -3,7 +3,7 @@ <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1" /> -<meta name="generator" content="Docutils 0.17: http://docutils.sourceforge.net/" /> +<meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" /> <title>Changelog for DBUtils</title> <link rel="stylesheet" href="doc.css" type="text/css" /> </head> diff --git a/docs/main.de.html b/docs/main.de.html index a41f502..c5f0dd4 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -3,7 +3,7 @@ <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1" /> -<meta name="generator" content="Docutils 0.17: http://docutils.sourceforge.net/" /> +<meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" /> <title>Benutzeranleitung für DBUtils</title> <link rel="stylesheet" href="doc.css" type="text/css" /> </head> @@ -328,7 +328,7 @@ <h3>PersistentDB (persistent_db)</h3> endet. Sie können dieses Verhalten ändern, indem Sie den Parameter namens <span class="docutils literal">closeable</span> setzen.</p> <p>Bitte beachten Sie, dass Transaktionen explizit durch Aufruf der Methode -<span class="docutils literal">begin()</span> eingeleiten werden müssen. Hierdurch wird sichergestellt, dass +<span class="docutils literal">begin()</span> eingeleitet werden müssen. Hierdurch wird sichergestellt, dass das transparente Neueröffnen von Verbindungen bis zum Ende der Transaktion ausgesetzt wird, und dass die Verbindung zurückgerollt wird, before sie vom gleichen Thread erneut benutzt wird.</p> @@ -434,7 +434,7 @@ <h3>PooledDB (pooled_db)</h3> cur.execute(...) res = cur.fetchone()</pre> <p>Bitte beachten Sie, dass Transaktionen explizit durch Aufruf der Methode -<span class="docutils literal">begin()</span> eingeleiten werden müssen. Hierdurch wird sichergestellt, +<span class="docutils literal">begin()</span> eingeleitet werden müssen. Hierdurch wird sichergestellt, dass die Verbindung nicht mehr mit anderen Threads geteilt wird, dass das transparente Neueröffnen von Verbindungen bis zum Ende der Transaktion ausgesetzt wird, und dass die Verbindung zurückgerollt wird, bevor sie @@ -452,7 +452,7 @@ <h2>Anmerkungen</h2> oder <a class="reference external" href="https://github.com/GrahamDumpleton/mod_wsgi">mod_wsgi</a>, dann sollten Sie bedenken, dass Ihr Python-Code normalerweise im Kontext der Kindprozesse des Webservers läuft. Wenn Sie also das <span class="docutils literal">pooled_db</span>-Modul einsetzen, und mehrere dieser Kindprozesse laufen, dann -werden Sie ebensoviele Pools mit Datenbankverbindungen erhalten. Wenn diese +werden Sie ebenso viele Pools mit Datenbankverbindungen erhalten. Wenn diese Prozesse viele Threads laufen lassen, dann mag dies eine sinnvoller Ansatz sein, wenn aber diese Prozesse nicht mehr als einen Worker-Thread starten, wie im Fall des Multi-Processing Moduls "prefork" für den Apache-Webserver, diff --git a/docs/main.de.rst b/docs/main.de.rst index dc3f315..b14bae6 100644 --- a/docs/main.de.rst +++ b/docs/main.de.rst @@ -317,7 +317,7 @@ endet. Sie können dieses Verhalten ändern, indem Sie den Parameter namens ``closeable`` setzen. Bitte beachten Sie, dass Transaktionen explizit durch Aufruf der Methode -``begin()`` eingeleiten werden müssen. Hierdurch wird sichergestellt, dass +``begin()`` eingeleitet werden müssen. Hierdurch wird sichergestellt, dass das transparente Neueröffnen von Verbindungen bis zum Ende der Transaktion ausgesetzt wird, und dass die Verbindung zurückgerollt wird, before sie vom gleichen Thread erneut benutzt wird. @@ -452,7 +452,7 @@ Sie können dies auch durch Verwendung von Kontext-Managern vereinfachen:: Bitte beachten Sie, dass Transaktionen explizit durch Aufruf der Methode -``begin()`` eingeleiten werden müssen. Hierdurch wird sichergestellt, +``begin()`` eingeleitet werden müssen. Hierdurch wird sichergestellt, dass die Verbindung nicht mehr mit anderen Threads geteilt wird, dass das transparente Neueröffnen von Verbindungen bis zum Ende der Transaktion ausgesetzt wird, und dass die Verbindung zurückgerollt wird, bevor sie @@ -471,7 +471,7 @@ Wenn Sie eine Lösung verwenden wie den Apache-Webserver mit mod_python_ oder mod_wsgi_, dann sollten Sie bedenken, dass Ihr Python-Code normalerweise im Kontext der Kindprozesse des Webservers läuft. Wenn Sie also das ``pooled_db``-Modul einsetzen, und mehrere dieser Kindprozesse laufen, dann -werden Sie ebensoviele Pools mit Datenbankverbindungen erhalten. Wenn diese +werden Sie ebenso viele Pools mit Datenbankverbindungen erhalten. Wenn diese Prozesse viele Threads laufen lassen, dann mag dies eine sinnvoller Ansatz sein, wenn aber diese Prozesse nicht mehr als einen Worker-Thread starten, wie im Fall des Multi-Processing Moduls "prefork" für den Apache-Webserver, diff --git a/docs/main.html b/docs/main.html index 2f81912..1c5aa93 100644 --- a/docs/main.html +++ b/docs/main.html @@ -3,7 +3,7 @@ <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1" /> -<meta name="generator" content="Docutils 0.17: http://docutils.sourceforge.net/" /> +<meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" /> <title>DBUtils User's Guide</title> <link rel="stylesheet" href="doc.css" type="text/css" /> </head> @@ -303,7 +303,7 @@ <h3>PersistentDB (persistent_db)</h3> ignored since it would be reopened at the next usage anyway and contrary to the intent of having persistent connections. Instead, the connection will be automatically closed when the thread dies. -You can change this behavior be setting the <span class="docutils literal">closeable</span> parameter.</p> +You can change this behavior by setting the <span class="docutils literal">closeable</span> parameter.</p> <p>Note that you need to explicitly start transactions by calling the <span class="docutils literal">begin()</span> method. This ensures that the transparent reopening will be suspended until the end of the transaction, and that the connection diff --git a/docs/main.rst b/docs/main.rst index 426a298..e11e301 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -291,7 +291,7 @@ Closing a persistent connection with ``db.close()`` will be silently ignored since it would be reopened at the next usage anyway and contrary to the intent of having persistent connections. Instead, the connection will be automatically closed when the thread dies. -You can change this behavior be setting the ``closeable`` parameter. +You can change this behavior by setting the ``closeable`` parameter. Note that you need to explicitly start transactions by calling the ``begin()`` method. This ensures that the transparent reopening will be From 3cd75e964ee2abcbf35d9ad13a96bd9177693842 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Fri, 14 May 2021 22:50:40 +0200 Subject: [PATCH 30/84] Make use of context managers for locks --- dbutils/pooled_db.py | 25 +++++-------------------- dbutils/simple_pooled_db.py | 5 +---- 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/dbutils/pooled_db.py b/dbutils/pooled_db.py index e8367b0..e7d9947 100644 --- a/dbutils/pooled_db.py +++ b/dbutils/pooled_db.py @@ -284,8 +284,7 @@ def connection(self, shareable=True): then the connection may be shared with other threads. """ if shareable and self._maxshared: - self._lock.acquire() - try: + with self._lock: while (not self._shared_cache and self._maxconnections and self._connections >= self._maxconnections): self._wait_lock() @@ -313,12 +312,9 @@ def connection(self, shareable=True): # put the connection (back) into the shared cache self._shared_cache.append(con) self._lock.notify() - finally: - self._lock.release() con = PooledSharedDBConnection(self, con) else: # try to get a dedicated connection - self._lock.acquire() - try: + with self._lock: while (self._maxconnections and self._connections >= self._maxconnections): self._wait_lock() @@ -331,8 +327,6 @@ def connection(self, shareable=True): con._ping_check() # check connection con = PooledDedicatedDBConnection(self, con) self._connections += 1 - finally: - self._lock.release() return con def dedicated_connection(self): @@ -341,8 +335,7 @@ def dedicated_connection(self): def unshare(self, con): """Decrease the share of a connection in the shared cache.""" - self._lock.acquire() - try: + with self._lock: con.unshare() shared = con.shared if not shared: # connection is idle, @@ -350,15 +343,12 @@ def unshare(self, con): self._shared_cache.remove(con) # from shared cache except ValueError: pass # pool has already been closed - finally: - self._lock.release() if not shared: # connection has become idle, self.cache(con.con) # so add it to the idle cache def cache(self, con): """Put a dedicated connection back into the idle cache.""" - self._lock.acquire() - try: + with self._lock: if not self._maxcached or len(self._idle_cache) < self._maxcached: con._reset(force=self._reset) # rollback possible transaction # the idle cache is not full, so put it there @@ -367,13 +357,10 @@ def cache(self, con): con.close() # then close the connection self._connections -= 1 self._lock.notify() - finally: - self._lock.release() def close(self): """Close all connections in the pool.""" - self._lock.acquire() - try: + with self._lock: while self._idle_cache: # close all idle connections con = self._idle_cache.pop(0) try: @@ -389,8 +376,6 @@ def close(self): pass self._connections -= 1 self._lock.notifyAll() - finally: - self._lock.release() def __del__(self): """Delete the pool.""" diff --git a/dbutils/simple_pooled_db.py b/dbutils/simple_pooled_db.py index 5e31b29..b66cf7f 100644 --- a/dbutils/simple_pooled_db.py +++ b/dbutils/simple_pooled_db.py @@ -196,8 +196,7 @@ def _unthreadsafe_return_connection(self, con): def _threadsafe_get_connection(self): """Get a connection from the pool.""" - self._lock.acquire() - try: + with self._lock: next = self._nextConnection con = PooledDBConnection(self, self._connections[next]) next += 1 @@ -205,8 +204,6 @@ def _threadsafe_get_connection(self): next = 0 self._nextConnection = next return con - finally: - self._lock.release() def _threadsafe_add_connection(self, con): """Add a connection to the pool.""" From b71bb5d529c0e2ac2da9b729ffde40396925d776 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Thu, 8 Jul 2021 18:45:21 +0200 Subject: [PATCH 31/84] Prepare patch release --- .bumpversion.cfg | 2 +- README.md | 2 +- dbutils/__init__.py | 2 +- docs/changelog.html | 2 +- docs/changelog.rst | 2 +- docs/main.de.html | 2 +- docs/main.de.rst | 2 +- docs/main.html | 2 +- docs/main.rst | 2 +- setup.py | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 456e479..e9be47b 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.0.1 +current_version = 2.0.2 [bumpversion:file:setup.py] search = __version__ = '{current_version}' diff --git a/README.md b/README.md index ffd3afd..1ebfde7 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ to a database that can be used in all kinds of multi-threaded environments. The suite supports DB-API 2 compliant database interfaces and the classic PyGreSQL interface. -The current version 2.0.1 of DBUtils supports Python versions 2.7 and 3.5 to 3.9. +The current version 2.0.2 of DBUtils supports Python versions 2.7 and 3.5 to 3.9. **Please have a look at the [changelog](https://webwareforpython.github.io/DBUtils/changelog.html), because there are some breaking changes in version 2.0.** diff --git a/dbutils/__init__.py b/dbutils/__init__.py index 23e2f58..3a26d9b 100644 --- a/dbutils/__init__.py +++ b/dbutils/__init__.py @@ -5,4 +5,4 @@ 'simple_pooled_pg', 'steady_pg', 'pooled_pg', 'persistent_pg', 'simple_pooled_db', 'steady_db', 'pooled_db', 'persistent_db'] -__version__ = '2.0.1' +__version__ = '2.0.2' diff --git a/docs/changelog.html b/docs/changelog.html index e38ae50..06a7831 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -13,7 +13,7 @@ <h1 class="title">Changelog for DBUtils</h1> <section id="id1"> <h2>2.0.2</h2> -<p>DBUtils 2.0.2 was released on ...</p> +<p>DBUtils 2.0.2 was released on June 8, 2021.</p> <p>Changes:</p> <ul class="simple"> <li><p>Allow using context managers for pooled connections.</p></li> diff --git a/docs/changelog.rst b/docs/changelog.rst index 5fd6765..d216900 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,7 @@ 2.0.2 ===== -DBUtils 2.0.2 was released on ... +DBUtils 2.0.2 was released on June 8, 2021. Changes: diff --git a/docs/main.de.html b/docs/main.de.html index c5f0dd4..7fecb2b 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -12,7 +12,7 @@ <h1 class="title">Benutzeranleitung für DBUtils</h1> <dl class="docinfo simple"> <dt class="version">Version</dt> -<dd class="version">2.0.1</dd> +<dd class="version">2.0.2</dd> <dt class="translations">Translations</dt> <dd class="translations"><p><a class="reference external" href="main.html">English</a> | German</p> </dd> diff --git a/docs/main.de.rst b/docs/main.de.rst index b14bae6..180da0e 100644 --- a/docs/main.de.rst +++ b/docs/main.de.rst @@ -1,7 +1,7 @@ Benutzeranleitung für DBUtils +++++++++++++++++++++++++++++ -:Version: 2.0.1 +:Version: 2.0.2 :Translations: English_ | German .. _English: main.html diff --git a/docs/main.html b/docs/main.html index 1c5aa93..0b9fcd7 100644 --- a/docs/main.html +++ b/docs/main.html @@ -12,7 +12,7 @@ <h1 class="title">DBUtils User's Guide</h1> <dl class="docinfo simple"> <dt class="version">Version</dt> -<dd class="version">2.0.1</dd> +<dd class="version">2.0.2</dd> <dt class="translations">Translations</dt> <dd class="translations"><p>English | <a class="reference external" href="main.de.html">German</a></p> </dd> diff --git a/docs/main.rst b/docs/main.rst index e11e301..11a7c7e 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -1,7 +1,7 @@ DBUtils User's Guide ++++++++++++++++++++ -:Version: 2.0.1 +:Version: 2.0.2 :Translations: English | German_ .. _German: main.de.html diff --git a/setup.py b/setup.py index c355a37..2ef3df7 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ warnings.filterwarnings('ignore', 'Unknown distribution option') -__version__ = '2.0.1' +__version__ = '2.0.2' readme = open('README.md').read() From 7d152632a2185a753fe8f23e04c91630b4ce4198 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Fri, 26 Nov 2021 20:51:02 +0100 Subject: [PATCH 32/84] Support Python 3.10 Replace deprecated spelling of Condition.notify_all(). --- .github/workflows/publish_on_pypi.yml | 2 +- .github/workflows/test_with_tox.yml | 2 +- dbutils/pooled_db.py | 2 +- docs/changelog.html | 30 +++++------ docs/main.de.html | 72 ++++++++++++--------------- docs/main.de.rst | 2 +- docs/main.html | 72 ++++++++++++--------------- docs/main.rst | 2 +- setup.py | 1 + tox.ini | 2 +- 10 files changed, 86 insertions(+), 101 deletions(-) diff --git a/.github/workflows/publish_on_pypi.yml b/.github/workflows/publish_on_pypi.yml index 92f72db..c9a7589 100644 --- a/.github/workflows/publish_on_pypi.yml +++ b/.github/workflows/publish_on_pypi.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: [3.9, 2.7] + python: ['3.9', '2.7'] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/test_with_tox.yml b/.github/workflows/test_with_tox.yml index 4ade996..904f2db 100644 --- a/.github/workflows/test_with_tox.yml +++ b/.github/workflows/test_with_tox.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: [2.7, 3.6, 3.7, 3.8, 3.9] + python: ['2.7', '3.6', '3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2 diff --git a/dbutils/pooled_db.py b/dbutils/pooled_db.py index e7d9947..a442bdb 100644 --- a/dbutils/pooled_db.py +++ b/dbutils/pooled_db.py @@ -375,7 +375,7 @@ def close(self): except Exception: pass self._connections -= 1 - self._lock.notifyAll() + self._lock.notify_all() def __del__(self): """Delete the pool.""" diff --git a/docs/changelog.html b/docs/changelog.html index 06a7831..4990b56 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -3,7 +3,7 @@ <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1" /> -<meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" /> +<meta name="generator" content="Docutils 0.18.1: http://docutils.sourceforge.net/" /> <title>Changelog for DBUtils</title> <link rel="stylesheet" href="doc.css" type="text/css" /> </head> @@ -11,7 +11,7 @@ <main id="changelog-for-dbutils"> <h1 class="title">Changelog for DBUtils</h1> -<section id="id1"> +<section id="section-1"> <h2>2.0.2</h2> <p>DBUtils 2.0.2 was released on June 8, 2021.</p> <p>Changes:</p> @@ -19,7 +19,7 @@ <h2>2.0.2</h2> <li><p>Allow using context managers for pooled connections.</p></li> </ul> </section> -<section id="id2"> +<section id="section-2"> <h2>2.0.1</h2> <p>DBUtils 2.0.1 was released on April 8, 2021.</p> <p>Changes:</p> @@ -27,7 +27,7 @@ <h2>2.0.1</h2> <li><p>Avoid "name Exception is not defined" when exiting.</p></li> </ul> </section> -<section id="id3"> +<section id="section-3"> <h2>2.0</h2> <p>DBUtils 2.0 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> @@ -43,7 +43,7 @@ <h2>2.0</h2> <li><p>This changelog has been compiled from the former release notes.</p></li> </ul> </section> -<section id="id4"> +<section id="section-4"> <h2>1.4</h2> <p>DBUtils 1.4 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> @@ -54,7 +54,7 @@ <h2>1.4</h2> inside a transaction.</p></li> </ul> </section> -<section id="id5"> +<section id="section-5"> <h2>1.3</h2> <p>DBUtils 1.3 was released on March 3, 2018.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.4 to 3.7.</p> @@ -63,12 +63,12 @@ <h2>1.3</h2> <li><p>This version now supports context handlers for connections and cursors.</p></li> </ul> </section> -<section id="id6"> +<section id="section-6"> <h2>1.2</h2> <p>DBUtils 1.2 was released on February 5, 2017.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.0 to 3.6.</p> </section> -<section id="id7"> +<section id="section-7"> <h2>1.1.1</h2> <p>DBUtils 1.1.1 was released on February 4, 2017.</p> <p>It is intended to be used with Python versions 2.3 to 2.7.</p> @@ -82,7 +82,7 @@ <h2>1.1.1</h2> <li><p>Fixed a problem when running under Jython (reported by Vitaly Kruglikov).</p></li> </ul> </section> -<section id="id8"> +<section id="section-8"> <h2>1.1</h2> <p>DBUtils 1.1 was released on August 14, 2011.</p> <p>Improvements:</p> @@ -111,7 +111,7 @@ <h2>1.1</h2> <li><p>Fixed some minor issues with the <span class="docutils literal">DBUtilsExample</span> for Webware.</p></li> </ul> </section> -<section id="id9"> +<section id="section-9"> <h2>1.0</h2> <p>DBUtils 1.0 was released on November 29, 2008.</p> <p>It is intended to be used with Python versions 2.2 to 2.6.</p> @@ -144,7 +144,7 @@ <h2>1.0</h2> the MySQLdb module (problem reported by Gregory Pinero).</p></li> </ul> </section> -<section id="id10"> +<section id="section-10"> <h2>0.9.4</h2> <p>DBUtils 0.9.4 was released on July 7, 2007.</p> <p>This release fixes a problem in the destructor code and has been supplemented @@ -153,7 +153,7 @@ <h2>0.9.4</h2> in the last release, since you can now pass custom creator functions for database connections instead of DB-API 2 modules.</p> </section> -<section id="id11"> +<section id="section-11"> <h2>0.9.3</h2> <p>DBUtils 0.9.3 was released on May 21, 2007.</p> <p>Changes:</p> @@ -168,7 +168,7 @@ <h2>0.9.3</h2> Added Chinese translation of the User's Guide, kindly contributed by gashero.</p></li> </ul> </section> -<section id="id12"> +<section id="section-12"> <h2>0.9.2</h2> <p>DBUtils 0.9.2 was released on September 22, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.5.</p> @@ -178,7 +178,7 @@ <h2>0.9.2</h2> storage engine. Accordingly, renamed <span class="docutils literal">SolidPg</span> to <span class="docutils literal">SteadyPg</span>.</p></li> </ul> </section> -<section id="id13"> +<section id="section-13"> <h2>0.9.1</h2> <p>DBUtils 0.9.1 was released on May 8, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.4.</p> @@ -192,7 +192,7 @@ <h2>0.9.1</h2> <li><p>Improved the documentation and added a User's Guide.</p></li> </ul> </section> -<section id="id14"> +<section id="section-14"> <h2>0.8.1 - 2005-09-13</h2> <p>DBUtils 0.8.1 was released on September 13, 2005.</p> <p>It is intended to be used with Python versions 2.0 to 2.4.</p> diff --git a/docs/main.de.html b/docs/main.de.html index 7fecb2b..5f72110 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -3,7 +3,7 @@ <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1" /> -<meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" /> +<meta name="generator" content="Docutils 0.18.1: http://docutils.sourceforge.net/" /> <title>Benutzeranleitung für DBUtils</title> <link rel="stylesheet" href="doc.css" type="text/css" /> </head> @@ -11,47 +11,47 @@ <main id="benutzeranleitung-fur-dbutils"> <h1 class="title">Benutzeranleitung für DBUtils</h1> <dl class="docinfo simple"> -<dt class="version">Version</dt> +<dt class="version">Version<span class="colon">:</span></dt> <dd class="version">2.0.2</dd> -<dt class="translations">Translations</dt> +<dt class="translations">Translations<span class="colon">:</span></dt> <dd class="translations"><p><a class="reference external" href="main.html">English</a> | German</p> </dd> </dl> -<div class="contents topic" id="inhalt"> +<nav class="contents" id="inhalt" role="doc-toc"> <p class="topic-title">Inhalt</p> <ul class="simple"> -<li><p><a class="reference internal" href="#zusammenfassung" id="id5">Zusammenfassung</a></p></li> -<li><p><a class="reference internal" href="#module" id="id6">Module</a></p></li> -<li><p><a class="reference internal" href="#download" id="id7">Download</a></p></li> -<li><p><a class="reference internal" href="#installation" id="id8">Installation</a></p> +<li><p><a class="reference internal" href="#zusammenfassung" id="toc-entry-1">Zusammenfassung</a></p></li> +<li><p><a class="reference internal" href="#module" id="toc-entry-2">Module</a></p></li> +<li><p><a class="reference internal" href="#download" id="toc-entry-3">Download</a></p></li> +<li><p><a class="reference internal" href="#installation" id="toc-entry-4">Installation</a></p> <ul> -<li><p><a class="reference internal" href="#id1" id="id9">Installation</a></p></li> +<li><p><a class="reference internal" href="#installation-1" id="toc-entry-5">Installation</a></p></li> </ul> </li> -<li><p><a class="reference internal" href="#anforderungen" id="id10">Anforderungen</a></p></li> -<li><p><a class="reference internal" href="#funktionalitat" id="id11">Funktionalität</a></p> +<li><p><a class="reference internal" href="#anforderungen" id="toc-entry-6">Anforderungen</a></p></li> +<li><p><a class="reference internal" href="#funktionalitat" id="toc-entry-7">Funktionalität</a></p> <ul> -<li><p><a class="reference internal" href="#simplepooleddb-simple-pooled-db" id="id12">SimplePooledDB (simple_pooled_db)</a></p></li> -<li><p><a class="reference internal" href="#steadydbconnection-steady-db" id="id13">SteadyDBConnection (steady_db)</a></p></li> -<li><p><a class="reference internal" href="#persistentdb-persistent-db" id="id14">PersistentDB (persistent_db)</a></p></li> -<li><p><a class="reference internal" href="#pooleddb-pooled-db" id="id15">PooledDB (pooled_db)</a></p></li> -<li><p><a class="reference internal" href="#die-qual-der-wahl" id="id16">Die Qual der Wahl</a></p></li> +<li><p><a class="reference internal" href="#simplepooleddb-simple-pooled-db" id="toc-entry-8">SimplePooledDB (simple_pooled_db)</a></p></li> +<li><p><a class="reference internal" href="#steadydbconnection-steady-db" id="toc-entry-9">SteadyDBConnection (steady_db)</a></p></li> +<li><p><a class="reference internal" href="#persistentdb-persistent-db" id="toc-entry-10">PersistentDB (persistent_db)</a></p></li> +<li><p><a class="reference internal" href="#pooleddb-pooled-db" id="toc-entry-11">PooledDB (pooled_db)</a></p></li> +<li><p><a class="reference internal" href="#die-qual-der-wahl" id="toc-entry-12">Die Qual der Wahl</a></p></li> </ul> </li> -<li><p><a class="reference internal" href="#benutzung" id="id17">Benutzung</a></p> +<li><p><a class="reference internal" href="#benutzung" id="toc-entry-13">Benutzung</a></p> <ul> -<li><p><a class="reference internal" href="#id2" id="id18">PersistentDB (persistent_db)</a></p></li> -<li><p><a class="reference internal" href="#id3" id="id19">PooledDB (pooled_db)</a></p></li> +<li><p><a class="reference internal" href="#persistentdb-persistent-db-1" id="toc-entry-14">PersistentDB (persistent_db)</a></p></li> +<li><p><a class="reference internal" href="#pooleddb-pooled-db-1" id="toc-entry-15">PooledDB (pooled_db)</a></p></li> </ul> </li> -<li><p><a class="reference internal" href="#anmerkungen" id="id20">Anmerkungen</a></p></li> -<li><p><a class="reference internal" href="#zukunft" id="id21">Zukunft</a></p></li> -<li><p><a class="reference internal" href="#fehlermeldungen-und-feedback" id="id22">Fehlermeldungen und Feedback</a></p></li> -<li><p><a class="reference internal" href="#links" id="id23">Links</a></p></li> -<li><p><a class="reference internal" href="#autoren" id="id24">Autoren</a></p></li> -<li><p><a class="reference internal" href="#copyright-und-lizenz" id="id25">Copyright und Lizenz</a></p></li> +<li><p><a class="reference internal" href="#anmerkungen" id="toc-entry-16">Anmerkungen</a></p></li> +<li><p><a class="reference internal" href="#zukunft" id="toc-entry-17">Zukunft</a></p></li> +<li><p><a class="reference internal" href="#fehlermeldungen-und-feedback" id="toc-entry-18">Fehlermeldungen und Feedback</a></p></li> +<li><p><a class="reference internal" href="#links" id="toc-entry-19">Links</a></p></li> +<li><p><a class="reference internal" href="#autoren" id="toc-entry-20">Autoren</a></p></li> +<li><p><a class="reference internal" href="#copyright-und-lizenz" id="toc-entry-21">Copyright und Lizenz</a></p></li> </ul> -</div> +</nav> <section id="zusammenfassung"> <h2>Zusammenfassung</h2> <p><a class="reference external" href="https://github.com/WebwareForPython/DBUtils">DBUtils</a> ist eine Sammlung von Python-Modulen, mit deren Hilfe man in <a class="reference external" href="https://www.python.org">Python</a> @@ -69,10 +69,6 @@ <h2>Module</h2> DB-API-2-Datenbankadaptern, und einer Gruppe zur Verwendung mit dem klassischen PyGreSQL-Datenbankadapter-Modul.</p> <table> -<colgroup> -<col style="width: 28%" /> -<col style="width: 72%" /> -</colgroup> <thead> <tr><th class="head" colspan="2"><p>Allgemeine Variante für beliebige DB-API-2-Adapter</p></th> </tr> @@ -93,10 +89,6 @@ <h2>Module</h2> </tbody> </table> <table> -<colgroup> -<col style="width: 28%" /> -<col style="width: 72%" /> -</colgroup> <thead> <tr><th class="head" colspan="2"><p>Variante speziell für den klassischen PyGreSQL-Adapter</p></th> </tr> @@ -133,7 +125,7 @@ <h2>Download</h2> </section> <section id="installation"> <h2>Installation</h2> -<section id="id1"> +<section id="installation-1"> <h3>Installation</h3> <p>Das Paket kann auf die übliche Weise installiert werden:</p> <pre class="literal-block">python setup.py install</pre> @@ -144,7 +136,7 @@ <h3>Installation</h3> </section> <section id="anforderungen"> <h2>Anforderungen</h2> -<p>DBUtils unterstützt die <a class="reference external" href="https://www.python.org">Python</a> Versionen 2.7 und 3.5 bis 3.9.</p> +<p>DBUtils unterstützt die <a class="reference external" href="https://www.python.org">Python</a> Versionen 2.7 und 3.5 bis 3.10.</p> <p>Die Module in der Variante für klassisches PyGreSQL benötigen <a class="reference external" href="https://www.pygresql.org/">PyGreSQL</a> Version 4.0 oder höher, während die Module in der allgemeinen Variante für DB-API 2 mit jedem beliebigen Python-Datenbankadapter-Modul zusammenarbeiten, @@ -274,7 +266,7 @@ <h2>Benutzung</h2> sich die Dokumentation des <span class="docutils literal">pooled_db</span>-Moduls wie folgt anzeigen lassen (dies funktioniert entsprechend auch mit den anderen Modulen):</p> <pre class="literal-block">help(pooled_db)</pre> -<section id="id2"> +<section id="persistentdb-persistent-db-1"> <h3>PersistentDB (persistent_db)</h3> <p>Wenn Sie das <span class="docutils literal">persistent_db</span>-Modul einsetzen möchten, müssen Sie zuerst einen Generator für die von Ihnen gewünschte Art von Datenbankverbindungen einrichten, @@ -338,7 +330,7 @@ <h3>PersistentDB (persistent_db)</h3> <span class="docutils literal">mod_wsgi</span> hier Probleme bereitet, da es Daten, die mit <span class="docutils literal">threading.local</span> gespeichert wurden, zwischen Requests löscht).</p> </section> -<section id="id3"> +<section id="pooleddb-pooled-db-1"> <h3>PooledDB (pooled_db)</h3> <p>Wenn Sie das <span class="docutils literal">pooled_db</span>-Modul einsetzen möchten, müssen Sie zuerst einen Pool für die von Ihnen gewünschte Art von Datenbankverbindungen einrichten, @@ -506,10 +498,10 @@ <h2>Links</h2> <section id="autoren"> <h2>Autoren</h2> <dl class="field-list simple"> -<dt>Autor</dt> +<dt>Autor<span class="colon">:</span></dt> <dd><p><a class="reference external" href="https://github.com/Cito">Christoph Zwerschke</a></p> </dd> -<dt>Beiträge</dt> +<dt>Beiträge<span class="colon">:</span></dt> <dd><p>DBUtils benutzt Code, Anmerkungen und Vorschläge von Ian Bicking, Chuck Esterbrook (Webware for Python), Dan Green (DBTools), Jay Love, Michael Palmer, Tom Schwaller, Geoffrey Talvola, diff --git a/docs/main.de.rst b/docs/main.de.rst index 180da0e..4184e76 100644 --- a/docs/main.de.rst +++ b/docs/main.de.rst @@ -98,7 +98,7 @@ herunterzuladen und zu installieren:: Anforderungen ============= -DBUtils unterstützt die Python_ Versionen 2.7 und 3.5 bis 3.9. +DBUtils unterstützt die Python_ Versionen 2.7 und 3.5 bis 3.10. Die Module in der Variante für klassisches PyGreSQL benötigen PyGreSQL_ Version 4.0 oder höher, während die Module in der allgemeinen Variante diff --git a/docs/main.html b/docs/main.html index 0b9fcd7..964126a 100644 --- a/docs/main.html +++ b/docs/main.html @@ -3,7 +3,7 @@ <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1" /> -<meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" /> +<meta name="generator" content="Docutils 0.18.1: http://docutils.sourceforge.net/" /> <title>DBUtils User's Guide</title> <link rel="stylesheet" href="doc.css" type="text/css" /> </head> @@ -11,47 +11,47 @@ <main id="dbutils-user-s-guide"> <h1 class="title">DBUtils User's Guide</h1> <dl class="docinfo simple"> -<dt class="version">Version</dt> +<dt class="version">Version<span class="colon">:</span></dt> <dd class="version">2.0.2</dd> -<dt class="translations">Translations</dt> +<dt class="translations">Translations<span class="colon">:</span></dt> <dd class="translations"><p>English | <a class="reference external" href="main.de.html">German</a></p> </dd> </dl> -<div class="contents topic" id="contents"> +<nav class="contents" id="contents" role="doc-toc"> <p class="topic-title">Contents</p> <ul class="simple"> -<li><p><a class="reference internal" href="#synopsis" id="id5">Synopsis</a></p></li> -<li><p><a class="reference internal" href="#modules" id="id6">Modules</a></p></li> -<li><p><a class="reference internal" href="#download" id="id7">Download</a></p></li> -<li><p><a class="reference internal" href="#installation" id="id8">Installation</a></p> +<li><p><a class="reference internal" href="#synopsis" id="toc-entry-1">Synopsis</a></p></li> +<li><p><a class="reference internal" href="#modules" id="toc-entry-2">Modules</a></p></li> +<li><p><a class="reference internal" href="#download" id="toc-entry-3">Download</a></p></li> +<li><p><a class="reference internal" href="#installation" id="toc-entry-4">Installation</a></p> <ul> -<li><p><a class="reference internal" href="#id1" id="id9">Installation</a></p></li> +<li><p><a class="reference internal" href="#installation-1" id="toc-entry-5">Installation</a></p></li> </ul> </li> -<li><p><a class="reference internal" href="#requirements" id="id10">Requirements</a></p></li> -<li><p><a class="reference internal" href="#functionality" id="id11">Functionality</a></p> +<li><p><a class="reference internal" href="#requirements" id="toc-entry-6">Requirements</a></p></li> +<li><p><a class="reference internal" href="#functionality" id="toc-entry-7">Functionality</a></p> <ul> -<li><p><a class="reference internal" href="#simplepooleddb-simple-pooled-db" id="id12">SimplePooledDB (simple_pooled_db)</a></p></li> -<li><p><a class="reference internal" href="#steadydbconnection-steady-db" id="id13">SteadyDBConnection (steady_db)</a></p></li> -<li><p><a class="reference internal" href="#persistentdb-persistent-db" id="id14">PersistentDB (persistent_db)</a></p></li> -<li><p><a class="reference internal" href="#pooleddb-pooled-db" id="id15">PooledDB (pooled_db)</a></p></li> -<li><p><a class="reference internal" href="#which-one-to-use" id="id16">Which one to use?</a></p></li> +<li><p><a class="reference internal" href="#simplepooleddb-simple-pooled-db" id="toc-entry-8">SimplePooledDB (simple_pooled_db)</a></p></li> +<li><p><a class="reference internal" href="#steadydbconnection-steady-db" id="toc-entry-9">SteadyDBConnection (steady_db)</a></p></li> +<li><p><a class="reference internal" href="#persistentdb-persistent-db" id="toc-entry-10">PersistentDB (persistent_db)</a></p></li> +<li><p><a class="reference internal" href="#pooleddb-pooled-db" id="toc-entry-11">PooledDB (pooled_db)</a></p></li> +<li><p><a class="reference internal" href="#which-one-to-use" id="toc-entry-12">Which one to use?</a></p></li> </ul> </li> -<li><p><a class="reference internal" href="#usage" id="id17">Usage</a></p> +<li><p><a class="reference internal" href="#usage" id="toc-entry-13">Usage</a></p> <ul> -<li><p><a class="reference internal" href="#id2" id="id18">PersistentDB (persistent_db)</a></p></li> -<li><p><a class="reference internal" href="#id3" id="id19">PooledDB (pooled_db)</a></p></li> +<li><p><a class="reference internal" href="#persistentdb-persistent-db-1" id="toc-entry-14">PersistentDB (persistent_db)</a></p></li> +<li><p><a class="reference internal" href="#pooleddb-pooled-db-1" id="toc-entry-15">PooledDB (pooled_db)</a></p></li> </ul> </li> -<li><p><a class="reference internal" href="#notes" id="id20">Notes</a></p></li> -<li><p><a class="reference internal" href="#future" id="id21">Future</a></p></li> -<li><p><a class="reference internal" href="#bug-reports-and-feedback" id="id22">Bug reports and feedback</a></p></li> -<li><p><a class="reference internal" href="#links" id="id23">Links</a></p></li> -<li><p><a class="reference internal" href="#credits" id="id24">Credits</a></p></li> -<li><p><a class="reference internal" href="#copyright-and-license" id="id25">Copyright and License</a></p></li> +<li><p><a class="reference internal" href="#notes" id="toc-entry-16">Notes</a></p></li> +<li><p><a class="reference internal" href="#future" id="toc-entry-17">Future</a></p></li> +<li><p><a class="reference internal" href="#bug-reports-and-feedback" id="toc-entry-18">Bug reports and feedback</a></p></li> +<li><p><a class="reference internal" href="#links" id="toc-entry-19">Links</a></p></li> +<li><p><a class="reference internal" href="#credits" id="toc-entry-20">Credits</a></p></li> +<li><p><a class="reference internal" href="#copyright-and-license" id="toc-entry-21">Copyright and License</a></p></li> </ul> -</div> +</nav> <section id="synopsis"> <h2>Synopsis</h2> <p><a class="reference external" href="https://github.com/WebwareForPython/DBUtils">DBUtils</a> is a suite of Python modules allowing to connect in a safe and @@ -67,10 +67,6 @@ <h2>Modules</h2> two subsets of modules, one for use with arbitrary DB-API 2 modules, the other one for use with the classic PyGreSQL module.</p> <table> -<colgroup> -<col style="width: 30%" /> -<col style="width: 70%" /> -</colgroup> <thead> <tr><th class="head" colspan="2"><p>Universal DB-API 2 variant</p></th> </tr> @@ -91,10 +87,6 @@ <h2>Modules</h2> </tbody> </table> <table> -<colgroup> -<col style="width: 30%" /> -<col style="width: 70%" /> -</colgroup> <thead> <tr><th class="head" colspan="2"><p>Classic PyGreSQL variant</p></th> </tr> @@ -131,7 +123,7 @@ <h2>Download</h2> </section> <section id="installation"> <h2>Installation</h2> -<section id="id1"> +<section id="installation-1"> <h3>Installation</h3> <p>The package can be installed in the usual way:</p> <pre class="literal-block">python setup.py install</pre> @@ -141,7 +133,7 @@ <h3>Installation</h3> </section> <section id="requirements"> <h2>Requirements</h2> -<p>DBUtils supports <a class="reference external" href="https://www.python.org">Python</a> version 2.7 and Python versions 3.5 to 3.9.</p> +<p>DBUtils supports <a class="reference external" href="https://www.python.org">Python</a> version 2.7 and Python versions 3.5 to 3.10.</p> <p>The modules in the classic PyGreSQL variant need <a class="reference external" href="https://www.pygresql.org/">PyGreSQL</a> version 4.0 or above, while the modules in the universal DB-API 2 variant run with any Python <a class="reference external" href="https://www.python.org/dev/peps/pep-0249/">DB-API 2</a> compliant database interface module.</p> @@ -255,7 +247,7 @@ <h2>Usage</h2> display the documentation of the <span class="docutils literal">pooled_db</span> module as follows (this works analogously for the other modules):</p> <pre class="literal-block">help(pooled_db)</pre> -<section id="id2"> +<section id="persistentdb-persistent-db-1"> <h3>PersistentDB (persistent_db)</h3> <p>In order to make use of the <span class="docutils literal">persistent_db</span> module, you first need to set up a generator for your kind of database connections by creating an instance @@ -313,7 +305,7 @@ <h3>PersistentDB (persistent_db)</h3> environments (for instance, <span class="docutils literal">mod_wsgi</span> is known to cause problems since it clears the <span class="docutils literal">threading.local</span> data between requests).</p> </section> -<section id="id3"> +<section id="pooleddb-pooled-db-1"> <h3>PooledDB (pooled_db)</h3> <p>In order to make use of the <span class="docutils literal">pooled_db</span> module, you first need to set up the database connection pool by creating an instance of <span class="docutils literal">pooled_db</span>, passing the @@ -465,10 +457,10 @@ <h2>Links</h2> <section id="credits"> <h2>Credits</h2> <dl class="field-list simple"> -<dt>Author</dt> +<dt>Author<span class="colon">:</span></dt> <dd><p><a class="reference external" href="https://github.com/Cito">Christoph Zwerschke</a></p> </dd> -<dt>Contributions</dt> +<dt>Contributions<span class="colon">:</span></dt> <dd><p>DBUtils uses code, input and suggestions made by Ian Bicking, Chuck Esterbrook (Webware for Python), Dan Green (DBTools), Jay Love, Michael Palmer, Tom Schwaller, Geoffrey Talvola, diff --git a/docs/main.rst b/docs/main.rst index 11a7c7e..de88b1e 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -95,7 +95,7 @@ It is even easier to download and install the package in one go using `pip`_:: Requirements ============ -DBUtils supports Python_ version 2.7 and Python versions 3.5 to 3.9. +DBUtils supports Python_ version 2.7 and Python versions 3.5 to 3.10. The modules in the classic PyGreSQL variant need PyGreSQL_ version 4.0 or above, while the modules in the universal DB-API 2 variant run with diff --git a/setup.py b/setup.py index 2ef3df7..685ef5d 100755 --- a/setup.py +++ b/setup.py @@ -37,6 +37,7 @@ 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Topic :: Database', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules' diff --git a/tox.ini b/tox.ini index a8b505b..f8a8e33 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{27,35,36,37,38,39}, flake8, manifest, docs, spell +envlist = py27,py3{5,6,7,8,9,10}, flake8, manifest, docs, spell [testenv] setenv = From 9101cd140fd4dad3e5b18a59019f5cf8b00be6c8 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Fri, 26 Nov 2021 21:00:05 +0100 Subject: [PATCH 33/84] Prepare patch release --- .bumpversion.cfg | 2 +- README.md | 2 +- dbutils/__init__.py | 2 +- docs/changelog.html | 34 +++++++++++++++++++++------------- docs/changelog.rst | 10 ++++++++++ setup.py | 2 +- 6 files changed, 35 insertions(+), 17 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index e9be47b..9379a28 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.0.2 +current_version = 2.0.3 [bumpversion:file:setup.py] search = __version__ = '{current_version}' diff --git a/README.md b/README.md index 1ebfde7..efb2289 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ to a database that can be used in all kinds of multi-threaded environments. The suite supports DB-API 2 compliant database interfaces and the classic PyGreSQL interface. -The current version 2.0.2 of DBUtils supports Python versions 2.7 and 3.5 to 3.9. +The current version 2.0.3 of DBUtils supports Python versions 2.7 and 3.5 to 3.10. **Please have a look at the [changelog](https://webwareforpython.github.io/DBUtils/changelog.html), because there are some breaking changes in version 2.0.** diff --git a/dbutils/__init__.py b/dbutils/__init__.py index 3a26d9b..4ae3298 100644 --- a/dbutils/__init__.py +++ b/dbutils/__init__.py @@ -5,4 +5,4 @@ 'simple_pooled_pg', 'steady_pg', 'pooled_pg', 'persistent_pg', 'simple_pooled_db', 'steady_db', 'pooled_db', 'persistent_db'] -__version__ = '2.0.2' +__version__ = '2.0.3' diff --git a/docs/changelog.html b/docs/changelog.html index 4990b56..879ab73 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -12,6 +12,14 @@ <h1 class="title">Changelog for DBUtils</h1> <section id="section-1"> +<h2>2.0.3</h2> +<p>DBUtils 2.0.3 was released on November 26, 2021.</p> +<p>Changes:</p> +<ul class="simple"> +<li><p>Support Python version 3.10.</p></li> +</ul> +</section> +<section id="section-2"> <h2>2.0.2</h2> <p>DBUtils 2.0.2 was released on June 8, 2021.</p> <p>Changes:</p> @@ -19,7 +27,7 @@ <h2>2.0.2</h2> <li><p>Allow using context managers for pooled connections.</p></li> </ul> </section> -<section id="section-2"> +<section id="section-3"> <h2>2.0.1</h2> <p>DBUtils 2.0.1 was released on April 8, 2021.</p> <p>Changes:</p> @@ -27,7 +35,7 @@ <h2>2.0.1</h2> <li><p>Avoid "name Exception is not defined" when exiting.</p></li> </ul> </section> -<section id="section-3"> +<section id="section-4"> <h2>2.0</h2> <p>DBUtils 2.0 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> @@ -43,7 +51,7 @@ <h2>2.0</h2> <li><p>This changelog has been compiled from the former release notes.</p></li> </ul> </section> -<section id="section-4"> +<section id="section-5"> <h2>1.4</h2> <p>DBUtils 1.4 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> @@ -54,7 +62,7 @@ <h2>1.4</h2> inside a transaction.</p></li> </ul> </section> -<section id="section-5"> +<section id="section-6"> <h2>1.3</h2> <p>DBUtils 1.3 was released on March 3, 2018.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.4 to 3.7.</p> @@ -63,12 +71,12 @@ <h2>1.3</h2> <li><p>This version now supports context handlers for connections and cursors.</p></li> </ul> </section> -<section id="section-6"> +<section id="section-7"> <h2>1.2</h2> <p>DBUtils 1.2 was released on February 5, 2017.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.0 to 3.6.</p> </section> -<section id="section-7"> +<section id="section-8"> <h2>1.1.1</h2> <p>DBUtils 1.1.1 was released on February 4, 2017.</p> <p>It is intended to be used with Python versions 2.3 to 2.7.</p> @@ -82,7 +90,7 @@ <h2>1.1.1</h2> <li><p>Fixed a problem when running under Jython (reported by Vitaly Kruglikov).</p></li> </ul> </section> -<section id="section-8"> +<section id="section-9"> <h2>1.1</h2> <p>DBUtils 1.1 was released on August 14, 2011.</p> <p>Improvements:</p> @@ -111,7 +119,7 @@ <h2>1.1</h2> <li><p>Fixed some minor issues with the <span class="docutils literal">DBUtilsExample</span> for Webware.</p></li> </ul> </section> -<section id="section-9"> +<section id="section-10"> <h2>1.0</h2> <p>DBUtils 1.0 was released on November 29, 2008.</p> <p>It is intended to be used with Python versions 2.2 to 2.6.</p> @@ -144,7 +152,7 @@ <h2>1.0</h2> the MySQLdb module (problem reported by Gregory Pinero).</p></li> </ul> </section> -<section id="section-10"> +<section id="section-11"> <h2>0.9.4</h2> <p>DBUtils 0.9.4 was released on July 7, 2007.</p> <p>This release fixes a problem in the destructor code and has been supplemented @@ -153,7 +161,7 @@ <h2>0.9.4</h2> in the last release, since you can now pass custom creator functions for database connections instead of DB-API 2 modules.</p> </section> -<section id="section-11"> +<section id="section-12"> <h2>0.9.3</h2> <p>DBUtils 0.9.3 was released on May 21, 2007.</p> <p>Changes:</p> @@ -168,7 +176,7 @@ <h2>0.9.3</h2> Added Chinese translation of the User's Guide, kindly contributed by gashero.</p></li> </ul> </section> -<section id="section-12"> +<section id="section-13"> <h2>0.9.2</h2> <p>DBUtils 0.9.2 was released on September 22, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.5.</p> @@ -178,7 +186,7 @@ <h2>0.9.2</h2> storage engine. Accordingly, renamed <span class="docutils literal">SolidPg</span> to <span class="docutils literal">SteadyPg</span>.</p></li> </ul> </section> -<section id="section-13"> +<section id="section-14"> <h2>0.9.1</h2> <p>DBUtils 0.9.1 was released on May 8, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.4.</p> @@ -192,7 +200,7 @@ <h2>0.9.1</h2> <li><p>Improved the documentation and added a User's Guide.</p></li> </ul> </section> -<section id="section-14"> +<section id="section-15"> <h2>0.8.1 - 2005-09-13</h2> <p>DBUtils 0.8.1 was released on September 13, 2005.</p> <p>It is intended to be used with Python versions 2.0 to 2.4.</p> diff --git a/docs/changelog.rst b/docs/changelog.rst index d216900..5b0fd91 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,16 @@ Changelog for DBUtils +++++++++++++++++++++ +2.0.3 +===== + +DBUtils 2.0.3 was released on November 26, 2021. + +Changes: + +* Support Python version 3.10. + + 2.0.2 ===== diff --git a/setup.py b/setup.py index 685ef5d..f377bdc 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ warnings.filterwarnings('ignore', 'Unknown distribution option') -__version__ = '2.0.2' +__version__ = '2.0.3' readme = open('README.md').read() From b120dd8842e2caa3615bc6391ef0a36092072260 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Fri, 26 Nov 2021 22:39:06 +0100 Subject: [PATCH 34/84] Cease support for Python 2 and 3.5, minor optimizations --- .bumpversion.cfg | 2 +- .github/workflows/publish_on_pypi.yml | 2 +- .github/workflows/test_with_tox.yml | 2 +- README.md | 4 +-- dbutils/__init__.py | 2 +- dbutils/pooled_db.py | 12 +++------ dbutils/pooled_pg.py | 8 ++---- dbutils/simple_pooled_db.py | 19 ++++++-------- dbutils/simple_pooled_pg.py | 5 +--- dbutils/steady_db.py | 17 ++++-------- dbutils/steady_pg.py | 37 +++++++-------------------- docs/changelog.html | 37 +++++++++++++++++---------- docs/changelog.rst | 13 ++++++++-- docs/main.de.html | 4 +-- docs/main.de.rst | 4 +-- docs/main.html | 4 +-- docs/main.rst | 4 +-- docs/make.py | 2 +- setup.py | 17 ++++++------ tests/test_persistent_db.py | 27 +++++++++---------- tests/test_persistent_pg.py | 27 +++++++++---------- tests/test_pooled_db.py | 9 +++---- tests/test_pooled_pg.py | 5 +--- tests/test_simple_pooled_db.py | 5 +--- tests/test_simple_pooled_pg.py | 5 +--- tests/test_steady_db.py | 16 ++++++------ tests/test_steady_pg.py | 12 ++++----- tox.ini | 2 +- 28 files changed, 131 insertions(+), 172 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 9379a28..277da9f 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.0.3 +current_version = 3.0.0 [bumpversion:file:setup.py] search = __version__ = '{current_version}' diff --git a/.github/workflows/publish_on_pypi.yml b/.github/workflows/publish_on_pypi.yml index c9a7589..9d887fa 100644 --- a/.github/workflows/publish_on_pypi.yml +++ b/.github/workflows/publish_on_pypi.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ['3.9', '2.7'] + python: ['3.9'] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/test_with_tox.yml b/.github/workflows/test_with_tox.yml index 904f2db..62246b9 100644 --- a/.github/workflows/test_with_tox.yml +++ b/.github/workflows/test_with_tox.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ['2.7', '3.6', '3.7', '3.8', '3.9', '3.10'] + python: ['3.6', '3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index efb2289..41b2119 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ to a database that can be used in all kinds of multi-threaded environments. The suite supports DB-API 2 compliant database interfaces and the classic PyGreSQL interface. -The current version 2.0.3 of DBUtils supports Python versions 2.7 and 3.5 to 3.10. +The current version 3.0.0 of DBUtils supports Python versions 3.6 to 3.10. -**Please have a look at the [changelog](https://webwareforpython.github.io/DBUtils/changelog.html), because there are some breaking changes in version 2.0.** +**Please have a look at the [changelog](https://webwareforpython.github.io/DBUtils/changelog.html), because there were some breaking changes in version 2.0.** The DBUtils home page can be found at https://webwareforpython.github.io/DBUtils/ diff --git a/dbutils/__init__.py b/dbutils/__init__.py index 4ae3298..19a4627 100644 --- a/dbutils/__init__.py +++ b/dbutils/__init__.py @@ -5,4 +5,4 @@ 'simple_pooled_pg', 'steady_pg', 'pooled_pg', 'persistent_pg', 'simple_pooled_db', 'steady_db', 'pooled_db', 'persistent_db'] -__version__ = '2.0.3' +__version__ = '3.0.0' diff --git a/dbutils/pooled_db.py b/dbutils/pooled_db.py index a442bdb..3108131 100644 --- a/dbutils/pooled_db.py +++ b/dbutils/pooled_db.py @@ -422,8 +422,7 @@ def __getattr__(self, name): """Proxy all members of the class.""" if self._con: return getattr(self._con, name) - else: - raise InvalidConnection + raise InvalidConnection def __del__(self): """Delete the pooled connection.""" @@ -455,14 +454,12 @@ def __init__(self, con): def __lt__(self, other): if self.con._transaction == other.con._transaction: return self.shared < other.shared - else: - return not self.con._transaction + return not self.con._transaction def __le__(self, other): if self.con._transaction == other.con._transaction: return self.shared <= other.shared - else: - return not self.con._transaction + return not self.con._transaction def __eq__(self, other): return (self.con._transaction == other.con._transaction @@ -517,8 +514,7 @@ def __getattr__(self, name): """Proxy all members of the class.""" if self._con: return getattr(self._con, name) - else: - raise InvalidConnection + raise InvalidConnection def __del__(self): """Delete the pooled connection.""" diff --git a/dbutils/pooled_pg.py b/dbutils/pooled_pg.py index 86c379d..f9776f7 100644 --- a/dbutils/pooled_pg.py +++ b/dbutils/pooled_pg.py @@ -112,10 +112,7 @@ Licensed under the MIT license. """ -try: - from Queue import Queue, Empty, Full -except ImportError: # Python 3 - from queue import Queue, Empty, Full +from queue import Queue, Empty, Full from . import __version__ from .steady_pg import SteadyPgConnection @@ -289,8 +286,7 @@ def __getattr__(self, name): """Proxy all members of the class.""" if self._con: return getattr(self._con, name) - else: - raise InvalidConnection + raise InvalidConnection def __del__(self): """Delete the pooled connection.""" diff --git a/dbutils/simple_pooled_db.py b/dbutils/simple_pooled_db.py index b66cf7f..606e188 100644 --- a/dbutils/simple_pooled_db.py +++ b/dbutils/simple_pooled_db.py @@ -134,14 +134,11 @@ def __init__(self, dbapi, maxconnections, *args, **kwargs): if threadsafety == 0: raise NotSupportedError( "Database module does not support any level of threading.") - elif threadsafety == 1: + if threadsafety == 1: # If there is no connection level safety, build # the pool using the synchronized queue class # that implements all the required locking semantics. - try: - from Queue import Queue - except ImportError: # Python 3 - from queue import Queue + from queue import Queue self._queue = Queue(maxconnections) # create the queue self.connection = self._unthreadsafe_get_connection self.addConnection = self._unthreadsafe_add_connection @@ -197,12 +194,12 @@ def _unthreadsafe_return_connection(self, con): def _threadsafe_get_connection(self): """Get a connection from the pool.""" with self._lock: - next = self._nextConnection - con = PooledDBConnection(self, self._connections[next]) - next += 1 - if next >= len(self._connections): - next = 0 - self._nextConnection = next + next_con = self._nextConnection + con = PooledDBConnection(self, self._connections[next_con]) + next_con += 1 + if next_con >= len(self._connections): + next_con = 0 + self._nextConnection = next_con return con def _threadsafe_add_connection(self, con): diff --git a/dbutils/simple_pooled_pg.py b/dbutils/simple_pooled_pg.py index 844fdd0..b18ad7b 100644 --- a/dbutils/simple_pooled_pg.py +++ b/dbutils/simple_pooled_pg.py @@ -118,10 +118,7 @@ def __init__(self, maxconnections, *args, **kwargs): # Since there is no connection level safety, we # build the pool using the synchronized queue class # that implements all the required locking semantics. - try: - from Queue import Queue - except ImportError: # Python 3 - from queue import Queue + from queue import Queue self._queue = Queue(maxconnections) # Establish all database connections (it would be better to # only establish a part of them now, and the rest on demand). diff --git a/dbutils/steady_db.py b/dbutils/steady_db.py index e5a6b03..a744841 100644 --- a/dbutils/steady_db.py +++ b/dbutils/steady_db.py @@ -93,11 +93,6 @@ from . import __version__ -try: - baseint = (int, long) -except NameError: # Python 3 - baseint = int - class SteadyDBError(Exception): """General SteadyDB error.""" @@ -173,10 +168,10 @@ def __init__( except AttributeError: self._threadsafety = None if not callable(self._creator): - raise TypeError("%r is not a connection provider." % (creator,)) + raise TypeError(f"{creator!r} is not a connection provider.") if maxusage is None: maxusage = 0 - if not isinstance(maxusage, baseint): + if not isinstance(maxusage, int): raise TypeError("'maxusage' must be an integer value.") self._maxusage = maxusage self._setsession_sql = setsession @@ -531,7 +526,7 @@ def __init__(self, con, *args, **kwargs): try: self._cursor = con._cursor(*args, **kwargs) except AttributeError: - raise TypeError("%r is not a SteadyDBConnection." % (con,)) + raise TypeError(f"{con!r} is not a SteadyDBConnection.") self._closed = False def __enter__(self): @@ -688,10 +683,8 @@ def __getattr__(self, name): if name.startswith(('execute', 'call')): # make execution methods "tough" return self._get_tough_method(name) - else: - return getattr(self._cursor, name) - else: - raise InvalidCursor + return getattr(self._cursor, name) + raise InvalidCursor def __del__(self): """Delete the steady cursor.""" diff --git a/dbutils/steady_pg.py b/dbutils/steady_pg.py index 3a500ee..c1a6b71 100644 --- a/dbutils/steady_pg.py +++ b/dbutils/steady_pg.py @@ -73,11 +73,6 @@ from . import __version__ -try: - baseint = (int, long) -except NameError: # Python 3 - baseint = int - class SteadyPgError(Exception): """General SteadyPg error.""" @@ -122,7 +117,7 @@ def __init__( # proper initialization of the connection if maxusage is None: maxusage = 0 - if not isinstance(maxusage, baseint): + if not isinstance(maxusage, int): raise TypeError("'maxusage' must be an integer value.") self._maxusage = maxusage self._setsession_sql = setsession @@ -229,10 +224,7 @@ def begin(self, sql=None): return self._con.query(sql or 'begin') else: # use existing method if available - if sql: - return begin(sql=sql) - else: - return begin() + return begin(sql=sql) if sql else begin() def end(self, sql=None): """Commit the current transaction.""" @@ -242,10 +234,7 @@ def end(self, sql=None): except AttributeError: return self._con.query(sql or 'end') else: - if sql: - return end(sql=sql) - else: - return end() + return end(sql=sql) if sql else end() def commit(self, sql=None): """Commit the current transaction.""" @@ -255,10 +244,7 @@ def commit(self, sql=None): except AttributeError: return self._con.query(sql or 'commit') else: - if sql: - return commit(sql=sql) - else: - return commit() + return commit(sql=sql) if sql else commit() def rollback(self, sql=None): """Rollback the current transaction.""" @@ -268,10 +254,7 @@ def rollback(self, sql=None): except AttributeError: return self._con.query(sql or 'rollback') else: - if sql: - return rollback(sql=sql) - else: - return rollback() + return rollback(sql=sql) if sql else rollback() def _get_tough_method(self, method): """Return a "tough" version of a connection class method. @@ -297,11 +280,10 @@ def tough_method(*args, **kwargs): if transaction: # inside a transaction self._transaction = False raise # propagate the error - elif self._con.db.status: # if it was not a connection problem + if self._con.db.status: # if it was not a connection problem raise # then propagate the error - else: # otherwise - self.reset() # reset the connection - result = method(*args, **kwargs) # and try one more time + self.reset() # reset the connection + result = method(*args, **kwargs) # and try one more time self._usage += 1 return result return tough_method @@ -317,8 +299,7 @@ def __getattr__(self, name): or name.startswith('get_')): attr = self._get_tough_method(attr) return attr - else: - raise InvalidConnection + raise InvalidConnection def __del__(self): """Delete the steady connection.""" diff --git a/docs/changelog.html b/docs/changelog.html index 879ab73..ea10c93 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -12,6 +12,15 @@ <h1 class="title">Changelog for DBUtils</h1> <section id="section-1"> +<h2>3.0.0</h2> +<p>DBUtils 2.0.3 was released on November 26, 2021.</p> +<p>It is intended to be used with Python versions 3.6 to 3.10.</p> +<p>Changes:</p> +<ul class="simple"> +<li><p>Cease support for Python 2 and 3.5, minor optimizations.</p></li> +</ul> +</section> +<section id="section-2"> <h2>2.0.3</h2> <p>DBUtils 2.0.3 was released on November 26, 2021.</p> <p>Changes:</p> @@ -19,7 +28,7 @@ <h2>2.0.3</h2> <li><p>Support Python version 3.10.</p></li> </ul> </section> -<section id="section-2"> +<section id="section-3"> <h2>2.0.2</h2> <p>DBUtils 2.0.2 was released on June 8, 2021.</p> <p>Changes:</p> @@ -27,7 +36,7 @@ <h2>2.0.2</h2> <li><p>Allow using context managers for pooled connections.</p></li> </ul> </section> -<section id="section-3"> +<section id="section-4"> <h2>2.0.1</h2> <p>DBUtils 2.0.1 was released on April 8, 2021.</p> <p>Changes:</p> @@ -35,7 +44,7 @@ <h2>2.0.1</h2> <li><p>Avoid "name Exception is not defined" when exiting.</p></li> </ul> </section> -<section id="section-4"> +<section id="section-5"> <h2>2.0</h2> <p>DBUtils 2.0 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> @@ -51,7 +60,7 @@ <h2>2.0</h2> <li><p>This changelog has been compiled from the former release notes.</p></li> </ul> </section> -<section id="section-5"> +<section id="section-6"> <h2>1.4</h2> <p>DBUtils 1.4 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> @@ -62,7 +71,7 @@ <h2>1.4</h2> inside a transaction.</p></li> </ul> </section> -<section id="section-6"> +<section id="section-7"> <h2>1.3</h2> <p>DBUtils 1.3 was released on March 3, 2018.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.4 to 3.7.</p> @@ -71,12 +80,12 @@ <h2>1.3</h2> <li><p>This version now supports context handlers for connections and cursors.</p></li> </ul> </section> -<section id="section-7"> +<section id="section-8"> <h2>1.2</h2> <p>DBUtils 1.2 was released on February 5, 2017.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.0 to 3.6.</p> </section> -<section id="section-8"> +<section id="section-9"> <h2>1.1.1</h2> <p>DBUtils 1.1.1 was released on February 4, 2017.</p> <p>It is intended to be used with Python versions 2.3 to 2.7.</p> @@ -90,7 +99,7 @@ <h2>1.1.1</h2> <li><p>Fixed a problem when running under Jython (reported by Vitaly Kruglikov).</p></li> </ul> </section> -<section id="section-9"> +<section id="section-10"> <h2>1.1</h2> <p>DBUtils 1.1 was released on August 14, 2011.</p> <p>Improvements:</p> @@ -119,7 +128,7 @@ <h2>1.1</h2> <li><p>Fixed some minor issues with the <span class="docutils literal">DBUtilsExample</span> for Webware.</p></li> </ul> </section> -<section id="section-10"> +<section id="section-11"> <h2>1.0</h2> <p>DBUtils 1.0 was released on November 29, 2008.</p> <p>It is intended to be used with Python versions 2.2 to 2.6.</p> @@ -152,7 +161,7 @@ <h2>1.0</h2> the MySQLdb module (problem reported by Gregory Pinero).</p></li> </ul> </section> -<section id="section-11"> +<section id="section-12"> <h2>0.9.4</h2> <p>DBUtils 0.9.4 was released on July 7, 2007.</p> <p>This release fixes a problem in the destructor code and has been supplemented @@ -161,7 +170,7 @@ <h2>0.9.4</h2> in the last release, since you can now pass custom creator functions for database connections instead of DB-API 2 modules.</p> </section> -<section id="section-12"> +<section id="section-13"> <h2>0.9.3</h2> <p>DBUtils 0.9.3 was released on May 21, 2007.</p> <p>Changes:</p> @@ -176,7 +185,7 @@ <h2>0.9.3</h2> Added Chinese translation of the User's Guide, kindly contributed by gashero.</p></li> </ul> </section> -<section id="section-13"> +<section id="section-14"> <h2>0.9.2</h2> <p>DBUtils 0.9.2 was released on September 22, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.5.</p> @@ -186,7 +195,7 @@ <h2>0.9.2</h2> storage engine. Accordingly, renamed <span class="docutils literal">SolidPg</span> to <span class="docutils literal">SteadyPg</span>.</p></li> </ul> </section> -<section id="section-14"> +<section id="section-15"> <h2>0.9.1</h2> <p>DBUtils 0.9.1 was released on May 8, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.4.</p> @@ -200,7 +209,7 @@ <h2>0.9.1</h2> <li><p>Improved the documentation and added a User's Guide.</p></li> </ul> </section> -<section id="section-15"> +<section id="section-16"> <h2>0.8.1 - 2005-09-13</h2> <p>DBUtils 0.8.1 was released on September 13, 2005.</p> <p>It is intended to be used with Python versions 2.0 to 2.4.</p> diff --git a/docs/changelog.rst b/docs/changelog.rst index 5b0fd91..495c6e1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,17 @@ Changelog for DBUtils +++++++++++++++++++++ +3.0.0 +===== + +DBUtils 2.0.3 was released on November 26, 2021. + +It is intended to be used with Python versions 3.6 to 3.10. + +Changes: + +* Cease support for Python 2 and 3.5, minor optimizations. + 2.0.3 ===== @@ -10,7 +21,6 @@ Changes: * Support Python version 3.10. - 2.0.2 ===== @@ -20,7 +30,6 @@ Changes: * Allow using context managers for pooled connections. - 2.0.1 ===== diff --git a/docs/main.de.html b/docs/main.de.html index 5f72110..a285997 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -12,7 +12,7 @@ <h1 class="title">Benutzeranleitung für DBUtils</h1> <dl class="docinfo simple"> <dt class="version">Version<span class="colon">:</span></dt> -<dd class="version">2.0.2</dd> +<dd class="version">3.0.0</dd> <dt class="translations">Translations<span class="colon">:</span></dt> <dd class="translations"><p><a class="reference external" href="main.html">English</a> | German</p> </dd> @@ -136,7 +136,7 @@ <h3>Installation</h3> </section> <section id="anforderungen"> <h2>Anforderungen</h2> -<p>DBUtils unterstützt die <a class="reference external" href="https://www.python.org">Python</a> Versionen 2.7 und 3.5 bis 3.10.</p> +<p>DBUtils unterstützt die <a class="reference external" href="https://www.python.org">Python</a> Versionen 3.6 bis 3.10.</p> <p>Die Module in der Variante für klassisches PyGreSQL benötigen <a class="reference external" href="https://www.pygresql.org/">PyGreSQL</a> Version 4.0 oder höher, während die Module in der allgemeinen Variante für DB-API 2 mit jedem beliebigen Python-Datenbankadapter-Modul zusammenarbeiten, diff --git a/docs/main.de.rst b/docs/main.de.rst index 4184e76..ff23e5d 100644 --- a/docs/main.de.rst +++ b/docs/main.de.rst @@ -1,7 +1,7 @@ Benutzeranleitung für DBUtils +++++++++++++++++++++++++++++ -:Version: 2.0.2 +:Version: 3.0.0 :Translations: English_ | German .. _English: main.html @@ -98,7 +98,7 @@ herunterzuladen und zu installieren:: Anforderungen ============= -DBUtils unterstützt die Python_ Versionen 2.7 und 3.5 bis 3.10. +DBUtils unterstützt die Python_ Versionen 3.6 bis 3.10. Die Module in der Variante für klassisches PyGreSQL benötigen PyGreSQL_ Version 4.0 oder höher, während die Module in der allgemeinen Variante diff --git a/docs/main.html b/docs/main.html index 964126a..2516340 100644 --- a/docs/main.html +++ b/docs/main.html @@ -12,7 +12,7 @@ <h1 class="title">DBUtils User's Guide</h1> <dl class="docinfo simple"> <dt class="version">Version<span class="colon">:</span></dt> -<dd class="version">2.0.2</dd> +<dd class="version">3.0.0</dd> <dt class="translations">Translations<span class="colon">:</span></dt> <dd class="translations"><p>English | <a class="reference external" href="main.de.html">German</a></p> </dd> @@ -133,7 +133,7 @@ <h3>Installation</h3> </section> <section id="requirements"> <h2>Requirements</h2> -<p>DBUtils supports <a class="reference external" href="https://www.python.org">Python</a> version 2.7 and Python versions 3.5 to 3.10.</p> +<p>DBUtils supports <a class="reference external" href="https://www.python.org">Python</a> versions 3.6 to 3.10.</p> <p>The modules in the classic PyGreSQL variant need <a class="reference external" href="https://www.pygresql.org/">PyGreSQL</a> version 4.0 or above, while the modules in the universal DB-API 2 variant run with any Python <a class="reference external" href="https://www.python.org/dev/peps/pep-0249/">DB-API 2</a> compliant database interface module.</p> diff --git a/docs/main.rst b/docs/main.rst index de88b1e..d5efa8e 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -1,7 +1,7 @@ DBUtils User's Guide ++++++++++++++++++++ -:Version: 2.0.2 +:Version: 3.0.0 :Translations: English | German_ .. _German: main.de.html @@ -95,7 +95,7 @@ It is even easier to download and install the package in one go using `pip`_:: Requirements ============ -DBUtils supports Python_ version 2.7 and Python versions 3.5 to 3.10. +DBUtils supports Python_ versions 3.6 to 3.10. The modules in the classic PyGreSQL variant need PyGreSQL_ version 4.0 or above, while the modules in the universal DB-API 2 variant run with diff --git a/docs/make.py b/docs/make.py index c2a0494..d7b2cad 100755 --- a/docs/make.py +++ b/docs/make.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3.8 +#!/usr/bin/python3.9 """Build HMTL from reST files.""" diff --git a/setup.py b/setup.py index f377bdc..8fbcc11 100755 --- a/setup.py +++ b/setup.py @@ -8,12 +8,13 @@ from sys import version_info py_version = version_info[:2] -if py_version != (2, 7) and not (3, 5) <= py_version < (4, 0): - raise ImportError('Python %d.%d is not supported by DBUtils.' % py_version) +if not (3, 6) <= py_version < (4, 0): + raise ImportError( + 'Python {}.{} is not supported by DBUtils.'.format(*py_version)) warnings.filterwarnings('ignore', 'Unknown distribution option') -__version__ = '2.0.3' +__version__ = '3.0.0' readme = open('README.md').read() @@ -27,20 +28,18 @@ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Intended Audience :: Developers', + 'Topic :: Database', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Topic :: Software Development :: Libraries :: Python Modules', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', - 'Topic :: Database', - 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', - 'Topic :: Software Development :: Libraries :: Python Modules' + 'Operating System :: OS Independent', ], author='Christoph Zwerschke', author_email='cito@online.de', diff --git a/tests/test_persistent_db.py b/tests/test_persistent_db.py index ed5bce6..6f27108 100644 --- a/tests/test_persistent_db.py +++ b/tests/test_persistent_db.py @@ -63,10 +63,7 @@ def test_connection(self): def test_threads(self): num_threads = 3 persist = PersistentDB(dbapi, closeable=True) - try: - from queue import Queue, Empty - except ImportError: # Python 2 - from Queue import Queue, Empty + from queue import Queue, Empty query_queue, result_queue = [], [] for i in range(num_threads): query_queue.append(Queue(1)) @@ -98,7 +95,7 @@ def run_queries(i): cursor.execute(q) r = cursor.fetchone() cursor.close() - r = '%d(%d): %s' % (i, db._usage, r) + r = f'{i}({db._usage}): {r}' try: result_queue[i].put(r, 1, 1) except TypeError: @@ -121,17 +118,17 @@ def run_queries(i): r = result_queue[i].get(1, 1) except TypeError: r = result_queue[i].get(1) - self.assertEqual(r, '%d(0): ok - thread alive' % i) + self.assertEqual(r, f'{i}(0): ok - thread alive') self.assertTrue(threads[i].is_alive()) for i in range(num_threads): for j in range(i + 1): try: - query_queue[i].put('select test%d' % j, 1, 1) + query_queue[i].put(f'select test{j}', 1, 1) r = result_queue[i].get(1, 1) except TypeError: - query_queue[i].put('select test%d' % j, 1) + query_queue[i].put(f'select test{j}', 1) r = result_queue[i].get(1) - self.assertEqual(r, '%d(%d): test%d' % (i, j + 1, j)) + self.assertEqual(r, f'{i}({j + 1}): test{j}') try: query_queue[1].put('select test4', 1, 1) except TypeError: @@ -150,12 +147,12 @@ def run_queries(i): self.assertEqual(r, '1(3): ok - connection closed') for j in range(2): try: - query_queue[1].put('select test%d' % j, 1, 1) + query_queue[1].put(f'select test{j}', 1, 1) r = result_queue[1].get(1, 1) except TypeError: - query_queue[1].put('select test%d' % j, 1) + query_queue[1].put(f'select test{j}', 1) r = result_queue[1].get(1) - self.assertEqual(r, '1(%d): test%d' % (j + 1, j)) + self.assertEqual(r, f'1({j + 1}): test{j}') for i in range(num_threads): self.assertTrue(threads[i].is_alive()) try: @@ -167,7 +164,7 @@ def run_queries(i): r = result_queue[i].get(1, 1) except TypeError: r = result_queue[i].get(1) - self.assertEqual(r, '%d(%d): ok - thread alive' % (i, i + 1)) + self.assertEqual(r, f'{i}({i + 1}): ok - thread alive') self.assertTrue(threads[i].is_alive()) for i in range(num_threads): try: @@ -181,10 +178,10 @@ def test_maxusage(self): self.assertEqual(db._maxusage, 20) for i in range(100): cursor = db.cursor() - cursor.execute('select test%d' % i) + cursor.execute(f'select test{i}') r = cursor.fetchone() cursor.close() - self.assertEqual(r, 'test%d' % i) + self.assertEqual(r, f'test{i}') self.assertTrue(db._con.valid) j = i % 20 + 1 self.assertEqual(db._usage, j) diff --git a/tests/test_persistent_pg.py b/tests/test_persistent_pg.py index 78c85ca..3c8ce83 100644 --- a/tests/test_persistent_pg.py +++ b/tests/test_persistent_pg.py @@ -43,10 +43,7 @@ def test_close(self): def test_threads(self): num_threads = 3 persist = PersistentPg() - try: - from queue import Queue, Empty - except ImportError: # Python 2 - from Queue import Queue, Empty + from queue import Queue, Empty query_queue, result_queue = [], [] for i in range(num_threads): query_queue.append(Queue(1)) @@ -75,7 +72,7 @@ def run_queries(i): r = 'ok - connection closed' else: r = db.query(q) - r = '%d(%d): %s' % (i, db._usage, r) + r = f'{i}({db._usage}): {r}' try: result_queue[i].put(r, 1, 1) except TypeError: @@ -98,17 +95,17 @@ def run_queries(i): r = result_queue[i].get(1, 1) except TypeError: r = result_queue[i].get(1) - self.assertEqual(r, '%d(0): ok - thread alive' % i) + self.assertEqual(r, f'{i}(0): ok - thread alive') self.assertTrue(threads[i].is_alive()) for i in range(num_threads): for j in range(i + 1): try: - query_queue[i].put('select test%d' % j, 1, 1) + query_queue[i].put(f'select test{j}', 1, 1) r = result_queue[i].get(1, 1) except TypeError: - query_queue[i].put('select test%d' % j, 1) + query_queue[i].put(f'select test{j}', 1) r = result_queue[i].get(1) - self.assertEqual(r, '%d(%d): test%d' % (i, j + 1, j)) + self.assertEqual(r, f'{i}({j + 1}): test{j}') try: query_queue[1].put('select test4', 1, 1) r = result_queue[1].get(1, 1) @@ -125,12 +122,12 @@ def run_queries(i): self.assertEqual(r, '1(3): ok - connection closed') for j in range(2): try: - query_queue[1].put('select test%d' % j, 1, 1) + query_queue[1].put(f'select test{j}', 1, 1) r = result_queue[1].get(1, 1) except TypeError: - query_queue[1].put('select test%d' % j, 1) + query_queue[1].put(f'select test{j}', 1) r = result_queue[1].get(1) - self.assertEqual(r, '1(%d): test%d' % (j + 1, j)) + self.assertEqual(r, f'1({j + 1}): test{j}') for i in range(num_threads): self.assertTrue(threads[i].is_alive()) try: @@ -142,7 +139,7 @@ def run_queries(i): r = result_queue[i].get(1, 1) except TypeError: r = result_queue[i].get(1) - self.assertEqual(r, '%d(%d): ok - thread alive' % (i, i + 1)) + self.assertEqual(r, f'{i}({i + 1}): ok - thread alive') self.assertTrue(threads[i].is_alive()) for i in range(num_threads): try: @@ -155,8 +152,8 @@ def test_maxusage(self): db = persist.connection() self.assertEqual(db._maxusage, 20) for i in range(100): - r = db.query('select test%d' % i) - self.assertEqual(r, 'test%d' % i) + r = db.query(f'select test{i}') + self.assertEqual(r, f'test{i}') self.assertTrue(db.db.status) j = i % 20 + 1 self.assertEqual(db._usage, j) diff --git a/tests/test_pooled_db.py b/tests/test_pooled_db.py index 80a8b45..3beb2d4 100644 --- a/tests/test_pooled_db.py +++ b/tests/test_pooled_db.py @@ -868,9 +868,9 @@ def test_maxusage(self): for i in range(20): cursor = db.cursor() self.assertEqual(db._con._con.open_cursors, 1) - cursor.execute('select test%i' % i) + cursor.execute(f'select test{i}') r = cursor.fetchone() - self.assertEqual(r, 'test%i' % i) + self.assertEqual(r, f'test{i}') cursor.close() self.assertEqual(db._con._con.open_cursors, 0) if maxusage: @@ -976,10 +976,7 @@ def test_tnree_threads_two_connections(self): for threadsafety in (1, 2): dbapi.threadsafety = threadsafety pool = PooledDB(dbapi, 2, 2, 0, 2, True) - try: - from queue import Queue, Empty - except ImportError: # Python 2 - from Queue import Queue, Empty + from queue import Queue, Empty queue = Queue(3) def connection(): diff --git a/tests/test_pooled_pg.py b/tests/test_pooled_pg.py index bf0e314..ec088a5 100644 --- a/tests/test_pooled_pg.py +++ b/tests/test_pooled_pg.py @@ -225,10 +225,7 @@ def test_one_thread_two_connections(self): def test_three_threads_two_connections(self): pool = PooledPg(2, 2, 2, True) - try: - from queue import Queue, Empty - except ImportError: # Python 2 - from Queue import Queue, Empty + from queue import Queue, Empty queue = Queue(3) def connection(): diff --git a/tests/test_simple_pooled_db.py b/tests/test_simple_pooled_db.py index 564fda3..4cf82ae 100644 --- a/tests/test_simple_pooled_db.py +++ b/tests/test_simple_pooled_db.py @@ -103,10 +103,7 @@ def test_two_connections(self): def test_threadsafety_1(self): db_pool = self.my_db_pool(1, 2) - try: - from queue import Queue, Empty - except ImportError: # Python 2 - from Queue import Queue, Empty + from queue import Queue, Empty queue = Queue(3) def connection(): diff --git a/tests/test_simple_pooled_pg.py b/tests/test_simple_pooled_pg.py index 7065d58..d3026cc 100644 --- a/tests/test_simple_pooled_pg.py +++ b/tests/test_simple_pooled_pg.py @@ -83,10 +83,7 @@ def test_two_connections(self): def test_threads(self): db_pool = self.my_db_pool(2) - try: - from queue import Queue, Empty - except ImportError: # Python 2 - from Queue import Queue, Empty + from queue import Queue, Empty queue = Queue(3) def connection(): diff --git a/tests/test_steady_db.py b/tests/test_steady_db.py index e32a711..d1a2628 100644 --- a/tests/test_steady_db.py +++ b/tests/test_steady_db.py @@ -64,8 +64,8 @@ def test_mocked_connection(self): for i in range(3): self.assertEqual(db.num_uses, i) self.assertEqual(db.num_queries, i) - cursor.execute('select test%d' % i) - self.assertEqual(cursor.fetchone(), 'test%d' % i) + cursor.execute(f'select test{i}') + self.assertEqual(cursor.fetchone(), f'test{i}') self.assertTrue(cursor.valid) self.assertEqual(db.open_cursors, 1) for i in range(4): @@ -173,8 +173,8 @@ def test_connection(self): self.assertEqual(db._usage, i) self.assertEqual(db._con.num_uses, i) self.assertEqual(db._con.num_queries, i) - cursor.execute('select test%d' % i) - self.assertEqual(cursor.fetchone(), 'test%d' % i) + cursor.execute(f'select test{i}') + self.assertEqual(cursor.fetchone(), f'test{i}') self.assertTrue(cursor.valid) self.assertEqual(db._con.open_cursors, 1) for i in range(4): @@ -292,9 +292,9 @@ def test_connection_maxusage(self): db = SteadyDBconnect(dbapi, 10) cursor = db.cursor() for i in range(100): - cursor.execute('select test%d' % i) + cursor.execute(f'select test{i}') r = cursor.fetchone() - self.assertEqual(r, 'test%d' % i) + self.assertEqual(r, f'test{i}') self.assertTrue(db._con.valid) j = i % 10 + 1 self.assertEqual(db._usage, j) @@ -315,9 +315,9 @@ def test_connection_maxusage(self): for i in range(10): if i == 7: db._con.valid = cursor._cursor.valid = False - cursor.execute('select test%d' % i) + cursor.execute(f'select test{i}') r = cursor.fetchone() - self.assertEqual(r, 'test%d' % i) + self.assertEqual(r, f'test{i}') j = i % 7 + 1 self.assertEqual(db._usage, j) self.assertEqual(db._con.num_uses, j) diff --git a/tests/test_steady_pg.py b/tests/test_steady_pg.py index f798f43..2b8a1ad 100644 --- a/tests/test_steady_pg.py +++ b/tests/test_steady_pg.py @@ -58,7 +58,7 @@ def test_mocked_connection(self): for i in range(3): self.assertEqual(db.num_queries, i) self.assertEqual( - db.query('select test%d' % i), 'test%d' % i) + db.query(f'select test{i}'), f'test{i}') self.assertTrue(db.db.status) db.reopen() self.assertTrue(db.db.status) @@ -135,7 +135,7 @@ def test_connection(self): self.assertEqual(db._usage, i) self.assertEqual(db.num_queries, i) self.assertEqual( - db.query('select test%d' % i), 'test%d' % i) + db.query(f'select test{i}'), f'test{i}') self.assertTrue(db.db.status) self.assertEqual(db.get_tables(), 'test') self.assertTrue(db.db.status) @@ -200,8 +200,8 @@ def test_connection_context_handler(self): def test_connection_maxusage(self): db = SteadyPgConnection(10) for i in range(100): - r = db.query('select test%d' % i) - self.assertEqual(r, 'test%d' % i) + r = db.query(f'select test{i}') + self.assertEqual(r, f'test{i}') self.assertTrue(db.db.status) j = i % 10 + 1 self.assertEqual(db._usage, j) @@ -220,8 +220,8 @@ def test_connection_maxusage(self): for i in range(10): if i == 7: db.db.status = False - r = db.query('select test%d' % i) - self.assertEqual(r, 'test%d' % i) + r = db.query(f'select test{i}') + self.assertEqual(r, f'test{i}') j = i % 7 + 1 self.assertEqual(db._usage, j) self.assertEqual(db.num_queries, j) diff --git a/tox.ini b/tox.ini index f8a8e33..404b6c5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py3{5,6,7,8,9,10}, flake8, manifest, docs, spell +envlist = py3{6,7,8,9,10}, flake8, manifest, docs, spell [testenv] setenv = From f818201267cf530c1a4b7a1b713143834ace2c23 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 27 Nov 2021 12:10:42 +0100 Subject: [PATCH 35/84] Fix version number in the changelog --- docs/changelog.html | 2 +- docs/changelog.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.html b/docs/changelog.html index ea10c93..5be9854 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -13,7 +13,7 @@ <h1 class="title">Changelog for DBUtils</h1> <section id="section-1"> <h2>3.0.0</h2> -<p>DBUtils 2.0.3 was released on November 26, 2021.</p> +<p>DBUtils 3.0.0 was released on November 26, 2021.</p> <p>It is intended to be used with Python versions 3.6 to 3.10.</p> <p>Changes:</p> <ul class="simple"> diff --git a/docs/changelog.rst b/docs/changelog.rst index 495c6e1..b8a0a5a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,7 @@ 3.0.0 ===== -DBUtils 2.0.3 was released on November 26, 2021. +DBUtils 3.0.0 was released on November 26, 2021. It is intended to be used with Python versions 3.6 to 3.10. From d94d869368696bf9118f85330d5d910b75e30675 Mon Sep 17 00:00:00 2001 From: qizidog <45590672+qizidog@users.noreply.github.com> Date: Sun, 5 Dec 2021 02:42:42 +0800 Subject: [PATCH 36/84] Fix a tutorial bug (#35) db.cursor is a method, not a property. --- docs/main.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/main.html b/docs/main.html index 2516340..f0109a1 100644 --- a/docs/main.html +++ b/docs/main.html @@ -384,7 +384,7 @@ <h3>PooledDB (pooled_db)</h3> db.close() # or del db</pre> <p>You can also use context managers for simpler code:</p> <pre class="literal-block">with pool.connection() as db: - with db.cursor as cur: + with db.cursor() as cur: cur.execute(...) res = cur.fetchone()</pre> <p>Note that you need to explicitly start transactions by calling the From 07b340d656eb0d68a186f70cf4e6711329f8a7e4 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 4 Dec 2021 19:48:20 +0100 Subject: [PATCH 37/84] Fix source and German version of tutorial (#35) --- docs/main.de.html | 2 +- docs/main.de.rst | 2 +- docs/main.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/main.de.html b/docs/main.de.html index a285997..2df8d6d 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -422,7 +422,7 @@ <h3>PooledDB (pooled_db)</h3> db.close() # oder del db</pre> <p>Sie können dies auch durch Verwendung von Kontext-Managern vereinfachen:</p> <pre class="literal-block">with pool.connection() as db: - with db.cursor as cur: + with db.cursor() as cur: cur.execute(...) res = cur.fetchone()</pre> <p>Bitte beachten Sie, dass Transaktionen explizit durch Aufruf der Methode diff --git a/docs/main.de.rst b/docs/main.de.rst index ff23e5d..0c7077a 100644 --- a/docs/main.de.rst +++ b/docs/main.de.rst @@ -446,7 +446,7 @@ sie gebraucht werden, etwa so:: Sie können dies auch durch Verwendung von Kontext-Managern vereinfachen:: with pool.connection() as db: - with db.cursor as cur: + with db.cursor() as cur: cur.execute(...) res = cur.fetchone() diff --git a/docs/main.rst b/docs/main.rst index d5efa8e..81c6427 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -408,7 +408,7 @@ object stays alive as long as you are using it, like that:: You can also use context managers for simpler code:: with pool.connection() as db: - with db.cursor as cur: + with db.cursor() as cur: cur.execute(...) res = cur.fetchone() From c5f639a35c2f3c62e80c9463c06f9550ede569ad Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Wed, 22 Dec 2021 17:33:57 +0100 Subject: [PATCH 38/84] Add InterfaceError to the default failures list (#36) --- dbutils/persistent_db.py | 6 ++++-- dbutils/pooled_db.py | 6 ++++-- dbutils/steady_db.py | 9 +++++++-- docs/changelog.html | 37 ++++++++++++++++++++++--------------- docs/changelog.rst | 10 ++++++++++ docs/main.de.html | 10 ++++++---- docs/main.de.rst | 10 ++++++---- docs/main.html | 6 ++++-- docs/main.rst | 6 ++++-- tests/mock_db.py | 4 ++++ tests/test_steady_db.py | 8 ++++++-- 11 files changed, 77 insertions(+), 35 deletions(-) diff --git a/dbutils/persistent_db.py b/dbutils/persistent_db.py index 8ebf43d..6873905 100644 --- a/dbutils/persistent_db.py +++ b/dbutils/persistent_db.py @@ -42,7 +42,8 @@ prepare the session, e.g. ["set datestyle to german", ...]. failures: an optional exception class or a tuple of exception classes for which the connection failover mechanism shall be applied, - if the default (OperationalError, InternalError) is not adequate + if the default (OperationalError, InterfaceError, InternalError) + is not adequate for the used database module ping: an optional flag controlling when connections are checked with the ping() method if such a method is available (0 = None = never, 1 = default = whenever it is requested, @@ -155,7 +156,8 @@ def __init__( the session, e.g. ["set datestyle to ...", "set time zone ..."] failures: an optional exception class or a tuple of exception classes for which the connection failover mechanism shall be applied, - if the default (OperationalError, InternalError) is not adequate + if the default (OperationalError, InterfaceError, InternalError) + is not adequate for the used database module ping: determines when the connection should be checked with ping() (0 = None = never, 1 = default = whenever it is requested, 2 = when a cursor is created, 4 = when a query is executed, diff --git a/dbutils/pooled_db.py b/dbutils/pooled_db.py index 3108131..1b47f2b 100644 --- a/dbutils/pooled_db.py +++ b/dbutils/pooled_db.py @@ -57,7 +57,8 @@ the default value True always issues a rollback for safety's sake) failures: an optional exception class or a tuple of exception classes for which the connection failover mechanism shall be applied, - if the default (OperationalError, InternalError) is not adequate + if the default (OperationalError, InterfaceError, InternalError) + is not adequate for the used database module ping: an optional flag controlling when connections are checked with the ping() method if such a method is available (0 = None = never, 1 = default = whenever fetched from the pool, @@ -210,7 +211,8 @@ def __init__( True to always issue a rollback for safety's sake) failures: an optional exception class or a tuple of exception classes for which the connection failover mechanism shall be applied, - if the default (OperationalError, InternalError) is not adequate + if the default (OperationalError, InterfaceError, InternalError) + is not adequate for the used database module ping: determines when the connection should be checked with ping() (0 = None = never, 1 = default = whenever fetched from the pool, 2 = when a cursor is created, 4 = when a query is executed, diff --git a/dbutils/steady_db.py b/dbutils/steady_db.py index a744841..8df2acb 100644 --- a/dbutils/steady_db.py +++ b/dbutils/steady_db.py @@ -117,7 +117,8 @@ def connect( the session, e.g. ["set datestyle to german", "set time zone mez"] failures: an optional exception class or a tuple of exception classes for which the failover mechanism shall be applied, if the default - (OperationalError, InternalError) is not adequate + (OperationalError, InternalError, Interface) is not adequate + for the used database module ping: determines when the connection should be checked with ping() (0 = None = never, 1 = default = when _ping_check() is called, 2 = whenever a cursor is created, 4 = when a query is executed, @@ -258,16 +259,20 @@ def _create(self): try: self._failures = ( self._dbapi.OperationalError, + self._dbapi.InterfaceError, self._dbapi.InternalError) except AttributeError: try: self._failures = ( self._creator.OperationalError, + self._creator.InterfaceError, self._creator.InternalError) except AttributeError: try: self._failures = ( - con.OperationalError, con.InternalError) + con.OperationalError, + con.InterfaceError, + con.InternalError) except AttributeError: raise AttributeError( "Could not determine failure exceptions" diff --git a/docs/changelog.html b/docs/changelog.html index 5be9854..13eda8b 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -12,6 +12,13 @@ <h1 class="title">Changelog for DBUtils</h1> <section id="section-1"> +<h2>3.0.1</h2> +<p>DBUtils 3.0.1 was released on December 22, 2021.</p> +<p>It includes <span class="docutils literal">InterfaceError</span> to the default list of exceptions +for which the connection failover mechanism is applied. +You can override this with the <span class="docutils literal">failures</span> parameter.</p> +</section> +<section id="section-2"> <h2>3.0.0</h2> <p>DBUtils 3.0.0 was released on November 26, 2021.</p> <p>It is intended to be used with Python versions 3.6 to 3.10.</p> @@ -20,7 +27,7 @@ <h2>3.0.0</h2> <li><p>Cease support for Python 2 and 3.5, minor optimizations.</p></li> </ul> </section> -<section id="section-2"> +<section id="section-3"> <h2>2.0.3</h2> <p>DBUtils 2.0.3 was released on November 26, 2021.</p> <p>Changes:</p> @@ -28,7 +35,7 @@ <h2>2.0.3</h2> <li><p>Support Python version 3.10.</p></li> </ul> </section> -<section id="section-3"> +<section id="section-4"> <h2>2.0.2</h2> <p>DBUtils 2.0.2 was released on June 8, 2021.</p> <p>Changes:</p> @@ -36,7 +43,7 @@ <h2>2.0.2</h2> <li><p>Allow using context managers for pooled connections.</p></li> </ul> </section> -<section id="section-4"> +<section id="section-5"> <h2>2.0.1</h2> <p>DBUtils 2.0.1 was released on April 8, 2021.</p> <p>Changes:</p> @@ -44,7 +51,7 @@ <h2>2.0.1</h2> <li><p>Avoid "name Exception is not defined" when exiting.</p></li> </ul> </section> -<section id="section-5"> +<section id="section-6"> <h2>2.0</h2> <p>DBUtils 2.0 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> @@ -60,7 +67,7 @@ <h2>2.0</h2> <li><p>This changelog has been compiled from the former release notes.</p></li> </ul> </section> -<section id="section-6"> +<section id="section-7"> <h2>1.4</h2> <p>DBUtils 1.4 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> @@ -71,7 +78,7 @@ <h2>1.4</h2> inside a transaction.</p></li> </ul> </section> -<section id="section-7"> +<section id="section-8"> <h2>1.3</h2> <p>DBUtils 1.3 was released on March 3, 2018.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.4 to 3.7.</p> @@ -80,12 +87,12 @@ <h2>1.3</h2> <li><p>This version now supports context handlers for connections and cursors.</p></li> </ul> </section> -<section id="section-8"> +<section id="section-9"> <h2>1.2</h2> <p>DBUtils 1.2 was released on February 5, 2017.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.0 to 3.6.</p> </section> -<section id="section-9"> +<section id="section-10"> <h2>1.1.1</h2> <p>DBUtils 1.1.1 was released on February 4, 2017.</p> <p>It is intended to be used with Python versions 2.3 to 2.7.</p> @@ -99,7 +106,7 @@ <h2>1.1.1</h2> <li><p>Fixed a problem when running under Jython (reported by Vitaly Kruglikov).</p></li> </ul> </section> -<section id="section-10"> +<section id="section-11"> <h2>1.1</h2> <p>DBUtils 1.1 was released on August 14, 2011.</p> <p>Improvements:</p> @@ -128,7 +135,7 @@ <h2>1.1</h2> <li><p>Fixed some minor issues with the <span class="docutils literal">DBUtilsExample</span> for Webware.</p></li> </ul> </section> -<section id="section-11"> +<section id="section-12"> <h2>1.0</h2> <p>DBUtils 1.0 was released on November 29, 2008.</p> <p>It is intended to be used with Python versions 2.2 to 2.6.</p> @@ -161,7 +168,7 @@ <h2>1.0</h2> the MySQLdb module (problem reported by Gregory Pinero).</p></li> </ul> </section> -<section id="section-12"> +<section id="section-13"> <h2>0.9.4</h2> <p>DBUtils 0.9.4 was released on July 7, 2007.</p> <p>This release fixes a problem in the destructor code and has been supplemented @@ -170,7 +177,7 @@ <h2>0.9.4</h2> in the last release, since you can now pass custom creator functions for database connections instead of DB-API 2 modules.</p> </section> -<section id="section-13"> +<section id="section-14"> <h2>0.9.3</h2> <p>DBUtils 0.9.3 was released on May 21, 2007.</p> <p>Changes:</p> @@ -185,7 +192,7 @@ <h2>0.9.3</h2> Added Chinese translation of the User's Guide, kindly contributed by gashero.</p></li> </ul> </section> -<section id="section-14"> +<section id="section-15"> <h2>0.9.2</h2> <p>DBUtils 0.9.2 was released on September 22, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.5.</p> @@ -195,7 +202,7 @@ <h2>0.9.2</h2> storage engine. Accordingly, renamed <span class="docutils literal">SolidPg</span> to <span class="docutils literal">SteadyPg</span>.</p></li> </ul> </section> -<section id="section-15"> +<section id="section-16"> <h2>0.9.1</h2> <p>DBUtils 0.9.1 was released on May 8, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.4.</p> @@ -209,7 +216,7 @@ <h2>0.9.1</h2> <li><p>Improved the documentation and added a User's Guide.</p></li> </ul> </section> -<section id="section-16"> +<section id="section-17"> <h2>0.8.1 - 2005-09-13</h2> <p>DBUtils 0.8.1 was released on September 13, 2005.</p> <p>It is intended to be used with Python versions 2.0 to 2.4.</p> diff --git a/docs/changelog.rst b/docs/changelog.rst index b8a0a5a..85a2f19 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,16 @@ Changelog for DBUtils +++++++++++++++++++++ + +3.0.1 +===== + +DBUtils 3.0.1 was released on December 22, 2021. + +It includes ``InterfaceError`` to the default list of exceptions +for which the connection failover mechanism is applied. +You can override this with the ``failures`` parameter. + 3.0.0 ===== diff --git a/docs/main.de.html b/docs/main.de.html index 2df8d6d..4728326 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -282,9 +282,10 @@ <h3>PersistentDB (persistent_db)</h3> </li> <li><p><span class="docutils literal">setsession</span>: eine optionale Liste von SQL-Befehlen zur Initialisierung der Datenbanksitzung, z.B. <span class="docutils literal">["set datestyle to german", <span class="pre">...]</span></span></p></li> -<li><p><span class="docutils literal">failures</span>: eine optionale Exception-Klasse oder ein Tupel von Exceptions +<li><p><span class="docutils literal">failures</span>: eine optionale Exception-Klasse oder ein Tupel von Exceptions, bei denen die Ausfallsicherung zum Tragen kommen soll, falls die Vorgabe -(OperationalError, InternalError) nicht geeignet sein sollte</p></li> +(OperationalError, InterfaceError, InternalError) für das verwendete +Datenbankadapter-Modul nicht geeignet sein sollte</p></li> <li><p><span class="docutils literal">ping</span>: mit diesem Parameter kann eingestellt werden, wann Verbindungen mit der <span class="docutils literal">ping()</span>-Methode geprüft werden, falls eine solche vorhanden ist (<span class="docutils literal">0</span> = <span class="docutils literal">None</span> = nie, <span class="docutils literal">1</span> = Standardwert = immer wenn neu angefragt, @@ -371,9 +372,10 @@ <h3>PooledDB (pooled_db)</h3> in den Verbindungspool zurückgegeben werden (<span class="docutils literal">False</span> oder <span class="docutils literal">None</span> um mit <span class="docutils literal">begin()</span> gestartete Transaktionen zurückzurollen, der Standardwert <span class="docutils literal">True</span> rollt sicherheitshalber mögliche Transaktionen immer zurück)</p></li> -<li><p><span class="docutils literal">failures</span>: eine optionale Exception-Klasse oder ein Tupel von Exceptions +<li><p><span class="docutils literal">failures</span>: eine optionale Exception-Klasse oder ein Tupel von Exceptions, bei denen die Ausfallsicherung zum Tragen kommen soll, falls die Vorgabe -(OperationalError, InternalError) nicht geeignet sein sollte</p></li> +(OperationalError, InterfaceError, InternalError) für das verwendete +Datenbankadapter-Modul nicht geeignet sein sollte</p></li> <li><p><span class="docutils literal">ping</span>: mit diesem Parameter kann eingestellt werden, wann Verbindungen mit der <span class="docutils literal">ping()</span>-Methode geprüft werden, falls eine solche vorhanden ist (<span class="docutils literal">0</span> = <span class="docutils literal">None</span> = nie, <span class="docutils literal">1</span> = Standardwert = immer wenn neu angefragt, diff --git a/docs/main.de.rst b/docs/main.de.rst index 0c7077a..3f6d8a3 100644 --- a/docs/main.de.rst +++ b/docs/main.de.rst @@ -269,9 +269,10 @@ Parameter angeben müssen: * ``setsession``: eine optionale Liste von SQL-Befehlen zur Initialisierung der Datenbanksitzung, z.B. ``["set datestyle to german", ...]`` -* ``failures``: eine optionale Exception-Klasse oder ein Tupel von Exceptions +* ``failures``: eine optionale Exception-Klasse oder ein Tupel von Exceptions, bei denen die Ausfallsicherung zum Tragen kommen soll, falls die Vorgabe - (OperationalError, InternalError) nicht geeignet sein sollte + (OperationalError, InterfaceError, InternalError) für das verwendete + Datenbankadapter-Modul nicht geeignet sein sollte * ``ping``: mit diesem Parameter kann eingestellt werden, wann Verbindungen mit der ``ping()``-Methode geprüft werden, falls eine solche vorhanden ist @@ -378,9 +379,10 @@ Parameter angeben müssen: um mit ``begin()`` gestartete Transaktionen zurückzurollen, der Standardwert ``True`` rollt sicherheitshalber mögliche Transaktionen immer zurück) -* ``failures``: eine optionale Exception-Klasse oder ein Tupel von Exceptions +* ``failures``: eine optionale Exception-Klasse oder ein Tupel von Exceptions, bei denen die Ausfallsicherung zum Tragen kommen soll, falls die Vorgabe - (OperationalError, InternalError) nicht geeignet sein sollte + (OperationalError, InterfaceError, InternalError) für das verwendete + Datenbankadapter-Modul nicht geeignet sein sollte * ``ping``: mit diesem Parameter kann eingestellt werden, wann Verbindungen mit der ``ping()``-Methode geprüft werden, falls eine solche vorhanden ist diff --git a/docs/main.html b/docs/main.html index f0109a1..e0ee8c8 100644 --- a/docs/main.html +++ b/docs/main.html @@ -263,7 +263,8 @@ <h3>PersistentDB (persistent_db)</h3> prepare the session, e.g. <span class="docutils literal">["set datestyle to german", <span class="pre">...]</span></span></p></li> <li><p><span class="docutils literal">failures</span>: an optional exception class or a tuple of exception classes for which the connection failover mechanism shall be applied, -if the default (OperationalError, InternalError) is not adequate</p></li> +if the default (OperationalError, InterfaceError, InternalError) +is not adequate for the used database module</p></li> <li><p><span class="docutils literal">ping</span>: an optional flag controlling when connections are checked with the <span class="docutils literal">ping()</span> method if such a method is available (<span class="docutils literal">0</span> = <span class="docutils literal">None</span> = never, <span class="docutils literal">1</span> = default = whenever it is requested, @@ -340,7 +341,8 @@ <h3>PooledDB (pooled_db)</h3> the default value <span class="docutils literal">True</span> always issues a rollback for safety's sake)</p></li> <li><p><span class="docutils literal">failures</span>: an optional exception class or a tuple of exception classes for which the connection failover mechanism shall be applied, -if the default (OperationalError, InternalError) is not adequate</p></li> +if the default (OperationalError, InterfaceError, InternalError) +is not adequate for the used database module</p></li> <li><p><span class="docutils literal">ping</span>: an optional flag controlling when connections are checked with the <span class="docutils literal">ping()</span> method if such a method is available (<span class="docutils literal">0</span> = <span class="docutils literal">None</span> = never, <span class="docutils literal">1</span> = default = whenever fetched from the pool, diff --git a/docs/main.rst b/docs/main.rst index 81c6427..6fdf6d2 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -250,7 +250,8 @@ of ``persistent_db``, passing the following parameters: * ``failures``: an optional exception class or a tuple of exception classes for which the connection failover mechanism shall be applied, - if the default (OperationalError, InternalError) is not adequate + if the default (OperationalError, InterfaceError, InternalError) + is not adequate for the used database module * ``ping``: an optional flag controlling when connections are checked with the ``ping()`` method if such a method is available @@ -347,7 +348,8 @@ following parameters: * ``failures``: an optional exception class or a tuple of exception classes for which the connection failover mechanism shall be applied, - if the default (OperationalError, InternalError) is not adequate + if the default (OperationalError, InterfaceError, InternalError) + is not adequate for the used database module * ``ping``: an optional flag controlling when connections are checked with the ``ping()`` method if such a method is available diff --git a/tests/mock_db.py b/tests/mock_db.py index 42296cb..ceca9e3 100644 --- a/tests/mock_db.py +++ b/tests/mock_db.py @@ -15,6 +15,10 @@ class OperationalError(DatabaseError): pass +class InterfaceError(DatabaseError): + pass + + class InternalError(DatabaseError): pass diff --git a/tests/test_steady_db.py b/tests/test_steady_db.py index d1a2628..6a22db2 100644 --- a/tests/test_steady_db.py +++ b/tests/test_steady_db.py @@ -406,8 +406,12 @@ def test_connection_failures(self): db = SteadyDBconnect(dbapi, failures=dbapi.OperationalError) db.close() self.assertRaises(dbapi.InternalError, db.cursor) - db = SteadyDBconnect( - dbapi, failures=(dbapi.OperationalError, dbapi.InternalError)) + db = SteadyDBconnect(dbapi, failures=( + dbapi.OperationalError, dbapi.InterfaceError)) + db.close() + self.assertRaises(dbapi.InternalError, db.cursor) + db = SteadyDBconnect(dbapi, failures=( + dbapi.OperationalError, dbapi.InterfaceError, dbapi.InternalError)) db.close() db.cursor() From b147745d709e7a7bfbc929ae7bfb1d41bd81ae15 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Wed, 22 Dec 2021 18:15:56 +0100 Subject: [PATCH 39/84] Prepare patch release --- .bumpversion.cfg | 2 +- README.md | 2 +- dbutils/__init__.py | 2 +- docs/main.de.html | 2 +- docs/main.de.rst | 2 +- docs/main.html | 2 +- docs/main.rst | 2 +- setup.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 277da9f..1bbd524 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.0.0 +current_version = 3.0.1 [bumpversion:file:setup.py] search = __version__ = '{current_version}' diff --git a/README.md b/README.md index 41b2119..761e944 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ to a database that can be used in all kinds of multi-threaded environments. The suite supports DB-API 2 compliant database interfaces and the classic PyGreSQL interface. -The current version 3.0.0 of DBUtils supports Python versions 3.6 to 3.10. +The current version 3.0.1 of DBUtils supports Python versions 3.6 to 3.10. **Please have a look at the [changelog](https://webwareforpython.github.io/DBUtils/changelog.html), because there were some breaking changes in version 2.0.** diff --git a/dbutils/__init__.py b/dbutils/__init__.py index 19a4627..b326f99 100644 --- a/dbutils/__init__.py +++ b/dbutils/__init__.py @@ -5,4 +5,4 @@ 'simple_pooled_pg', 'steady_pg', 'pooled_pg', 'persistent_pg', 'simple_pooled_db', 'steady_db', 'pooled_db', 'persistent_db'] -__version__ = '3.0.0' +__version__ = '3.0.1' diff --git a/docs/main.de.html b/docs/main.de.html index 4728326..020585e 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -12,7 +12,7 @@ <h1 class="title">Benutzeranleitung für DBUtils</h1> <dl class="docinfo simple"> <dt class="version">Version<span class="colon">:</span></dt> -<dd class="version">3.0.0</dd> +<dd class="version">3.0.1</dd> <dt class="translations">Translations<span class="colon">:</span></dt> <dd class="translations"><p><a class="reference external" href="main.html">English</a> | German</p> </dd> diff --git a/docs/main.de.rst b/docs/main.de.rst index 3f6d8a3..2df620c 100644 --- a/docs/main.de.rst +++ b/docs/main.de.rst @@ -1,7 +1,7 @@ Benutzeranleitung für DBUtils +++++++++++++++++++++++++++++ -:Version: 3.0.0 +:Version: 3.0.1 :Translations: English_ | German .. _English: main.html diff --git a/docs/main.html b/docs/main.html index e0ee8c8..4c7a09e 100644 --- a/docs/main.html +++ b/docs/main.html @@ -12,7 +12,7 @@ <h1 class="title">DBUtils User's Guide</h1> <dl class="docinfo simple"> <dt class="version">Version<span class="colon">:</span></dt> -<dd class="version">3.0.0</dd> +<dd class="version">3.0.1</dd> <dt class="translations">Translations<span class="colon">:</span></dt> <dd class="translations"><p>English | <a class="reference external" href="main.de.html">German</a></p> </dd> diff --git a/docs/main.rst b/docs/main.rst index 6fdf6d2..d2c0202 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -1,7 +1,7 @@ DBUtils User's Guide ++++++++++++++++++++ -:Version: 3.0.0 +:Version: 3.0.1 :Translations: English | German_ .. _German: main.de.html diff --git a/setup.py b/setup.py index 8fbcc11..2144f7e 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ warnings.filterwarnings('ignore', 'Unknown distribution option') -__version__ = '3.0.0' +__version__ = '3.0.1' readme = open('README.md').read() From ac477276d6667ce84b0a7e26d4cdb2dfac4cb303 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Wed, 12 Jan 2022 14:34:12 +0100 Subject: [PATCH 40/84] Support optional iterator protocol on cursors (#38) Use the original iterator if the cursor is already iterable, make it iterable otherwise. --- dbutils/steady_db.py | 8 ++++++++ tests/test_steady_db.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/dbutils/steady_db.py b/dbutils/steady_db.py index 8df2acb..3ad3b73 100644 --- a/dbutils/steady_db.py +++ b/dbutils/steady_db.py @@ -542,6 +542,14 @@ def __exit__(self, *exc): """Exit the runtime context for the cursor object.""" self.close() + def __iter__(self): + """Make cursor compatible to the iteration protocol.""" + cursor = self._cursor + try: # use iterator provided by original cursor + return iter(cursor) + except TypeError: # create iterator if not provided + return iter(cursor.fetchone, None) + def setinputsizes(self, sizes): """Store input sizes in case cursor needs to be reopened.""" self._inputsizes = sizes diff --git a/tests/test_steady_db.py b/tests/test_steady_db.py index 6a22db2..3c21cfd 100644 --- a/tests/test_steady_db.py +++ b/tests/test_steady_db.py @@ -273,6 +273,36 @@ def test_cursor_context_handler(self): self.assertEqual(cursor.fetchone(), 'test') self.assertEqual(db._con.open_cursors, 0) + def test_cursor_as_iterator_provided(self): + db = SteadyDBconnect( + dbapi, 0, None, None, None, True, + 'SteadyDBTestDB', user='SteadyDBTestUser') + self.assertEqual(db._con.open_cursors, 0) + cursor = db.cursor() + self.assertEqual(db._con.open_cursors, 1) + cursor.execute('select test') + _cursor = cursor._cursor + try: + assert not hasattr(_cursor, 'iter') + _cursor.__iter__ = lambda: ['test-iter'] + assert list(iter(cursor)) == ['test'] + finally: + del _cursor.__iter__ + cursor.close() + self.assertEqual(db._con.open_cursors, 0) + + def test_cursor_as_iterator_created(self): + db = SteadyDBconnect( + dbapi, 0, None, None, None, True, + 'SteadyDBTestDB', user='SteadyDBTestUser') + self.assertEqual(db._con.open_cursors, 0) + cursor = db.cursor() + self.assertEqual(db._con.open_cursors, 1) + cursor.execute('select test') + assert list(iter(cursor)) == ['test'] + cursor.close() + self.assertEqual(db._con.open_cursors, 0) + def test_connection_creator_function(self): db1 = SteadyDBconnect( dbapi, 0, None, None, None, True, From 798d5ad3bdfccdebc22fd3d7b16c8b9e86bdcaf7 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Fri, 14 Jan 2022 21:58:03 +0100 Subject: [PATCH 41/84] Prepare patch release --- .bumpversion.cfg | 2 +- LICENSE | 2 +- README.md | 2 +- dbutils/__init__.py | 2 +- docs/changelog.html | 37 +++++++++++++++++++++---------------- docs/changelog.rst | 6 ++++++ docs/main.de.html | 2 +- docs/main.de.rst | 2 +- docs/main.html | 4 ++-- docs/main.rst | 4 ++-- setup.py | 2 +- 11 files changed, 38 insertions(+), 27 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 1bbd524..64494c4 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.0.1 +current_version = 3.0.2 [bumpversion:file:setup.py] search = __version__ = '{current_version}' diff --git a/LICENSE b/LICENSE index 641db5d..6c2a74b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2021 Christoph Zwerschke +Copyright (c) 2022 Christoph Zwerschke Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 761e944..ac3ce03 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ to a database that can be used in all kinds of multi-threaded environments. The suite supports DB-API 2 compliant database interfaces and the classic PyGreSQL interface. -The current version 3.0.1 of DBUtils supports Python versions 3.6 to 3.10. +The current version 3.0.2 of DBUtils supports Python versions 3.6 to 3.10. **Please have a look at the [changelog](https://webwareforpython.github.io/DBUtils/changelog.html), because there were some breaking changes in version 2.0.** diff --git a/dbutils/__init__.py b/dbutils/__init__.py index b326f99..7a0bdf2 100644 --- a/dbutils/__init__.py +++ b/dbutils/__init__.py @@ -5,4 +5,4 @@ 'simple_pooled_pg', 'steady_pg', 'pooled_pg', 'persistent_pg', 'simple_pooled_db', 'steady_db', 'pooled_db', 'persistent_db'] -__version__ = '3.0.1' +__version__ = '3.0.2' diff --git a/docs/changelog.html b/docs/changelog.html index 13eda8b..82ceecf 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -12,13 +12,18 @@ <h1 class="title">Changelog for DBUtils</h1> <section id="section-1"> +<h2>3.0.2</h2> +<p>DBUtils 3.0.2 was released on January 14, 2022.</p> +<p>The the optional iterator protocol on cursors is now supported.</p> +</section> +<section id="section-2"> <h2>3.0.1</h2> <p>DBUtils 3.0.1 was released on December 22, 2021.</p> <p>It includes <span class="docutils literal">InterfaceError</span> to the default list of exceptions for which the connection failover mechanism is applied. You can override this with the <span class="docutils literal">failures</span> parameter.</p> </section> -<section id="section-2"> +<section id="section-3"> <h2>3.0.0</h2> <p>DBUtils 3.0.0 was released on November 26, 2021.</p> <p>It is intended to be used with Python versions 3.6 to 3.10.</p> @@ -27,7 +32,7 @@ <h2>3.0.0</h2> <li><p>Cease support for Python 2 and 3.5, minor optimizations.</p></li> </ul> </section> -<section id="section-3"> +<section id="section-4"> <h2>2.0.3</h2> <p>DBUtils 2.0.3 was released on November 26, 2021.</p> <p>Changes:</p> @@ -35,7 +40,7 @@ <h2>2.0.3</h2> <li><p>Support Python version 3.10.</p></li> </ul> </section> -<section id="section-4"> +<section id="section-5"> <h2>2.0.2</h2> <p>DBUtils 2.0.2 was released on June 8, 2021.</p> <p>Changes:</p> @@ -43,7 +48,7 @@ <h2>2.0.2</h2> <li><p>Allow using context managers for pooled connections.</p></li> </ul> </section> -<section id="section-5"> +<section id="section-6"> <h2>2.0.1</h2> <p>DBUtils 2.0.1 was released on April 8, 2021.</p> <p>Changes:</p> @@ -51,7 +56,7 @@ <h2>2.0.1</h2> <li><p>Avoid "name Exception is not defined" when exiting.</p></li> </ul> </section> -<section id="section-6"> +<section id="section-7"> <h2>2.0</h2> <p>DBUtils 2.0 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> @@ -67,7 +72,7 @@ <h2>2.0</h2> <li><p>This changelog has been compiled from the former release notes.</p></li> </ul> </section> -<section id="section-7"> +<section id="section-8"> <h2>1.4</h2> <p>DBUtils 1.4 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> @@ -78,7 +83,7 @@ <h2>1.4</h2> inside a transaction.</p></li> </ul> </section> -<section id="section-8"> +<section id="section-9"> <h2>1.3</h2> <p>DBUtils 1.3 was released on March 3, 2018.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.4 to 3.7.</p> @@ -87,12 +92,12 @@ <h2>1.3</h2> <li><p>This version now supports context handlers for connections and cursors.</p></li> </ul> </section> -<section id="section-9"> +<section id="section-10"> <h2>1.2</h2> <p>DBUtils 1.2 was released on February 5, 2017.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.0 to 3.6.</p> </section> -<section id="section-10"> +<section id="section-11"> <h2>1.1.1</h2> <p>DBUtils 1.1.1 was released on February 4, 2017.</p> <p>It is intended to be used with Python versions 2.3 to 2.7.</p> @@ -106,7 +111,7 @@ <h2>1.1.1</h2> <li><p>Fixed a problem when running under Jython (reported by Vitaly Kruglikov).</p></li> </ul> </section> -<section id="section-11"> +<section id="section-12"> <h2>1.1</h2> <p>DBUtils 1.1 was released on August 14, 2011.</p> <p>Improvements:</p> @@ -135,7 +140,7 @@ <h2>1.1</h2> <li><p>Fixed some minor issues with the <span class="docutils literal">DBUtilsExample</span> for Webware.</p></li> </ul> </section> -<section id="section-12"> +<section id="section-13"> <h2>1.0</h2> <p>DBUtils 1.0 was released on November 29, 2008.</p> <p>It is intended to be used with Python versions 2.2 to 2.6.</p> @@ -168,7 +173,7 @@ <h2>1.0</h2> the MySQLdb module (problem reported by Gregory Pinero).</p></li> </ul> </section> -<section id="section-13"> +<section id="section-14"> <h2>0.9.4</h2> <p>DBUtils 0.9.4 was released on July 7, 2007.</p> <p>This release fixes a problem in the destructor code and has been supplemented @@ -177,7 +182,7 @@ <h2>0.9.4</h2> in the last release, since you can now pass custom creator functions for database connections instead of DB-API 2 modules.</p> </section> -<section id="section-14"> +<section id="section-15"> <h2>0.9.3</h2> <p>DBUtils 0.9.3 was released on May 21, 2007.</p> <p>Changes:</p> @@ -192,7 +197,7 @@ <h2>0.9.3</h2> Added Chinese translation of the User's Guide, kindly contributed by gashero.</p></li> </ul> </section> -<section id="section-15"> +<section id="section-16"> <h2>0.9.2</h2> <p>DBUtils 0.9.2 was released on September 22, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.5.</p> @@ -202,7 +207,7 @@ <h2>0.9.2</h2> storage engine. Accordingly, renamed <span class="docutils literal">SolidPg</span> to <span class="docutils literal">SteadyPg</span>.</p></li> </ul> </section> -<section id="section-16"> +<section id="section-17"> <h2>0.9.1</h2> <p>DBUtils 0.9.1 was released on May 8, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.4.</p> @@ -216,7 +221,7 @@ <h2>0.9.1</h2> <li><p>Improved the documentation and added a User's Guide.</p></li> </ul> </section> -<section id="section-17"> +<section id="section-18"> <h2>0.8.1 - 2005-09-13</h2> <p>DBUtils 0.8.1 was released on September 13, 2005.</p> <p>It is intended to be used with Python versions 2.0 to 2.4.</p> diff --git a/docs/changelog.rst b/docs/changelog.rst index 85a2f19..7706d89 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ Changelog for DBUtils +++++++++++++++++++++ +3.0.2 +===== + +DBUtils 3.0.2 was released on January 14, 2022. + +The the optional iterator protocol on cursors is now supported. 3.0.1 ===== diff --git a/docs/main.de.html b/docs/main.de.html index 020585e..e60a2e7 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -12,7 +12,7 @@ <h1 class="title">Benutzeranleitung für DBUtils</h1> <dl class="docinfo simple"> <dt class="version">Version<span class="colon">:</span></dt> -<dd class="version">3.0.1</dd> +<dd class="version">3.0.2</dd> <dt class="translations">Translations<span class="colon">:</span></dt> <dd class="translations"><p><a class="reference external" href="main.html">English</a> | German</p> </dd> diff --git a/docs/main.de.rst b/docs/main.de.rst index 2df620c..574eb3f 100644 --- a/docs/main.de.rst +++ b/docs/main.de.rst @@ -1,7 +1,7 @@ Benutzeranleitung für DBUtils +++++++++++++++++++++++++++++ -:Version: 3.0.1 +:Version: 3.0.2 :Translations: English_ | German .. _English: main.html diff --git a/docs/main.html b/docs/main.html index 4c7a09e..42fdfa9 100644 --- a/docs/main.html +++ b/docs/main.html @@ -12,7 +12,7 @@ <h1 class="title">DBUtils User's Guide</h1> <dl class="docinfo simple"> <dt class="version">Version<span class="colon">:</span></dt> -<dd class="version">3.0.1</dd> +<dd class="version">3.0.2</dd> <dt class="translations">Translations<span class="colon">:</span></dt> <dd class="translations"><p>English | <a class="reference external" href="main.de.html">German</a></p> </dd> @@ -473,7 +473,7 @@ <h2>Credits</h2> </section> <section id="copyright-and-license"> <h2>Copyright and License</h2> -<p>Copyright © 2005-2021 by Christoph Zwerschke. +<p>Copyright © 2005-2022 by Christoph Zwerschke. All Rights Reserved.</p> <p>DBUtils is free and open source software, licensed under the <a class="reference external" href="https://opensource.org/licenses/MIT">MIT license</a>.</p> diff --git a/docs/main.rst b/docs/main.rst index d2c0202..150ef3c 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -1,7 +1,7 @@ DBUtils User's Guide ++++++++++++++++++++ -:Version: 3.0.1 +:Version: 3.0.2 :Translations: English | German_ .. _German: main.de.html @@ -521,7 +521,7 @@ Credits Copyright and License ===================== -Copyright © 2005-2021 by Christoph Zwerschke. +Copyright © 2005-2022 by Christoph Zwerschke. All Rights Reserved. DBUtils is free and open source software, diff --git a/setup.py b/setup.py index 2144f7e..c6c1d28 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ warnings.filterwarnings('ignore', 'Unknown distribution option') -__version__ = '3.0.1' +__version__ = '3.0.2' readme = open('README.md').read() From 3ba9f4975a92c3f562c57d452777acb6da6d8179 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Tue, 25 Apr 2023 11:24:17 +0200 Subject: [PATCH 42/84] Fix small typo in code and docs --- .gitignore | 1 + dbutils/pooled_db.py | 4 ++-- dbutils/pooled_pg.py | 2 +- dbutils/steady_pg.py | 2 +- docs/main.html | 2 +- docs/main.rst | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 1663b67..b9cc250 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ build dist +local .idea .tox diff --git a/dbutils/pooled_db.py b/dbutils/pooled_db.py index 1b47f2b..f395a57 100644 --- a/dbutils/pooled_db.py +++ b/dbutils/pooled_db.py @@ -53,7 +53,7 @@ setsession: an optional list of SQL commands that may serve to prepare the session, e.g. ["set datestyle to german", ...] reset: how connections should be reset when returned to the pool - (False or None to rollback transcations started with begin(), + (False or None to rollback transactions started with begin(), the default value True always issues a rollback for safety's sake) failures: an optional exception class or a tuple of exception classes for which the connection failover mechanism shall be applied, @@ -207,7 +207,7 @@ def __init__( setsession: optional list of SQL commands that may serve to prepare the session, e.g. ["set datestyle to ...", "set time zone ..."] reset: how connections should be reset when returned to the pool - (False or None to rollback transcations started with begin(), + (False or None to rollback transactions started with begin(), True to always issue a rollback for safety's sake) failures: an optional exception class or a tuple of exception classes for which the connection failover mechanism shall be applied, diff --git a/dbutils/pooled_pg.py b/dbutils/pooled_pg.py index f9776f7..7bab407 100644 --- a/dbutils/pooled_pg.py +++ b/dbutils/pooled_pg.py @@ -162,7 +162,7 @@ def __init__( setsession: optional list of SQL commands that may serve to prepare the session, e.g. ["set datestyle to ...", "set time zone ..."] reset: how connections should be reset when returned to the pool - (0 or None to rollback transcations started with begin(), + (0 or None to rollback transactions started with begin(), 1 to always issue a rollback, 2 for a complete reset) args, kwargs: the parameters that shall be used to establish the PostgreSQL connections using class PyGreSQL pg.DB() diff --git a/dbutils/steady_pg.py b/dbutils/steady_pg.py index c1a6b71..a553078 100644 --- a/dbutils/steady_pg.py +++ b/dbutils/steady_pg.py @@ -183,7 +183,7 @@ def reopen(self): try: self._con.reopen() except Exception: - if self._transcation: + if self._transaction: self._transaction = False try: self._con.query('rollback') diff --git a/docs/main.html b/docs/main.html index 42fdfa9..a677b7b 100644 --- a/docs/main.html +++ b/docs/main.html @@ -337,7 +337,7 @@ <h3>PooledDB (pooled_db)</h3> <li><p><span class="docutils literal">setsession</span>: an optional list of SQL commands that may serve to prepare the session, e.g. <span class="docutils literal">["set datestyle to german", <span class="pre">...]</span></span></p></li> <li><p><span class="docutils literal">reset</span>: how connections should be reset when returned to the pool -(<span class="docutils literal">False</span> or <span class="docutils literal">None</span> to rollback transcations started with <span class="docutils literal">begin()</span>, +(<span class="docutils literal">False</span> or <span class="docutils literal">None</span> to rollback transactions started with <span class="docutils literal">begin()</span>, the default value <span class="docutils literal">True</span> always issues a rollback for safety's sake)</p></li> <li><p><span class="docutils literal">failures</span>: an optional exception class or a tuple of exception classes for which the connection failover mechanism shall be applied, diff --git a/docs/main.rst b/docs/main.rst index 150ef3c..bb77fa5 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -343,7 +343,7 @@ following parameters: prepare the session, e.g. ``["set datestyle to german", ...]`` * ``reset``: how connections should be reset when returned to the pool - (``False`` or ``None`` to rollback transcations started with ``begin()``, + (``False`` or ``None`` to rollback transactions started with ``begin()``, the default value ``True`` always issues a rollback for safety's sake) * ``failures``: an optional exception class or a tuple of exception classes From c699443d31fce1ad20f27658b4a5887863a0764e Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Tue, 25 Apr 2023 11:39:22 +0200 Subject: [PATCH 43/84] Minor improvement in determining thread safety --- dbutils/persistent_db.py | 13 ++++++++----- dbutils/pooled_db.py | 13 ++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/dbutils/persistent_db.py b/dbutils/persistent_db.py index 6873905..9082488 100644 --- a/dbutils/persistent_db.py +++ b/dbutils/persistent_db.py @@ -174,12 +174,15 @@ def __init__( threadsafety = creator.threadsafety except AttributeError: try: - if not callable(creator.connect): - raise AttributeError + threadsafety = creator.dbapi.threadsafety except AttributeError: - threadsafety = 1 - else: - threadsafety = 0 + try: + if not callable(creator.connect): + raise AttributeError + except AttributeError: + threadsafety = 1 + else: + threadsafety = 0 if not threadsafety: raise NotSupportedError("Database module is not thread-safe.") self._creator = creator diff --git a/dbutils/pooled_db.py b/dbutils/pooled_db.py index f395a57..fb39ab4 100644 --- a/dbutils/pooled_db.py +++ b/dbutils/pooled_db.py @@ -224,12 +224,15 @@ def __init__( threadsafety = creator.threadsafety except AttributeError: try: - if not callable(creator.connect): - raise AttributeError + threadsafety = creator.dbapi.threadsafety except AttributeError: - threadsafety = 2 - else: - threadsafety = 0 + try: + if not callable(creator.connect): + raise AttributeError + except AttributeError: + threadsafety = 1 + else: + threadsafety = 0 if not threadsafety: raise NotSupportedError("Database module is not thread-safe.") self._creator = creator From f2186a8086834a365089a1cfcb09cca4ca8bd0ac Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Tue, 25 Apr 2023 11:46:25 +0200 Subject: [PATCH 44/84] Support Python 3.11 --- .flake8 | 2 +- .github/workflows/publish_on_pypi.yml | 8 ++++---- .github/workflows/test_with_tox.yml | 8 ++++---- README.md | 2 +- docs/main.de.html | 2 +- docs/main.de.rst | 2 +- docs/main.html | 2 +- docs/main.rst | 2 +- setup.py | 1 + tox.ini | 10 +++++----- 10 files changed, 20 insertions(+), 19 deletions(-) diff --git a/.flake8 b/.flake8 index 578558b..e25de91 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] ignore = E722,W503 -exclude = .git,.pytest_cache,.tox,.venv,.idea,__pycache__,build,dist,docs +exclude = .git,.pytest_cache,.tox,.venv,.idea,__pycache__,build,build,dist,docs,local max-line-length = 79 diff --git a/.github/workflows/publish_on_pypi.yml b/.github/workflows/publish_on_pypi.yml index 9d887fa..cd0df25 100644 --- a/.github/workflows/publish_on_pypi.yml +++ b/.github/workflows/publish_on_pypi.yml @@ -10,18 +10,18 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ['3.9'] + python: ['3.10'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - name: Build source tarball - if: matrix.python == 3.9 + if: matrix.python == 3.10 run: python setup.py sdist - name: Build wheel diff --git a/.github/workflows/test_with_tox.yml b/.github/workflows/test_with_tox.yml index 62246b9..24d7690 100644 --- a/.github/workflows/test_with_tox.yml +++ b/.github/workflows/test_with_tox.yml @@ -7,13 +7,13 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ['3.6', '3.7', '3.8', '3.9', '3.10'] + python: ['3.6', '3.7', '3.8', '3.9', '3.10', '3.11'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Python ${{ matrix.python }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} @@ -21,5 +21,5 @@ jobs: - run: tox -e py - - if: matrix.python == 3.9 + - if: matrix.python == 3.10 run: TOXENV=flake8,manifest,docs,spell tox diff --git a/README.md b/README.md index ac3ce03..ff2d82c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ to a database that can be used in all kinds of multi-threaded environments. The suite supports DB-API 2 compliant database interfaces and the classic PyGreSQL interface. -The current version 3.0.2 of DBUtils supports Python versions 3.6 to 3.10. +The current version 3.0.2 of DBUtils supports Python versions 3.6 to 3.11. **Please have a look at the [changelog](https://webwareforpython.github.io/DBUtils/changelog.html), because there were some breaking changes in version 2.0.** diff --git a/docs/main.de.html b/docs/main.de.html index e60a2e7..c28fe0a 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -136,7 +136,7 @@ <h3>Installation</h3> </section> <section id="anforderungen"> <h2>Anforderungen</h2> -<p>DBUtils unterstützt die <a class="reference external" href="https://www.python.org">Python</a> Versionen 3.6 bis 3.10.</p> +<p>DBUtils unterstützt die <a class="reference external" href="https://www.python.org">Python</a> Versionen 3.6 bis 3.11.</p> <p>Die Module in der Variante für klassisches PyGreSQL benötigen <a class="reference external" href="https://www.pygresql.org/">PyGreSQL</a> Version 4.0 oder höher, während die Module in der allgemeinen Variante für DB-API 2 mit jedem beliebigen Python-Datenbankadapter-Modul zusammenarbeiten, diff --git a/docs/main.de.rst b/docs/main.de.rst index 574eb3f..f76f654 100644 --- a/docs/main.de.rst +++ b/docs/main.de.rst @@ -98,7 +98,7 @@ herunterzuladen und zu installieren:: Anforderungen ============= -DBUtils unterstützt die Python_ Versionen 3.6 bis 3.10. +DBUtils unterstützt die Python_ Versionen 3.6 bis 3.11. Die Module in der Variante für klassisches PyGreSQL benötigen PyGreSQL_ Version 4.0 oder höher, während die Module in der allgemeinen Variante diff --git a/docs/main.html b/docs/main.html index a677b7b..16fcc4d 100644 --- a/docs/main.html +++ b/docs/main.html @@ -133,7 +133,7 @@ <h3>Installation</h3> </section> <section id="requirements"> <h2>Requirements</h2> -<p>DBUtils supports <a class="reference external" href="https://www.python.org">Python</a> versions 3.6 to 3.10.</p> +<p>DBUtils supports <a class="reference external" href="https://www.python.org">Python</a> versions 3.6 to 3.11.</p> <p>The modules in the classic PyGreSQL variant need <a class="reference external" href="https://www.pygresql.org/">PyGreSQL</a> version 4.0 or above, while the modules in the universal DB-API 2 variant run with any Python <a class="reference external" href="https://www.python.org/dev/peps/pep-0249/">DB-API 2</a> compliant database interface module.</p> diff --git a/docs/main.rst b/docs/main.rst index bb77fa5..8d25542 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -95,7 +95,7 @@ It is even easier to download and install the package in one go using `pip`_:: Requirements ============ -DBUtils supports Python_ versions 3.6 to 3.10. +DBUtils supports Python_ versions 3.6 to 3.11. The modules in the classic PyGreSQL variant need PyGreSQL_ version 4.0 or above, while the modules in the universal DB-API 2 variant run with diff --git a/setup.py b/setup.py index c6c1d28..9baee37 100755 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Operating System :: OS Independent', ], author='Christoph Zwerschke', diff --git a/tox.ini b/tox.ini index 404b6c5..6143fb1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py3{6,7,8,9,10}, flake8, manifest, docs, spell +envlist = py3{6,7,8,9,10,11}, flake8, manifest, docs, spell [testenv] setenv = @@ -9,25 +9,25 @@ commands = pytest {posargs} [testenv:spell] -basepython = python3.9 +basepython = python3.10 deps = codespell commands = codespell . [testenv:flake8] -basepython = python3.9 +basepython = python3.10 deps = flake8 commands = flake8 . [testenv:manifest] -basepython = python3.9 +basepython = python3.10 deps = check-manifest commands = check-manifest -v [testenv:docs] -basepython = python3.9 +basepython = python3.10 deps = docutils changedir = docs commands = From 6d517e2a8092a52a89274f6aca3815efacffeec0 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Tue, 25 Apr 2023 11:55:42 +0200 Subject: [PATCH 45/84] Improve determination of DBAPI module --- dbutils/steady_db.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dbutils/steady_db.py b/dbutils/steady_db.py index 3ad3b73..0e34a9b 100644 --- a/dbutils/steady_db.py +++ b/dbutils/steady_db.py @@ -148,7 +148,11 @@ def __init__( # proper initialization of the connection try: self._creator = creator.connect - self._dbapi = creator + try: + if creator.dbapi.connect: + self._dbapi = creator.dbapi + except AttributeError: + self._dbapi = creator except AttributeError: # try finding the DB-API 2 module via the connection creator self._creator = creator From 8d401797e8e8ad6cc1ea410a37a45f972a417f6e Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Tue, 25 Apr 2023 12:01:32 +0200 Subject: [PATCH 46/84] Fix spellcheck issue --- .codespellrc | 2 +- docs/changelog.html | 4 ++-- docs/main.de.html | 4 ++-- docs/main.html | 4 ++-- docs/make.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.codespellrc b/.codespellrc index 15761d2..ab81ffd 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,3 +1,3 @@ [codespell] -skip = .tox,.venv,*.de.html,*.de.rst +skip = .git,.tox,.venv,*.de.html,*.de.rst,build,dist,local quiet-level = 2 diff --git a/docs/changelog.html b/docs/changelog.html index 82ceecf..e623513 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -1,9 +1,9 @@ <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> -<meta charset="utf-8"/> +<meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> -<meta name="generator" content="Docutils 0.18.1: http://docutils.sourceforge.net/" /> +<meta name="generator" content="Docutils 0.19: https://docutils.sourceforge.io/" /> <title>Changelog for DBUtils</title> <link rel="stylesheet" href="doc.css" type="text/css" /> </head> diff --git a/docs/main.de.html b/docs/main.de.html index c28fe0a..1b43bff 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -1,9 +1,9 @@ <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="de" lang="de"> <head> -<meta charset="utf-8"/> +<meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> -<meta name="generator" content="Docutils 0.18.1: http://docutils.sourceforge.net/" /> +<meta name="generator" content="Docutils 0.19: https://docutils.sourceforge.io/" /> <title>Benutzeranleitung für DBUtils</title> <link rel="stylesheet" href="doc.css" type="text/css" /> </head> diff --git a/docs/main.html b/docs/main.html index 16fcc4d..ebf28a3 100644 --- a/docs/main.html +++ b/docs/main.html @@ -1,9 +1,9 @@ <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> -<meta charset="utf-8"/> +<meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> -<meta name="generator" content="Docutils 0.18.1: http://docutils.sourceforge.net/" /> +<meta name="generator" content="Docutils 0.19: https://docutils.sourceforge.io/" /> <title>DBUtils User's Guide</title> <link rel="stylesheet" href="doc.css" type="text/css" /> </head> diff --git a/docs/make.py b/docs/make.py index d7b2cad..28b9625 100755 --- a/docs/make.py +++ b/docs/make.py @@ -1,6 +1,6 @@ #!/usr/bin/python3.9 -"""Build HMTL from reST files.""" +"""Build HTML from reST files.""" from glob import glob from os.path import splitext From dfada0f9041e18e5ee50d1e5395dfd45a29a6474 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Thu, 27 Apr 2023 18:55:00 +0200 Subject: [PATCH 47/84] Add section on advanced usage to docs (#45) --- docs/main.de.html | 34 ++++++++++++++++++++++++++++------ docs/main.de.rst | 23 +++++++++++++++++++++++ docs/main.html | 33 +++++++++++++++++++++++++++------ docs/main.rst | 22 ++++++++++++++++++++++ 4 files changed, 100 insertions(+), 12 deletions(-) diff --git a/docs/main.de.html b/docs/main.de.html index 1b43bff..633b308 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -44,12 +44,13 @@ <h1 class="title">Benutzeranleitung für DBUtils</h1> <li><p><a class="reference internal" href="#pooleddb-pooled-db-1" id="toc-entry-15">PooledDB (pooled_db)</a></p></li> </ul> </li> -<li><p><a class="reference internal" href="#anmerkungen" id="toc-entry-16">Anmerkungen</a></p></li> -<li><p><a class="reference internal" href="#zukunft" id="toc-entry-17">Zukunft</a></p></li> -<li><p><a class="reference internal" href="#fehlermeldungen-und-feedback" id="toc-entry-18">Fehlermeldungen und Feedback</a></p></li> -<li><p><a class="reference internal" href="#links" id="toc-entry-19">Links</a></p></li> -<li><p><a class="reference internal" href="#autoren" id="toc-entry-20">Autoren</a></p></li> -<li><p><a class="reference internal" href="#copyright-und-lizenz" id="toc-entry-21">Copyright und Lizenz</a></p></li> +<li><p><a class="reference internal" href="#besonderheiten-bei-der-benutzung" id="toc-entry-16">Besonderheiten bei der Benutzung</a></p></li> +<li><p><a class="reference internal" href="#anmerkungen" id="toc-entry-17">Anmerkungen</a></p></li> +<li><p><a class="reference internal" href="#zukunft" id="toc-entry-18">Zukunft</a></p></li> +<li><p><a class="reference internal" href="#fehlermeldungen-und-feedback" id="toc-entry-19">Fehlermeldungen und Feedback</a></p></li> +<li><p><a class="reference internal" href="#links" id="toc-entry-20">Links</a></p></li> +<li><p><a class="reference internal" href="#autoren" id="toc-entry-21">Autoren</a></p></li> +<li><p><a class="reference internal" href="#copyright-und-lizenz" id="toc-entry-22">Copyright und Lizenz</a></p></li> </ul> </nav> <section id="zusammenfassung"> @@ -435,6 +436,27 @@ <h3>PooledDB (pooled_db)</h3> wieder an den Verbindungspool zurückgegeben wird.</p> </section> </section> +<section id="besonderheiten-bei-der-benutzung"> +<h2>Besonderheiten bei der Benutzung</h2> +<p>Manchmal möchte man Datenbankverbindung besonders vorbereiten, bevor sie +von DBUtils verwendet werden, und dies ist nicht immer durch Verwendung +der passenden Parameter möglich. Zum Beispiel kann es <span class="docutils literal">pyodb</span> erfordern, +dass man die Methode <span class="docutils literal">setencoding()</span> der Datenbankverbindung aufruft. +Sie können dies erreichen, indem Sie eine modifizierte Version der +Funktion <span class="docutils literal">connect()</span> verwenden und diese als <span class="docutils literal">creator</span> (dem ersten +Argument) an <span class="docutils literal">PersistentDB</span> oder <span class="docutils literal">PooledDB</span> übergeben, etwa so:</p> +<pre class="literal-block">from pyodbc import connect +from dbutils.pooled_db import PooledDB + +def creator(): + con = connect(...) + con.setdecoding(...) + return con + +creator.dbapi = pyodbc + +db_pool = PooledDB(creator, mincached=5)</pre> +</section> <section id="anmerkungen"> <h2>Anmerkungen</h2> <p>Wenn Sie einen der bekannten "Object-Relational Mapper" <a class="reference external" href="http://sqlobject.org/">SQLObject</a> oder diff --git a/docs/main.de.rst b/docs/main.de.rst index f76f654..147ce5e 100644 --- a/docs/main.de.rst +++ b/docs/main.de.rst @@ -461,6 +461,29 @@ ausgesetzt wird, und dass die Verbindung zurückgerollt wird, bevor sie wieder an den Verbindungspool zurückgegeben wird. +Besonderheiten bei der Benutzung +================================ +Manchmal möchte man Datenbankverbindung besonders vorbereiten, bevor sie +von DBUtils verwendet werden, und dies ist nicht immer durch Verwendung +der passenden Parameter möglich. Zum Beispiel kann es ``pyodb`` erfordern, +dass man die Methode ``setencoding()`` der Datenbankverbindung aufruft. +Sie können dies erreichen, indem Sie eine modifizierte Version der +Funktion ``connect()`` verwenden und diese als ``creator`` (dem ersten +Argument) an ``PersistentDB`` oder ``PooledDB`` übergeben, etwa so:: + + from pyodbc import connect + from dbutils.pooled_db import PooledDB + + def creator(): + con = connect(...) + con.setdecoding(...) + return con + + creator.dbapi = pyodbc + + db_pool = PooledDB(creator, mincached=5) + + Anmerkungen =========== Wenn Sie einen der bekannten "Object-Relational Mapper" SQLObject_ oder diff --git a/docs/main.html b/docs/main.html index ebf28a3..a1b2628 100644 --- a/docs/main.html +++ b/docs/main.html @@ -44,12 +44,13 @@ <h1 class="title">DBUtils User's Guide</h1> <li><p><a class="reference internal" href="#pooleddb-pooled-db-1" id="toc-entry-15">PooledDB (pooled_db)</a></p></li> </ul> </li> -<li><p><a class="reference internal" href="#notes" id="toc-entry-16">Notes</a></p></li> -<li><p><a class="reference internal" href="#future" id="toc-entry-17">Future</a></p></li> -<li><p><a class="reference internal" href="#bug-reports-and-feedback" id="toc-entry-18">Bug reports and feedback</a></p></li> -<li><p><a class="reference internal" href="#links" id="toc-entry-19">Links</a></p></li> -<li><p><a class="reference internal" href="#credits" id="toc-entry-20">Credits</a></p></li> -<li><p><a class="reference internal" href="#copyright-and-license" id="toc-entry-21">Copyright and License</a></p></li> +<li><p><a class="reference internal" href="#advanced-usage" id="toc-entry-16">Advanced Usage</a></p></li> +<li><p><a class="reference internal" href="#notes" id="toc-entry-17">Notes</a></p></li> +<li><p><a class="reference internal" href="#future" id="toc-entry-18">Future</a></p></li> +<li><p><a class="reference internal" href="#bug-reports-and-feedback" id="toc-entry-19">Bug reports and feedback</a></p></li> +<li><p><a class="reference internal" href="#links" id="toc-entry-20">Links</a></p></li> +<li><p><a class="reference internal" href="#credits" id="toc-entry-21">Credits</a></p></li> +<li><p><a class="reference internal" href="#copyright-and-license" id="toc-entry-22">Copyright and License</a></p></li> </ul> </nav> <section id="synopsis"> @@ -396,6 +397,26 @@ <h3>PooledDB (pooled_db)</h3> back before being given back to the connection pool.</p> </section> </section> +<section id="advanced-usage"> +<h2>Advanced Usage</h2> +<p>Sometimes you may want to prepare connections before they are used by +DBUtils, in ways that are not possible by just using the right parameters. +For instance, <span class="docutils literal">pyodbc</span> may require to configure connections by calling +the <span class="docutils literal">setencoding()</span> method of the connection. You can do this by passing +a modified <span class="docutils literal">connect()</span> function to <span class="docutils literal">PersistentDB</span> or <span class="docutils literal">PooledDB</span> as +<span class="docutils literal">creator</span> (the first argument), like this:</p> +<pre class="literal-block">from pyodbc import connect +from dbutils.pooled_db import PooledDB + +def creator(): + con = connect(...) + con.setdecoding(...) + return con + +creator.dbapi = pyodbc + +db_pool = PooledDB(creator, mincached=5)</pre> +</section> <section id="notes"> <h2>Notes</h2> <p>If you are using one of the popular object-relational mappers <a class="reference external" href="http://www.sqlobject.org/">SQLObject</a> diff --git a/docs/main.rst b/docs/main.rst index 8d25542..2549118 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -421,6 +421,28 @@ until the end of the transaction, and that the connection will be rolled back before being given back to the connection pool. +Advanced Usage +============== +Sometimes you may want to prepare connections before they are used by +DBUtils, in ways that are not possible by just using the right parameters. +For instance, ``pyodbc`` may require to configure connections by calling +the ``setencoding()`` method of the connection. You can do this by passing +a modified ``connect()`` function to ``PersistentDB`` or ``PooledDB`` as +``creator`` (the first argument), like this:: + + from pyodbc import connect + from dbutils.pooled_db import PooledDB + + def creator(): + con = connect(...) + con.setdecoding(...) + return con + + creator.dbapi = pyodbc + + db_pool = PooledDB(creator, mincached=5) + + Notes ===== If you are using one of the popular object-relational mappers SQLObject_ From 245cfbbb147bc0875384b0e3770fb0854ffc81aa Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Thu, 27 Apr 2023 19:14:43 +0200 Subject: [PATCH 48/84] Prepare patch release --- .bumpversion.cfg | 2 +- LICENSE | 2 +- README.md | 2 +- dbutils/__init__.py | 2 +- docs/changelog.html | 46 +++++++++++++++++++++++++++------------------ docs/changelog.rst | 13 ++++++++++++- docs/main.de.html | 4 ++-- docs/main.de.rst | 4 ++-- docs/main.html | 4 ++-- docs/main.rst | 4 ++-- setup.py | 2 +- 11 files changed, 53 insertions(+), 32 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 64494c4..663f79f 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.0.2 +current_version = 3.0.3 [bumpversion:file:setup.py] search = __version__ = '{current_version}' diff --git a/LICENSE b/LICENSE index 6c2a74b..15287a3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2022 Christoph Zwerschke +Copyright (c) 2023 Christoph Zwerschke Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ff2d82c..09ccb37 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ to a database that can be used in all kinds of multi-threaded environments. The suite supports DB-API 2 compliant database interfaces and the classic PyGreSQL interface. -The current version 3.0.2 of DBUtils supports Python versions 3.6 to 3.11. +The current version 3.0.3 of DBUtils supports Python versions 3.6 to 3.11. **Please have a look at the [changelog](https://webwareforpython.github.io/DBUtils/changelog.html), because there were some breaking changes in version 2.0.** diff --git a/dbutils/__init__.py b/dbutils/__init__.py index 7a0bdf2..f7a4ee8 100644 --- a/dbutils/__init__.py +++ b/dbutils/__init__.py @@ -5,4 +5,4 @@ 'simple_pooled_pg', 'steady_pg', 'pooled_pg', 'persistent_pg', 'simple_pooled_db', 'steady_db', 'pooled_db', 'persistent_db'] -__version__ = '3.0.2' +__version__ = '3.0.3' diff --git a/docs/changelog.html b/docs/changelog.html index e623513..1f5ce90 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -12,18 +12,28 @@ <h1 class="title">Changelog for DBUtils</h1> <section id="section-1"> +<h2>3.0.3</h2> +<p>DBUtils 3.0.3 was released on April 27, 2023.</p> +<p>Changes:</p> +<ul class="simple"> +<li><p>Support Python version 3.11.</p></li> +<li><p>Improve determination of DB API module if creator is specified.</p></li> +<li><p>Minor fixes and section an advanced usage in docs.</p></li> +</ul> +</section> +<section id="section-2"> <h2>3.0.2</h2> <p>DBUtils 3.0.2 was released on January 14, 2022.</p> -<p>The the optional iterator protocol on cursors is now supported.</p> +<p>The optional iterator protocol on cursors is now supported.</p> </section> -<section id="section-2"> +<section id="section-3"> <h2>3.0.1</h2> <p>DBUtils 3.0.1 was released on December 22, 2021.</p> <p>It includes <span class="docutils literal">InterfaceError</span> to the default list of exceptions for which the connection failover mechanism is applied. You can override this with the <span class="docutils literal">failures</span> parameter.</p> </section> -<section id="section-3"> +<section id="section-4"> <h2>3.0.0</h2> <p>DBUtils 3.0.0 was released on November 26, 2021.</p> <p>It is intended to be used with Python versions 3.6 to 3.10.</p> @@ -32,7 +42,7 @@ <h2>3.0.0</h2> <li><p>Cease support for Python 2 and 3.5, minor optimizations.</p></li> </ul> </section> -<section id="section-4"> +<section id="section-5"> <h2>2.0.3</h2> <p>DBUtils 2.0.3 was released on November 26, 2021.</p> <p>Changes:</p> @@ -40,7 +50,7 @@ <h2>2.0.3</h2> <li><p>Support Python version 3.10.</p></li> </ul> </section> -<section id="section-5"> +<section id="section-6"> <h2>2.0.2</h2> <p>DBUtils 2.0.2 was released on June 8, 2021.</p> <p>Changes:</p> @@ -48,7 +58,7 @@ <h2>2.0.2</h2> <li><p>Allow using context managers for pooled connections.</p></li> </ul> </section> -<section id="section-6"> +<section id="section-7"> <h2>2.0.1</h2> <p>DBUtils 2.0.1 was released on April 8, 2021.</p> <p>Changes:</p> @@ -56,7 +66,7 @@ <h2>2.0.1</h2> <li><p>Avoid "name Exception is not defined" when exiting.</p></li> </ul> </section> -<section id="section-7"> +<section id="section-8"> <h2>2.0</h2> <p>DBUtils 2.0 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> @@ -72,7 +82,7 @@ <h2>2.0</h2> <li><p>This changelog has been compiled from the former release notes.</p></li> </ul> </section> -<section id="section-8"> +<section id="section-9"> <h2>1.4</h2> <p>DBUtils 1.4 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> @@ -83,7 +93,7 @@ <h2>1.4</h2> inside a transaction.</p></li> </ul> </section> -<section id="section-9"> +<section id="section-10"> <h2>1.3</h2> <p>DBUtils 1.3 was released on March 3, 2018.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.4 to 3.7.</p> @@ -92,12 +102,12 @@ <h2>1.3</h2> <li><p>This version now supports context handlers for connections and cursors.</p></li> </ul> </section> -<section id="section-10"> +<section id="section-11"> <h2>1.2</h2> <p>DBUtils 1.2 was released on February 5, 2017.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.0 to 3.6.</p> </section> -<section id="section-11"> +<section id="section-12"> <h2>1.1.1</h2> <p>DBUtils 1.1.1 was released on February 4, 2017.</p> <p>It is intended to be used with Python versions 2.3 to 2.7.</p> @@ -111,7 +121,7 @@ <h2>1.1.1</h2> <li><p>Fixed a problem when running under Jython (reported by Vitaly Kruglikov).</p></li> </ul> </section> -<section id="section-12"> +<section id="section-13"> <h2>1.1</h2> <p>DBUtils 1.1 was released on August 14, 2011.</p> <p>Improvements:</p> @@ -140,7 +150,7 @@ <h2>1.1</h2> <li><p>Fixed some minor issues with the <span class="docutils literal">DBUtilsExample</span> for Webware.</p></li> </ul> </section> -<section id="section-13"> +<section id="section-14"> <h2>1.0</h2> <p>DBUtils 1.0 was released on November 29, 2008.</p> <p>It is intended to be used with Python versions 2.2 to 2.6.</p> @@ -173,7 +183,7 @@ <h2>1.0</h2> the MySQLdb module (problem reported by Gregory Pinero).</p></li> </ul> </section> -<section id="section-14"> +<section id="section-15"> <h2>0.9.4</h2> <p>DBUtils 0.9.4 was released on July 7, 2007.</p> <p>This release fixes a problem in the destructor code and has been supplemented @@ -182,7 +192,7 @@ <h2>0.9.4</h2> in the last release, since you can now pass custom creator functions for database connections instead of DB-API 2 modules.</p> </section> -<section id="section-15"> +<section id="section-16"> <h2>0.9.3</h2> <p>DBUtils 0.9.3 was released on May 21, 2007.</p> <p>Changes:</p> @@ -197,7 +207,7 @@ <h2>0.9.3</h2> Added Chinese translation of the User's Guide, kindly contributed by gashero.</p></li> </ul> </section> -<section id="section-16"> +<section id="section-17"> <h2>0.9.2</h2> <p>DBUtils 0.9.2 was released on September 22, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.5.</p> @@ -207,7 +217,7 @@ <h2>0.9.2</h2> storage engine. Accordingly, renamed <span class="docutils literal">SolidPg</span> to <span class="docutils literal">SteadyPg</span>.</p></li> </ul> </section> -<section id="section-17"> +<section id="section-18"> <h2>0.9.1</h2> <p>DBUtils 0.9.1 was released on May 8, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.4.</p> @@ -221,7 +231,7 @@ <h2>0.9.1</h2> <li><p>Improved the documentation and added a User's Guide.</p></li> </ul> </section> -<section id="section-18"> +<section id="section-19"> <h2>0.8.1 - 2005-09-13</h2> <p>DBUtils 0.8.1 was released on September 13, 2005.</p> <p>It is intended to be used with Python versions 2.0 to 2.4.</p> diff --git a/docs/changelog.rst b/docs/changelog.rst index 7706d89..6d0833f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,12 +1,23 @@ Changelog for DBUtils +++++++++++++++++++++ +3.0.3 +===== + +DBUtils 3.0.3 was released on April 27, 2023. + +Changes: + +* Support Python version 3.11. +* Improve determination of DB API module if creator is specified. +* Minor fixes and section an advanced usage in docs. + 3.0.2 ===== DBUtils 3.0.2 was released on January 14, 2022. -The the optional iterator protocol on cursors is now supported. +The optional iterator protocol on cursors is now supported. 3.0.1 ===== diff --git a/docs/main.de.html b/docs/main.de.html index 633b308..1a2dda7 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -12,7 +12,7 @@ <h1 class="title">Benutzeranleitung für DBUtils</h1> <dl class="docinfo simple"> <dt class="version">Version<span class="colon">:</span></dt> -<dd class="version">3.0.2</dd> +<dd class="version">3.0.3</dd> <dt class="translations">Translations<span class="colon">:</span></dt> <dd class="translations"><p><a class="reference external" href="main.html">English</a> | German</p> </dd> @@ -536,7 +536,7 @@ <h2>Autoren</h2> </section> <section id="copyright-und-lizenz"> <h2>Copyright und Lizenz</h2> -<p>Copyright © 2005-2021 Christoph Zwerschke. +<p>Copyright © 2005-2023 Christoph Zwerschke. Alle Rechte vorbehalten.</p> <p>DBUtils ist freie und quelloffene Software, lizenziert unter der <a class="reference external" href="https://opensource.org/licenses/MIT">MIT-Lizenz</a>.</p> diff --git a/docs/main.de.rst b/docs/main.de.rst index 147ce5e..aa800d6 100644 --- a/docs/main.de.rst +++ b/docs/main.de.rst @@ -1,7 +1,7 @@ Benutzeranleitung für DBUtils +++++++++++++++++++++++++++++ -:Version: 3.0.2 +:Version: 3.0.3 :Translations: English_ | German .. _English: main.html @@ -583,7 +583,7 @@ Autoren Copyright und Lizenz ==================== -Copyright © 2005-2021 Christoph Zwerschke. +Copyright © 2005-2023 Christoph Zwerschke. Alle Rechte vorbehalten. DBUtils ist freie und quelloffene Software, diff --git a/docs/main.html b/docs/main.html index a1b2628..c77a83b 100644 --- a/docs/main.html +++ b/docs/main.html @@ -12,7 +12,7 @@ <h1 class="title">DBUtils User's Guide</h1> <dl class="docinfo simple"> <dt class="version">Version<span class="colon">:</span></dt> -<dd class="version">3.0.2</dd> +<dd class="version">3.0.3</dd> <dt class="translations">Translations<span class="colon">:</span></dt> <dd class="translations"><p>English | <a class="reference external" href="main.de.html">German</a></p> </dd> @@ -494,7 +494,7 @@ <h2>Credits</h2> </section> <section id="copyright-and-license"> <h2>Copyright and License</h2> -<p>Copyright © 2005-2022 by Christoph Zwerschke. +<p>Copyright © 2005-2023 by Christoph Zwerschke. All Rights Reserved.</p> <p>DBUtils is free and open source software, licensed under the <a class="reference external" href="https://opensource.org/licenses/MIT">MIT license</a>.</p> diff --git a/docs/main.rst b/docs/main.rst index 2549118..4793d30 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -1,7 +1,7 @@ DBUtils User's Guide ++++++++++++++++++++ -:Version: 3.0.2 +:Version: 3.0.3 :Translations: English | German_ .. _German: main.de.html @@ -543,7 +543,7 @@ Credits Copyright and License ===================== -Copyright © 2005-2022 by Christoph Zwerschke. +Copyright © 2005-2023 by Christoph Zwerschke. All Rights Reserved. DBUtils is free and open source software, diff --git a/setup.py b/setup.py index 9baee37..f46fef6 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ warnings.filterwarnings('ignore', 'Unknown distribution option') -__version__ = '3.0.2' +__version__ = '3.0.3' readme = open('README.md').read() From 705e9400c983de5f7571d5cd824091feefda27e1 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Thu, 27 Apr 2023 19:18:33 +0200 Subject: [PATCH 49/84] Do not test with Python 3.6 on GitHub any more --- .github/workflows/test_with_tox.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_with_tox.yml b/.github/workflows/test_with_tox.yml index 24d7690..b68431e 100644 --- a/.github/workflows/test_with_tox.yml +++ b/.github/workflows/test_with_tox.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ['3.6', '3.7', '3.8', '3.9', '3.10', '3.11'] + python: ['3.7', '3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v3 From 6f52641b4fd262c2f6e0c933d0c393a0a1ef27a8 Mon Sep 17 00:00:00 2001 From: Christian Clauss <cclauss@me.com> Date: Sat, 29 Apr 2023 17:39:02 +0200 Subject: [PATCH 50/84] Replace flake8 with ruff (#46) --- .flake8 | 4 -- .github/workflows/test_with_tox.yml | 2 +- MANIFEST.in | 2 +- dbutils/pooled_pg.py | 2 +- dbutils/steady_db.py | 1 + docs/make.py | 13 ++--- pyproject.toml | 80 +++++++++++++++++++++++++++++ setup.py | 3 ++ tests/mock_pg.py | 2 + tox.ini | 8 +-- 10 files changed, 100 insertions(+), 17 deletions(-) delete mode 100644 .flake8 create mode 100644 pyproject.toml diff --git a/.flake8 b/.flake8 deleted file mode 100644 index e25de91..0000000 --- a/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -ignore = E722,W503 -exclude = .git,.pytest_cache,.tox,.venv,.idea,__pycache__,build,build,dist,docs,local -max-line-length = 79 diff --git a/.github/workflows/test_with_tox.yml b/.github/workflows/test_with_tox.yml index b68431e..dbc4ace 100644 --- a/.github/workflows/test_with_tox.yml +++ b/.github/workflows/test_with_tox.yml @@ -22,4 +22,4 @@ jobs: - run: tox -e py - if: matrix.python == 3.10 - run: TOXENV=flake8,manifest,docs,spell tox + run: TOXENV=ruff,manifest,docs,spell tox diff --git a/MANIFEST.in b/MANIFEST.in index be66fba..6408840 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,7 +5,7 @@ include README.md include .bumpversion.cfg include .codespellrc -include .flake8 +include pyproject.toml include tox.ini recursive-include tests *.py diff --git a/dbutils/pooled_pg.py b/dbutils/pooled_pg.py index 7bab407..e2389c7 100644 --- a/dbutils/pooled_pg.py +++ b/dbutils/pooled_pg.py @@ -112,7 +112,7 @@ Licensed under the MIT license. """ -from queue import Queue, Empty, Full +from queue import Empty, Full, Queue from . import __version__ from .steady_pg import SteadyPgConnection diff --git a/dbutils/steady_db.py b/dbutils/steady_db.py index 0e34a9b..15ddd6d 100644 --- a/dbutils/steady_db.py +++ b/dbutils/steady_db.py @@ -372,6 +372,7 @@ def _ping_check(self, ping=1, reconnect=True): self._store(con) alive = True return alive + return None def dbapi(self): """Return the underlying DB-API 2 module of the connection.""" diff --git a/docs/make.py b/docs/make.py index 28b9625..f6736b7 100755 --- a/docs/make.py +++ b/docs/make.py @@ -4,6 +4,7 @@ from glob import glob from os.path import splitext + from docutils.core import publish_file print("Creating the documentation...") @@ -25,11 +26,11 @@ output = publish_file( writer_name='html5', source=source, destination=destination, enable_exit_status=True, - settings_overrides=dict( - stylesheet_path='doc.css', - embed_stylesheet=False, - toc_backlinks=False, - language_code=lang, - exit_status_level=2)) + settings_overrides={ + "stylesheet_path": 'doc.css', + "embed_stylesheet": False, + "toc_backlinks": False, + "language_code": lang, + "exit_status_level": 2}) print("Done.") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ce67283 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,80 @@ +[tool.ruff] +select = [ + "A", # flake8-builtins + "ARG", # flake8-unused-arguments + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "C90", # McCabe cyclomatic complexity + "DTZ", # flake8-datetimez + "E", # pycodestyle + "EXE", # flake8-executable + "F", # Pyflakes + "G", # flake8-logging-format + "I", # isort + "ICN", # flake8-import-conventions + "INP", # flake8-no-pep420 + "INT", # flake8-gettext + "ISC", # flake8-implicit-str-concat + "N", # pep8-naming + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # Pylint + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "PYI", # flake8-pyi + "RET", # flake8-return + "RSE", # flake8-raise + "RUF", # Ruff-specific rules + "S", # flake8-bandit + "T10", # flake8-debugger + "TCH", # flake8-type-checking + "TID", # flake8-tidy-imports + "UP", # pyupgrade + "W", # pycodestyle + "YTT", # flake8-2020 + # "ANN", # flake8-annotations + # "BLE", # flake8-blind-except + # "COM", # flake8-commas + # "D", # pydocstyle + # "DJ", # flake8-django + # "EM", # flake8-errmsg + # "ERA", # eradicate + # "FBT", # flake8-boolean-trap + # "NPY", # NumPy-specific rules + # "PD", # pandas-vet + # "Q", # flake8-quotes + # "SIM", # flake8-simplify + # "SLF", # flake8-self + # "T20", # flake8-print + # "TRY", # tryceratops +] +# When removing rules from the `ignore` list, do `ruff rule ARG002` to see the docs +ignore = [ + "ARG002", + "B007", + "B904", + "E722", + "N811", + "N818", + "PIE790", + "PLR5501", + "PTH122", + "PTH123", + "S110", +] +line-length = 79 +target-version = "py37" + +[tool.ruff.mccabe] +max-complexity = 31 + +[tool.ruff.pylint] +allow-magic-value-types = ["int", "str"] +max-args = 12 +max-branches = 41 +max-statements = 95 + +[tool.ruff.per-file-ignores] +"docs/make.py" = ["INP001"] +"tests/*" = ["B023", "I001", "N8", "PGH004", "PLR0915", "PT009", "S101"] +"tests/mock_pg.py" = ["RET505"] diff --git a/setup.py b/setup.py index f46fef6..6a3c273 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,9 @@ +#!/usr/bin/env python3 + """Setup Script for DBUtils""" import warnings + try: from setuptools import setup except ImportError: diff --git a/tests/mock_pg.py b/tests/mock_pg.py index 7b4fcdc..3763aa9 100644 --- a/tests/mock_pg.py +++ b/tests/mock_pg.py @@ -59,11 +59,13 @@ def query(self, qstr): raise InternalError if qstr in ('begin', 'end', 'commit', 'rollback'): self.session.append(qstr) + return None elif qstr.startswith('select '): self.num_queries += 1 return qstr[7:] elif qstr.startswith('set '): self.session.append(qstr[4:]) + return None else: raise ProgrammingError diff --git a/tox.ini b/tox.ini index 6143fb1..42bd7a7 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py3{6,7,8,9,10,11}, flake8, manifest, docs, spell +envlist = py3{6,7,8,9,10,11}, ruff, manifest, docs, spell [testenv] setenv = @@ -14,11 +14,11 @@ deps = codespell commands = codespell . -[testenv:flake8] +[testenv:ruff] basepython = python3.10 -deps = flake8 +deps = ruff commands = - flake8 . + ruff . [testenv:manifest] basepython = python3.10 From 594b9fe70e0191251fccf29e475dfeac4119de4e Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 29 Apr 2023 17:43:43 +0200 Subject: [PATCH 51/84] Move to new minor release --- .bumpversion.cfg | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 663f79f..7d741e2 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.0.3 +current_version = 3.1.0b1 [bumpversion:file:setup.py] search = __version__ = '{current_version}' diff --git a/setup.py b/setup.py index 6a3c273..603cee3 100755 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ warnings.filterwarnings('ignore', 'Unknown distribution option') -__version__ = '3.0.3' +__version__ = '3.1.0b1' readme = open('README.md').read() From 6e0a4d0c496fa86da8ec8488d35a8e103972c111 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 29 Apr 2023 18:01:37 +0200 Subject: [PATCH 52/84] Require Python 3.7 --- setup.py | 6 ++++-- tox.ini | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 603cee3..4725628 100755 --- a/setup.py +++ b/setup.py @@ -37,7 +37,6 @@ 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', @@ -60,5 +59,8 @@ 'https://github.com/WebwareForPython/DBUtils'}, platforms=['any'], license='MIT License', - packages=['dbutils'] + packages=['dbutils'], + extras_require={ + "pg": ["PyGreSQL >= 5"] + } ) diff --git a/tox.ini b/tox.ini index 42bd7a7..1b03272 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py3{6,7,8,9,10,11}, ruff, manifest, docs, spell +envlist = py3{7,8,9,10,11}, ruff, manifest, docs, spell [testenv] setenv = From 540cc738389f0c6a2e0e509151f995391d1baa65 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 29 Apr 2023 21:07:36 +0200 Subject: [PATCH 53/84] Use pytest instead of unittest --- setup.py | 5 +- tests/__init__.py | 5 +- tests/mock_db.py | 15 + tests/mock_pg.py | 1 + tests/test_persistent_db.py | 516 +++---- tests/test_persistent_pg.py | 335 ++--- tests/test_pooled_db.py | 2505 ++++++++++++++++---------------- tests/test_pooled_pg.py | 573 ++++---- tests/test_simple_pooled_db.py | 260 ++-- tests/test_simple_pooled_pg.py | 199 +-- tests/test_steady_db.py | 1344 ++++++++--------- tests/test_steady_pg.py | 582 ++++---- tests/test_threading_local.py | 155 +- tox.ini | 2 +- 14 files changed, 3312 insertions(+), 3185 deletions(-) diff --git a/setup.py b/setup.py index 4725628..e5bb435 100755 --- a/setup.py +++ b/setup.py @@ -61,6 +61,7 @@ license='MIT License', packages=['dbutils'], extras_require={ - "pg": ["PyGreSQL >= 5"] - } + "pg": ["PyGreSQL>=5"] + }, + tests_require=["pytest>=7", "ruff"] ) diff --git a/tests/__init__.py b/tests/__init__.py index ec99602..49922b8 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1,4 @@ -# DBUtils tests +"""DBUtils tests""" + +# make sure the mock pg module is installed +from . import mock_pg as pg # noqa: F401 diff --git a/tests/mock_db.py b/tests/mock_db.py index ceca9e3..e3965a6 100644 --- a/tests/mock_db.py +++ b/tests/mock_db.py @@ -1,8 +1,23 @@ """This module serves as a mock object for the DB-API 2 module""" +import sys + +import pytest + +__all__ = ['dbapi'] + + threadsafety = 2 +@pytest.fixture() +def dbapi(): + """Get mock DB API 2 module.""" + mock_db = sys.modules[__name__] + mock_db.threadsafety = 2 + return mock_db + + class Error(Exception): pass diff --git a/tests/mock_pg.py b/tests/mock_pg.py index 3763aa9..bbb393c 100644 --- a/tests/mock_pg.py +++ b/tests/mock_pg.py @@ -2,6 +2,7 @@ import sys + sys.modules['pg'] = sys.modules[__name__] diff --git a/tests/test_persistent_db.py b/tests/test_persistent_db.py index 6f27108..25897fd 100644 --- a/tests/test_persistent_db.py +++ b/tests/test_persistent_db.py @@ -10,287 +10,293 @@ * This test was contributed by Christoph Zwerschke """ -import unittest +from queue import Queue, Empty +from threading import Thread -from . import mock_db as dbapi +import pytest -from dbutils.persistent_db import PersistentDB, local +from .mock_db import dbapi # noqa: F401 +from dbutils.persistent_db import NotSupportedError, PersistentDB, local -class TestPersistentDB(unittest.TestCase): - def setUp(self): - dbapi.threadsafety = 1 +def test_version(): + from dbutils import __version__, persistent_db + assert persistent_db.__version__ == __version__ + assert PersistentDB.version == __version__ - def test_version(self): - from dbutils import __version__, persistent_db - self.assertEqual(persistent_db.__version__, __version__) - self.assertEqual(PersistentDB.version, __version__) - def test_no_threadsafety(self): - from dbutils.persistent_db import NotSupportedError - for dbapi.threadsafety in (None, 0): - self.assertRaises(NotSupportedError, PersistentDB, dbapi) +@pytest.mark.parametrize("threadsafety", [None, 0]) +def test_no_threadsafety(dbapi, threadsafety): # noqa: F811 + dbapi.threadsafety = threadsafety + with pytest.raises(NotSupportedError): + PersistentDB(dbapi) - def test_close(self): - for closeable in (False, True): - persist = PersistentDB(dbapi, closeable=closeable) - db = persist.connection() - self.assertTrue(db._con.valid) - db.close() - self.assertTrue(closeable ^ db._con.valid) - db.close() - self.assertTrue(closeable ^ db._con.valid) - db._close() - self.assertFalse(db._con.valid) - db._close() - self.assertFalse(db._con.valid) - - def test_connection(self): - persist = PersistentDB(dbapi) - db = persist.connection() - db_con = db._con - self.assertIsNone(db_con.database) - self.assertIsNone(db_con.user) - db2 = persist.connection() - self.assertEqual(db, db2) - db3 = persist.dedicated_connection() - self.assertEqual(db, db3) - db3.close() - db2.close() - db.close() - - def test_threads(self): - num_threads = 3 - persist = PersistentDB(dbapi, closeable=True) - from queue import Queue, Empty - query_queue, result_queue = [], [] - for i in range(num_threads): - query_queue.append(Queue(1)) - result_queue.append(Queue(1)) - - def run_queries(i): - this_db = persist.connection() - while 1: - try: - try: - q = query_queue[i].get(1, 1) - except TypeError: - q = query_queue[i].get(1) - except Empty: - q = None - if not q: - break - db = persist.connection() - if db != this_db: - r = 'error - not persistent' - else: - if q == 'ping': - r = 'ok - thread alive' - elif q == 'close': - db.close() - r = 'ok - connection closed' - else: - cursor = db.cursor() - cursor.execute(q) - r = cursor.fetchone() - cursor.close() - r = f'{i}({db._usage}): {r}' + +@pytest.mark.parametrize("closeable", [False, True]) +def test_close(dbapi, closeable): # noqa: F811 + persist = PersistentDB(dbapi, closeable=closeable) + db = persist.connection() + assert db._con.valid is True + db.close() + assert closeable ^ db._con.valid + db.close() + assert closeable ^ db._con.valid + db._close() + assert db._con.valid is False + db._close() + assert db._con.valid is False + + +def test_connection(dbapi): # noqa: F811 + persist = PersistentDB(dbapi) + db = persist.connection() + db_con = db._con + assert db_con.database is None + assert db_con.user is None + db2 = persist.connection() + assert db == db2 + db3 = persist.dedicated_connection() + assert db == db3 + db3.close() + db2.close() + db.close() + + +def test_threads(dbapi): # noqa: F811 + num_threads = 3 + persist = PersistentDB(dbapi, closeable=True) + query_queue, result_queue = [], [] + for i in range(num_threads): + query_queue.append(Queue(1)) + result_queue.append(Queue(1)) + + def run_queries(i): + this_db = persist.connection() + db = None + while True: + try: try: - result_queue[i].put(r, 1, 1) + q = query_queue[i].get(1, 1) except TypeError: - result_queue[i].put(r, 1) - db.close() - - from threading import Thread - threads = [] - for i in range(num_threads): - thread = Thread(target=run_queries, args=(i,)) - threads.append(thread) - thread.start() - for i in range(num_threads): + q = query_queue[i].get(1) + except Empty: + q = None + if not q: + break + db = persist.connection() + if db != this_db: + r = 'error - not persistent' + else: + if q == 'ping': + r = 'ok - thread alive' + elif q == 'close': + db.close() + r = 'ok - connection closed' + else: + cursor = db.cursor() + cursor.execute(q) + r = cursor.fetchone() + cursor.close() + r = f'{i}({db._usage}): {r}' try: - query_queue[i].put('ping', 1, 1) + result_queue[i].put(r, 1, 1) except TypeError: - query_queue[i].put('ping', 1) - for i in range(num_threads): + result_queue[i].put(r, 1) + if db: + db.close() + + threads = [] + for i in range(num_threads): + thread = Thread(target=run_queries, args=(i,)) + threads.append(thread) + thread.start() + for i in range(num_threads): + try: + query_queue[i].put('ping', 1, 1) + except TypeError: + query_queue[i].put('ping', 1) + for i in range(num_threads): + try: + r = result_queue[i].get(1, 1) + except TypeError: + r = result_queue[i].get(1) + assert r == f'{i}(0): ok - thread alive' + assert threads[i].is_alive() + for i in range(num_threads): + for j in range(i + 1): try: + query_queue[i].put(f'select test{j}', 1, 1) r = result_queue[i].get(1, 1) except TypeError: + query_queue[i].put(f'select test{j}', 1) r = result_queue[i].get(1) - self.assertEqual(r, f'{i}(0): ok - thread alive') - self.assertTrue(threads[i].is_alive()) - for i in range(num_threads): - for j in range(i + 1): - try: - query_queue[i].put(f'select test{j}', 1, 1) - r = result_queue[i].get(1, 1) - except TypeError: - query_queue[i].put(f'select test{j}', 1) - r = result_queue[i].get(1) - self.assertEqual(r, f'{i}({j + 1}): test{j}') - try: - query_queue[1].put('select test4', 1, 1) - except TypeError: - query_queue[1].put('select test4', 1) + assert r == f'{i}({j + 1}): test{j}' + try: + query_queue[1].put('select test4', 1, 1) + except TypeError: + query_queue[1].put('select test4', 1) + try: + r = result_queue[1].get(1, 1) + except TypeError: + r = result_queue[1].get(1) + assert r == '1(3): test4' + try: + query_queue[1].put('close', 1, 1) + r = result_queue[1].get(1, 1) + except TypeError: + query_queue[1].put('close', 1) + r = result_queue[1].get(1) + assert r == '1(3): ok - connection closed' + for j in range(2): try: + query_queue[1].put(f'select test{j}', 1, 1) r = result_queue[1].get(1, 1) except TypeError: + query_queue[1].put(f'select test{j}', 1) r = result_queue[1].get(1) - self.assertEqual(r, '1(3): test4') + assert r == f'1({j + 1}): test{j}' + for i in range(num_threads): + assert threads[i].is_alive() try: - query_queue[1].put('close', 1, 1) - r = result_queue[1].get(1, 1) + query_queue[i].put('ping', 1, 1) except TypeError: - query_queue[1].put('close', 1) - r = result_queue[1].get(1) - self.assertEqual(r, '1(3): ok - connection closed') - for j in range(2): - try: - query_queue[1].put(f'select test{j}', 1, 1) - r = result_queue[1].get(1, 1) - except TypeError: - query_queue[1].put(f'select test{j}', 1) - r = result_queue[1].get(1) - self.assertEqual(r, f'1({j + 1}): test{j}') - for i in range(num_threads): - self.assertTrue(threads[i].is_alive()) - try: - query_queue[i].put('ping', 1, 1) - except TypeError: - query_queue[i].put('ping', 1) - for i in range(num_threads): - try: - r = result_queue[i].get(1, 1) - except TypeError: - r = result_queue[i].get(1) - self.assertEqual(r, f'{i}({i + 1}): ok - thread alive') - self.assertTrue(threads[i].is_alive()) - for i in range(num_threads): - try: - query_queue[i].put(None, 1, 1) - except TypeError: - query_queue[i].put(None, 1) - - def test_maxusage(self): - persist = PersistentDB(dbapi, 20) - db = persist.connection() - self.assertEqual(db._maxusage, 20) - for i in range(100): - cursor = db.cursor() - cursor.execute(f'select test{i}') - r = cursor.fetchone() - cursor.close() - self.assertEqual(r, f'test{i}') - self.assertTrue(db._con.valid) - j = i % 20 + 1 - self.assertEqual(db._usage, j) - self.assertEqual(db._con.num_uses, j) - self.assertEqual(db._con.num_queries, j) - - def test_setsession(self): - persist = PersistentDB(dbapi, 3, ('set datestyle',)) - db = persist.connection() - self.assertEqual(db._maxusage, 3) - self.assertEqual(db._setsession_sql, ('set datestyle',)) - self.assertEqual(db._con.session, ['datestyle']) + query_queue[i].put('ping', 1) + for i in range(num_threads): + try: + r = result_queue[i].get(1, 1) + except TypeError: + r = result_queue[i].get(1) + assert r == f'{i}({i + 1}): ok - thread alive' + assert threads[i].is_alive() + for i in range(num_threads): + try: + query_queue[i].put(None, 1, 1) + except TypeError: + query_queue[i].put(None, 1) + + +def test_maxusage(dbapi): # noqa: F811 + persist = PersistentDB(dbapi, 20) + db = persist.connection() + assert db._maxusage == 20 + for i in range(100): cursor = db.cursor() - cursor.execute('set test') - cursor.fetchone() + cursor.execute(f'select test{i}') + r = cursor.fetchone() cursor.close() - for i in range(3): - self.assertEqual(db._con.session, ['datestyle', 'test']) - cursor = db.cursor() - cursor.execute('select test') - cursor.fetchone() - cursor.close() - self.assertEqual(db._con.session, ['datestyle']) - - def test_threadlocal(self): - persist = PersistentDB(dbapi) - self.assertTrue(isinstance(persist.thread, local)) - - class threadlocal: - pass - - persist = PersistentDB(dbapi, threadlocal=threadlocal) - self.assertTrue(isinstance(persist.thread, threadlocal)) - - def test_ping_check(self): - Connection = dbapi.Connection - Connection.has_ping = True - Connection.num_pings = 0 - persist = PersistentDB(dbapi, 0, None, None, 0, True) - db = persist.connection() - self.assertTrue(db._con.valid) - self.assertEqual(Connection.num_pings, 0) - db.close() - db = persist.connection() - self.assertFalse(db._con.valid) - self.assertEqual(Connection.num_pings, 0) - persist = PersistentDB(dbapi, 0, None, None, 1, True) - db = persist.connection() - self.assertTrue(db._con.valid) - self.assertEqual(Connection.num_pings, 1) - db.close() - db = persist.connection() - self.assertTrue(db._con.valid) - self.assertEqual(Connection.num_pings, 2) - persist = PersistentDB(dbapi, 0, None, None, 2, True) - db = persist.connection() - self.assertTrue(db._con.valid) - self.assertEqual(Connection.num_pings, 2) - db.close() - db = persist.connection() - self.assertFalse(db._con.valid) - self.assertEqual(Connection.num_pings, 2) - cursor = db.cursor() - self.assertTrue(db._con.valid) - self.assertEqual(Connection.num_pings, 3) - cursor.execute('select test') - self.assertTrue(db._con.valid) - self.assertEqual(Connection.num_pings, 3) - persist = PersistentDB(dbapi, 0, None, None, 4, True) - db = persist.connection() - self.assertTrue(db._con.valid) - self.assertEqual(Connection.num_pings, 3) - db.close() - db = persist.connection() - self.assertFalse(db._con.valid) - self.assertEqual(Connection.num_pings, 3) - cursor = db.cursor() - db._con.close() - self.assertFalse(db._con.valid) - self.assertEqual(Connection.num_pings, 3) - cursor.execute('select test') - self.assertTrue(db._con.valid) - self.assertEqual(Connection.num_pings, 4) - Connection.has_ping = False - Connection.num_pings = 0 - - def test_failed_transaction(self): - persist = PersistentDB(dbapi) - db = persist.connection() + assert r == f'test{i}' + assert db._con.valid is True + j = i % 20 + 1 + assert db._usage == j + assert db._con.num_uses == j + assert db._con.num_queries == j + + +def test_setsession(dbapi): # noqa: F811 + persist = PersistentDB(dbapi, 3, ('set datestyle',)) + db = persist.connection() + assert db._maxusage == 3 + assert db._setsession_sql == ('set datestyle',) + assert db._con.session == ['datestyle'] + cursor = db.cursor() + cursor.execute('set test') + cursor.fetchone() + cursor.close() + for i in range(3): + assert db._con.session == ['datestyle', 'test'] cursor = db.cursor() - db._con.close() - cursor.execute('select test') - db.begin() - db._con.close() - self.assertRaises(dbapi.InternalError, cursor.execute, 'select test') - cursor.execute('select test') - db.begin() - db.cancel() - db._con.close() cursor.execute('select test') + cursor.fetchone() + cursor.close() + assert db._con.session == ['datestyle'] + + +def test_threadlocal(dbapi): # noqa: F811 + persist = PersistentDB(dbapi) + assert isinstance(persist.thread, local) - def test_context_manager(self): - persist = PersistentDB(dbapi) - with persist.connection() as db: - with db.cursor() as cursor: - cursor.execute('select test') - r = cursor.fetchone() - self.assertEqual(r, 'test') + class Threadlocal: + pass + persist = PersistentDB(dbapi, threadlocal=Threadlocal) + assert isinstance(persist.thread, Threadlocal) -if __name__ == '__main__': - unittest.main() + +def test_ping_check(dbapi): # noqa: F811 + con_cls = dbapi.Connection + con_cls.has_ping = True + con_cls.num_pings = 0 + persist = PersistentDB(dbapi, 0, None, None, 0, True) + db = persist.connection() + assert db._con.valid is True + assert con_cls.num_pings == 0 + db.close() + db = persist.connection() + assert db._con.valid is False + assert con_cls.num_pings == 0 + persist = PersistentDB(dbapi, 0, None, None, 1, True) + db = persist.connection() + assert db._con.valid is True + assert con_cls.num_pings == 1 + db.close() + db = persist.connection() + assert db._con.valid is True + assert con_cls.num_pings == 2 + persist = PersistentDB(dbapi, 0, None, None, 2, True) + db = persist.connection() + assert db._con.valid is True + assert con_cls.num_pings == 2 + db.close() + db = persist.connection() + assert db._con.valid is False + assert con_cls.num_pings == 2 + cursor = db.cursor() + assert db._con.valid is True + assert con_cls.num_pings == 3 + cursor.execute('select test') + assert db._con.valid is True + assert con_cls.num_pings == 3 + persist = PersistentDB(dbapi, 0, None, None, 4, True) + db = persist.connection() + assert db._con.valid is True + assert con_cls.num_pings == 3 + db.close() + db = persist.connection() + assert db._con.valid is False + assert con_cls.num_pings == 3 + cursor = db.cursor() + db._con.close() + assert db._con.valid is False + assert con_cls.num_pings == 3 + cursor.execute('select test') + assert db._con.valid is True + assert con_cls.num_pings == 4 + con_cls.has_ping = False + con_cls.num_pings = 0 + + +def test_failed_transaction(dbapi): # noqa: F811 + persist = PersistentDB(dbapi) + db = persist.connection() + cursor = db.cursor() + db._con.close() + cursor.execute('select test') + db.begin() + db._con.close() + with pytest.raises(dbapi.InternalError): + cursor.execute('select test') + cursor.execute('select test') + db.begin() + db.cancel() + db._con.close() + cursor.execute('select test') + + +def test_context_manager(dbapi): # noqa: F811 + persist = PersistentDB(dbapi) + with persist.connection() as db: + with db.cursor() as cursor: + cursor.execute('select test') + r = cursor.fetchone() + assert r == 'test' diff --git a/tests/test_persistent_pg.py b/tests/test_persistent_pg.py index 3c8ce83..bd6c21a 100644 --- a/tests/test_persistent_pg.py +++ b/tests/test_persistent_pg.py @@ -10,188 +10,191 @@ * This test was contributed by Christoph Zwerschke """ -import unittest +from queue import Queue, Empty +from threading import Thread -from . import mock_pg as pg +import pg +import pytest from dbutils.persistent_pg import PersistentPg -class TestPersistentPg(unittest.TestCase): - - def test_version(self): - from dbutils import __version__, persistent_pg - self.assertEqual(persistent_pg.__version__, __version__) - self.assertEqual(PersistentPg.version, __version__) - - def test_close(self): - for closeable in (False, True): - persist = PersistentPg(closeable=closeable) - db = persist.connection() - self.assertTrue(db._con.db and db._con.valid) - db.close() - self.assertTrue( - closeable ^ (db._con.db is not None and db._con.valid)) - db.close() - self.assertTrue( - closeable ^ (db._con.db is not None and db._con.valid)) - db._close() - self.assertFalse(db._con.db and db._con.valid) - db._close() - self.assertFalse(db._con.db and db._con.valid) - - def test_threads(self): - num_threads = 3 - persist = PersistentPg() - from queue import Queue, Empty - query_queue, result_queue = [], [] - for i in range(num_threads): - query_queue.append(Queue(1)) - result_queue.append(Queue(1)) - - def run_queries(i): - this_db = persist.connection().db - while 1: - try: - try: - q = query_queue[i].get(1, 1) - except TypeError: - q = query_queue[i].get(1) - except Empty: - q = None - if not q: - break - db = persist.connection() - if db.db != this_db: - r = 'error - not persistent' - else: - if q == 'ping': - r = 'ok - thread alive' - elif q == 'close': - db.db.close() - r = 'ok - connection closed' - else: - r = db.query(q) - r = f'{i}({db._usage}): {r}' +def test_version(): + from dbutils import __version__, persistent_pg + assert persistent_pg.__version__ == __version__ + assert PersistentPg.version == __version__ + + +@pytest.mark.parametrize("closeable", [False, True]) +def test_close(closeable): + persist = PersistentPg(closeable=closeable) + db = persist.connection() + assert db._con.db + assert db._con.valid is True + db.close() + assert closeable ^ (db._con.db is not None and db._con.valid) + db.close() + assert closeable ^ (db._con.db is not None and db._con.valid) + db._close() + assert not db._con.db + db._close() + assert not db._con.db + + +def test_threads(): + num_threads = 3 + persist = PersistentPg() + query_queue, result_queue = [], [] + for i in range(num_threads): + query_queue.append(Queue(1)) + result_queue.append(Queue(1)) + + def run_queries(i): + this_db = persist.connection().db + db = None + while True: + try: try: - result_queue[i].put(r, 1, 1) + q = query_queue[i].get(1, 1) except TypeError: - result_queue[i].put(r, 1) - db.close() - - from threading import Thread - threads = [] - for i in range(num_threads): - thread = Thread(target=run_queries, args=(i,)) - threads.append(thread) - thread.start() - for i in range(num_threads): + q = query_queue[i].get(1) + except Empty: + q = None + if not q: + break + db = persist.connection() + if db.db != this_db: + r = 'error - not persistent' + else: + if q == 'ping': + r = 'ok - thread alive' + elif q == 'close': + db.db.close() + r = 'ok - connection closed' + else: + r = db.query(q) + r = f'{i}({db._usage}): {r}' try: - query_queue[i].put('ping', 1, 1) + result_queue[i].put(r, 1, 1) except TypeError: - query_queue[i].put('ping', 1) - for i in range(num_threads): + result_queue[i].put(r, 1) + if db: + db.close() + + threads = [] + for i in range(num_threads): + thread = Thread(target=run_queries, args=(i,)) + threads.append(thread) + thread.start() + for i in range(num_threads): + try: + query_queue[i].put('ping', 1, 1) + except TypeError: + query_queue[i].put('ping', 1) + for i in range(num_threads): + try: + r = result_queue[i].get(1, 1) + except TypeError: + r = result_queue[i].get(1) + assert r == f'{i}(0): ok - thread alive' + assert threads[i].is_alive() + for i in range(num_threads): + for j in range(i + 1): try: + query_queue[i].put(f'select test{j}', 1, 1) r = result_queue[i].get(1, 1) except TypeError: + query_queue[i].put(f'select test{j}', 1) r = result_queue[i].get(1) - self.assertEqual(r, f'{i}(0): ok - thread alive') - self.assertTrue(threads[i].is_alive()) - for i in range(num_threads): - for j in range(i + 1): - try: - query_queue[i].put(f'select test{j}', 1, 1) - r = result_queue[i].get(1, 1) - except TypeError: - query_queue[i].put(f'select test{j}', 1) - r = result_queue[i].get(1) - self.assertEqual(r, f'{i}({j + 1}): test{j}') + assert r == f'{i}({j + 1}): test{j}' + try: + query_queue[1].put('select test4', 1, 1) + r = result_queue[1].get(1, 1) + except TypeError: + query_queue[1].put('select test4', 1) + r = result_queue[1].get(1) + assert r == '1(3): test4' + try: + query_queue[1].put('close', 1, 1) + r = result_queue[1].get(1, 1) + except TypeError: + query_queue[1].put('close', 1) + r = result_queue[1].get(1) + assert r == '1(3): ok - connection closed' + for j in range(2): try: - query_queue[1].put('select test4', 1, 1) + query_queue[1].put(f'select test{j}', 1, 1) r = result_queue[1].get(1, 1) except TypeError: - query_queue[1].put('select test4', 1) + query_queue[1].put(f'select test{j}', 1) r = result_queue[1].get(1) - self.assertEqual(r, '1(3): test4') + assert r == f'1({j + 1}): test{j}' + for i in range(num_threads): + assert threads[i].is_alive() try: - query_queue[1].put('close', 1, 1) - r = result_queue[1].get(1, 1) + query_queue[i].put('ping', 1, 1) except TypeError: - query_queue[1].put('close', 1) - r = result_queue[1].get(1) - self.assertEqual(r, '1(3): ok - connection closed') - for j in range(2): - try: - query_queue[1].put(f'select test{j}', 1, 1) - r = result_queue[1].get(1, 1) - except TypeError: - query_queue[1].put(f'select test{j}', 1) - r = result_queue[1].get(1) - self.assertEqual(r, f'1({j + 1}): test{j}') - for i in range(num_threads): - self.assertTrue(threads[i].is_alive()) - try: - query_queue[i].put('ping', 1, 1) - except TypeError: - query_queue[i].put('ping', 1) - for i in range(num_threads): - try: - r = result_queue[i].get(1, 1) - except TypeError: - r = result_queue[i].get(1) - self.assertEqual(r, f'{i}({i + 1}): ok - thread alive') - self.assertTrue(threads[i].is_alive()) - for i in range(num_threads): - try: - query_queue[i].put(None, 1, 1) - except TypeError: - query_queue[i].put(None, 1) - - def test_maxusage(self): - persist = PersistentPg(20) - db = persist.connection() - self.assertEqual(db._maxusage, 20) - for i in range(100): - r = db.query(f'select test{i}') - self.assertEqual(r, f'test{i}') - self.assertTrue(db.db.status) - j = i % 20 + 1 - self.assertEqual(db._usage, j) - self.assertEqual(db.num_queries, j) - - def test_setsession(self): - persist = PersistentPg(3, ('set datestyle',)) - db = persist.connection() - self.assertEqual(db._maxusage, 3) - self.assertEqual(db._setsession_sql, ('set datestyle',)) - self.assertEqual(db.db.session, ['datestyle']) - db.query('set test') - for i in range(3): - self.assertEqual(db.db.session, ['datestyle', 'test']) - db.query('select test') - self.assertEqual(db.db.session, ['datestyle']) - - def test_failed_transaction(self): - persist = PersistentPg() - db = persist.connection() - db._con.close() - self.assertEqual(db.query('select test'), 'test') - db.begin() - db._con.close() - self.assertRaises(pg.InternalError, db.query, 'select test') - self.assertEqual(db.query('select test'), 'test') - db.begin() - self.assertEqual(db.query('select test'), 'test') - db.rollback() - db._con.close() - self.assertEqual(db.query('select test'), 'test') - - def test_context_manager(self): - persist = PersistentPg() - with persist.connection() as db: - db.query('select test') - self.assertEqual(db.num_queries, 1) - - -if __name__ == '__main__': - unittest.main() + query_queue[i].put('ping', 1) + for i in range(num_threads): + try: + r = result_queue[i].get(1, 1) + except TypeError: + r = result_queue[i].get(1) + assert r == f'{i}({i + 1}): ok - thread alive' + assert threads[i].is_alive() + for i in range(num_threads): + try: + query_queue[i].put(None, 1, 1) + except TypeError: + query_queue[i].put(None, 1) + + +def test_maxusage(): + persist = PersistentPg(20) + db = persist.connection() + assert db._maxusage == 20 + for i in range(100): + r = db.query(f'select test{i}') + assert r == f'test{i}' + assert db.db.status + j = i % 20 + 1 + assert db._usage == j + assert db.num_queries == j + + +def test_setsession(): + persist = PersistentPg(3, ('set datestyle',)) + db = persist.connection() + assert db._maxusage == 3 + assert db._setsession_sql == ('set datestyle',) + assert db.db.session == ['datestyle'] + db.query('set test') + for i in range(3): + assert db.db.session == ['datestyle', 'test'] + db.query('select test') + assert db.db.session == ['datestyle'] + + +def test_failed_transaction(): + persist = PersistentPg() + db = persist.connection() + db._con.close() + assert db.query('select test') == 'test' + db.begin() + db._con.close() + with pytest.raises(pg.InternalError): + db.query('select test') + assert db.query('select test') == 'test' + db.begin() + assert db.query('select test') == 'test' + db.rollback() + db._con.close() + assert db.query('select test') == 'test' + + +def test_context_manager(): + persist = PersistentPg() + with persist.connection() as db: + db.query('select test') + assert db.num_queries == 1 + diff --git a/tests/test_pooled_db.py b/tests/test_pooled_db.py index 3beb2d4..ebbe88d 100644 --- a/tests/test_pooled_db.py +++ b/tests/test_pooled_db.py @@ -10,1248 +10,1295 @@ * This test was contributed by Christoph Zwerschke """ -import unittest +from queue import Queue, Empty +from threading import Thread -from . import mock_db as dbapi +import pytest + +from .mock_db import dbapi # noqa: F401 from dbutils.pooled_db import ( - PooledDB, SharedDBConnection, InvalidConnection, TooManyConnections) - - -class TestPooledDB(unittest.TestCase): - - def test_version(self): - from dbutils import __version__, pooled_db - self.assertEqual(pooled_db.__version__, __version__) - self.assertEqual(PooledDB.version, __version__) - - def test_no_threadsafety(self): - from dbutils.pooled_db import NotSupportedError - for threadsafety in (None, 0): - dbapi.threadsafety = threadsafety - self.assertRaises(NotSupportedError, PooledDB, dbapi) - - def test_threadsafety(self): - for threadsafety in (1, 2, 3): - dbapi.threadsafety = threadsafety - pool = PooledDB(dbapi, 0, 0, 1) - self.assertTrue(hasattr(pool, '_maxshared')) - if threadsafety > 1: - self.assertEqual(pool._maxshared, 1) - self.assertTrue(hasattr(pool, '_shared_cache')) - else: - self.assertEqual(pool._maxshared, 0) - self.assertFalse(hasattr(pool, '_shared_cache')) - - def test_create_connection(self): - for threadsafety in (1, 2): - dbapi.threadsafety = threadsafety - shareable = threadsafety > 1 - pool = PooledDB( - dbapi, 1, 1, 1, 0, False, None, None, True, None, None, - 'PooledDBTestDB', user='PooledDBTestUser') - self.assertTrue(hasattr(pool, '_idle_cache')) - self.assertEqual(len(pool._idle_cache), 1) - if shareable: - self.assertTrue(hasattr(pool, '_shared_cache')) - self.assertEqual(len(pool._shared_cache), 0) - else: - self.assertFalse(hasattr(pool, '_shared_cache')) - self.assertTrue(hasattr(pool, '_maxusage')) - self.assertIsNone(pool._maxusage) - self.assertTrue(hasattr(pool, '_setsession')) - self.assertIsNone(pool._setsession) - con = pool._idle_cache[0] - from dbutils.steady_db import SteadyDBConnection - self.assertTrue(isinstance(con, SteadyDBConnection)) - self.assertTrue(hasattr(con, '_maxusage')) - self.assertEqual(con._maxusage, 0) - self.assertTrue(hasattr(con, '_setsession_sql')) - self.assertIsNone(con._setsession_sql) - db = pool.connection() - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 1) - self.assertTrue(hasattr(db, '_con')) - self.assertEqual(db._con, con) - self.assertTrue(hasattr(db, 'cursor')) - self.assertTrue(hasattr(db, '_usage')) - self.assertEqual(db._usage, 0) - self.assertTrue(hasattr(con, '_con')) - db_con = con._con - self.assertTrue(hasattr(db_con, 'database')) - self.assertEqual(db_con.database, 'PooledDBTestDB') - self.assertTrue(hasattr(db_con, 'user')) - self.assertEqual(db_con.user, 'PooledDBTestUser') - self.assertTrue(hasattr(db_con, 'open_cursors')) - self.assertEqual(db_con.open_cursors, 0) - self.assertTrue(hasattr(db_con, 'num_uses')) - self.assertEqual(db_con.num_uses, 0) - self.assertTrue(hasattr(db_con, 'num_queries')) - self.assertEqual(db_con.num_queries, 0) - cursor = db.cursor() - self.assertEqual(db_con.open_cursors, 1) - cursor.execute('select test') - r = cursor.fetchone() - cursor.close() - self.assertEqual(db_con.open_cursors, 0) - self.assertEqual(r, 'test') - self.assertEqual(db_con.num_queries, 1) - self.assertEqual(db._usage, 1) - cursor = db.cursor() - self.assertEqual(db_con.open_cursors, 1) - cursor.execute('set sessiontest') - cursor2 = db.cursor() - self.assertEqual(db_con.open_cursors, 2) - cursor2.close() - self.assertEqual(db_con.open_cursors, 1) - cursor.close() - self.assertEqual(db_con.open_cursors, 0) - self.assertEqual(db_con.num_queries, 1) - self.assertEqual(db._usage, 2) - self.assertEqual( - db_con.session, ['rollback', 'sessiontest']) - pool = PooledDB(dbapi, 1, 1, 1) - self.assertEqual(len(pool._idle_cache), 1) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - db = pool.connection() - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 1) - db.close() - self.assertEqual(len(pool._idle_cache), 1) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - db = pool.connection(True) - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 1) - db.close() - self.assertEqual(len(pool._idle_cache), 1) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - db = pool.connection(False) - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - self.assertEqual(db._usage, 0) - db_con = db._con._con - self.assertIsNone(db_con.database) - self.assertIsNone(db_con.user) - db.close() - self.assertEqual(len(pool._idle_cache), 1) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - db = pool.dedicated_connection() - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - self.assertEqual(db._usage, 0) - db_con = db._con._con - self.assertIsNone(db_con.database) - self.assertIsNone(db_con.user) - db.close() - self.assertEqual(len(pool._idle_cache), 1) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - pool = PooledDB(dbapi, 0, 0, 0, 0, False, 3, ('set datestyle',)) - self.assertEqual(pool._maxusage, 3) - self.assertEqual(pool._setsession, ('set datestyle',)) - con = pool.connection()._con - self.assertEqual(con._maxusage, 3) - self.assertEqual(con._setsession_sql, ('set datestyle',)) - - def test_close_connection(self): - for threadsafety in (1, 2): - dbapi.threadsafety = threadsafety - shareable = threadsafety > 1 - pool = PooledDB( - dbapi, 0, 1, 1, 0, False, None, None, True, None, None, - 'PooledDBTestDB', user='PooledDBTestUser') - self.assertTrue(hasattr(pool, '_idle_cache')) - self.assertEqual(len(pool._idle_cache), 0) - db = pool.connection() - self.assertTrue(hasattr(db, '_con')) - con = db._con - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 1) - self.assertTrue(hasattr(db, '_shared_con')) - shared_con = db._shared_con - self.assertEqual(pool._shared_cache[0], shared_con) - self.assertTrue(hasattr(shared_con, 'shared')) - self.assertEqual(shared_con.shared, 1) - self.assertTrue(hasattr(shared_con, 'con')) - self.assertEqual(shared_con.con, con) - from dbutils.steady_db import SteadyDBConnection - self.assertTrue(isinstance(con, SteadyDBConnection)) - self.assertTrue(hasattr(con, '_con')) - db_con = con._con - self.assertTrue(hasattr(db_con, 'num_queries')) - self.assertEqual(db._usage, 0) - self.assertEqual(db_con.num_queries, 0) - db.cursor().execute('select test') - self.assertEqual(db._usage, 1) - self.assertEqual(db_con.num_queries, 1) - db.close() - self.assertIsNone(db._con) - if shareable: - self.assertIsNone(db._shared_con) - self.assertEqual(shared_con.shared, 0) - self.assertRaises(InvalidConnection, getattr, db, '_usage') - self.assertFalse(hasattr(db_con, '_num_queries')) - self.assertEqual(len(pool._idle_cache), 1) - self.assertEqual(pool._idle_cache[0]._con, db_con) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - db.close() - if shareable: - self.assertEqual(shared_con.shared, 0) - db = pool.connection() - self.assertEqual(db._con, con) - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 1) - shared_con = db._shared_con - self.assertEqual(pool._shared_cache[0], shared_con) - self.assertEqual(shared_con.con, con) - self.assertEqual(shared_con.shared, 1) - self.assertEqual(db._usage, 1) - self.assertEqual(db_con.num_queries, 1) - self.assertTrue(hasattr(db_con, 'database')) - self.assertEqual(db_con.database, 'PooledDBTestDB') - self.assertTrue(hasattr(db_con, 'user')) - self.assertEqual(db_con.user, 'PooledDBTestUser') - db.cursor().execute('select test') - self.assertEqual(db_con.num_queries, 2) - db.cursor().execute('select test') - self.assertEqual(db_con.num_queries, 3) - db.close() - self.assertEqual(len(pool._idle_cache), 1) - self.assertEqual(pool._idle_cache[0]._con, db_con) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - db = pool.connection(False) - self.assertEqual(db._con, con) - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - db.close() - self.assertEqual(len(pool._idle_cache), 1) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - - def test_close_all(self): - for threadsafety in (1, 2): - dbapi.threadsafety = threadsafety - shareable = threadsafety > 1 - pool = PooledDB(dbapi, 10) - self.assertEqual(len(pool._idle_cache), 10) - pool.close() - self.assertEqual(len(pool._idle_cache), 0) - pool = PooledDB(dbapi, 10) - closed = ['no'] - - def close(what=closed): - what[0] = 'yes' - - pool._idle_cache[7]._con.close = close - self.assertEqual(closed, ['no']) - del pool - self.assertEqual(closed, ['yes']) - pool = PooledDB(dbapi, 10, 10, 5) - self.assertEqual(len(pool._idle_cache), 10) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - cache = [] - for i in range(5): - cache.append(pool.connection()) - self.assertEqual(len(pool._idle_cache), 5) - if shareable: - self.assertEqual(len(pool._shared_cache), 5) - else: - self.assertEqual(len(pool._idle_cache), 5) - pool.close() - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - pool = PooledDB(dbapi, 10, 10, 5) - closed = [] - - def close_idle(what=closed): - what.append('idle') - - def close_shared(what=closed): - what.append('shared') - - if shareable: - cache = [] - for i in range(5): - cache.append(pool.connection()) - pool._shared_cache[3].con.close = close_shared - else: - pool._idle_cache[7]._con.close = close_shared - pool._idle_cache[3]._con.close = close_idle - self.assertEqual(closed, []) - del pool - if shareable: - del cache - self.assertEqual(closed, ['idle', 'shared']) - - def test_shareable_connection(self): - for threadsafety in (1, 2): - dbapi.threadsafety = threadsafety - shareable = threadsafety > 1 - pool = PooledDB(dbapi, 0, 1, 2) - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - db1 = pool.connection() - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 1) - db2 = pool.connection() - self.assertNotEqual(db1._con, db2._con) - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 2) - db3 = pool.connection() - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 2) - self.assertEqual(db3._con, db1._con) - self.assertEqual(db1._shared_con.shared, 2) - self.assertEqual(db2._shared_con.shared, 1) - else: - self.assertNotEqual(db3._con, db1._con) - self.assertNotEqual(db3._con, db2._con) - db4 = pool.connection() - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 2) - self.assertEqual(db4._con, db2._con) - self.assertEqual(db1._shared_con.shared, 2) - self.assertEqual(db2._shared_con.shared, 2) - else: - self.assertNotEqual(db4._con, db1._con) - self.assertNotEqual(db4._con, db2._con) - self.assertNotEqual(db4._con, db3._con) - db5 = pool.connection(False) - self.assertNotEqual(db5._con, db1._con) - self.assertNotEqual(db5._con, db2._con) - self.assertNotEqual(db5._con, db3._con) - self.assertNotEqual(db5._con, db4._con) - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 2) - self.assertEqual(db1._shared_con.shared, 2) - self.assertEqual(db2._shared_con.shared, 2) - db5.close() - self.assertEqual(len(pool._idle_cache), 1) - db5 = pool.connection() - if shareable: - self.assertEqual(len(pool._idle_cache), 1) - self.assertEqual(len(pool._shared_cache), 2) - self.assertEqual(db5._shared_con.shared, 3) - else: - self.assertEqual(len(pool._idle_cache), 0) - pool = PooledDB(dbapi, 0, 0, 1) - self.assertEqual(len(pool._idle_cache), 0) - db1 = pool.connection(False) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - db2 = pool.connection() - if shareable: - self.assertEqual(len(pool._shared_cache), 1) - db3 = pool.connection() - if shareable: - self.assertEqual(len(pool._shared_cache), 1) - self.assertEqual(db2._con, db3._con) - else: - self.assertNotEqual(db2._con, db3._con) - del db3 - if shareable: - self.assertEqual(len(pool._idle_cache), 0) - self.assertEqual(len(pool._shared_cache), 1) - else: - self.assertEqual(len(pool._idle_cache), 1) - del db2 - if shareable: - self.assertEqual(len(pool._idle_cache), 1) - self.assertEqual(len(pool._shared_cache), 0) - else: - self.assertEqual(len(pool._idle_cache), 2) - del db1 - if shareable: - self.assertEqual(len(pool._idle_cache), 2) - self.assertEqual(len(pool._shared_cache), 0) - else: - self.assertEqual(len(pool._idle_cache), 3) - - def test_min_max_cached(self): - for threadsafety in (1, 2): - dbapi.threadsafety = threadsafety - shareable = threadsafety > 1 - pool = PooledDB(dbapi, 3) - self.assertEqual(len(pool._idle_cache), 3) - cache = [pool.connection() for i in range(3)] - self.assertEqual(len(pool._idle_cache), 0) - del cache - self.assertEqual(len(pool._idle_cache), 3) - cache = [pool.connection() for i in range(6)] - self.assertEqual(len(pool._idle_cache), 0) - del cache - self.assertEqual(len(pool._idle_cache), 6) - pool = PooledDB(dbapi, 0, 3) - self.assertEqual(len(pool._idle_cache), 0) - cache = [pool.connection() for i in range(3)] - self.assertEqual(len(pool._idle_cache), 0) - del cache - self.assertEqual(len(pool._idle_cache), 3) - cache = [pool.connection() for i in range(6)] - self.assertEqual(len(pool._idle_cache), 0) - del cache - self.assertEqual(len(pool._idle_cache), 3) - pool = PooledDB(dbapi, 3, 3) - self.assertEqual(len(pool._idle_cache), 3) - cache = [pool.connection() for i in range(3)] - self.assertEqual(len(pool._idle_cache), 0) - del cache - self.assertEqual(len(pool._idle_cache), 3) - cache = [pool.connection() for i in range(6)] - self.assertEqual(len(pool._idle_cache), 0) - del cache - self.assertEqual(len(pool._idle_cache), 3) - pool = PooledDB(dbapi, 3, 2) - self.assertEqual(len(pool._idle_cache), 3) - cache = [pool.connection() for i in range(4)] - self.assertEqual(len(pool._idle_cache), 0) - del cache - self.assertEqual(len(pool._idle_cache), 3) - pool = PooledDB(dbapi, 2, 5) - self.assertEqual(len(pool._idle_cache), 2) - cache = [pool.connection() for i in range(10)] - self.assertEqual(len(pool._idle_cache), 0) - del cache - self.assertEqual(len(pool._idle_cache), 5) - pool = PooledDB(dbapi, 1, 2, 3) - self.assertEqual(len(pool._idle_cache), 1) - cache = [pool.connection(False) for i in range(4)] - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - del cache - self.assertEqual(len(pool._idle_cache), 2) - cache = [pool.connection() for i in range(10)] - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 3) - del cache - self.assertEqual(len(pool._idle_cache), 2) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - pool = PooledDB(dbapi, 1, 3, 2) - self.assertEqual(len(pool._idle_cache), 1) - cache = [pool.connection(False) for i in range(4)] - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - del cache - self.assertEqual(len(pool._idle_cache), 3) - cache = [pool.connection() for i in range(10)] - if shareable: - self.assertEqual(len(pool._idle_cache), 1) - self.assertEqual(len(pool._shared_cache), 2) - else: - self.assertEqual(len(pool._idle_cache), 0) - del cache - self.assertEqual(len(pool._idle_cache), 3) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - - def test_max_shared(self): - for threadsafety in (1, 2): - dbapi.threadsafety = threadsafety - shareable = threadsafety > 1 - pool = PooledDB(dbapi) - self.assertEqual(len(pool._idle_cache), 0) - cache = [pool.connection() for i in range(10)] - self.assertEqual(len(cache), 10) - self.assertEqual(len(pool._idle_cache), 0) - pool = PooledDB(dbapi, 1, 1, 0) - self.assertEqual(len(pool._idle_cache), 1) - cache = [pool.connection() for i in range(10)] - self.assertEqual(len(cache), 10) - self.assertEqual(len(pool._idle_cache), 0) - pool = PooledDB(dbapi, 0, 0, 1) - cache = [pool.connection() for i in range(10)] - self.assertEqual(len(cache), 10) - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 1) - pool = PooledDB(dbapi, 1, 1, 1) - self.assertEqual(len(pool._idle_cache), 1) - cache = [pool.connection() for i in range(10)] - self.assertEqual(len(cache), 10) - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 1) - pool = PooledDB(dbapi, 0, 0, 7) - cache = [pool.connection(False) for i in range(3)] - self.assertEqual(len(cache), 3) - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - cache = [pool.connection() for i in range(10)] - self.assertEqual(len(cache), 10) - self.assertEqual(len(pool._idle_cache), 3) - if shareable: - self.assertEqual(len(pool._shared_cache), 7) - - def test_sort_shared(self): - dbapi.threadsafety = 2 - pool = PooledDB(dbapi, 0, 4, 4) + NotSupportedError, PooledDB, SharedDBConnection, + InvalidConnection, TooManyConnections) +from dbutils.steady_db import SteadyDBConnection + + +def test_version(): + from dbutils import __version__, pooled_db + assert pooled_db.__version__ == __version__ + assert PooledDB.version == __version__ + + +@pytest.mark.parametrize("threadsafety", [None, 0]) +def test_no_threadsafety(dbapi, threadsafety): # noqa: F811 + dbapi.threadsafety = threadsafety + with pytest.raises(NotSupportedError): + PooledDB(dbapi) + + +@pytest.mark.parametrize("threadsafety", [1, 2, 3]) +def test_threadsafety(dbapi, threadsafety): # noqa: F811 + dbapi.threadsafety = threadsafety + pool = PooledDB(dbapi, 0, 0, 1) + assert hasattr(pool, '_maxshared') + if threadsafety > 1: + assert pool._maxshared == 1 + assert hasattr(pool, '_shared_cache') + else: + assert pool._maxshared == 0 + assert not hasattr(pool, '_shared_cache') + + +@pytest.mark.parametrize("threadsafety", [1, 2]) +def test_create_connection(dbapi, threadsafety): # noqa: F811 + dbapi.threadsafety = threadsafety + shareable = threadsafety > 1 + pool = PooledDB( + dbapi, 1, 1, 1, 0, False, None, None, True, None, None, + 'PooledDBTestDB', user='PooledDBTestUser') + assert hasattr(pool, '_idle_cache') + assert len(pool._idle_cache) == 1 + if shareable: + assert hasattr(pool, '_shared_cache') + assert len(pool._shared_cache) == 0 + else: + assert not hasattr(pool, '_shared_cache') + assert hasattr(pool, '_maxusage') + assert pool._maxusage is None + assert hasattr(pool, '_setsession') + assert pool._setsession is None + con = pool._idle_cache[0] + assert isinstance(con, SteadyDBConnection) + assert hasattr(con, '_maxusage') + assert con._maxusage == 0 + assert hasattr(con, '_setsession_sql') + assert con._setsession_sql is None + db = pool.connection() + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 1 + assert hasattr(db, '_con') + assert db._con == con + assert hasattr(db, 'cursor') + assert hasattr(db, '_usage') + assert db._usage == 0 + assert hasattr(con, '_con') + db_con = con._con + assert hasattr(db_con, 'database') + assert db_con.database == 'PooledDBTestDB' + assert hasattr(db_con, 'user') + assert db_con.user == 'PooledDBTestUser' + assert hasattr(db_con, 'open_cursors') + assert db_con.open_cursors == 0 + assert hasattr(db_con, 'num_uses') + assert db_con.num_uses == 0 + assert hasattr(db_con, 'num_queries') + assert db_con.num_queries == 0 + cursor = db.cursor() + assert db_con.open_cursors == 1 + cursor.execute('select test') + r = cursor.fetchone() + cursor.close() + assert db_con.open_cursors == 0 + assert r == 'test' + assert db_con.num_queries == 1 + assert db._usage == 1 + cursor = db.cursor() + assert db_con.open_cursors == 1 + cursor.execute('set sessiontest') + cursor2 = db.cursor() + assert db_con.open_cursors == 2 + cursor2.close() + assert db_con.open_cursors == 1 + cursor.close() + assert db_con.open_cursors == 0 + assert db_con.num_queries == 1 + assert db._usage == 2 + assert db_con.session == ['rollback', 'sessiontest'] + pool = PooledDB(dbapi, 1, 1, 1) + assert len(pool._idle_cache) == 1 + if shareable: + assert len(pool._shared_cache) == 0 + db = pool.connection() + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 1 + db.close() + assert len(pool._idle_cache) == 1 + if shareable: + assert len(pool._shared_cache) == 0 + db = pool.connection(True) + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 1 + db.close() + assert len(pool._idle_cache) == 1 + if shareable: + assert len(pool._shared_cache) == 0 + db = pool.connection(False) + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 0 + assert db._usage == 0 + db_con = db._con._con + assert db_con.database is None + assert db_con.user is None + db.close() + assert len(pool._idle_cache) == 1 + if shareable: + assert len(pool._shared_cache) == 0 + db = pool.dedicated_connection() + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 0 + assert db._usage == 0 + db_con = db._con._con + assert db_con.database is None + assert db_con.user is None + db.close() + assert len(pool._idle_cache) == 1 + if shareable: + assert len(pool._shared_cache) == 0 + pool = PooledDB(dbapi, 0, 0, 0, 0, False, 3, ('set datestyle',)) + assert pool._maxusage == 3 + assert pool._setsession == ('set datestyle',) + con = pool.connection()._con + assert con._maxusage == 3 + assert con._setsession_sql == ('set datestyle',) + + +@pytest.mark.parametrize("threadsafety", [1, 2]) +def test_close_connection(dbapi, threadsafety): # noqa: F811 + dbapi.threadsafety = threadsafety + shareable = threadsafety > 1 + pool = PooledDB( + dbapi, 0, 1, 1, 0, False, None, None, True, None, None, + 'PooledDBTestDB', user='PooledDBTestUser') + assert hasattr(pool, '_idle_cache') + assert len(pool._idle_cache) == 0 + db = pool.connection() + assert hasattr(db, '_con') + con = db._con + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 1 + assert hasattr(db, '_shared_con') + shared_con = db._shared_con + assert pool._shared_cache[0] == shared_con + assert hasattr(shared_con, 'shared') + assert shared_con.shared == 1 + assert hasattr(shared_con, 'con') + assert shared_con.con == con + assert isinstance(con, SteadyDBConnection) + assert hasattr(con, '_con') + db_con = con._con + assert hasattr(db_con, 'num_queries') + assert db._usage == 0 + assert db_con.num_queries == 0 + db.cursor().execute('select test') + assert db._usage == 1 + assert db_con.num_queries == 1 + db.close() + assert db._con is None + if shareable: + assert db._shared_con is None + assert shared_con.shared == 0 + with pytest.raises(InvalidConnection): + assert db._usage + assert not hasattr(db_con, '_num_queries') + assert len(pool._idle_cache) == 1 + assert pool._idle_cache[0]._con == db_con + if shareable: + assert len(pool._shared_cache) == 0 + db.close() + if shareable: + assert shared_con.shared == 0 + db = pool.connection() + assert db._con == con + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 1 + shared_con = db._shared_con + assert pool._shared_cache[0] == shared_con + assert shared_con.con == con + assert shared_con.shared == 1 + assert db._usage == 1 + assert db_con.num_queries == 1 + assert hasattr(db_con, 'database') + assert db_con.database == 'PooledDBTestDB' + assert hasattr(db_con, 'user') + assert db_con.user == 'PooledDBTestUser' + db.cursor().execute('select test') + assert db_con.num_queries == 2 + db.cursor().execute('select test') + assert db_con.num_queries == 3 + db.close() + assert len(pool._idle_cache) == 1 + assert pool._idle_cache[0]._con == db_con + if shareable: + assert len(pool._shared_cache) == 0 + db = pool.connection(False) + assert db._con == con + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 0 + db.close() + assert len(pool._idle_cache) == 1 + if shareable: + assert len(pool._shared_cache) == 0 + + +@pytest.mark.parametrize("threadsafety", [1, 2]) +def test_close_all(dbapi, threadsafety): # noqa: F811 + dbapi.threadsafety = threadsafety + shareable = threadsafety > 1 + pool = PooledDB(dbapi, 10) + assert len(pool._idle_cache) == 10 + pool.close() + assert len(pool._idle_cache) == 0 + pool = PooledDB(dbapi, 10) + closed = ['no'] + + def close(what=closed): + what[0] = 'yes' + + pool._idle_cache[7]._con.close = close + assert closed == ['no'] + del pool + assert closed == ['yes'] + pool = PooledDB(dbapi, 10, 10, 5) + assert len(pool._idle_cache) == 10 + if shareable: + assert len(pool._shared_cache) == 0 + cache = [] + for i in range(5): + cache.append(pool.connection()) + assert len(pool._idle_cache) == 5 + if shareable: + assert len(pool._shared_cache) == 5 + else: + assert len(pool._idle_cache) == 5 + pool.close() + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 0 + pool = PooledDB(dbapi, 10, 10, 5) + closed = [] + + def close_idle(what=closed): + what.append('idle') + + def close_shared(what=closed): + what.append('shared') + + if shareable: cache = [] - for i in range(6): - db = pool.connection() - db.cursor().execute('select test') - cache.append(db) - for i, db in enumerate(cache): - self.assertEqual(db._shared_con.shared, 1 if 2 <= i < 4 else 2) - cache[2].begin() - cache[3].begin() - db = pool.connection() - self.assertIs(db._con, cache[0]._con) - db.close() - cache[3].rollback() - db = pool.connection() - self.assertIs(db._con, cache[3]._con) - - def test_equally_shared(self): - for threadsafety in (1, 2): - dbapi.threadsafety = threadsafety - shareable = threadsafety > 1 - pool = PooledDB(dbapi, 5, 5, 5) - self.assertEqual(len(pool._idle_cache), 5) - for i in range(15): - db = pool.connection(False) - db.cursor().execute('select test') - db.close() - self.assertEqual(len(pool._idle_cache), 5) - for i in range(5): - con = pool._idle_cache[i] - self.assertEqual(con._usage, 3) - self.assertEqual(con._con.num_queries, 3) - cache = [] - for i in range(35): - db = pool.connection() - db.cursor().execute('select test') - cache.append(db) - del db - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 5) - for i in range(5): - con = pool._shared_cache[i] - self.assertEqual(con.shared, 7) - con = con.con - self.assertEqual(con._usage, 10) - self.assertEqual(con._con.num_queries, 10) - del cache - self.assertEqual(len(pool._idle_cache), 5) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - - def test_many_shared(self): - for threadsafety in (1, 2): - dbapi.threadsafety = threadsafety - shareable = threadsafety > 1 - pool = PooledDB(dbapi, 0, 0, 5) - cache = [] - for i in range(35): - db = pool.connection() - db.cursor().execute('select test1') - db.cursor().execute('select test2') - db.cursor().callproc('test3') - cache.append(db) - del db - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 5) - for i in range(5): - con = pool._shared_cache[i] - self.assertEqual(con.shared, 7) - con = con.con - self.assertEqual(con._usage, 21) - self.assertEqual(con._con.num_queries, 14) - cache[3] = cache[8] = cache[33] = None - cache[12] = cache[17] = cache[34] = None - self.assertEqual(len(pool._shared_cache), 5) - self.assertEqual(pool._shared_cache[0].shared, 7) - self.assertEqual(pool._shared_cache[1].shared, 7) - self.assertEqual(pool._shared_cache[2].shared, 5) - self.assertEqual(pool._shared_cache[3].shared, 4) - self.assertEqual(pool._shared_cache[4].shared, 6) - for db in cache: - if db: - db.cursor().callproc('test4') - for i in range(6): - db = pool.connection() - db.cursor().callproc('test4') - cache.append(db) - del db - for i in range(5): - con = pool._shared_cache[i] - self.assertEqual(con.shared, 7) - con = con.con - self.assertEqual(con._usage, 28) - self.assertEqual(con._con.num_queries, 14) - del cache - if shareable: - self.assertEqual(len(pool._idle_cache), 5) - self.assertEqual(len(pool._shared_cache), 0) - else: - self.assertEqual(len(pool._idle_cache), 35) - - def test_rollback(self): - for threadsafety in (1, 2): - dbapi.threadsafety = threadsafety - pool = PooledDB(dbapi, 0, 1) - self.assertEqual(len(pool._idle_cache), 0) - db = pool.connection(False) - self.assertEqual(len(pool._idle_cache), 0) - self.assertEqual(db._con._con.open_cursors, 0) - cursor = db.cursor() - self.assertEqual(db._con._con.open_cursors, 1) - cursor.execute('set doit1') - db.commit() - cursor.execute('set dont1') - cursor.close() - self.assertEqual(db._con._con.open_cursors, 0) - del db - self.assertEqual(len(pool._idle_cache), 1) - db = pool.connection(False) - self.assertEqual(len(pool._idle_cache), 0) - self.assertEqual(db._con._con.open_cursors, 0) - cursor = db.cursor() - self.assertEqual(db._con._con.open_cursors, 1) - cursor.execute('set doit2') - cursor.close() - self.assertEqual(db._con._con.open_cursors, 0) - db.commit() - session = db._con._con.session - db.close() - self.assertEqual(session, [ - 'doit1', 'commit', 'dont1', 'rollback', - 'doit2', 'commit', 'rollback']) - - def test_maxconnections(self): - for threadsafety in (1, 2): - dbapi.threadsafety = threadsafety - shareable = threadsafety > 1 - pool = PooledDB(dbapi, 1, 2, 2, 3) - self.assertTrue(hasattr(pool, '_maxconnections')) - self.assertEqual(pool._maxconnections, 3) - self.assertTrue(hasattr(pool, '_connections')) - self.assertEqual(pool._connections, 0) - self.assertEqual(len(pool._idle_cache), 1) - cache = [] - for i in range(3): - cache.append(pool.connection(False)) - self.assertEqual(pool._connections, 3) - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - self.assertRaises(TooManyConnections, pool.connection, 0) - self.assertRaises(TooManyConnections, pool.connection) - cache = [] - self.assertEqual(pool._connections, 0) - self.assertEqual(len(pool._idle_cache), 2) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - for i in range(3): - cache.append(pool.connection()) - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(pool._connections, 2) - self.assertEqual(len(pool._shared_cache), 2) - cache.append(pool.connection(False)) - self.assertEqual(pool._connections, 3) - self.assertEqual(len(pool._shared_cache), 2) - else: - self.assertEqual(pool._connections, 3) - self.assertRaises(TooManyConnections, pool.connection, 0) - if shareable: - cache.append(pool.connection(True)) - self.assertEqual(pool._connections, 3) - else: - self.assertRaises(TooManyConnections, pool.connection) - del cache - self.assertEqual(pool._connections, 0) - self.assertEqual(len(pool._idle_cache), 2) - pool = PooledDB(dbapi, 0, 1, 1, 1) - self.assertEqual(pool._maxconnections, 1) - self.assertEqual(pool._connections, 0) - self.assertEqual(len(pool._idle_cache), 0) - db = pool.connection(False) - self.assertEqual(pool._connections, 1) - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - self.assertRaises(TooManyConnections, pool.connection, 0) - self.assertRaises(TooManyConnections, pool.connection) - del db - self.assertEqual(pool._connections, 0) - self.assertEqual(len(pool._idle_cache), 1) - cache = [pool.connection()] - self.assertEqual(pool._connections, 1) - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 1) - cache.append(pool.connection()) - self.assertEqual(pool._connections, 1) - self.assertEqual(len(pool._shared_cache), 1) - self.assertEqual(pool._shared_cache[0].shared, 2) - else: - self.assertRaises(TooManyConnections, pool.connection) - self.assertRaises(TooManyConnections, pool.connection, 0) - if shareable: - cache.append(pool.connection(True)) - self.assertEqual(pool._connections, 1) - self.assertEqual(len(pool._shared_cache), 1) - self.assertEqual(pool._shared_cache[0].shared, 3) - else: - self.assertRaises(TooManyConnections, pool.connection, 1) - del cache - self.assertEqual(pool._connections, 0) - self.assertEqual(len(pool._idle_cache), 1) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - db = pool.connection(False) - self.assertEqual(pool._connections, 1) - self.assertEqual(len(pool._idle_cache), 0) - del db - self.assertEqual(pool._connections, 0) - self.assertEqual(len(pool._idle_cache), 1) - pool = PooledDB(dbapi, 1, 2, 2, 1) - self.assertEqual(pool._maxconnections, 2) - self.assertEqual(pool._connections, 0) - self.assertEqual(len(pool._idle_cache), 1) - cache = [] - cache.append(pool.connection(False)) - self.assertEqual(pool._connections, 1) - self.assertEqual(len(pool._idle_cache), 0) - cache.append(pool.connection(False)) - self.assertEqual(pool._connections, 2) - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - self.assertRaises(TooManyConnections, pool.connection, 0) - self.assertRaises(TooManyConnections, pool.connection) - pool = PooledDB(dbapi, 4, 3, 2, 1, False) - self.assertEqual(pool._maxconnections, 4) - self.assertEqual(pool._connections, 0) - self.assertEqual(len(pool._idle_cache), 4) - cache = [] - for i in range(4): - cache.append(pool.connection(False)) - self.assertEqual(pool._connections, 4) - self.assertEqual(len(pool._idle_cache), 0) - self.assertRaises(TooManyConnections, pool.connection, 0) - self.assertRaises(TooManyConnections, pool.connection) - pool = PooledDB(dbapi, 1, 2, 3, 4, False) - self.assertEqual(pool._maxconnections, 4) - self.assertEqual(pool._connections, 0) - self.assertEqual(len(pool._idle_cache), 1) - for i in range(4): - cache.append(pool.connection()) - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(pool._connections, 3) - self.assertEqual(len(pool._shared_cache), 3) - cache.append(pool.connection()) - self.assertEqual(pool._connections, 3) - cache.append(pool.connection(False)) - self.assertEqual(pool._connections, 4) - else: - self.assertEqual(pool._connections, 4) - self.assertRaises(TooManyConnections, pool.connection) - self.assertRaises(TooManyConnections, pool.connection, 0) - pool = PooledDB(dbapi, 0, 0, 3, 3, False) - self.assertEqual(pool._maxconnections, 3) - self.assertEqual(pool._connections, 0) - cache = [] - for i in range(3): - cache.append(pool.connection(False)) - self.assertEqual(pool._connections, 3) - self.assertRaises(TooManyConnections, pool.connection, 0) - self.assertRaises(TooManyConnections, pool.connection, 1) - cache = [] - self.assertEqual(pool._connections, 0) - for i in range(3): - cache.append(pool.connection()) - self.assertEqual(pool._connections, 3) - if shareable: - for i in range(3): - cache.append(pool.connection()) - self.assertEqual(pool._connections, 3) - else: - self.assertRaises(TooManyConnections, pool.connection) - self.assertRaises(TooManyConnections, pool.connection, 0) - pool = PooledDB(dbapi, 0, 0, 3) - self.assertEqual(pool._maxconnections, 0) - self.assertEqual(pool._connections, 0) - cache = [] - for i in range(10): - cache.append(pool.connection(False)) - cache.append(pool.connection()) - if shareable: - self.assertEqual(pool._connections, 13) - self.assertEqual(len(pool._shared_cache), 3) - else: - self.assertEqual(pool._connections, 20) - pool = PooledDB(dbapi, 1, 1, 1, 1, True) - self.assertEqual(pool._maxconnections, 1) - self.assertEqual(pool._connections, 0) - self.assertEqual(len(pool._idle_cache), 1) - db = pool.connection(False) - self.assertEqual(pool._connections, 1) - self.assertEqual(len(pool._idle_cache), 0) - - def connection(): - db = pool.connection() - cursor = db.cursor() - cursor.execute('set thread') - cursor.close() - db.close() - - from threading import Thread - thread = Thread(target=connection) - thread.start() - thread.join(0.1) - self.assertTrue(thread.is_alive()) - self.assertEqual(pool._connections, 1) - self.assertEqual(len(pool._idle_cache), 0) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - session = db._con._con.session - self.assertEqual(session, ['rollback']) - del db - thread.join(0.1) - self.assertFalse(thread.is_alive()) - self.assertEqual(pool._connections, 0) - self.assertEqual(len(pool._idle_cache), 1) - if shareable: - self.assertEqual(len(pool._shared_cache), 0) - db = pool.connection(False) - self.assertEqual(pool._connections, 1) - self.assertEqual(len(pool._idle_cache), 0) - self.assertEqual( - session, ['rollback', 'rollback', 'thread', 'rollback']) - del db + for i in range(5): + cache.append(pool.connection()) + pool._shared_cache[3].con.close = close_shared + else: + pool._idle_cache[7]._con.close = close_shared + pool._idle_cache[3]._con.close = close_idle + assert closed == [] + del pool + if shareable: + del cache + assert closed == ['idle', 'shared'] - def test_maxusage(self): - for threadsafety in (1, 2): - dbapi.threadsafety = threadsafety - for maxusage in (0, 3, 7): - pool = PooledDB(dbapi, 0, 0, 0, 1, False, maxusage) - self.assertEqual(pool._maxusage, maxusage) - self.assertEqual(len(pool._idle_cache), 0) - db = pool.connection(False) - self.assertEqual(db._con._maxusage, maxusage) - self.assertEqual(len(pool._idle_cache), 0) - self.assertEqual(db._con._con.open_cursors, 0) - self.assertEqual(db._usage, 0) - self.assertEqual(db._con._con.num_uses, 0) - self.assertEqual(db._con._con.num_queries, 0) - for i in range(20): - cursor = db.cursor() - self.assertEqual(db._con._con.open_cursors, 1) - cursor.execute(f'select test{i}') - r = cursor.fetchone() - self.assertEqual(r, f'test{i}') - cursor.close() - self.assertEqual(db._con._con.open_cursors, 0) - if maxusage: - j = i % maxusage + 1 - else: - j = i + 1 - self.assertEqual(db._usage, j) - self.assertEqual(db._con._con.num_uses, j) - self.assertEqual(db._con._con.num_queries, j) - db.cursor().callproc('test') - self.assertEqual(db._con._con.open_cursors, 0) - self.assertEqual(db._usage, j + 1) - self.assertEqual(db._con._con.num_uses, j + 1) - self.assertEqual(db._con._con.num_queries, j) - - def test_setsession(self): - for threadsafety in (1, 2): - dbapi.threadsafety = threadsafety - setsession = ('set time zone', 'set datestyle') - pool = PooledDB(dbapi, 0, 0, 0, 1, False, None, setsession) - self.assertEqual(pool._setsession, setsession) - db = pool.connection(False) - self.assertEqual(db._setsession_sql, setsession) - self.assertEqual( - db._con._con.session, ['time zone', 'datestyle']) - db.cursor().execute('select test') - db.cursor().execute('set test1') - self.assertEqual(db._usage, 2) - self.assertEqual(db._con._con.num_uses, 4) - self.assertEqual(db._con._con.num_queries, 1) - self.assertEqual( - db._con._con.session, ['time zone', 'datestyle', 'test1']) - db.close() - db = pool.connection(False) - self.assertEqual(db._setsession_sql, setsession) - self.assertEqual( - db._con._con.session, - ['time zone', 'datestyle', 'test1', 'rollback']) - db._con._con.close() - db.cursor().execute('select test') - db.cursor().execute('set test2') - self.assertEqual( - db._con._con.session, ['time zone', 'datestyle', 'test2']) - - def test_one_thread_two_connections(self): - for threadsafety in (1, 2): - dbapi.threadsafety = threadsafety - shareable = threadsafety > 1 - pool = PooledDB(dbapi, 2) - db1 = pool.connection() - for i in range(5): - db1.cursor().execute('select test') - db2 = pool.connection() - self.assertNotEqual(db1, db2) - self.assertNotEqual(db1._con, db2._con) - for i in range(7): - db2.cursor().execute('select test') - self.assertEqual(db1._con._con.num_queries, 5) - self.assertEqual(db2._con._con.num_queries, 7) - del db1 - db1 = pool.connection() - self.assertNotEqual(db1, db2) - self.assertNotEqual(db1._con, db2._con) - for i in range(3): - db1.cursor().execute('select test') - self.assertEqual(db1._con._con.num_queries, 8) - db2.cursor().execute('select test') - self.assertEqual(db2._con._con.num_queries, 8) - pool = PooledDB(dbapi, 0, 0, 2) - db1 = pool.connection() - for i in range(5): - db1.cursor().execute('select test') - db2 = pool.connection() - self.assertNotEqual(db1, db2) - self.assertNotEqual(db1._con, db2._con) - for i in range(7): - db2.cursor().execute('select test') - self.assertEqual(db1._con._con.num_queries, 5) - self.assertEqual(db2._con._con.num_queries, 7) - del db1 - db1 = pool.connection() - self.assertNotEqual(db1, db2) - self.assertNotEqual(db1._con, db2._con) - for i in range(3): - db1.cursor().execute('select test') - self.assertEqual(db1._con._con.num_queries, 8) - db2.cursor().execute('select test') - self.assertEqual(db2._con._con.num_queries, 8) - pool = PooledDB(dbapi, 0, 0, 1) - db1 = pool.connection() - db2 = pool.connection() - self.assertNotEqual(db1, db2) - if shareable: - self.assertEqual(db1._con, db2._con) - else: - self.assertNotEqual(db1._con, db2._con) - del db1 - db1 = pool.connection(False) - self.assertNotEqual(db1, db2) - self.assertNotEqual(db1._con, db2._con) - - def test_tnree_threads_two_connections(self): - for threadsafety in (1, 2): - dbapi.threadsafety = threadsafety - pool = PooledDB(dbapi, 2, 2, 0, 2, True) - from queue import Queue, Empty - queue = Queue(3) - - def connection(): - try: - queue.put(pool.connection(), 1, 1) - except Exception: - queue.put(pool.connection(), 1) - - from threading import Thread - for i in range(3): - Thread(target=connection).start() - try: - db1 = queue.get(1, 1) - db2 = queue.get(1, 1) - except TypeError: - db1 = queue.get(1) - db2 = queue.get(1) - self.assertNotEqual(db1, db2) - db1_con = db1._con - db2_con = db2._con - self.assertNotEqual(db1_con, db2_con) - try: - self.assertRaises(Empty, queue.get, 1, 0.1) - except TypeError: - self.assertRaises(Empty, queue.get, 0) - del db1 - try: - db1 = queue.get(1, 1) - except TypeError: - db1 = queue.get(1) - self.assertNotEqual(db1, db2) - self.assertNotEqual(db1._con, db2._con) - self.assertEqual(db1._con, db1_con) - pool = PooledDB(dbapi, 2, 2, 1, 2, True) - db1 = pool.connection(False) - db2 = pool.connection(False) - self.assertNotEqual(db1, db2) - db1_con = db1._con - db2_con = db2._con - self.assertNotEqual(db1_con, db2_con) - Thread(target=connection).start() - try: - self.assertRaises(Empty, queue.get, 1, 0.1) - except TypeError: - self.assertRaises(Empty, queue.get, 0) - del db1 - try: - db1 = queue.get(1, 1) - except TypeError: - db1 = queue.get(1) - self.assertNotEqual(db1, db2) - self.assertNotEqual(db1._con, db2._con) - self.assertEqual(db1._con, db1_con) - - def test_ping_check(self): - Connection = dbapi.Connection - Connection.has_ping = True - Connection.num_pings = 0 - dbapi.threadsafety = 2 - pool = PooledDB(dbapi, 1, 1, 0, 0, False, None, None, True, None, 0) - db = pool.connection() - self.assertTrue(db._con._con.valid) - self.assertEqual(Connection.num_pings, 0) - db._con.close() - db.close() - db = pool.connection() - self.assertFalse(db._con._con.valid) - self.assertEqual(Connection.num_pings, 0) - pool = PooledDB(dbapi, 1, 1, 1, 0, False, None, None, True, None, 0) - db = pool.connection() - self.assertTrue(db._con._con.valid) - self.assertEqual(Connection.num_pings, 0) - db._con.close() - db = pool.connection() - self.assertFalse(db._con._con.valid) - self.assertEqual(Connection.num_pings, 0) - pool = PooledDB(dbapi, 1, 1, 0, 0, False, None, None, True, None, 1) + +@pytest.mark.parametrize("threadsafety", [1, 2]) +def test_shareable_connection(dbapi, threadsafety): # noqa: F811 + dbapi.threadsafety = threadsafety + shareable = threadsafety > 1 + pool = PooledDB(dbapi, 0, 1, 2) + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 0 + db1 = pool.connection() + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 1 + db2 = pool.connection() + assert db1._con != db2._con + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 2 + db3 = pool.connection() + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 2 + assert db3._con == db1._con + assert db1._shared_con.shared == 2 + assert db2._shared_con.shared == 1 + else: + assert db3._con != db1._con + assert db3._con != db2._con + db4 = pool.connection() + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 2 + assert db4._con == db2._con + assert db1._shared_con.shared == 2 + assert db2._shared_con.shared == 2 + else: + assert db4._con != db1._con + assert db4._con != db2._con + assert db4._con != db3._con + db5 = pool.connection(False) + assert db5._con != db1._con + assert db5._con != db2._con + assert db5._con != db3._con + assert db5._con != db4._con + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 2 + assert db1._shared_con.shared == 2 + assert db2._shared_con.shared == 2 + db5.close() + assert len(pool._idle_cache) == 1 + db5 = pool.connection() + if shareable: + assert len(pool._idle_cache) == 1 + assert len(pool._shared_cache) == 2 + assert db5._shared_con.shared == 3 + else: + assert len(pool._idle_cache) == 0 + pool = PooledDB(dbapi, 0, 0, 1) + assert len(pool._idle_cache) == 0 + db1 = pool.connection(False) + if shareable: + assert len(pool._shared_cache) == 0 + db2 = pool.connection() + if shareable: + assert len(pool._shared_cache) == 1 + db3 = pool.connection() + if shareable: + assert len(pool._shared_cache) == 1 + assert db2._con == db3._con + else: + assert db2._con != db3._con + del db3 + if shareable: + assert len(pool._idle_cache) == 0 + assert len(pool._shared_cache) == 1 + else: + assert len(pool._idle_cache) == 1 + del db2 + if shareable: + assert len(pool._idle_cache) == 1 + assert len(pool._shared_cache) == 0 + else: + assert len(pool._idle_cache) == 2 + del db1 + if shareable: + assert len(pool._idle_cache) == 2 + assert len(pool._shared_cache) == 0 + else: + assert len(pool._idle_cache) == 3 + + +@pytest.mark.parametrize("threadsafety", [1, 2]) +def test_min_max_cached(dbapi, threadsafety): # noqa: F811 + dbapi.threadsafety = threadsafety + shareable = threadsafety > 1 + pool = PooledDB(dbapi, 3) + assert len(pool._idle_cache) == 3 + cache = [pool.connection() for i in range(3)] + assert len(pool._idle_cache) == 0 + assert cache + del cache + assert len(pool._idle_cache) == 3 + cache = [pool.connection() for i in range(6)] + assert len(pool._idle_cache) == 0 + assert cache + del cache + assert len(pool._idle_cache) == 6 + pool = PooledDB(dbapi, 0, 3) + assert len(pool._idle_cache) == 0 + cache = [pool.connection() for i in range(3)] + assert len(pool._idle_cache) == 0 + assert cache + del cache + assert len(pool._idle_cache) == 3 + cache = [pool.connection() for i in range(6)] + assert len(pool._idle_cache) == 0 + assert cache + del cache + assert len(pool._idle_cache) == 3 + pool = PooledDB(dbapi, 3, 3) + assert len(pool._idle_cache) == 3 + cache = [pool.connection() for i in range(3)] + assert len(pool._idle_cache) == 0 + assert cache + del cache + assert len(pool._idle_cache) == 3 + cache = [pool.connection() for i in range(6)] + assert len(pool._idle_cache) == 0 + assert cache + del cache + assert len(pool._idle_cache) == 3 + pool = PooledDB(dbapi, 3, 2) + assert len(pool._idle_cache) == 3 + cache = [pool.connection() for i in range(4)] + assert len(pool._idle_cache) == 0 + assert cache + del cache + assert len(pool._idle_cache) == 3 + pool = PooledDB(dbapi, 2, 5) + assert len(pool._idle_cache) == 2 + cache = [pool.connection() for i in range(10)] + assert len(pool._idle_cache) == 0 + assert cache + del cache + assert len(pool._idle_cache) == 5 + pool = PooledDB(dbapi, 1, 2, 3) + assert len(pool._idle_cache) == 1 + cache = [pool.connection(False) for i in range(4)] + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 0 + del cache + assert len(pool._idle_cache) == 2 + cache = [pool.connection() for i in range(10)] + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 3 + assert cache + del cache + assert len(pool._idle_cache) == 2 + if shareable: + assert len(pool._shared_cache) == 0 + pool = PooledDB(dbapi, 1, 3, 2) + assert len(pool._idle_cache) == 1 + cache = [pool.connection(False) for i in range(4)] + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 0 + assert cache + del cache + assert len(pool._idle_cache) == 3 + cache = [pool.connection() for i in range(10)] + if shareable: + assert len(pool._idle_cache) == 1 + assert len(pool._shared_cache) == 2 + else: + assert len(pool._idle_cache) == 0 + assert cache + del cache + assert len(pool._idle_cache) == 3 + if shareable: + assert len(pool._shared_cache) == 0 + + +@pytest.mark.parametrize("threadsafety", [1, 2]) +def test_max_shared(dbapi, threadsafety): # noqa: F811 + dbapi.threadsafety = threadsafety + shareable = threadsafety > 1 + pool = PooledDB(dbapi) + assert len(pool._idle_cache) == 0 + cache = [pool.connection() for i in range(10)] + assert len(cache) == 10 + assert len(pool._idle_cache) == 0 + pool = PooledDB(dbapi, 1, 1, 0) + assert len(pool._idle_cache) == 1 + cache = [pool.connection() for i in range(10)] + assert len(cache) == 10 + assert len(pool._idle_cache) == 0 + pool = PooledDB(dbapi, 0, 0, 1) + cache = [pool.connection() for i in range(10)] + assert len(cache) == 10 + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 1 + pool = PooledDB(dbapi, 1, 1, 1) + assert len(pool._idle_cache) == 1 + cache = [pool.connection() for i in range(10)] + assert len(cache) == 10 + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 1 + pool = PooledDB(dbapi, 0, 0, 7) + cache = [pool.connection(False) for i in range(3)] + assert len(cache) == 3 + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 0 + cache = [pool.connection() for i in range(10)] + assert len(cache) == 10 + assert len(pool._idle_cache) == 3 + if shareable: + assert len(pool._shared_cache) == 7 + + +def test_sort_shared(dbapi): # noqa: F811 + pool = PooledDB(dbapi, 0, 4, 4) + cache = [] + for i in range(6): db = pool.connection() - self.assertTrue(db._con._con.valid) - self.assertEqual(Connection.num_pings, 1) - db._con.close() + db.cursor().execute('select test') + cache.append(db) + for i, db in enumerate(cache): + assert db._shared_con.shared == 1 if 2 <= i < 4 else 2 + cache[2].begin() + cache[3].begin() + db = pool.connection() + assert db._con is cache[0]._con + db.close() + cache[3].rollback() + db = pool.connection() + assert db._con is cache[3]._con + + +@pytest.mark.parametrize("threadsafety", [1, 2]) +def test_equally_shared(dbapi, threadsafety): # noqa: F811 + dbapi.threadsafety = threadsafety + shareable = threadsafety > 1 + pool = PooledDB(dbapi, 5, 5, 5) + assert len(pool._idle_cache) == 5 + for i in range(15): + db = pool.connection(False) + db.cursor().execute('select test') db.close() + assert len(pool._idle_cache) == 5 + for i in range(5): + con = pool._idle_cache[i] + assert con._usage == 3 + assert con._con.num_queries == 3 + cache = [] + for i in range(35): db = pool.connection() - self.assertTrue(db._con._con.valid) - self.assertEqual(Connection.num_pings, 2) - pool = PooledDB(dbapi, 1, 1, 1, 0, False, None, None, True, None, 1) - db = pool.connection() - self.assertTrue(db._con._con.valid) - self.assertEqual(Connection.num_pings, 3) - db._con.close() - db = pool.connection() - self.assertTrue(db._con._con.valid) - self.assertEqual(Connection.num_pings, 4) - pool = PooledDB(dbapi, 1, 1, 1, 0, False, None, None, True, None, 2) - db = pool.connection() - self.assertTrue(db._con._con.valid) - self.assertEqual(Connection.num_pings, 4) - db._con.close() - db = pool.connection() - self.assertFalse(db._con._con.valid) - self.assertEqual(Connection.num_pings, 4) - db.cursor() - self.assertTrue(db._con._con.valid) - self.assertEqual(Connection.num_pings, 5) - pool = PooledDB(dbapi, 1, 1, 1, 0, False, None, None, True, None, 4) - db = pool.connection() - self.assertTrue(db._con._con.valid) - self.assertEqual(Connection.num_pings, 5) - db._con.close() + db.cursor().execute('select test') + cache.append(db) + del db + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 5 + for i in range(5): + con = pool._shared_cache[i] + assert con.shared == 7 + con = con.con + assert con._usage == 10 + assert con._con.num_queries == 10 + del cache + assert len(pool._idle_cache) == 5 + if shareable: + assert len(pool._shared_cache) == 0 + + +@pytest.mark.parametrize("threadsafety", [1, 2]) +def test_many_shared(dbapi, threadsafety): # noqa: F811 + dbapi.threadsafety = threadsafety + shareable = threadsafety > 1 + pool = PooledDB(dbapi, 0, 0, 5) + cache = [] + for i in range(35): db = pool.connection() - self.assertFalse(db._con._con.valid) - self.assertEqual(Connection.num_pings, 5) - cursor = db.cursor() - db._con.close() - self.assertFalse(db._con._con.valid) - self.assertEqual(Connection.num_pings, 5) - cursor.execute('select test') - self.assertTrue(db._con._con.valid) - self.assertEqual(Connection.num_pings, 6) - Connection.has_ping = False - Connection.num_pings = 0 - - def test_failed_transaction(self): - dbapi.threadsafety = 2 - pool = PooledDB(dbapi, 0, 1, 1) + db.cursor().execute('select test1') + db.cursor().execute('select test2') + db.cursor().callproc('test3') + cache.append(db) + del db + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 5 + for i in range(5): + con = pool._shared_cache[i] + assert con.shared == 7 + con = con.con + assert con._usage == 21 + assert con._con.num_queries == 14 + cache[3] = cache[8] = cache[33] = None + cache[12] = cache[17] = cache[34] = None + assert len(pool._shared_cache) == 5 + assert pool._shared_cache[0].shared == 7 + assert pool._shared_cache[1].shared == 7 + assert pool._shared_cache[2].shared == 5 + assert pool._shared_cache[3].shared == 4 + assert pool._shared_cache[4].shared == 6 + for db in cache: + if db: + db.cursor().callproc('test4') + for i in range(6): + db = pool.connection() + db.cursor().callproc('test4') + cache.append(db) + del db + for i in range(5): + con = pool._shared_cache[i] + assert con.shared == 7 + con = con.con + assert con._usage == 28 + assert con._con.num_queries == 14 + del cache + if shareable: + assert len(pool._idle_cache) == 5 + assert len(pool._shared_cache) == 0 + else: + assert len(pool._idle_cache) == 35 + + +@pytest.mark.parametrize("threadsafety", [1, 2]) +def test_rollback(dbapi, threadsafety): # noqa: F811 + dbapi.threadsafety = threadsafety + pool = PooledDB(dbapi, 0, 1) + assert len(pool._idle_cache) == 0 + db = pool.connection(False) + assert len(pool._idle_cache) == 0 + assert db._con._con.open_cursors == 0 + cursor = db.cursor() + assert db._con._con.open_cursors == 1 + cursor.execute('set doit1') + db.commit() + cursor.execute('set dont1') + cursor.close() + assert db._con._con.open_cursors == 0 + del db + assert len(pool._idle_cache) == 1 + db = pool.connection(False) + assert len(pool._idle_cache) == 0 + assert db._con._con.open_cursors == 0 + cursor = db.cursor() + assert db._con._con.open_cursors == 1 + cursor.execute('set doit2') + cursor.close() + assert db._con._con.open_cursors == 0 + db.commit() + session = db._con._con.session + db.close() + assert session == [ + 'doit1', 'commit', 'dont1', 'rollback', + 'doit2', 'commit', 'rollback'] + + +@pytest.mark.parametrize("threadsafety", [1, 2]) +def test_maxconnections(dbapi, threadsafety): # noqa: F811 + dbapi.threadsafety = threadsafety + shareable = threadsafety > 1 + pool = PooledDB(dbapi, 1, 2, 2, 3) + assert hasattr(pool, '_maxconnections') + assert pool._maxconnections == 3 + assert hasattr(pool, '_connections') + assert pool._connections == 0 + assert len(pool._idle_cache) == 1 + cache = [] + for i in range(3): + cache.append(pool.connection(False)) + assert pool._connections == 3 + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 0 + with pytest.raises(TooManyConnections): + pool.connection(False) + with pytest.raises(TooManyConnections): + pool.connection() + cache = [] + assert pool._connections == 0 + assert len(pool._idle_cache) == 2 + if shareable: + assert len(pool._shared_cache) == 0 + for i in range(3): + cache.append(pool.connection()) + assert len(pool._idle_cache) == 0 + if shareable: + assert pool._connections == 2 + assert len(pool._shared_cache) == 2 + cache.append(pool.connection(False)) + assert pool._connections == 3 + assert len(pool._shared_cache) == 2 + else: + assert pool._connections == 3 + with pytest.raises(TooManyConnections): + pool.connection(False) + if shareable: + cache.append(pool.connection(True)) + assert pool._connections == 3 + else: + with pytest.raises(TooManyConnections): + pool.connection() + del cache + assert pool._connections == 0 + assert len(pool._idle_cache) == 2 + pool = PooledDB(dbapi, 0, 1, 1, 1) + assert pool._maxconnections == 1 + assert pool._connections == 0 + assert len(pool._idle_cache) == 0 + db = pool.connection(False) + assert pool._connections == 1 + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 0 + with pytest.raises(TooManyConnections): + pool.connection(False) + with pytest.raises(TooManyConnections): + pool.connection() + assert db + del db + assert pool._connections == 0 + assert len(pool._idle_cache) == 1 + cache = [pool.connection()] + assert pool._connections == 1 + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 1 + cache.append(pool.connection()) + assert pool._connections == 1 + assert len(pool._shared_cache) == 1 + assert pool._shared_cache[0].shared == 2 + else: + with pytest.raises(TooManyConnections): + pool.connection() + with pytest.raises(TooManyConnections): + pool.connection(False) + if shareable: + cache.append(pool.connection(True)) + assert pool._connections == 1 + assert len(pool._shared_cache) == 1 + assert pool._shared_cache[0].shared == 3 + else: + with pytest.raises(TooManyConnections): + pool.connection(True) + del cache + assert pool._connections == 0 + assert len(pool._idle_cache) == 1 + if shareable: + assert len(pool._shared_cache) == 0 + db = pool.connection(False) + assert pool._connections == 1 + assert len(pool._idle_cache) == 0 + assert db + del db + assert pool._connections == 0 + assert len(pool._idle_cache) == 1 + pool = PooledDB(dbapi, 1, 2, 2, 1) + assert pool._maxconnections == 2 + assert pool._connections == 0 + assert len(pool._idle_cache) == 1 + cache = [pool.connection(False)] + assert pool._connections == 1 + assert len(pool._idle_cache) == 0 + cache.append(pool.connection(False)) + assert pool._connections == 2 + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 0 + with pytest.raises(TooManyConnections): + pool.connection(False) + with pytest.raises(TooManyConnections): + pool.connection() + pool = PooledDB(dbapi, 4, 3, 2, 1, False) + assert pool._maxconnections == 4 + assert pool._connections == 0 + assert len(pool._idle_cache) == 4 + cache = [] + for i in range(4): + cache.append(pool.connection(False)) + assert pool._connections == 4 + assert len(pool._idle_cache) == 0 + with pytest.raises(TooManyConnections): + pool.connection(False) + with pytest.raises(TooManyConnections): + pool.connection() + pool = PooledDB(dbapi, 1, 2, 3, 4, False) + assert pool._maxconnections == 4 + assert pool._connections == 0 + assert len(pool._idle_cache) == 1 + for i in range(4): + cache.append(pool.connection()) + assert len(pool._idle_cache) == 0 + if shareable: + assert pool._connections == 3 + assert len(pool._shared_cache) == 3 + cache.append(pool.connection()) + assert pool._connections == 3 + cache.append(pool.connection(False)) + assert pool._connections == 4 + else: + assert pool._connections == 4 + with pytest.raises(TooManyConnections): + pool.connection() + with pytest.raises(TooManyConnections): + pool.connection(False) + pool = PooledDB(dbapi, 0, 0, 3, 3, False) + assert pool._maxconnections == 3 + assert pool._connections == 0 + cache = [] + for i in range(3): + cache.append(pool.connection(False)) + assert pool._connections == 3 + with pytest.raises(TooManyConnections): + pool.connection(False) + with pytest.raises(TooManyConnections): + pool.connection(True) + cache = [] + assert pool._connections == 0 + for i in range(3): + cache.append(pool.connection()) + assert pool._connections == 3 + if shareable: + for i in range(3): + cache.append(pool.connection()) + assert pool._connections == 3 + else: + with pytest.raises(TooManyConnections): + pool.connection() + with pytest.raises(TooManyConnections): + pool.connection(False) + pool = PooledDB(dbapi, 0, 0, 3) + assert pool._maxconnections == 0 + assert pool._connections == 0 + cache = [] + for i in range(10): + cache.append(pool.connection(False)) + cache.append(pool.connection()) + if shareable: + assert pool._connections == 13 + assert len(pool._shared_cache) == 3 + else: + assert pool._connections == 20 + pool = PooledDB(dbapi, 1, 1, 1, 1, True) + assert pool._maxconnections == 1 + assert pool._connections == 0 + assert len(pool._idle_cache) == 1 + db = pool.connection(False) + assert pool._connections == 1 + assert len(pool._idle_cache) == 0 + + def connection(): db = pool.connection() cursor = db.cursor() - db._con._con.close() - cursor.execute('select test') - db.begin() - db._con._con.close() - self.assertRaises(dbapi.InternalError, cursor.execute, 'select test') - cursor.execute('select test') - db.begin() - db.cancel() - db._con._con.close() - cursor.execute('select test') - pool = PooledDB(dbapi, 1, 1, 0) - db = pool.connection() + cursor.execute('set thread') + cursor.close() + db.close() + + thread = Thread(target=connection) + thread.start() + thread.join(0.1) + assert thread.is_alive() + assert pool._connections == 1 + assert len(pool._idle_cache) == 0 + if shareable: + assert len(pool._shared_cache) == 0 + session = db._con._con.session + assert session == ['rollback'] + del db + thread.join(0.1) + assert not thread.is_alive() + assert pool._connections == 0 + assert len(pool._idle_cache) == 1 + if shareable: + assert len(pool._shared_cache) == 0 + db = pool.connection(False) + assert pool._connections == 1 + assert len(pool._idle_cache) == 0 + assert session == ['rollback', 'rollback', 'thread', 'rollback'] + assert db + del db + + +@pytest.mark.parametrize("threadsafety", [1, 2]) +@pytest.mark.parametrize("maxusage", [0, 3, 7]) +def test_maxusage(dbapi, threadsafety, maxusage): # noqa: F811 + dbapi.threadsafety = threadsafety + pool = PooledDB(dbapi, 0, 0, 0, 1, False, maxusage) + assert pool._maxusage == maxusage + assert len(pool._idle_cache) == 0 + db = pool.connection(False) + assert db._con._maxusage == maxusage + assert len(pool._idle_cache) == 0 + assert db._con._con.open_cursors == 0 + assert db._usage == 0 + assert db._con._con.num_uses == 0 + assert db._con._con.num_queries == 0 + for i in range(20): cursor = db.cursor() - db._con._con.close() - cursor.execute('select test') - db.begin() - db._con._con.close() - self.assertRaises(dbapi.InternalError, cursor.execute, 'select test') + assert db._con._con.open_cursors == 1 + cursor.execute(f'select test{i}') + r = cursor.fetchone() + assert r == f'test{i}' + cursor.close() + assert db._con._con.open_cursors == 0 + if maxusage: + j = i % maxusage + 1 + else: + j = i + 1 + assert db._usage == j + assert db._con._con.num_uses == j + assert db._con._con.num_queries == j + db.cursor().callproc('test') + assert db._con._con.open_cursors == 0 + assert db._usage == j + 1 + assert db._con._con.num_uses == j + 1 + assert db._con._con.num_queries == j + + +@pytest.mark.parametrize("threadsafety", [1, 2]) +def test_setsession(dbapi, threadsafety): # noqa: F811 + dbapi.threadsafety = threadsafety + setsession = ('set time zone', 'set datestyle') + pool = PooledDB(dbapi, 0, 0, 0, 1, False, None, setsession) + assert pool._setsession == setsession + db = pool.connection(False) + assert db._setsession_sql == setsession + assert db._con._con.session == ['time zone', 'datestyle'] + db.cursor().execute('select test') + db.cursor().execute('set test1') + assert db._usage == 2 + assert db._con._con.num_uses == 4 + assert db._con._con.num_queries == 1 + assert db._con._con.session == ['time zone', 'datestyle', 'test1'] + db.close() + db = pool.connection(False) + assert db._setsession_sql == setsession + assert db._con._con.session == \ + ['time zone', 'datestyle', 'test1', 'rollback'] + db._con._con.close() + db.cursor().execute('select test') + db.cursor().execute('set test2') + assert db._con._con.session == ['time zone', 'datestyle', 'test2'] + + +@pytest.mark.parametrize("threadsafety", [1, 2]) +def test_one_thread_two_connections(dbapi, threadsafety): # noqa: F811 + dbapi.threadsafety = threadsafety + shareable = threadsafety > 1 + pool = PooledDB(dbapi, 2) + db1 = pool.connection() + for i in range(5): + db1.cursor().execute('select test') + db2 = pool.connection() + assert db1 != db2 + assert db1._con != db2._con + for i in range(7): + db2.cursor().execute('select test') + assert db1._con._con.num_queries == 5 + assert db2._con._con.num_queries == 7 + del db1 + db1 = pool.connection() + assert db1 != db2 + assert db1._con != db2._con + for i in range(3): + db1.cursor().execute('select test') + assert db1._con._con.num_queries == 8 + db2.cursor().execute('select test') + assert db2._con._con.num_queries == 8 + pool = PooledDB(dbapi, 0, 0, 2) + db1 = pool.connection() + for i in range(5): + db1.cursor().execute('select test') + db2 = pool.connection() + assert db1 != db2 + assert db1._con != db2._con + for i in range(7): + db2.cursor().execute('select test') + assert db1._con._con.num_queries == 5 + assert db2._con._con.num_queries == 7 + del db1 + db1 = pool.connection() + assert db1 != db2 + assert db1._con != db2._con + for i in range(3): + db1.cursor().execute('select test') + assert db1._con._con.num_queries == 8 + db2.cursor().execute('select test') + assert db2._con._con.num_queries == 8 + pool = PooledDB(dbapi, 0, 0, 1) + db1 = pool.connection() + db2 = pool.connection() + assert db1 != db2 + if shareable: + assert db1._con == db2._con + else: + assert db1._con != db2._con + del db1 + db1 = pool.connection(False) + assert db1 != db2 + assert db1._con != db2._con + + +@pytest.mark.parametrize("threadsafety", [1, 2]) +def test_three_threads_two_connections(dbapi, threadsafety): # noqa: F811 + dbapi.threadsafety = threadsafety + pool = PooledDB(dbapi, 2, 2, 0, 2, True) + queue = Queue(3) + + def connection(): + try: + queue.put(pool.connection(), 1, 1) + except Exception: + queue.put(pool.connection(), 1) + + for i in range(3): + Thread(target=connection).start() + try: + db1 = queue.get(1, 1) + db2 = queue.get(1, 1) + except TypeError: + db1 = queue.get(1) + db2 = queue.get(1) + assert db1 != db2 + db1_con = db1._con + db2_con = db2._con + assert db1_con != db2_con + try: + with pytest.raises(Empty): + queue.get(1, 0.1) + except TypeError: + with pytest.raises(Empty): + queue.get(0) + del db1 + try: + db1 = queue.get(1, 1) + except TypeError: + db1 = queue.get(1) + assert db1 != db2 + assert db1._con != db2._con + assert db1._con == db1_con + pool = PooledDB(dbapi, 2, 2, 1, 2, True) + db1 = pool.connection(False) + db2 = pool.connection(False) + assert db1 != db2 + db1_con = db1._con + db2_con = db2._con + assert db1_con != db2_con + Thread(target=connection).start() + try: + with pytest.raises(Empty): + queue.get(1, 0.1) + except TypeError: + with pytest.raises(Empty): + queue.get(0) + del db1 + try: + db1 = queue.get(1, 1) + except TypeError: + db1 = queue.get(1) + assert db1 != db2 + assert db1._con != db2._con + assert db1._con == db1_con + + +def test_ping_check(dbapi): # noqa: F811 + con_cls = dbapi.Connection + con_cls.has_ping = True + con_cls.num_pings = 0 + pool = PooledDB(dbapi, 1, 1, 0, 0, False, None, None, True, None, 0) + db = pool.connection() + assert db._con._con.valid + assert con_cls.num_pings == 0 + db._con.close() + db.close() + db = pool.connection() + assert not db._con._con.valid + assert con_cls.num_pings == 0 + pool = PooledDB(dbapi, 1, 1, 1, 0, False, None, None, True, None, 0) + db = pool.connection() + assert db._con._con.valid + assert con_cls.num_pings == 0 + db._con.close() + db = pool.connection() + assert not db._con._con.valid + assert con_cls.num_pings == 0 + pool = PooledDB(dbapi, 1, 1, 0, 0, False, None, None, True, None, 1) + db = pool.connection() + assert db._con._con.valid + assert con_cls.num_pings == 1 + db._con.close() + db.close() + db = pool.connection() + assert db._con._con.valid + assert con_cls.num_pings == 2 + pool = PooledDB(dbapi, 1, 1, 1, 0, False, None, None, True, None, 1) + db = pool.connection() + assert db._con._con.valid + assert con_cls.num_pings == 3 + db._con.close() + db = pool.connection() + assert db._con._con.valid + assert con_cls.num_pings == 4 + pool = PooledDB(dbapi, 1, 1, 1, 0, False, None, None, True, None, 2) + db = pool.connection() + assert db._con._con.valid + assert con_cls.num_pings == 4 + db._con.close() + db = pool.connection() + assert not db._con._con.valid + assert con_cls.num_pings == 4 + db.cursor() + assert db._con._con.valid + assert con_cls.num_pings == 5 + pool = PooledDB(dbapi, 1, 1, 1, 0, False, None, None, True, None, 4) + db = pool.connection() + assert db._con._con.valid + assert con_cls.num_pings == 5 + db._con.close() + db = pool.connection() + assert not db._con._con.valid + assert con_cls.num_pings == 5 + cursor = db.cursor() + db._con.close() + assert not db._con._con.valid + assert con_cls.num_pings == 5 + cursor.execute('select test') + assert db._con._con.valid + assert con_cls.num_pings == 6 + con_cls.has_ping = False + con_cls.num_pings = 0 + + +def test_failed_transaction(dbapi): # noqa: F811 + pool = PooledDB(dbapi, 0, 1, 1) + db = pool.connection() + cursor = db.cursor() + db._con._con.close() + cursor.execute('select test') + db.begin() + db._con._con.close() + with pytest.raises(dbapi.InternalError): cursor.execute('select test') - db.begin() - db.cancel() - db._con._con.close() + cursor.execute('select test') + db.begin() + db.cancel() + db._con._con.close() + cursor.execute('select test') + pool = PooledDB(dbapi, 1, 1, 0) + db = pool.connection() + cursor = db.cursor() + db._con._con.close() + cursor.execute('select test') + db.begin() + db._con._con.close() + with pytest.raises(dbapi.InternalError): cursor.execute('select test') + cursor.execute('select test') + db.begin() + db.cancel() + db._con._con.close() + cursor.execute('select test') - def test_shared_in_transaction(self): - dbapi.threadsafety = 2 - pool = PooledDB(dbapi, 0, 1, 1) - db = pool.connection() - db.begin() - pool.connection(False) - self.assertRaises(TooManyConnections, pool.connection) - pool = PooledDB(dbapi, 0, 2, 2) - db1 = pool.connection() - db2 = pool.connection() - self.assertIsNot(db2._con, db1._con) - db2.close() - db2 = pool.connection() - self.assertIsNot(db2._con, db1._con) - db = pool.connection() - self.assertIs(db._con, db1._con) - db.close() - db1.begin() - db = pool.connection() - self.assertIs(db._con, db2._con) - db.close() - db2.begin() - pool.connection(False) - self.assertRaises(TooManyConnections, pool.connection) - db1.rollback() - db = pool.connection() - self.assertIs(db._con, db1._con) - def test_reset_transaction(self): - pool = PooledDB(dbapi, 1, 1, 0) - db = pool.connection() - db.begin() - con = db._con - self.assertTrue(con._transaction) - self.assertEqual(con._con.session, ['rollback']) - db.close() - self.assertIs(pool.connection()._con, con) - self.assertFalse(con._transaction) - self.assertEqual(con._con.session, ['rollback'] * 3) - pool = PooledDB(dbapi, 1, 1, 0, reset=False) - db = pool.connection() - db.begin() - con = db._con - self.assertTrue(con._transaction) - self.assertEqual(con._con.session, []) - db.close() - self.assertIs(pool.connection()._con, con) - self.assertFalse(con._transaction) - self.assertEqual(con._con.session, ['rollback']) - - def test_context_manager(self): - pool = PooledDB(dbapi, 1, 1, 1) - con = pool._idle_cache[0]._con - with pool.connection() as db: - self.assertTrue(hasattr(db, '_shared_con')) - self.assertFalse(pool._idle_cache) - self.assertTrue(con.valid) - with db.cursor() as cursor: - self.assertEqual(con.open_cursors, 1) - cursor.execute('select test') - r = cursor.fetchone() - self.assertEqual(con.open_cursors, 0) - self.assertEqual(r, 'test') - self.assertEqual(con.num_queries, 1) - self.assertTrue(pool._idle_cache) - with pool.dedicated_connection() as db: - self.assertFalse(hasattr(db, '_shared_con')) - self.assertFalse(pool._idle_cache) - with db.cursor() as cursor: - self.assertEqual(con.open_cursors, 1) - cursor.execute('select test') - r = cursor.fetchone() - self.assertEqual(con.open_cursors, 0) - self.assertEqual(r, 'test') - self.assertEqual(con.num_queries, 2) - self.assertTrue(pool._idle_cache) - - -class TestSharedDBConnection(unittest.TestCase): - - def test_create_connection(self): - db_con = dbapi.connect() - con = SharedDBConnection(db_con) - self.assertEqual(con.con, db_con) - self.assertEqual(con.shared, 1) - - def test_share_and_unshare(self): - con = SharedDBConnection(dbapi.connect()) - self.assertEqual(con.shared, 1) - con.share() - self.assertEqual(con.shared, 2) - con.share() - self.assertEqual(con.shared, 3) - con.unshare() - self.assertEqual(con.shared, 2) - con.unshare() - self.assertEqual(con.shared, 1) - - def test_comparison(self): - con1 = SharedDBConnection(dbapi.connect()) - con1.con._transaction = False - con2 = SharedDBConnection(dbapi.connect()) - con2.con._transaction = False - self.assertTrue(con1 == con2) - self.assertTrue(con1 <= con2) - self.assertTrue(con1 >= con2) - self.assertFalse(con1 != con2) - self.assertFalse(con1 < con2) - self.assertFalse(con1 > con2) - con2.share() - self.assertFalse(con1 == con2) - self.assertTrue(con1 <= con2) - self.assertFalse(con1 >= con2) - self.assertTrue(con1 != con2) - self.assertTrue(con1 < con2) - self.assertFalse(con1 > con2) - con1.con._transaction = True - self.assertFalse(con1 == con2) - self.assertFalse(con1 <= con2) - self.assertTrue(con1 >= con2) - self.assertTrue(con1 != con2) - self.assertFalse(con1 < con2) - self.assertTrue(con1 > con2) - - -if __name__ == '__main__': - unittest.main() +def test_shared_in_transaction(dbapi): # noqa: F811 + pool = PooledDB(dbapi, 0, 1, 1) + db = pool.connection() + db.begin() + pool.connection(False) + with pytest.raises(TooManyConnections): + pool.connection() + pool = PooledDB(dbapi, 0, 2, 2) + db1 = pool.connection() + db2 = pool.connection() + assert db2._con is not db1._con + db2.close() + db2 = pool.connection() + assert db2._con is not db1._con + db = pool.connection() + assert db._con is db1._con + db.close() + db1.begin() + db = pool.connection() + assert db._con is db2._con + db.close() + db2.begin() + pool.connection(False) + with pytest.raises(TooManyConnections): + pool.connection() + db1.rollback() + db = pool.connection() + assert db._con is db1._con + + +def test_reset_transaction(dbapi): # noqa: F811 + pool = PooledDB(dbapi, 1, 1, 0) + db = pool.connection() + db.begin() + con = db._con + assert con._transaction + assert con._con.session == ['rollback'] + db.close() + assert pool.connection()._con is con + assert not con._transaction + assert con._con.session == ['rollback'] * 3 + pool = PooledDB(dbapi, 1, 1, 0, reset=False) + db = pool.connection() + db.begin() + con = db._con + assert con._transaction + assert con._con.session == [] + db.close() + assert pool.connection()._con is con + assert not con._transaction + assert con._con.session == ['rollback'] + + +def test_context_manager(dbapi): # noqa: F811 + pool = PooledDB(dbapi, 1, 1, 1) + con = pool._idle_cache[0]._con + with pool.connection() as db: + assert hasattr(db, '_shared_con') + assert not pool._idle_cache + assert con.valid + with db.cursor() as cursor: + assert con.open_cursors == 1 + cursor.execute('select test') + r = cursor.fetchone() + assert con.open_cursors == 0 + assert r == 'test' + assert con.num_queries == 1 + assert pool._idle_cache + with pool.dedicated_connection() as db: + assert not hasattr(db, '_shared_con') + assert not pool._idle_cache + with db.cursor() as cursor: + assert con.open_cursors == 1 + cursor.execute('select test') + r = cursor.fetchone() + assert con.open_cursors == 0 + assert r == 'test' + assert con.num_queries == 2 + assert pool._idle_cache + + +def test_shared_db_connection_create(dbapi): # noqa: F811 + db_con = dbapi.connect() + con = SharedDBConnection(db_con) + assert con.con == db_con + assert con.shared == 1 + + +def test_shared_db_connection_share_and_unshare(dbapi): # noqa: F811 + con = SharedDBConnection(dbapi.connect()) + assert con.shared == 1 + con.share() + assert con.shared == 2 + con.share() + assert con.shared == 3 + con.unshare() + assert con.shared == 2 + con.unshare() + assert con.shared == 1 + + +def test_shared_db_connection_compare(dbapi): # noqa: F811 + con1 = SharedDBConnection(dbapi.connect()) + con1.con._transaction = False + con2 = SharedDBConnection(dbapi.connect()) + con2.con._transaction = False + assert con1 == con2 + assert con1 <= con2 + assert con1 >= con2 + assert not con1 != con2 + assert not con1 < con2 + assert not con1 > con2 + con2.share() + assert not con1 == con2 + assert con1 <= con2 + assert not con1 >= con2 + assert con1 != con2 + assert con1 < con2 + assert not con1 > con2 + con1.con._transaction = True + assert not con1 == con2 + assert not con1 <= con2 + assert con1 >= con2 + assert con1 != con2 + assert not con1 < con2 + assert con1 > con2 diff --git a/tests/test_pooled_pg.py b/tests/test_pooled_pg.py index ec088a5..b09469f 100644 --- a/tests/test_pooled_pg.py +++ b/tests/test_pooled_pg.py @@ -10,310 +10,317 @@ * This test was contributed by Christoph Zwerschke """ -import unittest +from queue import Queue, Empty +from threading import Thread -from . import mock_pg # noqa +import pg # noqa: F401 +import pytest from dbutils.pooled_pg import PooledPg, InvalidConnection, TooManyConnections +from dbutils.steady_pg import SteadyPgConnection -class TestPooledPg(unittest.TestCase): +def test_version(): + from dbutils import __version__, pooled_pg + assert pooled_pg.__version__ == __version__ + assert PooledPg.version == __version__ - def test_version(self): - from dbutils import __version__, pooled_pg - self.assertEqual(pooled_pg.__version__, __version__) - self.assertEqual(PooledPg.version, __version__) - def test_create_connection(self): - pool = PooledPg( - 1, 1, 0, False, None, None, False, - 'PooledPgTestDB', user='PooledPgTestUser') - self.assertTrue(hasattr(pool, '_cache')) - self.assertEqual(pool._cache.qsize(), 1) - self.assertTrue(hasattr(pool, '_maxusage')) - self.assertIsNone(pool._maxusage) - self.assertTrue(hasattr(pool, '_setsession')) - self.assertIsNone(pool._setsession) - self.assertTrue(hasattr(pool, '_reset')) - self.assertFalse(pool._reset) - db_con = pool._cache.get(0) - pool._cache.put(db_con, 0) - from dbutils.steady_pg import SteadyPgConnection - self.assertTrue(isinstance(db_con, SteadyPgConnection)) - db = pool.connection() - self.assertEqual(pool._cache.qsize(), 0) - self.assertTrue(hasattr(db, '_con')) - self.assertEqual(db._con, db_con) - self.assertTrue(hasattr(db, 'query')) - self.assertTrue(hasattr(db, 'num_queries')) - self.assertEqual(db.num_queries, 0) - self.assertTrue(hasattr(db, '_maxusage')) - self.assertEqual(db._maxusage, 0) - self.assertTrue(hasattr(db, '_setsession_sql')) - self.assertIsNone(db._setsession_sql) - self.assertTrue(hasattr(db, 'dbname')) - self.assertEqual(db.dbname, 'PooledPgTestDB') - self.assertTrue(hasattr(db, 'user')) - self.assertEqual(db.user, 'PooledPgTestUser') - db.query('select test') - self.assertEqual(db.num_queries, 1) - pool = PooledPg(1) - db = pool.connection() - self.assertTrue(hasattr(db, 'dbname')) - self.assertIsNone(db.dbname) - self.assertTrue(hasattr(db, 'user')) - self.assertIsNone(db.user) - self.assertTrue(hasattr(db, 'num_queries')) - self.assertEqual(db.num_queries, 0) - pool = PooledPg(0, 0, 0, False, 3, ('set datestyle',),) - self.assertEqual(pool._maxusage, 3) - self.assertEqual(pool._setsession, ('set datestyle',)) - db = pool.connection() - self.assertEqual(db._maxusage, 3) - self.assertEqual(db._setsession_sql, ('set datestyle',)) +def test_create_connection(): + pool = PooledPg( + 1, 1, 0, False, None, None, False, + 'PooledPgTestDB', user='PooledPgTestUser') + assert hasattr(pool, '_cache') + assert pool._cache.qsize() == 1 + assert hasattr(pool, '_maxusage') + assert pool._maxusage is None + assert hasattr(pool, '_setsession') + assert pool._setsession is None + assert hasattr(pool, '_reset') + assert not pool._reset + db_con = pool._cache.get(0) + pool._cache.put(db_con, 0) + assert isinstance(db_con, SteadyPgConnection) + db = pool.connection() + assert pool._cache.qsize() == 0 + assert hasattr(db, '_con') + assert db._con == db_con + assert hasattr(db, 'query') + assert hasattr(db, 'num_queries') + assert db.num_queries == 0 + assert hasattr(db, '_maxusage') + assert db._maxusage == 0 + assert hasattr(db, '_setsession_sql') + assert db._setsession_sql is None + assert hasattr(db, 'dbname') + assert db.dbname == 'PooledPgTestDB' + assert hasattr(db, 'user') + assert db.user == 'PooledPgTestUser' + db.query('select test') + assert db.num_queries == 1 + pool = PooledPg(1) + db = pool.connection() + assert hasattr(db, 'dbname') + assert db.dbname is None + assert hasattr(db, 'user') + assert db.user is None + assert hasattr(db, 'num_queries') + assert db.num_queries == 0 + pool = PooledPg(0, 0, 0, False, 3, ('set datestyle',),) + assert pool._maxusage == 3 + assert pool._setsession == ('set datestyle',) + db = pool.connection() + assert db._maxusage == 3 + assert db._setsession_sql == ('set datestyle',) - def test_close_connection(self): - pool = PooledPg( - 0, 1, 0, False, None, None, False, - 'PooledPgTestDB', user='PooledPgTestUser') - db = pool.connection() - self.assertTrue(hasattr(db, '_con')) - db_con = db._con - from dbutils.steady_pg import SteadyPgConnection - self.assertTrue(isinstance(db_con, SteadyPgConnection)) - self.assertTrue(hasattr(pool, '_cache')) - self.assertEqual(pool._cache.qsize(), 0) - self.assertEqual(db.num_queries, 0) - db.query('select test') - self.assertEqual(db.num_queries, 1) - db.close() - self.assertRaises(InvalidConnection, getattr, db, 'num_queries') - db = pool.connection() - self.assertTrue(hasattr(db, 'dbname')) - self.assertEqual(db.dbname, 'PooledPgTestDB') - self.assertTrue(hasattr(db, 'user')) - self.assertEqual(db.user, 'PooledPgTestUser') - self.assertEqual(db.num_queries, 1) - db.query('select test') - self.assertEqual(db.num_queries, 2) - db = pool.connection() - self.assertEqual(pool._cache.qsize(), 1) - self.assertEqual(pool._cache.get(0), db_con) - def test_min_max_cached(self): - pool = PooledPg(3) - self.assertTrue(hasattr(pool, '_cache')) - self.assertEqual(pool._cache.qsize(), 3) - cache = [pool.connection() for i in range(3)] - self.assertEqual(pool._cache.qsize(), 0) - for i in range(3): - cache.pop().close() - self.assertEqual(pool._cache.qsize(), 3) - for i in range(6): - cache.append(pool.connection()) - self.assertEqual(pool._cache.qsize(), 0) - for i in range(6): - cache.pop().close() - self.assertEqual(pool._cache.qsize(), 6) - pool = PooledPg(3, 4) - self.assertTrue(hasattr(pool, '_cache')) - self.assertEqual(pool._cache.qsize(), 3) - cache = [pool.connection() for i in range(3)] - self.assertEqual(pool._cache.qsize(), 0) - for i in range(3): - cache.pop().close() - self.assertEqual(pool._cache.qsize(), 3) - for i in range(6): - cache.append(pool.connection()) - self.assertEqual(pool._cache.qsize(), 0) - for i in range(6): - cache.pop().close() - self.assertEqual(pool._cache.qsize(), 4) - pool = PooledPg(3, 2) - self.assertTrue(hasattr(pool, '_cache')) - self.assertEqual(pool._cache.qsize(), 3) - cache = [pool.connection() for i in range(4)] - self.assertEqual(pool._cache.qsize(), 0) - for i in range(4): - cache.pop().close() - self.assertEqual(pool._cache.qsize(), 3) - pool = PooledPg(2, 5) - self.assertTrue(hasattr(pool, '_cache')) - self.assertEqual(pool._cache.qsize(), 2) - cache = [pool.connection() for i in range(10)] - self.assertEqual(pool._cache.qsize(), 0) - for i in range(10): - cache.pop().close() - self.assertEqual(pool._cache.qsize(), 5) +def test_close_connection(): + pool = PooledPg( + 0, 1, 0, False, None, None, False, + 'PooledPgTestDB', user='PooledPgTestUser') + db = pool.connection() + assert hasattr(db, '_con') + db_con = db._con + assert isinstance(db_con, SteadyPgConnection) + assert hasattr(pool, '_cache') + assert pool._cache.qsize() == 0 + assert db.num_queries == 0 + db.query('select test') + assert db.num_queries == 1 + db.close() + with pytest.raises(InvalidConnection): + assert db.num_queries + db = pool.connection() + assert hasattr(db, 'dbname') + assert db.dbname == 'PooledPgTestDB' + assert hasattr(db, 'user') + assert db.user == 'PooledPgTestUser' + assert db.num_queries == 1 + db.query('select test') + assert db.num_queries == 2 + db = pool.connection() + assert pool._cache.qsize() == 1 + assert pool._cache.get(0) == db_con - def test_max_connections(self): - from dbutils.pooled_pg import TooManyConnections - pool = PooledPg(1, 2, 3) - self.assertEqual(pool._cache.qsize(), 1) - cache = [pool.connection() for i in range(3)] - self.assertEqual(pool._cache.qsize(), 0) - self.assertRaises(TooManyConnections, pool.connection) - pool = PooledPg(0, 1, 1, False) - self.assertEqual(pool._blocking, 0) - self.assertEqual(pool._cache.qsize(), 0) - db = pool.connection() - self.assertEqual(pool._cache.qsize(), 0) - self.assertRaises(TooManyConnections, pool.connection) - del db - del cache - pool = PooledPg(1, 2, 1) - self.assertEqual(pool._cache.qsize(), 1) - cache = [pool.connection()] - self.assertEqual(pool._cache.qsize(), 0) + +def test_min_max_cached(): + pool = PooledPg(3) + assert hasattr(pool, '_cache') + assert pool._cache.qsize() == 3 + cache = [pool.connection() for i in range(3)] + assert pool._cache.qsize() == 0 + for i in range(3): + cache.pop().close() + assert pool._cache.qsize() == 3 + for i in range(6): + cache.append(pool.connection()) + assert pool._cache.qsize() == 0 + for i in range(6): + cache.pop().close() + assert pool._cache.qsize() == 6 + pool = PooledPg(3, 4) + assert hasattr(pool, '_cache') + assert pool._cache.qsize() == 3 + cache = [pool.connection() for i in range(3)] + assert pool._cache.qsize() == 0 + for i in range(3): + cache.pop().close() + assert pool._cache.qsize() == 3 + for i in range(6): cache.append(pool.connection()) - self.assertEqual(pool._cache.qsize(), 0) - self.assertRaises(TooManyConnections, pool.connection) - pool = PooledPg(3, 2, 1, False) - self.assertEqual(pool._cache.qsize(), 3) - cache = [pool.connection() for i in range(3)] - self.assertEqual(len(cache), 3) - self.assertEqual(pool._cache.qsize(), 0) - self.assertRaises(TooManyConnections, pool.connection) - pool = PooledPg(1, 1, 1, True) - self.assertEqual(pool._blocking, 1) - self.assertEqual(pool._cache.qsize(), 1) - db = pool.connection() - self.assertEqual(pool._cache.qsize(), 0) + assert pool._cache.qsize() == 0 + for i in range(6): + cache.pop().close() + assert pool._cache.qsize() == 4 + pool = PooledPg(3, 2) + assert hasattr(pool, '_cache') + assert pool._cache.qsize() == 3 + cache = [pool.connection() for i in range(4)] + assert pool._cache.qsize() == 0 + for i in range(4): + cache.pop().close() + assert pool._cache.qsize() == 3 + pool = PooledPg(2, 5) + assert hasattr(pool, '_cache') + assert pool._cache.qsize() == 2 + cache = [pool.connection() for i in range(10)] + assert pool._cache.qsize() == 0 + for i in range(10): + cache.pop().close() + assert pool._cache.qsize() == 5 - def connection(): - pool.connection().query('set thread') - from threading import Thread - thread = Thread(target=connection) - thread.start() - thread.join(0.1) - self.assertTrue(thread.is_alive()) - self.assertEqual(pool._cache.qsize(), 0) - session = db._con.session - self.assertEqual(session, []) - del db - thread.join(0.1) - self.assertFalse(thread.is_alive()) - self.assertEqual(pool._cache.qsize(), 1) - db = pool.connection() - self.assertEqual(pool._cache.qsize(), 0) - self.assertEqual(session, ['thread']) - del db +def test_max_connections(): + pool = PooledPg(1, 2, 3) + assert pool._cache.qsize() == 1 + cache = [pool.connection() for i in range(3)] + assert pool._cache.qsize() == 0 + with pytest.raises(TooManyConnections): + pool.connection() + pool = PooledPg(0, 1, 1, False) + assert pool._blocking == 0 + assert pool._cache.qsize() == 0 + db = pool.connection() + assert pool._cache.qsize() == 0 + with pytest.raises(TooManyConnections): + pool.connection() + del db + del cache + pool = PooledPg(1, 2, 1) + assert pool._cache.qsize() == 1 + cache = [pool.connection()] + assert pool._cache.qsize() == 0 + cache.append(pool.connection()) + assert pool._cache.qsize() == 0 + with pytest.raises(TooManyConnections): + pool.connection() + pool = PooledPg(3, 2, 1, False) + assert pool._cache.qsize() == 3 + cache = [pool.connection() for i in range(3)] + assert len(cache) == 3 + assert pool._cache.qsize() == 0 + with pytest.raises(TooManyConnections): + pool.connection() + pool = PooledPg(1, 1, 1, True) + assert pool._blocking == 1 + assert pool._cache.qsize() == 1 + db = pool.connection() + assert pool._cache.qsize() == 0 - def test_one_thread_two_connections(self): - pool = PooledPg(2) - db1 = pool.connection() - for i in range(5): - db1.query('select test') - db2 = pool.connection() - self.assertNotEqual(db1, db2) - self.assertNotEqual(db1._con, db2._con) - for i in range(7): - db2.query('select test') - self.assertEqual(db1.num_queries, 5) - self.assertEqual(db2.num_queries, 7) - del db1 - db1 = pool.connection() - self.assertNotEqual(db1, db2) - self.assertNotEqual(db1._con, db2._con) - self.assertTrue(hasattr(db1, 'query')) - for i in range(3): - db1.query('select test') - self.assertEqual(db1.num_queries, 8) + def connection(): + pool.connection().query('set thread') + + thread = Thread(target=connection) + thread.start() + thread.join(0.1) + assert thread.is_alive() + assert pool._cache.qsize() == 0 + session = db._con.session + assert session == [] + del db + thread.join(0.1) + assert not thread.is_alive() + assert pool._cache.qsize() == 1 + db = pool.connection() + assert pool._cache.qsize() == 0 + assert session == ['thread'] + del db + + +def test_one_thread_two_connections(): + pool = PooledPg(2) + db1 = pool.connection() + for i in range(5): + db1.query('select test') + db2 = pool.connection() + assert db1 != db2 + assert db1._con != db2._con + for i in range(7): db2.query('select test') - self.assertEqual(db2.num_queries, 8) + assert db1.num_queries == 5 + assert db2.num_queries == 7 + del db1 + db1 = pool.connection() + assert db1 != db2 + assert db1._con != db2._con + assert hasattr(db1, 'query') + for i in range(3): + db1.query('select test') + assert db1.num_queries == 8 + db2.query('select test') + assert db2.num_queries == 8 - def test_three_threads_two_connections(self): - pool = PooledPg(2, 2, 2, True) - from queue import Queue, Empty - queue = Queue(3) - def connection(): - try: - queue.put(pool.connection(), 1, 1) - except TypeError: - queue.put(pool.connection(), 1) +def test_three_threads_two_connections(): + pool = PooledPg(2, 2, 2, True) + queue = Queue(3) - from threading import Thread - for i in range(3): - Thread(target=connection).start() - try: - db1 = queue.get(1, 1) - db2 = queue.get(1, 1) - except TypeError: - db1 = queue.get(1) - db2 = queue.get(1) - db1_con = db1._con - db2_con = db2._con - self.assertNotEqual(db1, db2) - self.assertNotEqual(db1_con, db2_con) - try: - self.assertRaises(Empty, queue.get, 1, 0.1) - except TypeError: - self.assertRaises(Empty, queue.get, 0) - del db1 + def connection(): try: - db1 = queue.get(1, 1) + queue.put(pool.connection(), 1, 1) except TypeError: - db1 = queue.get(1) - self.assertNotEqual(db1, db2) - self.assertNotEqual(db1._con, db2._con) - self.assertEqual(db1._con, db1_con) + queue.put(pool.connection(), 1) + + for i in range(3): + Thread(target=connection).start() + try: + db1 = queue.get(1, 1) + db2 = queue.get(1, 1) + except TypeError: + db1 = queue.get(1) + db2 = queue.get(1) + db1_con = db1._con + db2_con = db2._con + assert db1 != db2 + assert db1_con != db2_con + try: + with pytest.raises(Empty): + queue.get(1, 0.1) + except TypeError: + with pytest.raises(Empty): + queue.get(0) + del db1 + try: + db1 = queue.get(1, 1) + except TypeError: + db1 = queue.get(1) + assert db1 != db2 + assert db1._con != db2._con + assert db1._con == db1_con - def test_reset_transaction(self): - pool = PooledPg(1) - db = pool.connection() - db.begin() - con = db._con - self.assertTrue(con._transaction) - db.query('select test') - self.assertEqual(con.num_queries, 1) - db.close() - self.assertIs(pool.connection()._con, con) - self.assertFalse(con._transaction) - self.assertEqual(con.session, ['begin', 'rollback']) - self.assertEqual(con.num_queries, 1) - pool = PooledPg(1, reset=1) - db = pool.connection() - db.begin() - con = db._con - self.assertTrue(con._transaction) - self.assertEqual(con.session, ['rollback', 'begin']) - db.query('select test') - self.assertEqual(con.num_queries, 1) - db.close() - self.assertIs(pool.connection()._con, con) - self.assertFalse(con._transaction) - self.assertEqual( - con.session, ['rollback', 'begin', 'rollback', 'rollback']) - self.assertEqual(con.num_queries, 1) - pool = PooledPg(1, reset=2) - db = pool.connection() - db.begin() - con = db._con - self.assertTrue(con._transaction) - self.assertEqual(con.session, ['begin']) - db.query('select test') - self.assertEqual(con.num_queries, 1) - db.close() - self.assertIs(pool.connection()._con, con) - self.assertFalse(con._transaction) - self.assertEqual(con.session, []) - self.assertEqual(con.num_queries, 0) - def test_context_manager(self): - pool = PooledPg(1, 1, 1) - with pool.connection() as db: - db_con = db._con._con - db.query('select test') - self.assertEqual(db_con.num_queries, 1) - self.assertRaises(TooManyConnections, pool.connection) - with pool.connection() as db: - db_con = db._con._con - db.query('select test') - self.assertEqual(db_con.num_queries, 2) - self.assertRaises(TooManyConnections, pool.connection) +def test_reset_transaction(): + pool = PooledPg(1) + db = pool.connection() + db.begin() + con = db._con + assert con._transaction + db.query('select test') + assert con.num_queries == 1 + db.close() + assert pool.connection()._con is con + assert not con._transaction + assert con.session == ['begin', 'rollback'] + assert con.num_queries == 1 + pool = PooledPg(1, reset=1) + db = pool.connection() + db.begin() + con = db._con + assert con._transaction + assert con.session == ['rollback', 'begin'] + db.query('select test') + assert con.num_queries == 1 + db.close() + assert pool.connection()._con is con + assert not con._transaction + assert con.session == ['rollback', 'begin', 'rollback', 'rollback'] + assert con.num_queries == 1 + pool = PooledPg(1, reset=2) + db = pool.connection() + db.begin() + con = db._con + assert con._transaction + assert con.session == ['begin'] + db.query('select test') + assert con.num_queries == 1 + db.close() + assert pool.connection()._con is con + assert not con._transaction + assert con.session == [] + assert con.num_queries == 0 -if __name__ == '__main__': - unittest.main() +def test_context_manager(): + pool = PooledPg(1, 1, 1) + with pool.connection() as db: + db_con = db._con._con + db.query('select test') + assert db_con.num_queries == 1 + with pytest.raises(TooManyConnections): + pool.connection() + with pool.connection() as db: + db_con = db._con._con + db.query('select test') + assert db_con.num_queries == 2 + with pytest.raises(TooManyConnections): + pool.connection() diff --git a/tests/test_simple_pooled_db.py b/tests/test_simple_pooled_db.py index 4cf82ae..3cfd99e 100644 --- a/tests/test_simple_pooled_db.py +++ b/tests/test_simple_pooled_db.py @@ -11,137 +11,141 @@ * This test was contributed by Christoph Zwerschke """ -import unittest +from queue import Queue, Empty + +import pytest from . import mock_db as dbapi from dbutils import simple_pooled_db -class TestSimplePooledDB(unittest.TestCase): - - def my_db_pool(self, mythreadsafety, maxConnections): - threadsafety = dbapi.threadsafety - dbapi.threadsafety = mythreadsafety - try: - return simple_pooled_db.PooledDB( - dbapi, maxConnections, - 'SimplePooledDBTestDB', 'SimplePooledDBTestUser') - finally: - dbapi.threadsafety = threadsafety - - def test_version(self): - from dbutils import __version__ - self.assertEqual(simple_pooled_db.__version__, __version__) - self.assertEqual(simple_pooled_db.PooledDB.version, __version__) - - def test_no_threadsafety(self): - for threadsafety in (None, -1, 0, 4): - self.assertRaises( - simple_pooled_db.NotSupportedError, - self.my_db_pool, threadsafety, 1) - - def test_create_connection(self): - for threadsafety in (1, 2, 3): - dbpool = self.my_db_pool(threadsafety, 1) - db = dbpool.connection() - self.assertTrue(hasattr(db, 'cursor')) - self.assertTrue(hasattr(db, 'open_cursors')) - self.assertEqual(db.open_cursors, 0) - self.assertTrue(hasattr(db, 'database')) - self.assertEqual(db.database, 'SimplePooledDBTestDB') - self.assertTrue(hasattr(db, 'user')) - self.assertEqual(db.user, 'SimplePooledDBTestUser') - cursor = db.cursor() - self.assertIsNotNone(cursor) - self.assertEqual(db.open_cursors, 1) - del cursor - - def test_close_connection(self): - for threadsafety in (1, 2, 3): - db_pool = self.my_db_pool(threadsafety, 1) - db = db_pool.connection() - self.assertEqual(db.open_cursors, 0) - cursor1 = db.cursor() - self.assertIsNotNone(cursor1) - self.assertEqual(db.open_cursors, 1) - db.close() - self.assertFalse(hasattr(db, 'open_cursors')) - db = db_pool.connection() - self.assertTrue(hasattr(db, 'database')) - self.assertEqual(db.database, 'SimplePooledDBTestDB') - self.assertTrue(hasattr(db, 'user')) - self.assertEqual(db.user, 'SimplePooledDBTestUser') - self.assertEqual(db.open_cursors, 1) - cursor2 = db.cursor() - self.assertIsNotNone(cursor2) - self.assertEqual(db.open_cursors, 2) - del cursor2 - del cursor1 - - def test_two_connections(self): - for threadsafety in (1, 2, 3): - db_pool = self.my_db_pool(threadsafety, 2) - db1 = db_pool.connection() - cursors1 = [db1.cursor() for i in range(5)] - db2 = db_pool.connection() - self.assertNotEqual(db1, db2) - cursors2 = [db2.cursor() for i in range(7)] - self.assertEqual(db1.open_cursors, 5) - self.assertEqual(db2.open_cursors, 7) - db1.close() - db1 = db_pool.connection() - self.assertNotEqual(db1, db2) - self.assertTrue(hasattr(db1, 'cursor')) - for i in range(3): - cursors1.append(db1.cursor()) - self.assertEqual(db1.open_cursors, 8) - cursors2.append(db2.cursor()) - self.assertEqual(db2.open_cursors, 8) - del cursors2 - del cursors1 - - def test_threadsafety_1(self): - db_pool = self.my_db_pool(1, 2) - from queue import Queue, Empty - queue = Queue(3) - - def connection(): - queue.put(db_pool.connection()) - - from threading import Thread - threads = [Thread(target=connection).start() for i in range(3)] - self.assertEqual(len(threads), 3) - try: - db1 = queue.get(1, 1) - db2 = queue.get(1, 1) - except TypeError: - db1 = queue.get(1) - db2 = queue.get(1) - self.assertNotEqual(db1, db2) - self.assertNotEqual(db1._con, db2._con) - try: - self.assertRaises(Empty, queue.get, 1, 0.1) - except TypeError: - self.assertRaises(Empty, queue.get, 0) - db2.close() - try: - db3 = queue.get(1, 1) - except TypeError: - db3 = queue.get(1) - self.assertNotEqual(db1, db3) - self.assertNotEqual(db1._con, db3._con) - - def test_threadsafety_2(self): - for threadsafety in (2, 3): - dbpool = self.my_db_pool(threadsafety, 2) - db1 = dbpool.connection() - db2 = dbpool.connection() - cursors = [dbpool.connection().cursor() for i in range(100)] - self.assertEqual(db1.open_cursors, 50) - self.assertEqual(db2.open_cursors, 50) - del cursors - - -if __name__ == '__main__': - unittest.main() +def my_db_pool(mythreadsafety, max_connections): + threadsafety = dbapi.threadsafety + dbapi.threadsafety = mythreadsafety + try: + return simple_pooled_db.PooledDB( + dbapi, max_connections, + 'SimplePooledDBTestDB', 'SimplePooledDBTestUser') + finally: + dbapi.threadsafety = threadsafety + + +def test_version(): + from dbutils import __version__ + assert simple_pooled_db.__version__ == __version__ + assert simple_pooled_db.PooledDB.version == __version__ + + +@pytest.mark.parametrize("threadsafety", [None, -1, 0, 4]) +def test_no_threadsafety(threadsafety): + with pytest.raises(simple_pooled_db.NotSupportedError): + my_db_pool(threadsafety, 1) + + +@pytest.mark.parametrize("threadsafety", [1, 2, 3]) +def test_create_connection(threadsafety): + dbpool = my_db_pool(threadsafety, 1) + db = dbpool.connection() + assert hasattr(db, 'cursor') + assert hasattr(db, 'open_cursors') + assert db.open_cursors == 0 + assert hasattr(db, 'database') + assert db.database == 'SimplePooledDBTestDB' + assert hasattr(db, 'user') + assert db.user == 'SimplePooledDBTestUser' + cursor = db.cursor() + assert cursor is not None + assert db.open_cursors == 1 + del cursor + + +@pytest.mark.parametrize("threadsafety", [1, 2, 3]) +def test_close_connection(threadsafety): + db_pool = my_db_pool(threadsafety, 1) + db = db_pool.connection() + assert db.open_cursors == 0 + cursor1 = db.cursor() + assert cursor1 is not None + assert db.open_cursors == 1 + db.close() + assert not hasattr(db, 'open_cursors') + db = db_pool.connection() + assert hasattr(db, 'database') + assert db.database == 'SimplePooledDBTestDB' + assert hasattr(db, 'user') + assert db.user == 'SimplePooledDBTestUser' + assert db.open_cursors == 1 + cursor2 = db.cursor() + assert cursor2 is not None + assert db.open_cursors == 2 + del cursor2 + del cursor1 + + +@pytest.mark.parametrize("threadsafety", [1, 2, 3]) +def test_two_connections(threadsafety): + db_pool = my_db_pool(threadsafety, 2) + db1 = db_pool.connection() + cursors1 = [db1.cursor() for i in range(5)] + db2 = db_pool.connection() + assert db1 != db2 + cursors2 = [db2.cursor() for i in range(7)] + assert db1.open_cursors == 5 + assert db2.open_cursors == 7 + db1.close() + db1 = db_pool.connection() + assert db1 != db2 + assert hasattr(db1, 'cursor') + for i in range(3): + cursors1.append(db1.cursor()) + assert db1.open_cursors == 8 + cursors2.append(db2.cursor()) + assert db2.open_cursors == 8 + del cursors2 + del cursors1 + + +def test_threadsafety_1(): + db_pool = my_db_pool(1, 2) + queue = Queue(3) + + def connection(): + queue.put(db_pool.connection()) + + from threading import Thread + threads = [Thread(target=connection).start() for i in range(3)] + assert len(threads) == 3 + try: + db1 = queue.get(1, 1) + db2 = queue.get(1, 1) + except TypeError: + db1 = queue.get(1) + db2 = queue.get(1) + assert db1 != db2 + assert db1._con != db2._con + try: + with pytest.raises(Empty): + queue.get(1, 0.1) + except TypeError: + with pytest.raises(Empty): + queue.get(0) + db2.close() + try: + db3 = queue.get(1, 1) + except TypeError: + db3 = queue.get(1) + assert db1 != db3 + assert db1._con != db3._con + + +@pytest.mark.parametrize("threadsafety", [2, 3]) +def test_threadsafety_2(threadsafety): + dbpool = my_db_pool(threadsafety, 2) + db1 = dbpool.connection() + db2 = dbpool.connection() + cursors = [dbpool.connection().cursor() for _i in range(100)] + assert db1.open_cursors == 50 + assert db2.open_cursors == 50 + assert cursors + del cursors diff --git a/tests/test_simple_pooled_pg.py b/tests/test_simple_pooled_pg.py index d3026cc..2652c4c 100644 --- a/tests/test_simple_pooled_pg.py +++ b/tests/test_simple_pooled_pg.py @@ -10,108 +10,109 @@ * This test was contributed by Christoph Zwerschke """ -import unittest +from queue import Queue, Empty +from threading import Thread -from . import mock_pg # noqa +import pg # noqa: F401 +import pytest from dbutils import simple_pooled_pg -class TestSimplePooledPg(unittest.TestCase): - - def my_db_pool(self, maxConnections): - return simple_pooled_pg.PooledPg( - maxConnections, 'SimplePooledPgTestDB', 'SimplePooledPgTestUser') - - def test_version(self): - from dbutils import __version__ - self.assertEqual(simple_pooled_pg.__version__, __version__) - self.assertEqual(simple_pooled_pg.PooledPg.version, __version__) - - def test_create_connection(self): - db_pool = self.my_db_pool(1) - db = db_pool.connection() - self.assertTrue(hasattr(db, 'query')) - self.assertTrue(hasattr(db, 'num_queries')) - self.assertEqual(db.num_queries, 0) - self.assertTrue(hasattr(db, 'dbname')) - self.assertEqual(db.dbname, 'SimplePooledPgTestDB') - self.assertTrue(hasattr(db, 'user')) - self.assertEqual(db.user, 'SimplePooledPgTestUser') - db.query('select 1') - self.assertEqual(db.num_queries, 1) - - def test_close_connection(self): - db_pool = self.my_db_pool(1) - db = db_pool.connection() - self.assertEqual(db.num_queries, 0) - db.query('select 1') - self.assertEqual(db.num_queries, 1) - db.close() - self.assertFalse(hasattr(db, 'num_queries')) - db = db_pool.connection() - self.assertTrue(hasattr(db, 'dbname')) - self.assertEqual(db.dbname, 'SimplePooledPgTestDB') - self.assertTrue(hasattr(db, 'user')) - self.assertEqual(db.user, 'SimplePooledPgTestUser') - self.assertEqual(db.num_queries, 1) - db.query('select 1') - self.assertEqual(db.num_queries, 2) - - def test_two_connections(self): - db_pool = self.my_db_pool(2) - db1 = db_pool.connection() - for i in range(5): - db1.query('select 1') - db2 = db_pool.connection() - self.assertNotEqual(db1, db2) - self.assertNotEqual(db1._con, db2._con) - for i in range(7): - db2.query('select 1') - self.assertEqual(db1.num_queries, 5) - self.assertEqual(db2.num_queries, 7) - db1.close() - db1 = db_pool.connection() - self.assertNotEqual(db1, db2) - self.assertNotEqual(db1._con, db2._con) - self.assertTrue(hasattr(db1, 'query')) - for i in range(3): - db1.query('select 1') - self.assertEqual(db1.num_queries, 8) +def my_db_pool(max_connections): + return simple_pooled_pg.PooledPg( + max_connections, 'SimplePooledPgTestDB', 'SimplePooledPgTestUser') + + +def test_version(): + from dbutils import __version__ + assert simple_pooled_pg.__version__ == __version__ + assert simple_pooled_pg.PooledPg.version == __version__ + + +def test_create_connection(): + db_pool = my_db_pool(1) + db = db_pool.connection() + assert hasattr(db, 'query') + assert hasattr(db, 'num_queries') + assert db.num_queries == 0 + assert hasattr(db, 'dbname') + assert db.dbname == 'SimplePooledPgTestDB' + assert hasattr(db, 'user') + assert db.user == 'SimplePooledPgTestUser' + db.query('select 1') + assert db.num_queries == 1 + + +def test_close_connection(): + db_pool = my_db_pool(1) + db = db_pool.connection() + assert db.num_queries == 0 + db.query('select 1') + assert db.num_queries == 1 + db.close() + assert not hasattr(db, 'num_queries') + db = db_pool.connection() + assert hasattr(db, 'dbname') + assert db.dbname == 'SimplePooledPgTestDB' + assert hasattr(db, 'user') + assert db.user == 'SimplePooledPgTestUser' + assert db.num_queries == 1 + db.query('select 1') + assert db.num_queries == 2 + + +def test_two_connections(): + db_pool = my_db_pool(2) + db1 = db_pool.connection() + for i in range(5): + db1.query('select 1') + db2 = db_pool.connection() + assert db1 != db2 + assert db1._con != db2._con + for i in range(7): db2.query('select 1') - self.assertEqual(db2.num_queries, 8) - - def test_threads(self): - db_pool = self.my_db_pool(2) - from queue import Queue, Empty - queue = Queue(3) - - def connection(): - queue.put(db_pool.connection()) - - from threading import Thread - threads = [Thread(target=connection).start() for i in range(3)] - self.assertEqual(len(threads), 3) - try: - db1 = queue.get(1, 1) - db2 = queue.get(1, 1) - except TypeError: - db1 = queue.get(1) - db2 = queue.get(1) - self.assertNotEqual(db1, db2) - self.assertNotEqual(db1._con, db2._con) - try: - self.assertRaises(Empty, queue.get, 1, 0.1) - except TypeError: - self.assertRaises(Empty, queue.get, 0) - db2.close() - try: - db3 = queue.get(1, 1) - except TypeError: - db3 = queue.get(1) - self.assertNotEqual(db1, db3) - self.assertNotEqual(db1._con, db3._con) - - -if __name__ == '__main__': - unittest.main() + assert db1.num_queries == 5 + assert db2.num_queries == 7 + db1.close() + db1 = db_pool.connection() + assert db1 != db2 + assert db1._con != db2._con + assert hasattr(db1, 'query') + for i in range(3): + db1.query('select 1') + assert db1.num_queries == 8 + db2.query('select 1') + assert db2.num_queries == 8 + + +def test_threads(): + db_pool = my_db_pool(2) + queue = Queue(3) + + def connection(): + queue.put(db_pool.connection()) + + threads = [Thread(target=connection).start() for _i in range(3)] + assert len(threads) == 3 + try: + db1 = queue.get(1, 1) + db2 = queue.get(1, 1) + except TypeError: + db1 = queue.get(1) + db2 = queue.get(1) + assert db1 != db2 + assert db1._con != db2._con + try: + with pytest.raises(Empty): + queue.get(1, 0.1) + except TypeError: + with pytest.raises(Empty): + queue.get(0) + db2.close() + try: + db3 = queue.get(1, 1) + except TypeError: + db3 = queue.get(1) + assert db1 != db3 + assert db1._con != db3._con diff --git a/tests/test_steady_db.py b/tests/test_steady_db.py index 3c21cfd..c2d1f95 100644 --- a/tests/test_steady_db.py +++ b/tests/test_steady_db.py @@ -9,7 +9,7 @@ * This test was contributed by Christoph Zwerschke """ -import unittest +import pytest from . import mock_db as dbapi @@ -17,675 +17,711 @@ connect as SteadyDBconnect, SteadyDBConnection, SteadyDBCursor) -class TestSteadyDB(unittest.TestCase): - - def test_version(self): - from dbutils import __version__, steady_db - self.assertEqual(steady_db.__version__, __version__) - self.assertEqual(steady_db.SteadyDBConnection.version, __version__) - - def test_mocked_connection(self): - db = dbapi.connect( - 'SteadyDBTestDB', user='SteadyDBTestUser') - db.__class__.has_ping = False - db.__class__.num_pings = 0 - self.assertTrue(hasattr(db, 'database')) - self.assertEqual(db.database, 'SteadyDBTestDB') - self.assertTrue(hasattr(db, 'user')) - self.assertEqual(db.user, 'SteadyDBTestUser') - self.assertTrue(hasattr(db, 'cursor')) - self.assertTrue(hasattr(db, 'close')) - self.assertTrue(hasattr(db, 'open_cursors')) - self.assertTrue(hasattr(db, 'num_uses')) - self.assertTrue(hasattr(db, 'num_queries')) - self.assertTrue(hasattr(db, 'session')) - self.assertTrue(hasattr(db, 'valid')) - self.assertTrue(db.valid) - self.assertEqual(db.open_cursors, 0) - for i in range(3): - cursor = db.cursor() - self.assertEqual(db.open_cursors, 1) - cursor.close() - self.assertEqual(db.open_cursors, 0) - cursor = [] - for i in range(3): - cursor.append(db.cursor()) - self.assertEqual(db.open_cursors, i + 1) - del cursor - self.assertEqual(db.open_cursors, 0) +def test_version(): + from dbutils import __version__, steady_db + assert steady_db.__version__ == __version__ + assert steady_db.SteadyDBConnection.version == __version__ + + +def test_mocked_connection(): + db = dbapi.connect( + 'SteadyDBTestDB', user='SteadyDBTestUser') + db.__class__.has_ping = False + db.__class__.num_pings = 0 + assert hasattr(db, 'database') + assert db.database == 'SteadyDBTestDB' + assert hasattr(db, 'user') + assert db.user == 'SteadyDBTestUser' + assert hasattr(db, 'cursor') + assert hasattr(db, 'close') + assert hasattr(db, 'open_cursors') + assert hasattr(db, 'num_uses') + assert hasattr(db, 'num_queries') + assert hasattr(db, 'session') + assert hasattr(db, 'valid') + assert db.valid + assert db.open_cursors == 0 + for i in range(3): cursor = db.cursor() - self.assertTrue(hasattr(cursor, 'execute')) - self.assertTrue(hasattr(cursor, 'fetchone')) - self.assertTrue(hasattr(cursor, 'callproc')) - self.assertTrue(hasattr(cursor, 'close')) - self.assertTrue(hasattr(cursor, 'valid')) - self.assertTrue(cursor.valid) - self.assertEqual(db.open_cursors, 1) - for i in range(3): - self.assertEqual(db.num_uses, i) - self.assertEqual(db.num_queries, i) - cursor.execute(f'select test{i}') - self.assertEqual(cursor.fetchone(), f'test{i}') - self.assertTrue(cursor.valid) - self.assertEqual(db.open_cursors, 1) - for i in range(4): - cursor.callproc('test') + assert db.open_cursors == 1 cursor.close() - self.assertFalse(cursor.valid) - self.assertEqual(db.open_cursors, 0) - self.assertEqual(db.num_uses, 7) - self.assertEqual(db.num_queries, 3) - self.assertRaises(dbapi.InternalError, cursor.close) - self.assertRaises(dbapi.InternalError, cursor.execute, 'select test') - self.assertTrue(db.valid) - self.assertFalse(db.__class__.has_ping) - self.assertEqual(db.__class__.num_pings, 0) - self.assertRaises(AttributeError, db.ping) - self.assertEqual(db.__class__.num_pings, 1) - db.__class__.has_ping = True - self.assertIsNone(db.ping()) - self.assertEqual(db.__class__.num_pings, 2) - db.close() - self.assertFalse(db.valid) - self.assertEqual(db.num_uses, 0) - self.assertEqual(db.num_queries, 0) - self.assertRaises(dbapi.InternalError, db.close) - self.assertRaises(dbapi.InternalError, db.cursor) - self.assertRaises(dbapi.OperationalError, db.ping) - self.assertEqual(db.__class__.num_pings, 3) - db.__class__.has_ping = False - db.__class__.num_pings = 0 - - def test_broken_connection(self): - self.assertRaises(TypeError, SteadyDBConnection, None) - self.assertRaises(TypeError, SteadyDBCursor, None) - db = SteadyDBconnect(dbapi, database='ok') - for i in range(3): - db.close() - del db - self.assertRaises( - dbapi.OperationalError, SteadyDBconnect, dbapi, database='error') - db = SteadyDBconnect(dbapi, database='ok') - cursor = db.cursor() - for i in range(3): - cursor.close() - cursor = db.cursor('ok') - for i in range(3): - cursor.close() - self.assertRaises(dbapi.OperationalError, db.cursor, 'error') - - def test_close(self): - for closeable in (False, True): - db = SteadyDBconnect(dbapi, closeable=closeable) - self.assertTrue(db._con.valid) - db.close() - self.assertTrue(closeable ^ db._con.valid) - db.close() - self.assertTrue(closeable ^ db._con.valid) - db._close() - self.assertFalse(db._con.valid) - db._close() - self.assertFalse(db._con.valid) - - def test_connection(self): - db = SteadyDBconnect( - dbapi, 0, None, None, None, True, - 'SteadyDBTestDB', user='SteadyDBTestUser') - self.assertTrue(isinstance(db, SteadyDBConnection)) - self.assertTrue(hasattr(db, '_con')) - self.assertTrue(hasattr(db, '_usage')) - self.assertEqual(db._usage, 0) - self.assertTrue(hasattr(db._con, 'valid')) - self.assertTrue(db._con.valid) - self.assertTrue(hasattr(db._con, 'cursor')) - self.assertTrue(hasattr(db._con, 'close')) - self.assertTrue(hasattr(db._con, 'open_cursors')) - self.assertTrue(hasattr(db._con, 'num_uses')) - self.assertTrue(hasattr(db._con, 'num_queries')) - self.assertTrue(hasattr(db._con, 'session')) - self.assertTrue(hasattr(db._con, 'database')) - self.assertEqual(db._con.database, 'SteadyDBTestDB') - self.assertTrue(hasattr(db._con, 'user')) - self.assertEqual(db._con.user, 'SteadyDBTestUser') - self.assertTrue(hasattr(db, 'cursor')) - self.assertTrue(hasattr(db, 'close')) - self.assertEqual(db._con.open_cursors, 0) - for i in range(3): - cursor = db.cursor() - self.assertEqual(db._con.open_cursors, 1) - cursor.close() - self.assertEqual(db._con.open_cursors, 0) - cursor = [] - for i in range(3): - cursor.append(db.cursor()) - self.assertEqual(db._con.open_cursors, i + 1) - del cursor - self.assertEqual(db._con.open_cursors, 0) - cursor = db.cursor() - self.assertTrue(hasattr(cursor, 'execute')) - self.assertTrue(hasattr(cursor, 'fetchone')) - self.assertTrue(hasattr(cursor, 'callproc')) - self.assertTrue(hasattr(cursor, 'close')) - self.assertTrue(hasattr(cursor, 'valid')) - self.assertTrue(cursor.valid) - self.assertEqual(db._con.open_cursors, 1) - for i in range(3): - self.assertEqual(db._usage, i) - self.assertEqual(db._con.num_uses, i) - self.assertEqual(db._con.num_queries, i) - cursor.execute(f'select test{i}') - self.assertEqual(cursor.fetchone(), f'test{i}') - self.assertTrue(cursor.valid) - self.assertEqual(db._con.open_cursors, 1) - for i in range(4): - cursor.callproc('test') - cursor.close() - self.assertFalse(cursor.valid) - self.assertEqual(db._con.open_cursors, 0) - self.assertEqual(db._usage, 7) - self.assertEqual(db._con.num_uses, 7) - self.assertEqual(db._con.num_queries, 3) + assert db.open_cursors == 0 + cursor = [] + for i in range(3): + cursor.append(db.cursor()) + assert db.open_cursors == i + 1 + del cursor + assert db.open_cursors == 0 + cursor = db.cursor() + assert hasattr(cursor, 'execute') + assert hasattr(cursor, 'fetchone') + assert hasattr(cursor, 'callproc') + assert hasattr(cursor, 'close') + assert hasattr(cursor, 'valid') + assert cursor.valid + assert db.open_cursors == 1 + for i in range(3): + assert db.num_uses == i + assert db.num_queries == i + cursor.execute(f'select test{i}') + assert cursor.fetchone() == f'test{i}' + assert cursor.valid + assert db.open_cursors == 1 + for i in range(4): + cursor.callproc('test') + cursor.close() + assert not cursor.valid + assert db.open_cursors == 0 + assert db.num_uses == 7 + assert db.num_queries == 3 + with pytest.raises(dbapi.InternalError): cursor.close() - cursor.execute('select test8') - self.assertTrue(cursor.valid) - self.assertEqual(db._con.open_cursors, 1) - self.assertEqual(cursor.fetchone(), 'test8') - self.assertEqual(db._usage, 8) - self.assertEqual(db._con.num_uses, 8) - self.assertEqual(db._con.num_queries, 4) - self.assertTrue(db._con.valid) - db.close() - self.assertFalse(db._con.valid) - self.assertEqual(db._con.open_cursors, 0) - self.assertEqual(db._usage, 8) - self.assertEqual(db._con.num_uses, 0) - self.assertEqual(db._con.num_queries, 0) - self.assertRaises(dbapi.InternalError, db._con.close) + with pytest.raises(dbapi.InternalError): + cursor.execute('select test') + assert db.valid + assert not db.__class__.has_ping + assert db.__class__.num_pings == 0 + with pytest.raises(AttributeError): + db.ping() + assert db.__class__.num_pings == 1 + db.__class__.has_ping = True + assert db.ping() is None + assert db.__class__.num_pings == 2 + db.close() + assert not db.valid + assert db.num_uses == 0 + assert db.num_queries == 0 + with pytest.raises(dbapi.InternalError): db.close() - self.assertRaises(dbapi.InternalError, db._con.cursor) - cursor = db.cursor() - self.assertTrue(db._con.valid) - cursor.execute('select test11') - self.assertEqual(cursor.fetchone(), 'test11') - cursor.execute('select test12') - self.assertEqual(cursor.fetchone(), 'test12') - cursor.callproc('test') - self.assertEqual(db._usage, 3) - self.assertEqual(db._con.num_uses, 3) - self.assertEqual(db._con.num_queries, 2) - cursor2 = db.cursor() - self.assertEqual(db._con.open_cursors, 2) - cursor2.execute('select test13') - self.assertEqual(cursor2.fetchone(), 'test13') - self.assertEqual(db._con.num_queries, 3) + with pytest.raises(dbapi.InternalError): + db.cursor() + with pytest.raises(dbapi.OperationalError): + db.ping() + assert db.__class__.num_pings == 3 + db.__class__.has_ping = False + db.__class__.num_pings = 0 + + +def test_broken_connection(): + with pytest.raises(TypeError): + SteadyDBConnection(None) + with pytest.raises(TypeError): + SteadyDBCursor(None) + db = SteadyDBconnect(dbapi, database='ok') + for i in range(3): db.close() - self.assertEqual(db._con.open_cursors, 0) - self.assertEqual(db._con.num_queries, 0) + del db + with pytest.raises(dbapi.OperationalError):# + SteadyDBconnect(dbapi, database='error') + db = SteadyDBconnect(dbapi, database='ok') + cursor = db.cursor() + for i in range(3): + cursor.close() + cursor = db.cursor('ok') + for i in range(3): + cursor.close() + with pytest.raises(dbapi.OperationalError): + db.cursor('error') + + +@pytest.mark.parametrize("closeable", [False, True]) +def test_close(closeable): + db = SteadyDBconnect(dbapi, closeable=closeable) + assert db._con.valid + db.close() + assert closeable ^ db._con.valid + db.close() + assert closeable ^ db._con.valid + db._close() + assert not db._con.valid + db._close() + assert not db._con.valid + + +def test_connection(): + db = SteadyDBconnect( + dbapi, 0, None, None, None, True, + 'SteadyDBTestDB', user='SteadyDBTestUser') + assert isinstance(db, SteadyDBConnection) + assert hasattr(db, '_con') + assert hasattr(db, '_usage') + assert db._usage == 0 + assert hasattr(db._con, 'valid') + assert db._con.valid + assert hasattr(db._con, 'cursor') + assert hasattr(db._con, 'close') + assert hasattr(db._con, 'open_cursors') + assert hasattr(db._con, 'num_uses') + assert hasattr(db._con, 'num_queries') + assert hasattr(db._con, 'session') + assert hasattr(db._con, 'database') + assert db._con.database == 'SteadyDBTestDB' + assert hasattr(db._con, 'user') + assert db._con.user == 'SteadyDBTestUser' + assert hasattr(db, 'cursor') + assert hasattr(db, 'close') + assert db._con.open_cursors == 0 + for i in range(3): cursor = db.cursor() - self.assertTrue(cursor.valid) - cursor.callproc('test') - cursor._cursor.valid = False - self.assertFalse(cursor.valid) - self.assertRaises(dbapi.InternalError, cursor._cursor.callproc, 'test') + assert db._con.open_cursors == 1 + cursor.close() + assert db._con.open_cursors == 0 + cursor = [] + for i in range(3): + cursor.append(db.cursor()) + assert db._con.open_cursors == i + 1 + del cursor + assert db._con.open_cursors == 0 + cursor = db.cursor() + assert hasattr(cursor, 'execute') + assert hasattr(cursor, 'fetchone') + assert hasattr(cursor, 'callproc') + assert hasattr(cursor, 'close') + assert hasattr(cursor, 'valid') + assert cursor.valid + assert db._con.open_cursors == 1 + for i in range(3): + assert db._usage == i + assert db._con.num_uses == i + assert db._con.num_queries == i + cursor.execute(f'select test{i}') + assert cursor.fetchone() == f'test{i}' + assert cursor.valid + assert db._con.open_cursors == 1 + for i in range(4): cursor.callproc('test') - self.assertTrue(cursor.valid) + cursor.close() + assert not cursor.valid + assert db._con.open_cursors == 0 + assert db._usage == 7 + assert db._con.num_uses == 7 + assert db._con.num_queries == 3 + cursor.close() + cursor.execute('select test8') + assert cursor.valid + assert db._con.open_cursors == 1 + assert cursor.fetchone() == 'test8' + assert db._usage == 8 + assert db._con.num_uses == 8 + assert db._con.num_queries == 4 + assert db._con.valid + db.close() + assert not db._con.valid + assert db._con.open_cursors == 0 + assert db._usage == 8 + assert db._con.num_uses == 0 + assert db._con.num_queries == 0 + with pytest.raises(dbapi.InternalError): + db._con.close() + db.close() + with pytest.raises(dbapi.InternalError): + db._con.cursor() + cursor = db.cursor() + assert db._con.valid + cursor.execute('select test11') + assert cursor.fetchone() == 'test11' + cursor.execute('select test12') + assert cursor.fetchone() == 'test12' + cursor.callproc('test') + assert db._usage == 3 + assert db._con.num_uses == 3 + assert db._con.num_queries == 2 + cursor2 = db.cursor() + assert db._con.open_cursors == 2 + cursor2.execute('select test13') + assert cursor2.fetchone() == 'test13' + assert db._con.num_queries == 3 + db.close() + assert db._con.open_cursors == 0 + assert db._con.num_queries == 0 + cursor = db.cursor() + assert cursor.valid + cursor.callproc('test') + cursor._cursor.valid = False + assert not cursor.valid + with pytest.raises(dbapi.InternalError): cursor._cursor.callproc('test') - self.assertEqual(db._usage, 2) - self.assertEqual(db._con.num_uses, 3) - db._con.valid = cursor._cursor.valid = False - cursor.callproc('test') - self.assertTrue(cursor.valid) - self.assertEqual(db._usage, 1) - self.assertEqual(db._con.num_uses, 1) - cursor.execute('set this') - db.commit() - cursor.execute('set that') - db.rollback() - self.assertEqual( - db._con.session, ['this', 'commit', 'that', 'rollback']) - - def test_connection_context_handler(self): - db = SteadyDBconnect( - dbapi, 0, None, None, None, True, - 'SteadyDBTestDB', user='SteadyDBTestUser') - self.assertEqual(db._con.session, []) + cursor.callproc('test') + assert cursor.valid + cursor._cursor.callproc('test') + assert db._usage == 2 + assert db._con.num_uses == 3 + db._con.valid = cursor._cursor.valid = False + cursor.callproc('test') + assert cursor.valid + assert db._usage == 1 + assert db._con.num_uses == 1 + cursor.execute('set this') + db.commit() + cursor.execute('set that') + db.rollback() + assert db._con.session == ['this', 'commit', 'that', 'rollback'] + + +def test_connection_context_handler(): + db = SteadyDBconnect( + dbapi, 0, None, None, None, True, + 'SteadyDBTestDB', user='SteadyDBTestUser') + assert db._con.session == [] + with db as con: + con.cursor().execute('select test') + assert db._con.session == ['commit'] + try: with db as con: - con.cursor().execute('select test') - self.assertEqual(db._con.session, ['commit']) - try: - with db as con: - con.cursor().execute('error') - except dbapi.ProgrammingError: - error = True - else: - error = False - self.assertTrue(error) - self.assertEqual(db._con.session, ['commit', 'rollback']) - - def test_cursor_context_handler(self): - db = SteadyDBconnect( - dbapi, 0, None, None, None, True, - 'SteadyDBTestDB', user='SteadyDBTestUser') - self.assertEqual(db._con.open_cursors, 0) - with db.cursor() as cursor: - self.assertEqual(db._con.open_cursors, 1) - cursor.execute('select test') - self.assertEqual(cursor.fetchone(), 'test') - self.assertEqual(db._con.open_cursors, 0) - - def test_cursor_as_iterator_provided(self): - db = SteadyDBconnect( - dbapi, 0, None, None, None, True, - 'SteadyDBTestDB', user='SteadyDBTestUser') - self.assertEqual(db._con.open_cursors, 0) - cursor = db.cursor() - self.assertEqual(db._con.open_cursors, 1) - cursor.execute('select test') - _cursor = cursor._cursor - try: - assert not hasattr(_cursor, 'iter') - _cursor.__iter__ = lambda: ['test-iter'] - assert list(iter(cursor)) == ['test'] - finally: - del _cursor.__iter__ - cursor.close() - self.assertEqual(db._con.open_cursors, 0) - - def test_cursor_as_iterator_created(self): - db = SteadyDBconnect( - dbapi, 0, None, None, None, True, - 'SteadyDBTestDB', user='SteadyDBTestUser') - self.assertEqual(db._con.open_cursors, 0) - cursor = db.cursor() - self.assertEqual(db._con.open_cursors, 1) + con.cursor().execute('error') + except dbapi.ProgrammingError: + error = True + else: + error = False + assert error + assert db._con.session == ['commit', 'rollback'] + + +def test_cursor_context_handler(): + db = SteadyDBconnect( + dbapi, 0, None, None, None, True, + 'SteadyDBTestDB', user='SteadyDBTestUser') + assert db._con.open_cursors == 0 + with db.cursor() as cursor: + assert db._con.open_cursors == 1 cursor.execute('select test') + assert cursor.fetchone() == 'test' + assert db._con.open_cursors == 0 + + +def test_cursor_as_iterator_provided(): + db = SteadyDBconnect( + dbapi, 0, None, None, None, True, + 'SteadyDBTestDB', user='SteadyDBTestUser') + assert db._con.open_cursors == 0 + cursor = db.cursor() + assert db._con.open_cursors == 1 + cursor.execute('select test') + _cursor = cursor._cursor + try: + assert not hasattr(_cursor, 'iter') + _cursor.__iter__ = lambda: ['test-iter'] assert list(iter(cursor)) == ['test'] - cursor.close() - self.assertEqual(db._con.open_cursors, 0) - - def test_connection_creator_function(self): - db1 = SteadyDBconnect( - dbapi, 0, None, None, None, True, - 'SteadyDBTestDB', user='SteadyDBTestUser') - db2 = SteadyDBconnect( - dbapi.connect, 0, None, None, None, True, - 'SteadyDBTestDB', user='SteadyDBTestUser') - self.assertEqual(db1.dbapi(), db2.dbapi()) - self.assertEqual(db1.threadsafety(), db2.threadsafety()) - self.assertEqual(db1._creator, db2._creator) - self.assertEqual(db1._args, db2._args) - self.assertEqual(db1._kwargs, db2._kwargs) - db2.close() - db1.close() - - def test_connection_maxusage(self): - db = SteadyDBconnect(dbapi, 10) - cursor = db.cursor() - for i in range(100): - cursor.execute(f'select test{i}') - r = cursor.fetchone() - self.assertEqual(r, f'test{i}') - self.assertTrue(db._con.valid) - j = i % 10 + 1 - self.assertEqual(db._usage, j) - self.assertEqual(db._con.num_uses, j) - self.assertEqual(db._con.num_queries, j) - self.assertEqual(db._con.open_cursors, 1) - db.begin() - for i in range(100): - cursor.callproc('test') - self.assertTrue(db._con.valid) - if i == 49: - db.commit() - j = i % 10 + 1 if i > 49 else i + 11 - self.assertEqual(db._usage, j) - self.assertEqual(db._con.num_uses, j) - j = 0 if i > 49 else 10 - self.assertEqual(db._con.num_queries, j) - for i in range(10): - if i == 7: - db._con.valid = cursor._cursor.valid = False - cursor.execute(f'select test{i}') - r = cursor.fetchone() - self.assertEqual(r, f'test{i}') - j = i % 7 + 1 - self.assertEqual(db._usage, j) - self.assertEqual(db._con.num_uses, j) - self.assertEqual(db._con.num_queries, j) - for i in range(10): - if i == 5: - db._con.valid = cursor._cursor.valid = False - cursor.callproc('test') - j = (i + (3 if i < 5 else -5)) % 10 + 1 - self.assertEqual(db._usage, j) - self.assertEqual(db._con.num_uses, j) - j = 3 if i < 5 else 0 - self.assertEqual(db._con.num_queries, j) - db.close() - cursor.execute('select test1') - self.assertEqual(cursor.fetchone(), 'test1') - self.assertEqual(db._usage, 1) - self.assertEqual(db._con.num_uses, 1) - self.assertEqual(db._con.num_queries, 1) - - def test_connection_setsession(self): - db = SteadyDBconnect(dbapi, 3, ('set time zone', 'set datestyle')) - self.assertTrue(hasattr(db, '_usage')) - self.assertEqual(db._usage, 0) - self.assertTrue(hasattr(db._con, 'open_cursors')) - self.assertEqual(db._con.open_cursors, 0) - self.assertTrue(hasattr(db._con, 'num_uses')) - self.assertEqual(db._con.num_uses, 2) - self.assertTrue(hasattr(db._con, 'num_queries')) - self.assertEqual(db._con.num_queries, 0) - self.assertTrue(hasattr(db._con, 'session')) - self.assertEqual(tuple(db._con.session), ('time zone', 'datestyle')) - for i in range(11): - db.cursor().execute('select test') - self.assertEqual(db._con.open_cursors, 0) - self.assertEqual(db._usage, 2) - self.assertEqual(db._con.num_uses, 4) - self.assertEqual(db._con.num_queries, 2) - self.assertEqual(db._con.session, ['time zone', 'datestyle']) - db.cursor().execute('set test') - self.assertEqual(db._con.open_cursors, 0) - self.assertEqual(db._usage, 3) - self.assertEqual(db._con.num_uses, 5) - self.assertEqual(db._con.num_queries, 2) - self.assertEqual(db._con.session, ['time zone', 'datestyle', 'test']) - db.cursor().execute('select test') - self.assertEqual(db._con.open_cursors, 0) - self.assertEqual(db._usage, 1) - self.assertEqual(db._con.num_uses, 3) - self.assertEqual(db._con.num_queries, 1) - self.assertEqual(db._con.session, ['time zone', 'datestyle']) - db.cursor().execute('set test') - self.assertEqual(db._con.open_cursors, 0) - self.assertEqual(db._usage, 2) - self.assertEqual(db._con.num_uses, 4) - self.assertEqual(db._con.num_queries, 1) - self.assertEqual(db._con.session, ['time zone', 'datestyle', 'test']) - db.cursor().execute('select test') - self.assertEqual(db._con.open_cursors, 0) - self.assertEqual(db._usage, 3) - self.assertEqual(db._con.num_uses, 5) - self.assertEqual(db._con.num_queries, 2) - self.assertEqual(db._con.session, ['time zone', 'datestyle', 'test']) - db.close() - db.cursor().execute('set test') - self.assertEqual(db._con.open_cursors, 0) - self.assertEqual(db._usage, 1) - self.assertEqual(db._con.num_uses, 3) - self.assertEqual(db._con.num_queries, 0) - self.assertEqual(db._con.session, ['time zone', 'datestyle', 'test']) - db.close() - db.cursor().execute('select test') - self.assertEqual(db._con.open_cursors, 0) - self.assertEqual(db._usage, 1) - self.assertEqual(db._con.num_uses, 3) - self.assertEqual(db._con.num_queries, 1) - self.assertEqual(db._con.session, ['time zone', 'datestyle']) - - def test_connection_failures(self): - db = SteadyDBconnect(dbapi) - db.close() - db.cursor() - db = SteadyDBconnect(dbapi, failures=dbapi.InternalError) - db.close() - db.cursor() - db = SteadyDBconnect(dbapi, failures=dbapi.OperationalError) - db.close() - self.assertRaises(dbapi.InternalError, db.cursor) - db = SteadyDBconnect(dbapi, failures=( - dbapi.OperationalError, dbapi.InterfaceError)) - db.close() - self.assertRaises(dbapi.InternalError, db.cursor) - db = SteadyDBconnect(dbapi, failures=( - dbapi.OperationalError, dbapi.InterfaceError, dbapi.InternalError)) - db.close() - db.cursor() - - def test_connection_failure_error(self): - db = SteadyDBconnect(dbapi) - cursor = db.cursor() - db.close() - cursor.execute('select test') - cursor = db.cursor() - db.close() - self.assertRaises(dbapi.ProgrammingError, cursor.execute, 'error') - - def test_connection_set_sizes(self): - db = SteadyDBconnect(dbapi) - cursor = db.cursor() - cursor.execute('get sizes') - result = cursor.fetchone() - self.assertEqual(result, ([], {})) - cursor.setinputsizes([7, 42, 6]) - cursor.setoutputsize(9) - cursor.setoutputsize(15, 3) - cursor.setoutputsize(42, 7) - cursor.execute('get sizes') - result = cursor.fetchone() - self.assertEqual(result, ([7, 42, 6], {None: 9, 3: 15, 7: 42})) - cursor.execute('get sizes') - result = cursor.fetchone() - self.assertEqual(result, ([], {})) - cursor.setinputsizes([6, 42, 7]) - cursor.setoutputsize(7) - cursor.setoutputsize(15, 3) - cursor.setoutputsize(42, 9) - db.close() - cursor.execute('get sizes') - result = cursor.fetchone() - self.assertEqual(result, ([6, 42, 7], {None: 7, 3: 15, 9: 42})) - - def test_connection_ping_check(self): - Connection = dbapi.Connection - Connection.has_ping = False - Connection.num_pings = 0 - db = SteadyDBconnect(dbapi) - db.cursor().execute('select test') - self.assertEqual(Connection.num_pings, 0) - db.close() - db.cursor().execute('select test') - self.assertEqual(Connection.num_pings, 0) - self.assertIsNone(db._ping_check()) - self.assertEqual(Connection.num_pings, 1) - db = SteadyDBconnect(dbapi, ping=7) - db.cursor().execute('select test') - self.assertEqual(Connection.num_pings, 2) - db.close() - db.cursor().execute('select test') - self.assertEqual(Connection.num_pings, 2) - self.assertIsNone(db._ping_check()) - self.assertEqual(Connection.num_pings, 2) - Connection.has_ping = True - db = SteadyDBconnect(dbapi) - db.cursor().execute('select test') - self.assertEqual(Connection.num_pings, 2) - db.close() - db.cursor().execute('select test') - self.assertEqual(Connection.num_pings, 2) - self.assertTrue(db._ping_check()) - self.assertEqual(Connection.num_pings, 3) - db = SteadyDBconnect(dbapi, ping=1) - db.cursor().execute('select test') - self.assertEqual(Connection.num_pings, 3) - db.close() - db.cursor().execute('select test') - self.assertEqual(Connection.num_pings, 3) - self.assertTrue(db._ping_check()) - self.assertEqual(Connection.num_pings, 4) - db.close() - self.assertTrue(db._ping_check()) - self.assertEqual(Connection.num_pings, 5) - db = SteadyDBconnect(dbapi, ping=7) - db.cursor().execute('select test') - self.assertEqual(Connection.num_pings, 7) - db.close() + finally: + del _cursor.__iter__ + cursor.close() + assert db._con.open_cursors == 0 + + +def test_cursor_as_iterator_created(): + db = SteadyDBconnect( + dbapi, 0, None, None, None, True, + 'SteadyDBTestDB', user='SteadyDBTestUser') + assert db._con.open_cursors == 0 + cursor = db.cursor() + assert db._con.open_cursors == 1 + cursor.execute('select test') + assert list(iter(cursor)) == ['test'] + cursor.close() + assert db._con.open_cursors == 0 + + +def test_connection_creator_function(): + db1 = SteadyDBconnect( + dbapi, 0, None, None, None, True, + 'SteadyDBTestDB', user='SteadyDBTestUser') + db2 = SteadyDBconnect( + dbapi.connect, 0, None, None, None, True, + 'SteadyDBTestDB', user='SteadyDBTestUser') + assert db1.dbapi() == db2.dbapi() + assert db1.threadsafety() == db2.threadsafety() + assert db1._creator == db2._creator + assert db1._args == db2._args + assert db1._kwargs == db2._kwargs + db2.close() + db1.close() + + +def test_connection_maxusage(): + db = SteadyDBconnect(dbapi, 10) + cursor = db.cursor() + for i in range(100): + cursor.execute(f'select test{i}') + r = cursor.fetchone() + assert r == f'test{i}' + assert db._con.valid + j = i % 10 + 1 + assert db._usage == j + assert db._con.num_uses == j + assert db._con.num_queries == j + assert db._con.open_cursors == 1 + db.begin() + for i in range(100): + cursor.callproc('test') + assert db._con.valid + if i == 49: + db.commit() + j = i % 10 + 1 if i > 49 else i + 11 + assert db._usage == j + assert db._con.num_uses == j + j = 0 if i > 49 else 10 + assert db._con.num_queries == j + for i in range(10): + if i == 7: + db._con.valid = cursor._cursor.valid = False + cursor.execute(f'select test{i}') + r = cursor.fetchone() + assert r == f'test{i}' + j = i % 7 + 1 + assert db._usage == j + assert db._con.num_uses == j + assert db._con.num_queries == j + for i in range(10): + if i == 5: + db._con.valid = cursor._cursor.valid = False + cursor.callproc('test') + j = (i + (3 if i < 5 else -5)) % 10 + 1 + assert db._usage == j + assert db._con.num_uses == j + j = 3 if i < 5 else 0 + assert db._con.num_queries == j + db.close() + cursor.execute('select test1') + assert cursor.fetchone() == 'test1' + assert db._usage == 1 + assert db._con.num_uses == 1 + assert db._con.num_queries == 1 + + +def test_connection_setsession(): + db = SteadyDBconnect(dbapi, 3, ('set time zone', 'set datestyle')) + assert hasattr(db, '_usage') + assert db._usage == 0 + assert hasattr(db._con, 'open_cursors') + assert db._con.open_cursors == 0 + assert hasattr(db._con, 'num_uses') + assert db._con.num_uses == 2 + assert hasattr(db._con, 'num_queries') + assert db._con.num_queries == 0 + assert hasattr(db._con, 'session') + assert tuple(db._con.session) == ('time zone', 'datestyle') + for i in range(11): db.cursor().execute('select test') - self.assertEqual(Connection.num_pings, 9) - db = SteadyDBconnect(dbapi, ping=3) - self.assertEqual(Connection.num_pings, 9) - db.cursor() - self.assertEqual(Connection.num_pings, 10) - db.close() - cursor = db.cursor() - self.assertEqual(Connection.num_pings, 11) - cursor.execute('select test') - self.assertEqual(Connection.num_pings, 11) - db = SteadyDBconnect(dbapi, ping=5) - self.assertEqual(Connection.num_pings, 11) + assert db._con.open_cursors == 0 + assert db._usage == 2 + assert db._con.num_uses == 4 + assert db._con.num_queries == 2 + assert db._con.session == ['time zone', 'datestyle'] + db.cursor().execute('set test') + assert db._con.open_cursors == 0 + assert db._usage == 3 + assert db._con.num_uses == 5 + assert db._con.num_queries == 2 + assert db._con.session == ['time zone', 'datestyle', 'test'] + db.cursor().execute('select test') + assert db._con.open_cursors == 0 + assert db._usage == 1 + assert db._con.num_uses == 3 + assert db._con.num_queries == 1 + assert db._con.session == ['time zone', 'datestyle'] + db.cursor().execute('set test') + assert db._con.open_cursors == 0 + assert db._usage == 2 + assert db._con.num_uses == 4 + assert db._con.num_queries == 1 + assert db._con.session == ['time zone', 'datestyle', 'test'] + db.cursor().execute('select test') + assert db._con.open_cursors == 0 + assert db._usage == 3 + assert db._con.num_uses == 5 + assert db._con.num_queries == 2 + assert db._con.session == ['time zone', 'datestyle', 'test'] + db.close() + db.cursor().execute('set test') + assert db._con.open_cursors == 0 + assert db._usage == 1 + assert db._con.num_uses == 3 + assert db._con.num_queries == 0 + assert db._con.session == ['time zone', 'datestyle', 'test'] + db.close() + db.cursor().execute('select test') + assert db._con.open_cursors == 0 + assert db._usage == 1 + assert db._con.num_uses == 3 + assert db._con.num_queries == 1 + assert db._con.session == ['time zone', 'datestyle'] + + +def test_connection_failures(): + db = SteadyDBconnect(dbapi) + db.close() + db.cursor() + db = SteadyDBconnect(dbapi, failures=dbapi.InternalError) + db.close() + db.cursor() + db = SteadyDBconnect(dbapi, failures=dbapi.OperationalError) + db.close() + with pytest.raises(dbapi.InternalError): db.cursor() - self.assertEqual(Connection.num_pings, 11) - db.close() - cursor = db.cursor() - self.assertEqual(Connection.num_pings, 11) - cursor.execute('select test') - self.assertEqual(Connection.num_pings, 12) - db.close() - cursor = db.cursor() - self.assertEqual(Connection.num_pings, 12) - cursor.execute('select test') - self.assertEqual(Connection.num_pings, 13) - db = SteadyDBconnect(dbapi, ping=7) - self.assertEqual(Connection.num_pings, 13) + db = SteadyDBconnect(dbapi, failures=( + dbapi.OperationalError, dbapi.InterfaceError)) + db.close() + with pytest.raises(dbapi.InternalError): db.cursor() - self.assertEqual(Connection.num_pings, 14) - db.close() - cursor = db.cursor() - self.assertEqual(Connection.num_pings, 15) - cursor.execute('select test') - self.assertEqual(Connection.num_pings, 16) - db.close() - cursor = db.cursor() - self.assertEqual(Connection.num_pings, 17) - cursor.execute('select test') - self.assertEqual(Connection.num_pings, 18) - db.close() - cursor.execute('select test') - self.assertEqual(Connection.num_pings, 20) - Connection.has_ping = False - Connection.num_pings = 0 - - def test_begin_transaction(self): - db = SteadyDBconnect(dbapi, database='ok') - cursor = db.cursor() - cursor.close() + db = SteadyDBconnect(dbapi, failures=( + dbapi.OperationalError, dbapi.InterfaceError, dbapi.InternalError)) + db.close() + db.cursor() + + +def test_connection_failure_error(): + db = SteadyDBconnect(dbapi) + cursor = db.cursor() + db.close() + cursor.execute('select test') + cursor = db.cursor() + db.close() + with pytest.raises(dbapi.ProgrammingError): + cursor.execute('error') + + +def test_connection_set_sizes(): + db = SteadyDBconnect(dbapi) + cursor = db.cursor() + cursor.execute('get sizes') + result = cursor.fetchone() + assert result == ([], {}) + cursor.setinputsizes([7, 42, 6]) + cursor.setoutputsize(9) + cursor.setoutputsize(15, 3) + cursor.setoutputsize(42, 7) + cursor.execute('get sizes') + result = cursor.fetchone() + assert result == ([7, 42, 6], {None: 9, 3: 15, 7: 42}) + cursor.execute('get sizes') + result = cursor.fetchone() + assert result == ([], {}) + cursor.setinputsizes([6, 42, 7]) + cursor.setoutputsize(7) + cursor.setoutputsize(15, 3) + cursor.setoutputsize(42, 9) + db.close() + cursor.execute('get sizes') + result = cursor.fetchone() + assert result == ([6, 42, 7], {None: 7, 3: 15, 9: 42}) + + +def test_connection_ping_check(): + con_cls = dbapi.Connection + con_cls.has_ping = False + con_cls.num_pings = 0 + db = SteadyDBconnect(dbapi) + db.cursor().execute('select test') + assert con_cls.num_pings == 0 + db.close() + db.cursor().execute('select test') + assert con_cls.num_pings == 0 + assert db._ping_check() is None + assert con_cls.num_pings == 1 + db = SteadyDBconnect(dbapi, ping=7) + db.cursor().execute('select test') + assert con_cls.num_pings == 2 + db.close() + db.cursor().execute('select test') + assert con_cls.num_pings == 2 + assert db._ping_check() is None + assert con_cls.num_pings == 2 + con_cls.has_ping = True + db = SteadyDBconnect(dbapi) + db.cursor().execute('select test') + assert con_cls.num_pings == 2 + db.close() + db.cursor().execute('select test') + assert con_cls.num_pings == 2 + assert db._ping_check() + assert con_cls.num_pings == 3 + db = SteadyDBconnect(dbapi, ping=1) + db.cursor().execute('select test') + assert con_cls.num_pings == 3 + db.close() + db.cursor().execute('select test') + assert con_cls.num_pings == 3 + assert db._ping_check() + assert con_cls.num_pings == 4 + db.close() + assert db._ping_check() + assert con_cls.num_pings == 5 + db = SteadyDBconnect(dbapi, ping=7) + db.cursor().execute('select test') + assert con_cls.num_pings == 7 + db.close() + db.cursor().execute('select test') + assert con_cls.num_pings == 9 + db = SteadyDBconnect(dbapi, ping=3) + assert con_cls.num_pings == 9 + db.cursor() + assert con_cls.num_pings == 10 + db.close() + cursor = db.cursor() + assert con_cls.num_pings == 11 + cursor.execute('select test') + assert con_cls.num_pings == 11 + db = SteadyDBconnect(dbapi, ping=5) + assert con_cls.num_pings == 11 + db.cursor() + assert con_cls.num_pings == 11 + db.close() + cursor = db.cursor() + assert con_cls.num_pings == 11 + cursor.execute('select test') + assert con_cls.num_pings == 12 + db.close() + cursor = db.cursor() + assert con_cls.num_pings == 12 + cursor.execute('select test') + assert con_cls.num_pings == 13 + db = SteadyDBconnect(dbapi, ping=7) + assert con_cls.num_pings == 13 + db.cursor() + assert con_cls.num_pings == 14 + db.close() + cursor = db.cursor() + assert con_cls.num_pings == 15 + cursor.execute('select test') + assert con_cls.num_pings == 16 + db.close() + cursor = db.cursor() + assert con_cls.num_pings == 17 + cursor.execute('select test') + assert con_cls.num_pings == 18 + db.close() + cursor.execute('select test') + assert con_cls.num_pings == 20 + con_cls.has_ping = False + con_cls.num_pings = 0 + + +def test_begin_transaction(): + db = SteadyDBconnect(dbapi, database='ok') + cursor = db.cursor() + cursor.close() + cursor.execute('select test12') + assert cursor.fetchone() == 'test12' + db.begin() + cursor = db.cursor() + cursor.close() + with pytest.raises(dbapi.InternalError): cursor.execute('select test12') - self.assertEqual(cursor.fetchone(), 'test12') - db.begin() - cursor = db.cursor() - cursor.close() - self.assertRaises(dbapi.InternalError, cursor.execute, 'select test12') - cursor.execute('select test12') - self.assertEqual(cursor.fetchone(), 'test12') - db.close() - db.begin() - self.assertRaises(dbapi.InternalError, cursor.execute, 'select test12') - cursor.execute('select test12') - self.assertEqual(cursor.fetchone(), 'test12') - db.begin() - self.assertRaises(dbapi.ProgrammingError, cursor.execute, 'error') - cursor.close() + cursor.execute('select test12') + assert cursor.fetchone() == 'test12' + db.close() + db.begin() + with pytest.raises(dbapi.InternalError): cursor.execute('select test12') - self.assertEqual(cursor.fetchone(), 'test12') - - def test_with_begin_extension(self): - db = SteadyDBconnect(dbapi, database='ok') - db._con._begin_called_with = None - - def begin(a, b=None, c=7): - db._con._begin_called_with = (a, b, c) - - db._con.begin = begin - db.begin(42, 6) - cursor = db.cursor() - cursor.execute('select test13') - self.assertEqual(cursor.fetchone(), 'test13') - self.assertEqual(db._con._begin_called_with, (42, 6, 7)) - - def test_cancel_transaction(self): - db = SteadyDBconnect(dbapi, database='ok') - cursor = db.cursor() - db.begin() - cursor.execute('select test14') - self.assertEqual(cursor.fetchone(), 'test14') - db.cancel() - cursor.execute('select test14') - self.assertEqual(cursor.fetchone(), 'test14') - - def test_with_cancel_extension(self): - db = SteadyDBconnect(dbapi, database='ok') - db._con._cancel_called = None - - def cancel(): - db._con._cancel_called = 'yes' - - db._con.cancel = cancel - db.begin() - cursor = db.cursor() - cursor.execute('select test15') - self.assertEqual(cursor.fetchone(), 'test15') - db.cancel() - self.assertEqual(db._con._cancel_called, 'yes') - - def test_reset_transaction(self): - db = SteadyDBconnect(dbapi, database='ok') - db.begin() - self.assertFalse(db._con.session) - db.close() - self.assertFalse(db._con.session) - db = SteadyDBconnect(dbapi, database='ok', closeable=False) - db.begin() - self.assertFalse(db._con.session) - db.close() - self.assertEqual(db._con.session, ['rollback']) - - def test_commit_error(self): - db = SteadyDBconnect(dbapi, database='ok') - db.begin() - self.assertFalse(db._con.session) - self.assertTrue(db._con.valid) + cursor.execute('select test12') + assert cursor.fetchone() == 'test12' + db.begin() + with pytest.raises(dbapi.ProgrammingError): + cursor.execute('error') + cursor.close() + cursor.execute('select test12') + assert cursor.fetchone() == 'test12' + + +def test_with_begin_extension(): + db = SteadyDBconnect(dbapi, database='ok') + db._con._begin_called_with = None + + def begin(a, b=None, c=7): + db._con._begin_called_with = (a, b, c) + + db._con.begin = begin + db.begin(42, 6) + cursor = db.cursor() + cursor.execute('select test13') + assert cursor.fetchone() == 'test13' + assert db._con._begin_called_with == (42, 6, 7) + + +def test_cancel_transaction(): + db = SteadyDBconnect(dbapi, database='ok') + cursor = db.cursor() + db.begin() + cursor.execute('select test14') + assert cursor.fetchone() == 'test14' + db.cancel() + cursor.execute('select test14') + assert cursor.fetchone() == 'test14' + + +def test_with_cancel_extension(): + db = SteadyDBconnect(dbapi, database='ok') + db._con._cancel_called = None + + def cancel(): + db._con._cancel_called = 'yes' + + db._con.cancel = cancel + db.begin() + cursor = db.cursor() + cursor.execute('select test15') + assert cursor.fetchone() == 'test15' + db.cancel() + assert db._con._cancel_called == 'yes' + + +def test_reset_transaction(): + db = SteadyDBconnect(dbapi, database='ok') + db.begin() + assert not db._con.session + db.close() + assert not db._con.session + db = SteadyDBconnect(dbapi, database='ok', closeable=False) + db.begin() + assert not db._con.session + db.close() + assert db._con.session == ['rollback'] + + +def test_commit_error(): + db = SteadyDBconnect(dbapi, database='ok') + db.begin() + assert not db._con.session + assert db._con.valid + db.commit() + assert db._con.session == ['commit'] + assert db._con.valid + db.begin() + db._con.valid = False + con = db._con + with pytest.raises(dbapi.InternalError): db.commit() - self.assertEqual(db._con.session, ['commit']) - self.assertTrue(db._con.valid) - db.begin() - db._con.valid = False - con = db._con - self.assertRaises(dbapi.InternalError, db.commit) - self.assertFalse(db._con.session) - self.assertTrue(db._con.valid) - self.assertIsNot(con, db._con) - db.begin() - self.assertFalse(db._con.session) - self.assertTrue(db._con.valid) - db.commit() - self.assertEqual(db._con.session, ['commit']) - self.assertTrue(db._con.valid) - - def test_rollback_error(self): - db = SteadyDBconnect(dbapi, database='ok') - db.begin() - self.assertFalse(db._con.session) - self.assertTrue(db._con.valid) - db.rollback() - self.assertEqual(db._con.session, ['rollback']) - self.assertTrue(db._con.valid) - db.begin() - db._con.valid = False - con = db._con - self.assertRaises(dbapi.InternalError, db.rollback) - self.assertFalse(db._con.session) - self.assertTrue(db._con.valid) - self.assertIsNot(con, db._con) - db.begin() - self.assertFalse(db._con.session) - self.assertTrue(db._con.valid) + assert not db._con.session + assert db._con.valid + assert con is not db._con + db.begin() + assert not db._con.session + assert db._con.valid + db.commit() + assert db._con.session == ['commit'] + assert db._con.valid + + +def test_rollback_error(): + db = SteadyDBconnect(dbapi, database='ok') + db.begin() + assert not db._con.session + assert db._con.valid + db.rollback() + assert db._con.session == ['rollback'] + assert db._con.valid + db.begin() + db._con.valid = False + con = db._con + with pytest.raises(dbapi.InternalError): db.rollback() - self.assertEqual(db._con.session, ['rollback']) - self.assertTrue(db._con.valid) - + assert not db._con.session + assert db._con.valid + assert con is not db._con + db.begin() + assert not db._con.session + assert db._con.valid + db.rollback() + assert db._con.session == ['rollback'] + assert db._con.valid -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_steady_pg.py b/tests/test_steady_pg.py index 2b8a1ad..094c7bc 100644 --- a/tests/test_steady_pg.py +++ b/tests/test_steady_pg.py @@ -11,312 +11,318 @@ * This test was contributed by Christoph Zwerschke """ -import unittest import sys -from . import mock_pg as pg +import pg +import pytest from dbutils.steady_pg import SteadyPgConnection -class TestSteadyPg(unittest.TestCase): +def test_version(): + from dbutils import __version__, steady_pg + assert steady_pg.__version__ == __version__ + assert steady_pg.SteadyPgConnection.version == __version__ - def test_version(self): - from dbutils import __version__, steady_pg - self.assertEqual(steady_pg.__version__, __version__) - self.assertEqual(steady_pg.SteadyPgConnection.version, __version__) +def test_mocked_connection(): + db_cls = pg.DB + db = db_cls( + 'SteadyPgTestDB', user='SteadyPgTestUser') + assert hasattr(db, 'db') + assert hasattr(db.db, 'status') + assert db.db.status + assert hasattr(db.db, 'query') + assert hasattr(db.db, 'close') + assert not hasattr(db.db, 'reopen') + assert hasattr(db, 'reset') + assert hasattr(db.db, 'num_queries') + assert hasattr(db.db, 'session') + assert not hasattr(db.db, 'get_tables') + assert hasattr(db.db, 'db') + assert db.db.db == 'SteadyPgTestDB' + assert hasattr(db.db, 'user') + assert db.db.user == 'SteadyPgTestUser' + assert hasattr(db, 'query') + assert hasattr(db, 'close') + assert hasattr(db, 'reopen') + assert hasattr(db, 'reset') + assert hasattr(db, 'num_queries') + assert hasattr(db, 'session') + assert hasattr(db, 'get_tables') + assert hasattr(db, 'dbname') + assert db.dbname == 'SteadyPgTestDB' + assert hasattr(db, 'user') + assert db.user == 'SteadyPgTestUser' + for i in range(3): + assert db.num_queries == i + assert db.query(f'select test{i}') == f'test{i}' + assert db.db.status + db.reopen() + assert db.db.status + assert db.num_queries == 0 + assert db.query('select test4') == 'test4' + assert db.get_tables() == 'test' + db.close() + try: + status = db.db.status + except AttributeError: + status = False + assert not status + with pytest.raises(pg.InternalError): + db.close() + with pytest.raises(pg.InternalError): + db.query('select test') + with pytest.raises(pg.InternalError): + db.get_tables() - def test_mocked_connection(self): - PgConnection = pg.DB - db = PgConnection( - 'SteadyPgTestDB', user='SteadyPgTestUser') - self.assertTrue(hasattr(db, 'db')) - self.assertTrue(hasattr(db.db, 'status')) - self.assertTrue(db.db.status) - self.assertTrue(hasattr(db.db, 'query')) - self.assertTrue(hasattr(db.db, 'close')) - self.assertFalse(hasattr(db.db, 'reopen')) - self.assertTrue(hasattr(db, 'reset')) - self.assertTrue(hasattr(db.db, 'num_queries')) - self.assertTrue(hasattr(db.db, 'session')) - self.assertFalse(hasattr(db.db, 'get_tables')) - self.assertTrue(hasattr(db.db, 'db')) - self.assertEqual(db.db.db, 'SteadyPgTestDB') - self.assertTrue(hasattr(db.db, 'user')) - self.assertEqual(db.db.user, 'SteadyPgTestUser') - self.assertTrue(hasattr(db, 'query')) - self.assertTrue(hasattr(db, 'close')) - self.assertTrue(hasattr(db, 'reopen')) - self.assertTrue(hasattr(db, 'reset')) - self.assertTrue(hasattr(db, 'num_queries')) - self.assertTrue(hasattr(db, 'session')) - self.assertTrue(hasattr(db, 'get_tables')) - self.assertTrue(hasattr(db, 'dbname')) - self.assertEqual(db.dbname, 'SteadyPgTestDB') - self.assertTrue(hasattr(db, 'user')) - self.assertEqual(db.user, 'SteadyPgTestUser') - for i in range(3): - self.assertEqual(db.num_queries, i) - self.assertEqual( - db.query(f'select test{i}'), f'test{i}') - self.assertTrue(db.db.status) - db.reopen() - self.assertTrue(db.db.status) - self.assertEqual(db.num_queries, 0) - self.assertEqual(db.query('select test4'), 'test4') - self.assertEqual(db.get_tables(), 'test') + +def test_broken_connection(): + with pytest.raises(TypeError): + SteadyPgConnection('wrong') + db = SteadyPgConnection(dbname='ok') + internal_error_cls = sys.modules[db._con.__module__].InternalError + for i in range(3): db.close() - try: - status = db.db.status - except AttributeError: - status = False - self.assertFalse(status) - self.assertRaises(pg.InternalError, db.close) - self.assertRaises(pg.InternalError, db.query, 'select test') - self.assertRaises(pg.InternalError, db.get_tables) + del db + with pytest.raises(internal_error_cls): + SteadyPgConnection(dbname='error') - def test_broken_connection(self): - self.assertRaises(TypeError, SteadyPgConnection, 'wrong') - db = SteadyPgConnection(dbname='ok') - InternalError = sys.modules[db._con.__module__].InternalError - for i in range(3): - db.close() - del db - self.assertRaises(InternalError, SteadyPgConnection, dbname='error') - def test_close(self): - for closeable in (False, True): - db = SteadyPgConnection(closeable=closeable) - self.assertTrue(db._con.db and db._con.valid) - db.close() - self.assertTrue( - closeable ^ (db._con.db is not None and db._con.valid)) - db.close() - self.assertTrue( - closeable ^ (db._con.db is not None and db._con.valid)) - db._close() - self.assertFalse(db._con.db and db._con.valid) - db._close() - self.assertFalse(db._con.db and db._con.valid) +@pytest.mark.parametrize("closeable", [False, True]) +def test_close(closeable): + db = SteadyPgConnection(closeable=closeable) + assert db._con.db + assert db._con.valid is True + db.close() + assert closeable ^ (db._con.db is not None and db._con.valid) + db.close() + assert closeable ^ (db._con.db is not None and db._con.valid) + db._close() + assert not db._con.db + db._close() + assert not db._con.db - def test_connection(self): - db = SteadyPgConnection( - 0, None, 1, 'SteadyPgTestDB', user='SteadyPgTestUser') - self.assertTrue(hasattr(db, 'db')) - self.assertTrue(hasattr(db, '_con')) - self.assertEqual(db.db, db._con.db) - self.assertTrue(hasattr(db, '_usage')) - self.assertEqual(db._usage, 0) - self.assertTrue(hasattr(db.db, 'status')) - self.assertTrue(db.db.status) - self.assertTrue(hasattr(db.db, 'query')) - self.assertTrue(hasattr(db.db, 'close')) - self.assertFalse(hasattr(db.db, 'reopen')) - self.assertTrue(hasattr(db.db, 'reset')) - self.assertTrue(hasattr(db.db, 'num_queries')) - self.assertTrue(hasattr(db.db, 'session')) - self.assertTrue(hasattr(db.db, 'db')) - self.assertEqual(db.db.db, 'SteadyPgTestDB') - self.assertTrue(hasattr(db.db, 'user')) - self.assertEqual(db.db.user, 'SteadyPgTestUser') - self.assertFalse(hasattr(db.db, 'get_tables')) - self.assertTrue(hasattr(db, 'query')) - self.assertTrue(hasattr(db, 'close')) - self.assertTrue(hasattr(db, 'reopen')) - self.assertTrue(hasattr(db, 'reset')) - self.assertTrue(hasattr(db, 'num_queries')) - self.assertTrue(hasattr(db, 'session')) - self.assertTrue(hasattr(db, 'dbname')) - self.assertEqual(db.dbname, 'SteadyPgTestDB') - self.assertTrue(hasattr(db, 'user')) - self.assertEqual(db.user, 'SteadyPgTestUser') - self.assertTrue(hasattr(db, 'get_tables')) - for i in range(3): - self.assertEqual(db._usage, i) - self.assertEqual(db.num_queries, i) - self.assertEqual( - db.query(f'select test{i}'), f'test{i}') - self.assertTrue(db.db.status) - self.assertEqual(db.get_tables(), 'test') - self.assertTrue(db.db.status) - self.assertEqual(db._usage, 4) - self.assertEqual(db.num_queries, 3) - db.reopen() - self.assertTrue(db.db.status) - self.assertEqual(db._usage, 0) - self.assertEqual(db.num_queries, 0) - self.assertEqual(db.query('select test'), 'test') - self.assertTrue(db.db.status) - self.assertTrue(hasattr(db._con, 'status')) - self.assertTrue(db._con.status) - self.assertTrue(hasattr(db._con, 'close')) - self.assertTrue(hasattr(db._con, 'query')) - db.close() - try: - status = db.db.status - except AttributeError: - status = False - self.assertFalse(status) - self.assertTrue(hasattr(db._con, 'close')) - self.assertTrue(hasattr(db._con, 'query')) - InternalError = sys.modules[db._con.__module__].InternalError - self.assertRaises(InternalError, db._con.close) - self.assertRaises(InternalError, db._con.query, 'select test') - self.assertEqual(db.query('select test'), 'test') - self.assertTrue(db.db.status) - self.assertEqual(db._usage, 1) - self.assertEqual(db.num_queries, 1) - db.db.status = False - self.assertFalse(db.db.status) - self.assertEqual(db.query('select test'), 'test') - self.assertTrue(db.db.status) - self.assertEqual(db._usage, 1) - self.assertEqual(db.num_queries, 1) - db.db.status = False - self.assertFalse(db.db.status) - self.assertEqual(db.get_tables(), 'test') - self.assertTrue(db.db.status) - self.assertEqual(db._usage, 1) - self.assertEqual(db.num_queries, 0) - def test_connection_context_handler(self): - db = SteadyPgConnection( - 0, None, 1, 'SteadyPgTestDB', user='SteadyPgTestUser') - self.assertEqual(db.session, []) - with db: - db.query('select test') - self.assertEqual(db.session, ['begin', 'commit']) - try: - with db: - db.query('error') - except pg.ProgrammingError: - error = True - else: - error = False - self.assertTrue(error) - self.assertEqual( - db._con.session, ['begin', 'commit', 'begin', 'rollback']) +def test_connection(): + db = SteadyPgConnection( + 0, None, 1, 'SteadyPgTestDB', user='SteadyPgTestUser') + assert hasattr(db, 'db') + assert hasattr(db, '_con') + assert db.db == db._con.db + assert hasattr(db, '_usage') + assert db._usage == 0 + assert hasattr(db.db, 'status') + assert db.db.status + assert hasattr(db.db, 'query') + assert hasattr(db.db, 'close') + assert not hasattr(db.db, 'reopen') + assert hasattr(db.db, 'reset') + assert hasattr(db.db, 'num_queries') + assert hasattr(db.db, 'session') + assert hasattr(db.db, 'db') + assert db.db.db == 'SteadyPgTestDB' + assert hasattr(db.db, 'user') + assert db.db.user == 'SteadyPgTestUser' + assert not hasattr(db.db, 'get_tables') + assert hasattr(db, 'query') + assert hasattr(db, 'close') + assert hasattr(db, 'reopen') + assert hasattr(db, 'reset') + assert hasattr(db, 'num_queries') + assert hasattr(db, 'session') + assert hasattr(db, 'dbname') + assert db.dbname == 'SteadyPgTestDB' + assert hasattr(db, 'user') + assert db.user == 'SteadyPgTestUser' + assert hasattr(db, 'get_tables') + for i in range(3): + assert db._usage == i + assert db.num_queries == i + assert db.query(f'select test{i}') == f'test{i}' + assert db.db.status + assert db.get_tables() == 'test' + assert db.db.status + assert db._usage == 4 + assert db.num_queries == 3 + db.reopen() + assert db.db.status + assert db._usage == 0 + assert db.num_queries == 0 + assert db.query('select test') == 'test' + assert db.db.status + assert hasattr(db._con, 'status') + assert db._con.status + assert hasattr(db._con, 'close') + assert hasattr(db._con, 'query') + db.close() + try: + status = db.db.status + except AttributeError: + status = False + assert not status + assert hasattr(db._con, 'close') + assert hasattr(db._con, 'query') + internal_error_cls = sys.modules[db._con.__module__].InternalError + with pytest.raises(internal_error_cls): + db._con.close() + with pytest.raises(internal_error_cls): + db._con.query('select test') + assert db.query('select test') == 'test' + assert db.db.status + assert db._usage == 1 + assert db.num_queries == 1 + db.db.status = False + assert not db.db.status + assert db.query('select test') == 'test' + assert db.db.status + assert db._usage == 1 + assert db.num_queries == 1 + db.db.status = False + assert not db.db.status + assert db.get_tables() == 'test' + assert db.db.status + assert db._usage == 1 + assert db.num_queries == 0 - def test_connection_maxusage(self): - db = SteadyPgConnection(10) - for i in range(100): - r = db.query(f'select test{i}') - self.assertEqual(r, f'test{i}') - self.assertTrue(db.db.status) - j = i % 10 + 1 - self.assertEqual(db._usage, j) - self.assertEqual(db.num_queries, j) - db.begin() - for i in range(100): - r = db.get_tables() - self.assertEqual(r, 'test') - self.assertTrue(db.db.status) - if i == 49: - db.commit() - j = i % 10 + 1 if i > 49 else i + 11 - self.assertEqual(db._usage, j) - j = 0 if i > 49 else 10 - self.assertEqual(db.num_queries, j) - for i in range(10): - if i == 7: - db.db.status = False - r = db.query(f'select test{i}') - self.assertEqual(r, f'test{i}') - j = i % 7 + 1 - self.assertEqual(db._usage, j) - self.assertEqual(db.num_queries, j) - for i in range(10): - if i == 5: - db.db.status = False - r = db.get_tables() - self.assertEqual(r, 'test') - j = (i + (3 if i < 5 else -5)) % 10 + 1 - self.assertEqual(db._usage, j) - j = 3 if i < 5 else 0 - self.assertEqual(db.num_queries, j) - db.close() - self.assertEqual(db.query('select test1'), 'test1') - self.assertEqual(db._usage, 1) - self.assertEqual(db.num_queries, 1) - db.reopen() - self.assertEqual(db._usage, 0) - self.assertEqual(db.num_queries, 0) - self.assertEqual(db.query('select test2'), 'test2') - self.assertEqual(db._usage, 1) - self.assertEqual(db.num_queries, 1) - def test_connection_setsession(self): - db = SteadyPgConnection(3, ('set time zone', 'set datestyle')) - self.assertTrue(hasattr(db, 'num_queries')) - self.assertEqual(db.num_queries, 0) - self.assertTrue(hasattr(db, 'session')) - self.assertEqual(tuple(db.session), ('time zone', 'datestyle')) - for i in range(11): - db.query('select test') - self.assertEqual(db.num_queries, 2) - self.assertEqual(db.session, ['time zone', 'datestyle']) - db.query('set test') - self.assertEqual(db.num_queries, 2) - self.assertEqual(db.session, ['time zone', 'datestyle', 'test']) +def test_connection_context_handler(): + db = SteadyPgConnection( + 0, None, 1, 'SteadyPgTestDB', user='SteadyPgTestUser') + assert db.session == [] + with db: db.query('select test') - self.assertEqual(db.num_queries, 1) - self.assertEqual(db.session, ['time zone', 'datestyle']) - db.close() - db.query('set test') - self.assertEqual(db.num_queries, 0) - self.assertEqual(db.session, ['time zone', 'datestyle', 'test']) + assert db.session == ['begin', 'commit'] + try: + with db: + db.query('error') + except pg.ProgrammingError: + error = True + else: + error = False + assert error + assert db._con.session == ['begin', 'commit', 'begin', 'rollback'] - def test_begin(self): - for closeable in (False, True): - db = SteadyPgConnection(closeable=closeable) - db.begin() - self.assertEqual(db.session, ['begin']) - db.query('select test') - self.assertEqual(db.num_queries, 1) - db.close() - db.query('select test') - self.assertEqual(db.num_queries, 1) - db.begin() - self.assertEqual(db.session, ['begin']) - db.db.close() - self.assertRaises(pg.InternalError, db.query, 'select test') - self.assertEqual(db.num_queries, 0) - db.query('select test') - self.assertEqual(db.num_queries, 1) - self.assertEqual(db.begin('select sql:begin'), 'sql:begin') - self.assertEqual(db.num_queries, 2) - def test_end(self): - for closeable in (False, True): - db = SteadyPgConnection(closeable=closeable) - db.begin() - db.query('select test') - db.end() - self.assertEqual(db.session, ['begin', 'end']) - db.db.close() - db.query('select test') - self.assertEqual(db.num_queries, 1) - self.assertEqual(db.begin('select sql:end'), 'sql:end') - self.assertEqual(db.num_queries, 2) - db.begin() - db.query('select test') +def test_connection_maxusage(): + db = SteadyPgConnection(10) + for i in range(100): + r = db.query(f'select test{i}') + assert r == f'test{i}' + assert db.db.status + j = i % 10 + 1 + assert db._usage == j + assert db.num_queries == j + db.begin() + for i in range(100): + r = db.get_tables() + assert r == 'test' + assert db.db.status + if i == 49: db.commit() - self.assertEqual(db.session, ['begin', 'commit']) - db.db.close() - db.query('select test') - self.assertEqual(db.num_queries, 1) - self.assertEqual(db.begin('select sql:commit'), 'sql:commit') - self.assertEqual(db.num_queries, 2) - db.begin() - db.query('select test') - db.rollback() - self.assertEqual(db.session, ['begin', 'rollback']) - db.db.close() - db.query('select test') - self.assertEqual(db.num_queries, 1) - self.assertEqual(db.begin('select sql:rollback'), 'sql:rollback') - self.assertEqual(db.num_queries, 2) + j = i % 10 + 1 if i > 49 else i + 11 + assert db._usage == j + j = 0 if i > 49 else 10 + assert db.num_queries == j + for i in range(10): + if i == 7: + db.db.status = False + r = db.query(f'select test{i}') + assert r == f'test{i}' + j = i % 7 + 1 + assert db._usage == j + assert db.num_queries == j + for i in range(10): + if i == 5: + db.db.status = False + r = db.get_tables() + assert r == 'test' + j = (i + (3 if i < 5 else -5)) % 10 + 1 + assert db._usage == j + j = 3 if i < 5 else 0 + assert db.num_queries == j + db.close() + assert db.query('select test1') == 'test1' + assert db._usage == 1 + assert db.num_queries == 1 + db.reopen() + assert db._usage == 0 + assert db.num_queries == 0 + assert db.query('select test2') == 'test2' + assert db._usage == 1 + assert db.num_queries == 1 + + +def test_connection_setsession(): + db = SteadyPgConnection(3, ('set time zone', 'set datestyle')) + assert hasattr(db, 'num_queries') + assert db.num_queries == 0 + assert hasattr(db, 'session') + assert tuple(db.session) == ('time zone', 'datestyle') + for i in range(11): + db.query('select test') + assert db.num_queries == 2 + assert db.session == ['time zone', 'datestyle'] + db.query('set test') + assert db.num_queries == 2 + assert db.session == ['time zone', 'datestyle', 'test'] + db.query('select test') + assert db.num_queries == 1 + assert db.session == ['time zone', 'datestyle'] + db.close() + db.query('set test') + assert db.num_queries == 0 + assert db.session == ['time zone', 'datestyle', 'test'] + + +@pytest.mark.parametrize("closeable", [False, True]) +def test_begin(closeable): + db = SteadyPgConnection(closeable=closeable) + db.begin() + assert db.session == ['begin'] + db.query('select test') + assert db.num_queries == 1 + db.close() + db.query('select test') + assert db.num_queries == 1 + db.begin() + assert db.session == ['begin'] + db.db.close() + with pytest.raises(pg.InternalError): + db.query('select test') + assert db.num_queries == 0 + db.query('select test') + assert db.num_queries == 1 + assert db.begin('select sql:begin') == 'sql:begin' + assert db.num_queries == 2 -if __name__ == '__main__': - unittest.main() +@pytest.mark.parametrize("closeable", [False, True]) +def test_end(closeable): + db = SteadyPgConnection(closeable=closeable) + db.begin() + db.query('select test') + db.end() + assert db.session == ['begin', 'end'] + db.db.close() + db.query('select test') + assert db.num_queries == 1 + assert db.begin('select sql:end') == 'sql:end' + assert db.num_queries == 2 + db.begin() + db.query('select test') + db.commit() + assert db.session == ['begin', 'commit'] + db.db.close() + db.query('select test') + assert db.num_queries == 1 + assert db.begin('select sql:commit') == 'sql:commit' + assert db.num_queries == 2 + db.begin() + db.query('select test') + db.rollback() + assert db.session == ['begin', 'rollback'] + db.db.close() + db.query('select test') + assert db.num_queries == 1 + assert db.begin('select sql:rollback') == 'sql:rollback' + assert db.num_queries == 2 diff --git a/tests/test_threading_local.py b/tests/test_threading_local.py index b44c613..2caf138 100644 --- a/tests/test_threading_local.py +++ b/tests/test_threading_local.py @@ -1,86 +1,83 @@ """Test the ThreadingLocal module.""" -import unittest from threading import Thread from dbutils.persistent_db import local -class TestThreadingLocal(unittest.TestCase): - - def test_getattr(self): - my_data = local() - my_data.number = 42 - self.assertEqual(my_data.number, 42) - - def test_dict(self): - my_data = local() - my_data.number = 42 - self.assertEqual(my_data.__dict__, {'number': 42}) - my_data.__dict__.setdefault('widgets', []) - self.assertEqual(my_data.widgets, []) - - def test_threadlocal(self): - def f(): - items = sorted(my_data.__dict__.items()) - log.append(items) - my_data.number = 11 - log.append(my_data.number) - my_data = local() - my_data.number = 42 - log = [] - thread = Thread(target=f) - thread.start() - thread.join() - self.assertEqual(log, [[], 11]) - self.assertEqual(my_data.number, 42) - - def test_subclass(self): - - class MyLocal(local): - number = 2 - initialized = 0 - - def __init__(self, **kw): - if self.initialized: - raise SystemError - self.initialized = 1 - self.__dict__.update(kw) - - def squared(self): - return self.number ** 2 - - my_data = MyLocal(color='red') - self.assertEqual(my_data.number, 2) - self.assertEqual(my_data.color, 'red') - del my_data.color - self.assertEqual(my_data.squared(), 4) - - def f(): - items = sorted(my_data.__dict__.items()) - log.append(items) - my_data.number = 7 - log.append(my_data.number) - - log = [] - thread = Thread(target=f) - thread.start() - thread.join() - self.assertEqual(log, [[('color', 'red'), ('initialized', 1)], 7]) - self.assertEqual(my_data.number, 2) - self.assertFalse(hasattr(my_data, 'color')) - - class MyLocal(local): - __slots__ = 'number' - - my_data = MyLocal() - my_data.number = 42 - my_data.color = 'red' - thread = Thread(target=f) - thread.start() - thread.join() - self.assertEqual(my_data.number, 7) - - -if __name__ == '__main__': - unittest.main() +def test_getattr(): + my_data = local() + my_data.number = 42 + assert my_data.number == 42 + + +def test_dict(): + my_data = local() + my_data.number = 42 + assert my_data.__dict__ == {'number': 42} + my_data.__dict__.setdefault('widgets', []) + assert my_data.widgets == [] + + +def test_threadlocal(): + def f(): + items = sorted(my_data.__dict__.items()) + log.append(items) + my_data.number = 11 + log.append(my_data.number) + my_data = local() + my_data.number = 42 + log = [] + thread = Thread(target=f) + thread.start() + thread.join() + assert log == [[], 11] + assert my_data.number == 42 + + +def test_subclass(): + + class MyLocal(local): + number = 2 + initialized = 0 + + def __init__(self, **kw): + if self.initialized: + raise SystemError + self.initialized = 1 + self.__dict__.update(kw) + + def squared(self): + return self.number ** 2 + + my_data = MyLocal(color='red') + assert my_data.number == 2 + assert my_data.color == 'red' + del my_data.color + assert my_data.squared() == 4 + + def f(): + items = sorted(my_data.__dict__.items()) + log.append(items) + my_data.number = 7 + log.append(my_data.number) + + log = [] + thread = Thread(target=f) + thread.start() + thread.join() + assert log == [[('color', 'red'), ('initialized', 1)], 7] + assert my_data.number == 2 + assert not hasattr(my_data, 'color') + + class MyLocal(local): + __slots__ = 'number' + + my_data = MyLocal() + my_data.number = 42 + my_data.color = 'red' + thread = Thread(target=f) + thread.start() + thread.join() + assert my_data.number == 7 + diff --git a/tox.ini b/tox.ini index 1b03272..812d428 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ envlist = py3{7,8,9,10,11}, ruff, manifest, docs, spell [testenv] setenv = PYTHONPATH = {toxinidir} -deps = pytest +deps = pytest>=7 commands = pytest {posargs} From 7e2d00a91fca169675cca56bc89b0431c78da2ea Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 29 Apr 2023 21:32:46 +0200 Subject: [PATCH 54/84] Assume modern Python when using Queue class The timeout parameter actually exists since Python 2.3... --- tests/test_persistent_db.py | 69 ++++++++-------------------------- tests/test_persistent_pg.py | 67 ++++++++------------------------- tests/test_pooled_db.py | 39 +++++-------------- tests/test_pooled_pg.py | 43 +++++++++------------ tests/test_simple_pooled_db.py | 38 +++++++------------ tests/test_simple_pooled_pg.py | 22 +++-------- tests/test_steady_db.py | 3 +- tests/test_steady_pg.py | 1 + tests/test_threading_local.py | 1 - 9 files changed, 78 insertions(+), 205 deletions(-) diff --git a/tests/test_persistent_db.py b/tests/test_persistent_db.py index 25897fd..d8fb504 100644 --- a/tests/test_persistent_db.py +++ b/tests/test_persistent_db.py @@ -76,10 +76,7 @@ def run_queries(i): db = None while True: try: - try: - q = query_queue[i].get(1, 1) - except TypeError: - q = query_queue[i].get(1) + q = query_queue[i].get(timeout=1) except Empty: q = None if not q: @@ -99,10 +96,7 @@ def run_queries(i): r = cursor.fetchone() cursor.close() r = f'{i}({db._usage}): {r}' - try: - result_queue[i].put(r, 1, 1) - except TypeError: - result_queue[i].put(r, 1) + result_queue[i].put(r, timeout=1) if db: db.close() @@ -112,68 +106,35 @@ def run_queries(i): threads.append(thread) thread.start() for i in range(num_threads): - try: - query_queue[i].put('ping', 1, 1) - except TypeError: - query_queue[i].put('ping', 1) + query_queue[i].put('ping', timeout=1) for i in range(num_threads): - try: - r = result_queue[i].get(1, 1) - except TypeError: - r = result_queue[i].get(1) + r = result_queue[i].get(timeout=1) assert r == f'{i}(0): ok - thread alive' assert threads[i].is_alive() for i in range(num_threads): for j in range(i + 1): - try: - query_queue[i].put(f'select test{j}', 1, 1) - r = result_queue[i].get(1, 1) - except TypeError: - query_queue[i].put(f'select test{j}', 1) - r = result_queue[i].get(1) + query_queue[i].put(f'select test{j}', timeout=1) + r = result_queue[i].get(timeout=1) assert r == f'{i}({j + 1}): test{j}' - try: - query_queue[1].put('select test4', 1, 1) - except TypeError: - query_queue[1].put('select test4', 1) - try: - r = result_queue[1].get(1, 1) - except TypeError: - r = result_queue[1].get(1) + query_queue[1].put('select test4', timeout=1) + r = result_queue[1].get(timeout=1) assert r == '1(3): test4' - try: - query_queue[1].put('close', 1, 1) - r = result_queue[1].get(1, 1) - except TypeError: - query_queue[1].put('close', 1) - r = result_queue[1].get(1) + query_queue[1].put('close', timeout=1) + r = result_queue[1].get(timeout=1) assert r == '1(3): ok - connection closed' for j in range(2): - try: - query_queue[1].put(f'select test{j}', 1, 1) - r = result_queue[1].get(1, 1) - except TypeError: - query_queue[1].put(f'select test{j}', 1) - r = result_queue[1].get(1) + query_queue[1].put(f'select test{j}', timeout=1) + r = result_queue[1].get(timeout=1) assert r == f'1({j + 1}): test{j}' for i in range(num_threads): assert threads[i].is_alive() - try: - query_queue[i].put('ping', 1, 1) - except TypeError: - query_queue[i].put('ping', 1) + query_queue[i].put('ping', timeout=1) for i in range(num_threads): - try: - r = result_queue[i].get(1, 1) - except TypeError: - r = result_queue[i].get(1) + r = result_queue[i].get(timeout=1) assert r == f'{i}({i + 1}): ok - thread alive' assert threads[i].is_alive() for i in range(num_threads): - try: - query_queue[i].put(None, 1, 1) - except TypeError: - query_queue[i].put(None, 1) + query_queue[i].put(None, timeout=1) def test_maxusage(dbapi): # noqa: F811 diff --git a/tests/test_persistent_pg.py b/tests/test_persistent_pg.py index bd6c21a..c8d66d6 100644 --- a/tests/test_persistent_pg.py +++ b/tests/test_persistent_pg.py @@ -54,10 +54,7 @@ def run_queries(i): db = None while True: try: - try: - q = query_queue[i].get(1, 1) - except TypeError: - q = query_queue[i].get(1) + q = query_queue[i].get(timeout=1) except Empty: q = None if not q: @@ -74,10 +71,7 @@ def run_queries(i): else: r = db.query(q) r = f'{i}({db._usage}): {r}' - try: - result_queue[i].put(r, 1, 1) - except TypeError: - result_queue[i].put(r, 1) + result_queue[i].put(r, timeout=1) if db: db.close() @@ -87,66 +81,35 @@ def run_queries(i): threads.append(thread) thread.start() for i in range(num_threads): - try: - query_queue[i].put('ping', 1, 1) - except TypeError: - query_queue[i].put('ping', 1) + query_queue[i].put('ping', timeout=1) for i in range(num_threads): - try: - r = result_queue[i].get(1, 1) - except TypeError: - r = result_queue[i].get(1) + r = result_queue[i].get(timeout=1) assert r == f'{i}(0): ok - thread alive' assert threads[i].is_alive() for i in range(num_threads): for j in range(i + 1): - try: - query_queue[i].put(f'select test{j}', 1, 1) - r = result_queue[i].get(1, 1) - except TypeError: - query_queue[i].put(f'select test{j}', 1) - r = result_queue[i].get(1) + query_queue[i].put(f'select test{j}', timeout=1) + r = result_queue[i].get(timeout=1) assert r == f'{i}({j + 1}): test{j}' - try: - query_queue[1].put('select test4', 1, 1) - r = result_queue[1].get(1, 1) - except TypeError: - query_queue[1].put('select test4', 1) - r = result_queue[1].get(1) + query_queue[1].put('select test4', timeout=1) + r = result_queue[1].get(timeout=1) assert r == '1(3): test4' - try: - query_queue[1].put('close', 1, 1) - r = result_queue[1].get(1, 1) - except TypeError: - query_queue[1].put('close', 1) - r = result_queue[1].get(1) + query_queue[1].put('close', timeout=1) + r = result_queue[1].get(timeout=1) assert r == '1(3): ok - connection closed' for j in range(2): - try: - query_queue[1].put(f'select test{j}', 1, 1) - r = result_queue[1].get(1, 1) - except TypeError: - query_queue[1].put(f'select test{j}', 1) - r = result_queue[1].get(1) + query_queue[1].put(f'select test{j}', timeout=1) + r = result_queue[1].get(timeout=1) assert r == f'1({j + 1}): test{j}' for i in range(num_threads): assert threads[i].is_alive() - try: - query_queue[i].put('ping', 1, 1) - except TypeError: - query_queue[i].put('ping', 1) + query_queue[i].put('ping', timeout=1) for i in range(num_threads): - try: - r = result_queue[i].get(1, 1) - except TypeError: - r = result_queue[i].get(1) + r = result_queue[i].get(timeout=1) assert r == f'{i}({i + 1}): ok - thread alive' assert threads[i].is_alive() for i in range(num_threads): - try: - query_queue[i].put(None, 1, 1) - except TypeError: - query_queue[i].put(None, 1) + query_queue[i].put(None, timeout=1) def test_maxusage(): diff --git a/tests/test_pooled_db.py b/tests/test_pooled_db.py index ebbe88d..8bbd866 100644 --- a/tests/test_pooled_db.py +++ b/tests/test_pooled_db.py @@ -1022,34 +1022,20 @@ def test_three_threads_two_connections(dbapi, threadsafety): # noqa: F811 queue = Queue(3) def connection(): - try: - queue.put(pool.connection(), 1, 1) - except Exception: - queue.put(pool.connection(), 1) + queue.put(pool.connection(), timeout=1) for i in range(3): Thread(target=connection).start() - try: - db1 = queue.get(1, 1) - db2 = queue.get(1, 1) - except TypeError: - db1 = queue.get(1) - db2 = queue.get(1) + db1 = queue.get(timeout=1) + db2 = queue.get(timeout=1) assert db1 != db2 db1_con = db1._con db2_con = db2._con assert db1_con != db2_con - try: - with pytest.raises(Empty): - queue.get(1, 0.1) - except TypeError: - with pytest.raises(Empty): - queue.get(0) + with pytest.raises(Empty): + queue.get(timeout=0.1) del db1 - try: - db1 = queue.get(1, 1) - except TypeError: - db1 = queue.get(1) + db1 = queue.get(timeout=1) assert db1 != db2 assert db1._con != db2._con assert db1._con == db1_con @@ -1061,17 +1047,10 @@ def connection(): db2_con = db2._con assert db1_con != db2_con Thread(target=connection).start() - try: - with pytest.raises(Empty): - queue.get(1, 0.1) - except TypeError: - with pytest.raises(Empty): - queue.get(0) + with pytest.raises(Empty): + queue.get(timeout=0.1) del db1 - try: - db1 = queue.get(1, 1) - except TypeError: - db1 = queue.get(1) + db1 = queue.get(timeout=1) assert db1 != db2 assert db1._con != db2._con assert db1._con == db1_con diff --git a/tests/test_pooled_pg.py b/tests/test_pooled_pg.py index b09469f..93c266c 100644 --- a/tests/test_pooled_pg.py +++ b/tests/test_pooled_pg.py @@ -101,13 +101,15 @@ def test_close_connection(): db = pool.connection() assert pool._cache.qsize() == 1 assert pool._cache.get(0) == db_con + assert db + del db def test_min_max_cached(): pool = PooledPg(3) assert hasattr(pool, '_cache') assert pool._cache.qsize() == 3 - cache = [pool.connection() for i in range(3)] + cache = [pool.connection() for _i in range(3)] assert pool._cache.qsize() == 0 for i in range(3): cache.pop().close() @@ -121,7 +123,7 @@ def test_min_max_cached(): pool = PooledPg(3, 4) assert hasattr(pool, '_cache') assert pool._cache.qsize() == 3 - cache = [pool.connection() for i in range(3)] + cache = [pool.connection() for _i in range(3)] assert pool._cache.qsize() == 0 for i in range(3): cache.pop().close() @@ -135,7 +137,7 @@ def test_min_max_cached(): pool = PooledPg(3, 2) assert hasattr(pool, '_cache') assert pool._cache.qsize() == 3 - cache = [pool.connection() for i in range(4)] + cache = [pool.connection() for _i in range(4)] assert pool._cache.qsize() == 0 for i in range(4): cache.pop().close() @@ -143,7 +145,7 @@ def test_min_max_cached(): pool = PooledPg(2, 5) assert hasattr(pool, '_cache') assert pool._cache.qsize() == 2 - cache = [pool.connection() for i in range(10)] + cache = [pool.connection() for _i in range(10)] assert pool._cache.qsize() == 0 for i in range(10): cache.pop().close() @@ -153,7 +155,7 @@ def test_min_max_cached(): def test_max_connections(): pool = PooledPg(1, 2, 3) assert pool._cache.qsize() == 1 - cache = [pool.connection() for i in range(3)] + cache = [pool.connection() for _i in range(3)] assert pool._cache.qsize() == 0 with pytest.raises(TooManyConnections): pool.connection() @@ -164,7 +166,9 @@ def test_max_connections(): assert pool._cache.qsize() == 0 with pytest.raises(TooManyConnections): pool.connection() + assert db del db + assert cache del cache pool = PooledPg(1, 2, 1) assert pool._cache.qsize() == 1 @@ -176,7 +180,7 @@ def test_max_connections(): pool.connection() pool = PooledPg(3, 2, 1, False) assert pool._cache.qsize() == 3 - cache = [pool.connection() for i in range(3)] + cache = [pool.connection() for _i in range(3)] assert len(cache) == 3 assert pool._cache.qsize() == 0 with pytest.raises(TooManyConnections): @@ -204,6 +208,7 @@ def connection(): db = pool.connection() assert pool._cache.qsize() == 0 assert session == ['thread'] + assert db del db @@ -236,34 +241,20 @@ def test_three_threads_two_connections(): queue = Queue(3) def connection(): - try: - queue.put(pool.connection(), 1, 1) - except TypeError: - queue.put(pool.connection(), 1) + queue.put(pool.connection(), timeout=1) for i in range(3): Thread(target=connection).start() - try: - db1 = queue.get(1, 1) - db2 = queue.get(1, 1) - except TypeError: - db1 = queue.get(1) - db2 = queue.get(1) + db1 = queue.get(timeout=1) + db2 = queue.get(timeout=1) db1_con = db1._con db2_con = db2._con assert db1 != db2 assert db1_con != db2_con - try: - with pytest.raises(Empty): - queue.get(1, 0.1) - except TypeError: - with pytest.raises(Empty): - queue.get(0) + with pytest.raises(Empty): + queue.get(timeout=0.1) del db1 - try: - db1 = queue.get(1, 1) - except TypeError: - db1 = queue.get(1) + db1 = queue.get(timeout=1) assert db1 != db2 assert db1._con != db2._con assert db1._con == db1_con diff --git a/tests/test_simple_pooled_db.py b/tests/test_simple_pooled_db.py index 3cfd99e..f112d64 100644 --- a/tests/test_simple_pooled_db.py +++ b/tests/test_simple_pooled_db.py @@ -12,6 +12,7 @@ """ from queue import Queue, Empty +from threading import Thread import pytest @@ -20,15 +21,16 @@ from dbutils import simple_pooled_db -def my_db_pool(mythreadsafety, max_connections): - threadsafety = dbapi.threadsafety - dbapi.threadsafety = mythreadsafety +def my_db_pool(threadsafety, max_connections): + """Get simple PooledDB connection.""" + dbapi_threadsafety = dbapi.threadsafety + dbapi.threadsafety = threadsafety try: return simple_pooled_db.PooledDB( dbapi, max_connections, 'SimplePooledDBTestDB', 'SimplePooledDBTestUser') finally: - dbapi.threadsafety = threadsafety + dbapi.threadsafety = dbapi_threadsafety def test_version(): @@ -87,10 +89,10 @@ def test_close_connection(threadsafety): def test_two_connections(threadsafety): db_pool = my_db_pool(threadsafety, 2) db1 = db_pool.connection() - cursors1 = [db1.cursor() for i in range(5)] + cursors1 = [db1.cursor() for _i_ in range(5)] db2 = db_pool.connection() assert db1 != db2 - cursors2 = [db2.cursor() for i in range(7)] + cursors2 = [db2.cursor() for _i in range(7)] assert db1.open_cursors == 5 assert db2.open_cursors == 7 db1.close() @@ -113,28 +115,16 @@ def test_threadsafety_1(): def connection(): queue.put(db_pool.connection()) - from threading import Thread - threads = [Thread(target=connection).start() for i in range(3)] + threads = [Thread(target=connection).start() for _i in range(3)] assert len(threads) == 3 - try: - db1 = queue.get(1, 1) - db2 = queue.get(1, 1) - except TypeError: - db1 = queue.get(1) - db2 = queue.get(1) + db1 = queue.get(timeout=1) + db2 = queue.get(timeout=1) assert db1 != db2 assert db1._con != db2._con - try: - with pytest.raises(Empty): - queue.get(1, 0.1) - except TypeError: - with pytest.raises(Empty): - queue.get(0) + with pytest.raises(Empty): + queue.get(timeout=0.1) db2.close() - try: - db3 = queue.get(1, 1) - except TypeError: - db3 = queue.get(1) + db3 = queue.get(timeout=1) assert db1 != db3 assert db1._con != db3._con diff --git a/tests/test_simple_pooled_pg.py b/tests/test_simple_pooled_pg.py index 2652c4c..1492573 100644 --- a/tests/test_simple_pooled_pg.py +++ b/tests/test_simple_pooled_pg.py @@ -20,6 +20,7 @@ def my_db_pool(max_connections): + """Get simple PooledPg connection.""" return simple_pooled_pg.PooledPg( max_connections, 'SimplePooledPgTestDB', 'SimplePooledPgTestUser') @@ -95,24 +96,13 @@ def connection(): threads = [Thread(target=connection).start() for _i in range(3)] assert len(threads) == 3 - try: - db1 = queue.get(1, 1) - db2 = queue.get(1, 1) - except TypeError: - db1 = queue.get(1) - db2 = queue.get(1) + db1 = queue.get(timeout=1) + db2 = queue.get(timeout=1) assert db1 != db2 assert db1._con != db2._con - try: - with pytest.raises(Empty): - queue.get(1, 0.1) - except TypeError: - with pytest.raises(Empty): - queue.get(0) + with pytest.raises(Empty): + queue.get(timeout=0.1) db2.close() - try: - db3 = queue.get(1, 1) - except TypeError: - db3 = queue.get(1) + db3 = queue.get(timeout=1) assert db1 != db3 assert db1._con != db3._con diff --git a/tests/test_steady_db.py b/tests/test_steady_db.py index c2d1f95..c734928 100644 --- a/tests/test_steady_db.py +++ b/tests/test_steady_db.py @@ -111,7 +111,7 @@ def test_broken_connection(): for i in range(3): db.close() del db - with pytest.raises(dbapi.OperationalError):# + with pytest.raises(dbapi.OperationalError): SteadyDBconnect(dbapi, database='error') db = SteadyDBconnect(dbapi, database='ok') cursor = db.cursor() @@ -724,4 +724,3 @@ def test_rollback_error(): db.rollback() assert db._con.session == ['rollback'] assert db._con.valid - diff --git a/tests/test_steady_pg.py b/tests/test_steady_pg.py index 094c7bc..ef1659d 100644 --- a/tests/test_steady_pg.py +++ b/tests/test_steady_pg.py @@ -24,6 +24,7 @@ def test_version(): assert steady_pg.__version__ == __version__ assert steady_pg.SteadyPgConnection.version == __version__ + def test_mocked_connection(): db_cls = pg.DB db = db_cls( diff --git a/tests/test_threading_local.py b/tests/test_threading_local.py index 2caf138..2c86525 100644 --- a/tests/test_threading_local.py +++ b/tests/test_threading_local.py @@ -80,4 +80,3 @@ class MyLocal(local): thread.start() thread.join() assert my_data.number == 7 - From 9e56dfca11960048d3c27a3ed95a69235b4743cd Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 29 Apr 2023 21:44:51 +0200 Subject: [PATCH 55/84] Avoid shadowing names --- tests/test_persistent_db.py | 16 ++++++++-------- tests/test_persistent_pg.py | 17 ++++++++--------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/tests/test_persistent_db.py b/tests/test_persistent_db.py index d8fb504..84d7fd6 100644 --- a/tests/test_persistent_db.py +++ b/tests/test_persistent_db.py @@ -71,32 +71,32 @@ def test_threads(dbapi): # noqa: F811 query_queue.append(Queue(1)) result_queue.append(Queue(1)) - def run_queries(i): + def run_queries(idx): this_db = persist.connection() db = None while True: try: - q = query_queue[i].get(timeout=1) + q = query_queue[idx].get(timeout=1) except Empty: q = None if not q: break db = persist.connection() if db != this_db: - r = 'error - not persistent' + res = 'error - not persistent' else: if q == 'ping': - r = 'ok - thread alive' + res = 'ok - thread alive' elif q == 'close': db.close() - r = 'ok - connection closed' + res = 'ok - connection closed' else: cursor = db.cursor() cursor.execute(q) - r = cursor.fetchone() + res = cursor.fetchone() cursor.close() - r = f'{i}({db._usage}): {r}' - result_queue[i].put(r, timeout=1) + res = f'{idx}({db._usage}): {res}' + result_queue[idx].put(res, timeout=1) if db: db.close() diff --git a/tests/test_persistent_pg.py b/tests/test_persistent_pg.py index c8d66d6..03e5acf 100644 --- a/tests/test_persistent_pg.py +++ b/tests/test_persistent_pg.py @@ -49,29 +49,29 @@ def test_threads(): query_queue.append(Queue(1)) result_queue.append(Queue(1)) - def run_queries(i): + def run_queries(idx): this_db = persist.connection().db db = None while True: try: - q = query_queue[i].get(timeout=1) + q = query_queue[idx].get(timeout=1) except Empty: q = None if not q: break db = persist.connection() if db.db != this_db: - r = 'error - not persistent' + res = 'error - not persistent' else: if q == 'ping': - r = 'ok - thread alive' + res = 'ok - thread alive' elif q == 'close': db.db.close() - r = 'ok - connection closed' + res = 'ok - connection closed' else: - r = db.query(q) - r = f'{i}({db._usage}): {r}' - result_queue[i].put(r, timeout=1) + res = db.query(q) + res = f'{idx}({db._usage}): {res}' + result_queue[idx].put(res, timeout=1) if db: db.close() @@ -160,4 +160,3 @@ def test_context_manager(): with persist.connection() as db: db.query('select test') assert db.num_queries == 1 - From 24f98dfa03f851a9c0fda7e0d76d3d98799d6a71 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 29 Apr 2023 21:54:04 +0200 Subject: [PATCH 56/84] Simplify setup.py --- setup.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/setup.py b/setup.py index e5bb435..43d4ce6 100755 --- a/setup.py +++ b/setup.py @@ -2,20 +2,10 @@ """Setup Script for DBUtils""" -import warnings - try: from setuptools import setup except ImportError: from distutils.core import setup -from sys import version_info - -py_version = version_info[:2] -if not (3, 6) <= py_version < (4, 0): - raise ImportError( - 'Python {}.{} is not supported by DBUtils.'.format(*py_version)) - -warnings.filterwarnings('ignore', 'Unknown distribution option') __version__ = '3.1.0b1' @@ -60,6 +50,7 @@ platforms=['any'], license='MIT License', packages=['dbutils'], + python_requires='>3.7', extras_require={ "pg": ["PyGreSQL>=5"] }, From de277e264f7c40106bd3b40221818ee060014461 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 29 Apr 2023 22:09:03 +0200 Subject: [PATCH 57/84] Use Queue nowait methods for better readability --- dbutils/pooled_pg.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dbutils/pooled_pg.py b/dbutils/pooled_pg.py index e2389c7..b3752df 100644 --- a/dbutils/pooled_pg.py +++ b/dbutils/pooled_pg.py @@ -206,7 +206,7 @@ def connection(self): if not self._connections.acquire(self._blocking): raise TooManyConnections try: - con = self._cache.get(0) + con = self._cache.get_nowait() except Empty: con = self.steady_connection() return PooledPgConnection(self, con) @@ -222,7 +222,7 @@ def cache(self, con): con.rollback() # rollback a possible transaction except Exception: pass - self._cache.put(con, 0) # and then put it back into the cache + self._cache.put_nowait(con) # and then put it back into the cache except Full: con.close() if self._connections: @@ -232,7 +232,7 @@ def close(self): """Close all connections in the pool.""" while 1: try: - con = self._cache.get(0) + con = self._cache.get_nowait() try: con.close() except Exception: From 45f40b9772e9266c40891533fb777246c23db305 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 29 Apr 2023 22:13:23 +0200 Subject: [PATCH 58/84] Some grammar fixes, use American spelling --- dbutils/pooled_db.py | 8 ++++---- dbutils/pooled_pg.py | 8 ++++---- dbutils/simple_pooled_db.py | 2 +- dbutils/simple_pooled_pg.py | 2 +- dbutils/steady_db.py | 4 ++-- dbutils/steady_pg.py | 2 +- docs/main.html | 4 ++-- docs/main.rst | 4 ++-- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/dbutils/pooled_db.py b/dbutils/pooled_db.py index fb39ab4..163ad9d 100644 --- a/dbutils/pooled_db.py +++ b/dbutils/pooled_db.py @@ -97,7 +97,7 @@ db = pool.dedicated_connection() -If you don't need it any more, you should immediately return it to the +If you don't need it anymore, you should immediately return it to the pool with db.close(). You can get another connection in the same way. Warning: In a threaded environment, never do the following: @@ -386,7 +386,7 @@ def __del__(self): """Delete the pool.""" try: self.close() - except: # builtin Exceptions might not exist any more + except: # builtin Exceptions might not exist anymore pass def _wait_lock(self): @@ -433,7 +433,7 @@ def __del__(self): """Delete the pooled connection.""" try: self.close() - except: # builtin Exceptions might not exist any more + except: # builtin Exceptions might not exist anymore pass def __enter__(self): @@ -525,7 +525,7 @@ def __del__(self): """Delete the pooled connection.""" try: self.close() - except: # builtin Exceptions might not exist any more + except: # builtin Exceptions might not exist anymore pass def __enter__(self): diff --git a/dbutils/pooled_pg.py b/dbutils/pooled_pg.py index b3752df..767a032 100644 --- a/dbutils/pooled_pg.py +++ b/dbutils/pooled_pg.py @@ -68,7 +68,7 @@ proxy class for the hardened SteadyPg version of the connection. The connection will not be shared with other threads. If you don't need -it any more, you should immediately return it to the pool with db.close(). +it anymore, you should immediately return it to the pool with db.close(). You can get another connection in the same way or with db.reopen(). Warning: In a threaded environment, never do the following: @@ -246,7 +246,7 @@ def __del__(self): """Delete the pool.""" try: self.close() - except: # builtin Exceptions might not exist any more + except: # builtin Exceptions might not exist anymore pass @@ -267,7 +267,7 @@ def __init__(self, pool, con): def close(self): """Close the pooled connection.""" # Instead of actually closing the connection, - # return it to the pool so it can be reused. + # return it to the pool so that it can be reused. if self._con: self._pool.cache(self._con) self._con = None @@ -292,7 +292,7 @@ def __del__(self): """Delete the pooled connection.""" try: self.close() - except: # builtin Exceptions might not exist any more + except: # builtin Exceptions might not exist anymore pass def __enter__(self): diff --git a/dbutils/simple_pooled_db.py b/dbutils/simple_pooled_db.py index 606e188..9a37a7b 100644 --- a/dbutils/simple_pooled_db.py +++ b/dbutils/simple_pooled_db.py @@ -97,7 +97,7 @@ def __init__(self, pool, con): def close(self): """Close the pooled connection.""" # Instead of actually closing the connection, - # return it to the pool so it can be reused. + # return it to the pool so that it can be reused. if self._con is not None: self._pool.returnConnection(self._con) self._con = None diff --git a/dbutils/simple_pooled_pg.py b/dbutils/simple_pooled_pg.py index b18ad7b..94dfed7 100644 --- a/dbutils/simple_pooled_pg.py +++ b/dbutils/simple_pooled_pg.py @@ -86,7 +86,7 @@ def __init__(self, pool, con): def close(self): """Close the pooled connection.""" # Instead of actually closing the connection, - # return it to the pool so it can be reused. + # return it to the pool so that it can be reused. if self._con is not None: self._pool.cache(self._con) self._con = None diff --git a/dbutils/steady_db.py b/dbutils/steady_db.py index 15ddd6d..667611e 100644 --- a/dbutils/steady_db.py +++ b/dbutils/steady_db.py @@ -517,7 +517,7 @@ def __del__(self): """Delete the steady connection.""" try: self._close() # make sure the connection is closed - except: # builtin Exceptions might not exist any more + except: # builtin Exceptions might not exist anymore pass @@ -708,5 +708,5 @@ def __del__(self): """Delete the steady cursor.""" try: self.close() # make sure the cursor is closed - except: # builtin Exceptions might not exist any more + except: # builtin Exceptions might not exist anymore pass diff --git a/dbutils/steady_pg.py b/dbutils/steady_pg.py index a553078..6ccefe1 100644 --- a/dbutils/steady_pg.py +++ b/dbutils/steady_pg.py @@ -305,5 +305,5 @@ def __del__(self): """Delete the steady connection.""" try: self._close() # make sure the connection is closed - except: # builtin Exceptions might not exist any more + except: # builtin Exceptions might not exist anymore pass diff --git a/docs/main.html b/docs/main.html index c77a83b..50c9ffd 100644 --- a/docs/main.html +++ b/docs/main.html @@ -213,7 +213,7 @@ <h3>PooledDB (pooled_db)</h3> at least <span class="docutils literal">mincached</span> and at the most <span class="docutils literal">maxcached</span> idle connections that will be used whenever a thread is requesting a dedicated database connection or the pool of shared connections is not yet full. When a thread closes a -connection that is not shared any more, it is returned back to the pool of +connection that is not shared anymore, it is returned back to the pool of idle connections so that it can be recycled again.</p> <p>If the underlying DB-API module is not thread-safe, thread locks will be used to ensure that the <span class="docutils literal">pooled_db</span> connections are thread-safe. So you @@ -372,7 +372,7 @@ <h3>PooledDB (pooled_db)</h3> <pre class="literal-block">db = pool.connection(shareable=False)</pre> <p>Instead of this, you can also get a dedicated connection as follows:</p> <pre class="literal-block">db = pool.dedicated_connection()</pre> -<p>If you don't need it any more, you should immediately return it to the +<p>If you don't need it anymore, you should immediately return it to the pool with <span class="docutils literal">db.close()</span>. You can get another connection in the same way.</p> <p><em>Warning:</em> In a threaded environment, never do the following:</p> <pre class="literal-block">pool.connection().cursor().execute(...)</pre> diff --git a/docs/main.rst b/docs/main.rst index 4793d30..fa574b7 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -189,7 +189,7 @@ Besides the pool of shared connections, you can also set up a pool of at least ``mincached`` and at the most ``maxcached`` idle connections that will be used whenever a thread is requesting a dedicated database connection or the pool of shared connections is not yet full. When a thread closes a -connection that is not shared any more, it is returned back to the pool of +connection that is not shared anymore, it is returned back to the pool of idle connections so that it can be recycled again. If the underlying DB-API module is not thread-safe, thread locks will be @@ -389,7 +389,7 @@ Instead of this, you can also get a dedicated connection as follows:: db = pool.dedicated_connection() -If you don't need it any more, you should immediately return it to the +If you don't need it anymore, you should immediately return it to the pool with ``db.close()``. You can get another connection in the same way. *Warning:* In a threaded environment, never do the following:: From 21a9b33aafe413f6e97e027db6d12684c66f218f Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 29 Apr 2023 22:20:20 +0200 Subject: [PATCH 59/84] Simplify control flow in mock_pg --- pyproject.toml | 1 - tests/mock_pg.py | 19 ++++++++----------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ce67283..d523c13 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,4 +77,3 @@ max-statements = 95 [tool.ruff.per-file-ignores] "docs/make.py" = ["INP001"] "tests/*" = ["B023", "I001", "N8", "PGH004", "PLR0915", "PT009", "S101"] -"tests/mock_pg.py" = ["RET505"] diff --git a/tests/mock_pg.py b/tests/mock_pg.py index bbb393c..380215e 100644 --- a/tests/mock_pg.py +++ b/tests/mock_pg.py @@ -61,14 +61,13 @@ def query(self, qstr): if qstr in ('begin', 'end', 'commit', 'rollback'): self.session.append(qstr) return None - elif qstr.startswith('select '): + if qstr.startswith('select '): self.num_queries += 1 return qstr[7:] - elif qstr.startswith('set '): + if qstr.startswith('set '): self.session.append(qstr[4:]) return None - else: - raise ProgrammingError + raise ProgrammingError class DB: @@ -80,17 +79,15 @@ def __init__(self, *args, **kw): self.__args = args, kw def __getattr__(self, name): - if self.db: - return getattr(self.db, name) - else: + if not self.db: raise AttributeError + return getattr(self.db, name) def close(self): - if self.db: - self.db.close() - self.db = None - else: + if not self.db: raise InternalError + self.db.close() + self.db = None def reopen(self): if self.db: From c60205d91cffe4d3bae191dbe47101acf8189771 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 29 Apr 2023 22:26:50 +0200 Subject: [PATCH 60/84] Fix import sorting in tests --- pyproject.toml | 5 +++-- tests/mock_pg.py | 1 - tests/test_persistent_db.py | 6 +++--- tests/test_persistent_pg.py | 2 +- tests/test_pooled_db.py | 14 +++++++++----- tests/test_pooled_pg.py | 4 ++-- tests/test_simple_pooled_db.py | 6 +++--- tests/test_simple_pooled_pg.py | 2 +- tests/test_steady_db.py | 6 +++--- 9 files changed, 25 insertions(+), 21 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d523c13..ac2345b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ select = [ # "T20", # flake8-print # "TRY", # tryceratops ] -# When removing rules from the `ignore` list, do `ruff rule ARG002` to see the docs +# You can use `ruff rule ...` to see what these ignored rules check ignore = [ "ARG002", "B007", @@ -75,5 +75,6 @@ max-branches = 41 max-statements = 95 [tool.ruff.per-file-ignores] +# You can use `ruff rule ...` to see what these ignored rules check "docs/make.py" = ["INP001"] -"tests/*" = ["B023", "I001", "N8", "PGH004", "PLR0915", "PT009", "S101"] +"tests/*" = ["N8", "PGH004", "PLR0915", "PT009", "S101"] diff --git a/tests/mock_pg.py b/tests/mock_pg.py index 380215e..b341bea 100644 --- a/tests/mock_pg.py +++ b/tests/mock_pg.py @@ -2,7 +2,6 @@ import sys - sys.modules['pg'] = sys.modules[__name__] diff --git a/tests/test_persistent_db.py b/tests/test_persistent_db.py index 84d7fd6..2f11b7c 100644 --- a/tests/test_persistent_db.py +++ b/tests/test_persistent_db.py @@ -10,15 +10,15 @@ * This test was contributed by Christoph Zwerschke """ -from queue import Queue, Empty +from queue import Empty, Queue from threading import Thread import pytest -from .mock_db import dbapi # noqa: F401 - from dbutils.persistent_db import NotSupportedError, PersistentDB, local +from .mock_db import dbapi # noqa: F401 + def test_version(): from dbutils import __version__, persistent_db diff --git a/tests/test_persistent_pg.py b/tests/test_persistent_pg.py index 03e5acf..de866c3 100644 --- a/tests/test_persistent_pg.py +++ b/tests/test_persistent_pg.py @@ -10,7 +10,7 @@ * This test was contributed by Christoph Zwerschke """ -from queue import Queue, Empty +from queue import Empty, Queue from threading import Thread import pg diff --git a/tests/test_pooled_db.py b/tests/test_pooled_db.py index 8bbd866..453c6e2 100644 --- a/tests/test_pooled_db.py +++ b/tests/test_pooled_db.py @@ -10,18 +10,22 @@ * This test was contributed by Christoph Zwerschke """ -from queue import Queue, Empty +from queue import Empty, Queue from threading import Thread import pytest -from .mock_db import dbapi # noqa: F401 - from dbutils.pooled_db import ( - NotSupportedError, PooledDB, SharedDBConnection, - InvalidConnection, TooManyConnections) + InvalidConnection, + NotSupportedError, + PooledDB, + SharedDBConnection, + TooManyConnections, +) from dbutils.steady_db import SteadyDBConnection +from .mock_db import dbapi # noqa: F401 + def test_version(): from dbutils import __version__, pooled_db diff --git a/tests/test_pooled_pg.py b/tests/test_pooled_pg.py index 93c266c..8b25259 100644 --- a/tests/test_pooled_pg.py +++ b/tests/test_pooled_pg.py @@ -10,13 +10,13 @@ * This test was contributed by Christoph Zwerschke """ -from queue import Queue, Empty +from queue import Empty, Queue from threading import Thread import pg # noqa: F401 import pytest -from dbutils.pooled_pg import PooledPg, InvalidConnection, TooManyConnections +from dbutils.pooled_pg import InvalidConnection, PooledPg, TooManyConnections from dbutils.steady_pg import SteadyPgConnection diff --git a/tests/test_simple_pooled_db.py b/tests/test_simple_pooled_db.py index f112d64..b0bff30 100644 --- a/tests/test_simple_pooled_db.py +++ b/tests/test_simple_pooled_db.py @@ -11,15 +11,15 @@ * This test was contributed by Christoph Zwerschke """ -from queue import Queue, Empty +from queue import Empty, Queue from threading import Thread import pytest -from . import mock_db as dbapi - from dbutils import simple_pooled_db +from . import mock_db as dbapi + def my_db_pool(threadsafety, max_connections): """Get simple PooledDB connection.""" diff --git a/tests/test_simple_pooled_pg.py b/tests/test_simple_pooled_pg.py index 1492573..c0856cc 100644 --- a/tests/test_simple_pooled_pg.py +++ b/tests/test_simple_pooled_pg.py @@ -10,7 +10,7 @@ * This test was contributed by Christoph Zwerschke """ -from queue import Queue, Empty +from queue import Empty, Queue from threading import Thread import pg # noqa: F401 diff --git a/tests/test_steady_db.py b/tests/test_steady_db.py index c734928..05e7f2e 100644 --- a/tests/test_steady_db.py +++ b/tests/test_steady_db.py @@ -11,10 +11,10 @@ import pytest -from . import mock_db as dbapi +from dbutils.steady_db import SteadyDBConnection, SteadyDBCursor +from dbutils.steady_db import connect as SteadyDBconnect -from dbutils.steady_db import ( - connect as SteadyDBconnect, SteadyDBConnection, SteadyDBCursor) +from . import mock_db as dbapi def test_version(): From d547aef1ea6c3d1f5f277eb4838c45578f591ac0 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 29 Apr 2023 22:39:13 +0200 Subject: [PATCH 61/84] Remove some ignored rules --- docs/make.py | 2 +- pyproject.toml | 9 +++-- tests/test_pooled_db.py | 4 +-- tests/test_steady_db.py | 76 ++++++++++++++++++++--------------------- 4 files changed, 47 insertions(+), 44 deletions(-) diff --git a/docs/make.py b/docs/make.py index f6736b7..ef88183 100755 --- a/docs/make.py +++ b/docs/make.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3.9 +#!/usr/bin/python3.10 """Build HTML from reST files.""" diff --git a/pyproject.toml b/pyproject.toml index ac2345b..f3aa784 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,6 +75,9 @@ max-branches = 41 max-statements = 95 [tool.ruff.per-file-ignores] -# You can use `ruff rule ...` to see what these ignored rules check -"docs/make.py" = ["INP001"] -"tests/*" = ["N8", "PGH004", "PLR0915", "PT009", "S101"] +"docs/*" = [ + "INP001" # allow stand-alone scripts +] +"tests/*" = [ + "S101" # allow assert statements +] diff --git a/tests/test_pooled_db.py b/tests/test_pooled_db.py index 453c6e2..49f8ac4 100644 --- a/tests/test_pooled_db.py +++ b/tests/test_pooled_db.py @@ -54,7 +54,7 @@ def test_threadsafety(dbapi, threadsafety): # noqa: F811 @pytest.mark.parametrize("threadsafety", [1, 2]) -def test_create_connection(dbapi, threadsafety): # noqa: F811 +def test_create_connection(dbapi, threadsafety): # noqa: F811, PLR0915 dbapi.threadsafety = threadsafety shareable = threadsafety > 1 pool = PooledDB( @@ -675,7 +675,7 @@ def test_rollback(dbapi, threadsafety): # noqa: F811 @pytest.mark.parametrize("threadsafety", [1, 2]) -def test_maxconnections(dbapi, threadsafety): # noqa: F811 +def test_maxconnections(dbapi, threadsafety): # noqa: F811, PLR0915 dbapi.threadsafety = threadsafety shareable = threadsafety > 1 pool = PooledDB(dbapi, 1, 2, 2, 3) diff --git a/tests/test_steady_db.py b/tests/test_steady_db.py index 05e7f2e..dca75af 100644 --- a/tests/test_steady_db.py +++ b/tests/test_steady_db.py @@ -12,7 +12,7 @@ import pytest from dbutils.steady_db import SteadyDBConnection, SteadyDBCursor -from dbutils.steady_db import connect as SteadyDBconnect +from dbutils.steady_db import connect as steady_db_connect from . import mock_db as dbapi @@ -107,13 +107,13 @@ def test_broken_connection(): SteadyDBConnection(None) with pytest.raises(TypeError): SteadyDBCursor(None) - db = SteadyDBconnect(dbapi, database='ok') + db = steady_db_connect(dbapi, database='ok') for i in range(3): db.close() del db with pytest.raises(dbapi.OperationalError): - SteadyDBconnect(dbapi, database='error') - db = SteadyDBconnect(dbapi, database='ok') + steady_db_connect(dbapi, database='error') + db = steady_db_connect(dbapi, database='ok') cursor = db.cursor() for i in range(3): cursor.close() @@ -126,7 +126,7 @@ def test_broken_connection(): @pytest.mark.parametrize("closeable", [False, True]) def test_close(closeable): - db = SteadyDBconnect(dbapi, closeable=closeable) + db = steady_db_connect(dbapi, closeable=closeable) assert db._con.valid db.close() assert closeable ^ db._con.valid @@ -138,8 +138,8 @@ def test_close(closeable): assert not db._con.valid -def test_connection(): - db = SteadyDBconnect( +def test_connection(): # noqa: PLR0915 + db = steady_db_connect( dbapi, 0, None, None, None, True, 'SteadyDBTestDB', user='SteadyDBTestUser') assert isinstance(db, SteadyDBConnection) @@ -259,7 +259,7 @@ def test_connection(): def test_connection_context_handler(): - db = SteadyDBconnect( + db = steady_db_connect( dbapi, 0, None, None, None, True, 'SteadyDBTestDB', user='SteadyDBTestUser') assert db._con.session == [] @@ -278,7 +278,7 @@ def test_connection_context_handler(): def test_cursor_context_handler(): - db = SteadyDBconnect( + db = steady_db_connect( dbapi, 0, None, None, None, True, 'SteadyDBTestDB', user='SteadyDBTestUser') assert db._con.open_cursors == 0 @@ -290,7 +290,7 @@ def test_cursor_context_handler(): def test_cursor_as_iterator_provided(): - db = SteadyDBconnect( + db = steady_db_connect( dbapi, 0, None, None, None, True, 'SteadyDBTestDB', user='SteadyDBTestUser') assert db._con.open_cursors == 0 @@ -309,7 +309,7 @@ def test_cursor_as_iterator_provided(): def test_cursor_as_iterator_created(): - db = SteadyDBconnect( + db = steady_db_connect( dbapi, 0, None, None, None, True, 'SteadyDBTestDB', user='SteadyDBTestUser') assert db._con.open_cursors == 0 @@ -322,10 +322,10 @@ def test_cursor_as_iterator_created(): def test_connection_creator_function(): - db1 = SteadyDBconnect( + db1 = steady_db_connect( dbapi, 0, None, None, None, True, 'SteadyDBTestDB', user='SteadyDBTestUser') - db2 = SteadyDBconnect( + db2 = steady_db_connect( dbapi.connect, 0, None, None, None, True, 'SteadyDBTestDB', user='SteadyDBTestUser') assert db1.dbapi() == db2.dbapi() @@ -338,7 +338,7 @@ def test_connection_creator_function(): def test_connection_maxusage(): - db = SteadyDBconnect(dbapi, 10) + db = steady_db_connect(dbapi, 10) cursor = db.cursor() for i in range(100): cursor.execute(f'select test{i}') @@ -389,7 +389,7 @@ def test_connection_maxusage(): def test_connection_setsession(): - db = SteadyDBconnect(dbapi, 3, ('set time zone', 'set datestyle')) + db = steady_db_connect(dbapi, 3, ('set time zone', 'set datestyle')) assert hasattr(db, '_usage') assert db._usage == 0 assert hasattr(db._con, 'open_cursors') @@ -448,29 +448,29 @@ def test_connection_setsession(): def test_connection_failures(): - db = SteadyDBconnect(dbapi) + db = steady_db_connect(dbapi) db.close() db.cursor() - db = SteadyDBconnect(dbapi, failures=dbapi.InternalError) + db = steady_db_connect(dbapi, failures=dbapi.InternalError) db.close() db.cursor() - db = SteadyDBconnect(dbapi, failures=dbapi.OperationalError) + db = steady_db_connect(dbapi, failures=dbapi.OperationalError) db.close() with pytest.raises(dbapi.InternalError): db.cursor() - db = SteadyDBconnect(dbapi, failures=( + db = steady_db_connect(dbapi, failures=( dbapi.OperationalError, dbapi.InterfaceError)) db.close() with pytest.raises(dbapi.InternalError): db.cursor() - db = SteadyDBconnect(dbapi, failures=( + db = steady_db_connect(dbapi, failures=( dbapi.OperationalError, dbapi.InterfaceError, dbapi.InternalError)) db.close() db.cursor() def test_connection_failure_error(): - db = SteadyDBconnect(dbapi) + db = steady_db_connect(dbapi) cursor = db.cursor() db.close() cursor.execute('select test') @@ -481,7 +481,7 @@ def test_connection_failure_error(): def test_connection_set_sizes(): - db = SteadyDBconnect(dbapi) + db = steady_db_connect(dbapi) cursor = db.cursor() cursor.execute('get sizes') result = cursor.fetchone() @@ -510,7 +510,7 @@ def test_connection_ping_check(): con_cls = dbapi.Connection con_cls.has_ping = False con_cls.num_pings = 0 - db = SteadyDBconnect(dbapi) + db = steady_db_connect(dbapi) db.cursor().execute('select test') assert con_cls.num_pings == 0 db.close() @@ -518,7 +518,7 @@ def test_connection_ping_check(): assert con_cls.num_pings == 0 assert db._ping_check() is None assert con_cls.num_pings == 1 - db = SteadyDBconnect(dbapi, ping=7) + db = steady_db_connect(dbapi, ping=7) db.cursor().execute('select test') assert con_cls.num_pings == 2 db.close() @@ -527,7 +527,7 @@ def test_connection_ping_check(): assert db._ping_check() is None assert con_cls.num_pings == 2 con_cls.has_ping = True - db = SteadyDBconnect(dbapi) + db = steady_db_connect(dbapi) db.cursor().execute('select test') assert con_cls.num_pings == 2 db.close() @@ -535,7 +535,7 @@ def test_connection_ping_check(): assert con_cls.num_pings == 2 assert db._ping_check() assert con_cls.num_pings == 3 - db = SteadyDBconnect(dbapi, ping=1) + db = steady_db_connect(dbapi, ping=1) db.cursor().execute('select test') assert con_cls.num_pings == 3 db.close() @@ -546,13 +546,13 @@ def test_connection_ping_check(): db.close() assert db._ping_check() assert con_cls.num_pings == 5 - db = SteadyDBconnect(dbapi, ping=7) + db = steady_db_connect(dbapi, ping=7) db.cursor().execute('select test') assert con_cls.num_pings == 7 db.close() db.cursor().execute('select test') assert con_cls.num_pings == 9 - db = SteadyDBconnect(dbapi, ping=3) + db = steady_db_connect(dbapi, ping=3) assert con_cls.num_pings == 9 db.cursor() assert con_cls.num_pings == 10 @@ -561,7 +561,7 @@ def test_connection_ping_check(): assert con_cls.num_pings == 11 cursor.execute('select test') assert con_cls.num_pings == 11 - db = SteadyDBconnect(dbapi, ping=5) + db = steady_db_connect(dbapi, ping=5) assert con_cls.num_pings == 11 db.cursor() assert con_cls.num_pings == 11 @@ -575,7 +575,7 @@ def test_connection_ping_check(): assert con_cls.num_pings == 12 cursor.execute('select test') assert con_cls.num_pings == 13 - db = SteadyDBconnect(dbapi, ping=7) + db = steady_db_connect(dbapi, ping=7) assert con_cls.num_pings == 13 db.cursor() assert con_cls.num_pings == 14 @@ -597,7 +597,7 @@ def test_connection_ping_check(): def test_begin_transaction(): - db = SteadyDBconnect(dbapi, database='ok') + db = steady_db_connect(dbapi, database='ok') cursor = db.cursor() cursor.close() cursor.execute('select test12') @@ -624,7 +624,7 @@ def test_begin_transaction(): def test_with_begin_extension(): - db = SteadyDBconnect(dbapi, database='ok') + db = steady_db_connect(dbapi, database='ok') db._con._begin_called_with = None def begin(a, b=None, c=7): @@ -639,7 +639,7 @@ def begin(a, b=None, c=7): def test_cancel_transaction(): - db = SteadyDBconnect(dbapi, database='ok') + db = steady_db_connect(dbapi, database='ok') cursor = db.cursor() db.begin() cursor.execute('select test14') @@ -650,7 +650,7 @@ def test_cancel_transaction(): def test_with_cancel_extension(): - db = SteadyDBconnect(dbapi, database='ok') + db = steady_db_connect(dbapi, database='ok') db._con._cancel_called = None def cancel(): @@ -666,12 +666,12 @@ def cancel(): def test_reset_transaction(): - db = SteadyDBconnect(dbapi, database='ok') + db = steady_db_connect(dbapi, database='ok') db.begin() assert not db._con.session db.close() assert not db._con.session - db = SteadyDBconnect(dbapi, database='ok', closeable=False) + db = steady_db_connect(dbapi, database='ok', closeable=False) db.begin() assert not db._con.session db.close() @@ -679,7 +679,7 @@ def test_reset_transaction(): def test_commit_error(): - db = SteadyDBconnect(dbapi, database='ok') + db = steady_db_connect(dbapi, database='ok') db.begin() assert not db._con.session assert db._con.valid @@ -703,7 +703,7 @@ def test_commit_error(): def test_rollback_error(): - db = SteadyDBconnect(dbapi, database='ok') + db = steady_db_connect(dbapi, database='ok') db.begin() assert not db._con.session assert db._con.valid From 464afdc3f7eb53d30cadb20383ee3fd515a4d43b Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 29 Apr 2023 22:56:48 +0200 Subject: [PATCH 62/84] Check that no magic values are used --- dbutils/pooled_pg.py | 8 ++++++-- pyproject.toml | 14 +++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/dbutils/pooled_pg.py b/dbutils/pooled_pg.py index 767a032..4b3a251 100644 --- a/dbutils/pooled_pg.py +++ b/dbutils/pooled_pg.py @@ -117,6 +117,10 @@ from . import __version__ from .steady_pg import SteadyPgConnection +# constants for "reset" parameter +RESET_ALWAYS_ROLLBACK = 1 +RESET_COMPLETELY = 2 + class PooledPgError(Exception): """General PooledPg error.""" @@ -214,10 +218,10 @@ def connection(self): def cache(self, con): """Put a connection back into the pool cache.""" try: - if self._reset == 2: + if self._reset == RESET_COMPLETELY: con.reset() # reset the connection completely else: - if self._reset or con._transaction: + if self._reset == RESET_ALWAYS_ROLLBACK or con._transaction: try: con.rollback() # rollback a possible transaction except Exception: diff --git a/pyproject.toml b/pyproject.toml index f3aa784..dedf4ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,18 +66,18 @@ line-length = 79 target-version = "py37" [tool.ruff.mccabe] -max-complexity = 31 +max-complexity = 32 [tool.ruff.pylint] -allow-magic-value-types = ["int", "str"] -max-args = 12 -max-branches = 41 -max-statements = 95 +max-args = 15 +max-branches = 50 +max-statements = 100 [tool.ruff.per-file-ignores] "docs/*" = [ - "INP001" # allow stand-alone scripts + "INP001", # allow stand-alone scripts ] "tests/*" = [ - "S101" # allow assert statements + "PLR2004", # allow magic values + "S101", # allow assert statements ] From 23d54b8a992489db482713d1e714ec80d422af60 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 29 Apr 2023 22:59:51 +0200 Subject: [PATCH 63/84] Enforce check for unused arguments --- dbutils/persistent_db.py | 2 +- pyproject.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/dbutils/persistent_db.py b/dbutils/persistent_db.py index 9082488..54ddfcc 100644 --- a/dbutils/persistent_db.py +++ b/dbutils/persistent_db.py @@ -201,7 +201,7 @@ def steady_connection(self): self._failures, self._ping, self._closeable, *self._args, **self._kwargs) - def connection(self, shareable=False): + def connection(self, shareable=False): # noqa: ARG002 """Get a steady, persistent DB-API 2 connection. The shareable parameter exists only for compatibility with the diff --git a/pyproject.toml b/pyproject.toml index dedf4ba..434bb27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,6 @@ select = [ ] # You can use `ruff rule ...` to see what these ignored rules check ignore = [ - "ARG002", "B007", "B904", "E722", From d67f5abf95cd4c0dae5f31ec6fdfa329371d4718 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 29 Apr 2023 23:08:53 +0200 Subject: [PATCH 64/84] Check that variables are used inside loops --- dbutils/simple_pooled_db.py | 2 +- dbutils/simple_pooled_pg.py | 2 +- pyproject.toml | 1 - tests/test_persistent_db.py | 4 +- tests/test_persistent_pg.py | 4 +- tests/test_pooled_db.py | 81 +++++++++++++++++----------------- tests/test_pooled_pg.py | 24 +++++----- tests/test_simple_pooled_db.py | 2 +- tests/test_simple_pooled_pg.py | 6 +-- tests/test_steady_db.py | 16 +++---- tests/test_steady_pg.py | 4 +- 11 files changed, 73 insertions(+), 73 deletions(-) diff --git a/dbutils/simple_pooled_db.py b/dbutils/simple_pooled_db.py index 9a37a7b..d95f5d2 100644 --- a/dbutils/simple_pooled_db.py +++ b/dbutils/simple_pooled_db.py @@ -159,7 +159,7 @@ def __init__(self, dbapi, maxconnections, *args, **kwargs): "Database module threading support cannot be determined.") # Establish all database connections (it would be better to # only establish a part of them now, and the rest on demand). - for i in range(maxconnections): + for _i in range(maxconnections): self.addConnection(dbapi.connect(*args, **kwargs)) # The following functions are used with DB-API 2 modules diff --git a/dbutils/simple_pooled_pg.py b/dbutils/simple_pooled_pg.py index 94dfed7..fe90d57 100644 --- a/dbutils/simple_pooled_pg.py +++ b/dbutils/simple_pooled_pg.py @@ -122,7 +122,7 @@ def __init__(self, maxconnections, *args, **kwargs): self._queue = Queue(maxconnections) # Establish all database connections (it would be better to # only establish a part of them now, and the rest on demand). - for i in range(maxconnections): + for _i in range(maxconnections): self.cache(PgConnection(*args, **kwargs)) def cache(self, con): diff --git a/pyproject.toml b/pyproject.toml index 434bb27..e9a5f0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,6 @@ select = [ ] # You can use `ruff rule ...` to see what these ignored rules check ignore = [ - "B007", "B904", "E722", "N811", diff --git a/tests/test_persistent_db.py b/tests/test_persistent_db.py index 2f11b7c..cd35127 100644 --- a/tests/test_persistent_db.py +++ b/tests/test_persistent_db.py @@ -67,7 +67,7 @@ def test_threads(dbapi): # noqa: F811 num_threads = 3 persist = PersistentDB(dbapi, closeable=True) query_queue, result_queue = [], [] - for i in range(num_threads): + for _i in range(num_threads): query_queue.append(Queue(1)) result_queue.append(Queue(1)) @@ -164,7 +164,7 @@ def test_setsession(dbapi): # noqa: F811 cursor.execute('set test') cursor.fetchone() cursor.close() - for i in range(3): + for _i in range(3): assert db._con.session == ['datestyle', 'test'] cursor = db.cursor() cursor.execute('select test') diff --git a/tests/test_persistent_pg.py b/tests/test_persistent_pg.py index de866c3..6e1a258 100644 --- a/tests/test_persistent_pg.py +++ b/tests/test_persistent_pg.py @@ -45,7 +45,7 @@ def test_threads(): num_threads = 3 persist = PersistentPg() query_queue, result_queue = [], [] - for i in range(num_threads): + for _i in range(num_threads): query_queue.append(Queue(1)) result_queue.append(Queue(1)) @@ -132,7 +132,7 @@ def test_setsession(): assert db._setsession_sql == ('set datestyle',) assert db.db.session == ['datestyle'] db.query('set test') - for i in range(3): + for _i in range(3): assert db.db.session == ['datestyle', 'test'] db.query('select test') assert db.db.session == ['datestyle'] diff --git a/tests/test_pooled_db.py b/tests/test_pooled_db.py index 49f8ac4..1b16322 100644 --- a/tests/test_pooled_db.py +++ b/tests/test_pooled_db.py @@ -275,7 +275,7 @@ def close(what=closed): if shareable: assert len(pool._shared_cache) == 0 cache = [] - for i in range(5): + for _i in range(5): cache.append(pool.connection()) assert len(pool._idle_cache) == 5 if shareable: @@ -297,7 +297,7 @@ def close_shared(what=closed): if shareable: cache = [] - for i in range(5): + for _i in range(5): cache.append(pool.connection()) pool._shared_cache[3].con.close = close_shared else: @@ -407,63 +407,64 @@ def test_min_max_cached(dbapi, threadsafety): # noqa: F811 shareable = threadsafety > 1 pool = PooledDB(dbapi, 3) assert len(pool._idle_cache) == 3 - cache = [pool.connection() for i in range(3)] + cache = [pool.connection() for _i in range(3)] assert len(pool._idle_cache) == 0 assert cache del cache assert len(pool._idle_cache) == 3 - cache = [pool.connection() for i in range(6)] + cache = [pool.connection() for _i in range(6)] assert len(pool._idle_cache) == 0 assert cache del cache assert len(pool._idle_cache) == 6 pool = PooledDB(dbapi, 0, 3) assert len(pool._idle_cache) == 0 - cache = [pool.connection() for i in range(3)] + cache = [pool.connection() for _i in range(3)] assert len(pool._idle_cache) == 0 assert cache del cache assert len(pool._idle_cache) == 3 - cache = [pool.connection() for i in range(6)] + cache = [pool.connection() for _i in range(6)] assert len(pool._idle_cache) == 0 assert cache del cache assert len(pool._idle_cache) == 3 pool = PooledDB(dbapi, 3, 3) assert len(pool._idle_cache) == 3 - cache = [pool.connection() for i in range(3)] + cache = [pool.connection() for _i in range(3)] assert len(pool._idle_cache) == 0 assert cache del cache assert len(pool._idle_cache) == 3 - cache = [pool.connection() for i in range(6)] + cache = [pool.connection() for _i in range(6)] assert len(pool._idle_cache) == 0 assert cache del cache assert len(pool._idle_cache) == 3 pool = PooledDB(dbapi, 3, 2) assert len(pool._idle_cache) == 3 - cache = [pool.connection() for i in range(4)] + cache = [pool.connection() for _i in range(4)] assert len(pool._idle_cache) == 0 assert cache del cache assert len(pool._idle_cache) == 3 pool = PooledDB(dbapi, 2, 5) assert len(pool._idle_cache) == 2 - cache = [pool.connection() for i in range(10)] + cache = [pool.connection() for _i in range(10)] assert len(pool._idle_cache) == 0 assert cache del cache assert len(pool._idle_cache) == 5 pool = PooledDB(dbapi, 1, 2, 3) assert len(pool._idle_cache) == 1 - cache = [pool.connection(False) for i in range(4)] + cache = [pool.connection(False) for _i in range(4)] assert len(pool._idle_cache) == 0 if shareable: assert len(pool._shared_cache) == 0 + assert cache del cache assert len(pool._idle_cache) == 2 - cache = [pool.connection() for i in range(10)] + cache = [pool.connection() for _i in range(10)] assert len(pool._idle_cache) == 0 if shareable: assert len(pool._shared_cache) == 3 @@ -474,14 +475,14 @@ def test_min_max_cached(dbapi, threadsafety): # noqa: F811 assert len(pool._shared_cache) == 0 pool = PooledDB(dbapi, 1, 3, 2) assert len(pool._idle_cache) == 1 - cache = [pool.connection(False) for i in range(4)] + cache = [pool.connection(False) for _i in range(4)] assert len(pool._idle_cache) == 0 if shareable: assert len(pool._shared_cache) == 0 assert cache del cache assert len(pool._idle_cache) == 3 - cache = [pool.connection() for i in range(10)] + cache = [pool.connection() for _i in range(10)] if shareable: assert len(pool._idle_cache) == 1 assert len(pool._shared_cache) == 2 @@ -500,34 +501,34 @@ def test_max_shared(dbapi, threadsafety): # noqa: F811 shareable = threadsafety > 1 pool = PooledDB(dbapi) assert len(pool._idle_cache) == 0 - cache = [pool.connection() for i in range(10)] + cache = [pool.connection() for _i in range(10)] assert len(cache) == 10 assert len(pool._idle_cache) == 0 pool = PooledDB(dbapi, 1, 1, 0) assert len(pool._idle_cache) == 1 - cache = [pool.connection() for i in range(10)] + cache = [pool.connection() for _i in range(10)] assert len(cache) == 10 assert len(pool._idle_cache) == 0 pool = PooledDB(dbapi, 0, 0, 1) - cache = [pool.connection() for i in range(10)] + cache = [pool.connection() for _i in range(10)] assert len(cache) == 10 assert len(pool._idle_cache) == 0 if shareable: assert len(pool._shared_cache) == 1 pool = PooledDB(dbapi, 1, 1, 1) assert len(pool._idle_cache) == 1 - cache = [pool.connection() for i in range(10)] + cache = [pool.connection() for _i in range(10)] assert len(cache) == 10 assert len(pool._idle_cache) == 0 if shareable: assert len(pool._shared_cache) == 1 pool = PooledDB(dbapi, 0, 0, 7) - cache = [pool.connection(False) for i in range(3)] + cache = [pool.connection(False) for _i in range(3)] assert len(cache) == 3 assert len(pool._idle_cache) == 0 if shareable: assert len(pool._shared_cache) == 0 - cache = [pool.connection() for i in range(10)] + cache = [pool.connection() for _i in range(10)] assert len(cache) == 10 assert len(pool._idle_cache) == 3 if shareable: @@ -537,7 +538,7 @@ def test_max_shared(dbapi, threadsafety): # noqa: F811 def test_sort_shared(dbapi): # noqa: F811 pool = PooledDB(dbapi, 0, 4, 4) cache = [] - for i in range(6): + for _i in range(6): db = pool.connection() db.cursor().execute('select test') cache.append(db) @@ -559,7 +560,7 @@ def test_equally_shared(dbapi, threadsafety): # noqa: F811 shareable = threadsafety > 1 pool = PooledDB(dbapi, 5, 5, 5) assert len(pool._idle_cache) == 5 - for i in range(15): + for _i in range(15): db = pool.connection(False) db.cursor().execute('select test') db.close() @@ -569,7 +570,7 @@ def test_equally_shared(dbapi, threadsafety): # noqa: F811 assert con._usage == 3 assert con._con.num_queries == 3 cache = [] - for i in range(35): + for _i in range(35): db = pool.connection() db.cursor().execute('select test') cache.append(db) @@ -595,7 +596,7 @@ def test_many_shared(dbapi, threadsafety): # noqa: F811 shareable = threadsafety > 1 pool = PooledDB(dbapi, 0, 0, 5) cache = [] - for i in range(35): + for _i in range(35): db = pool.connection() db.cursor().execute('select test1') db.cursor().execute('select test2') @@ -622,7 +623,7 @@ def test_many_shared(dbapi, threadsafety): # noqa: F811 for db in cache: if db: db.cursor().callproc('test4') - for i in range(6): + for _i in range(6): db = pool.connection() db.cursor().callproc('test4') cache.append(db) @@ -685,7 +686,7 @@ def test_maxconnections(dbapi, threadsafety): # noqa: F811, PLR0915 assert pool._connections == 0 assert len(pool._idle_cache) == 1 cache = [] - for i in range(3): + for _i in range(3): cache.append(pool.connection(False)) assert pool._connections == 3 assert len(pool._idle_cache) == 0 @@ -700,7 +701,7 @@ def test_maxconnections(dbapi, threadsafety): # noqa: F811, PLR0915 assert len(pool._idle_cache) == 2 if shareable: assert len(pool._shared_cache) == 0 - for i in range(3): + for _i in range(3): cache.append(pool.connection()) assert len(pool._idle_cache) == 0 if shareable: @@ -794,7 +795,7 @@ def test_maxconnections(dbapi, threadsafety): # noqa: F811, PLR0915 assert pool._connections == 0 assert len(pool._idle_cache) == 4 cache = [] - for i in range(4): + for _i in range(4): cache.append(pool.connection(False)) assert pool._connections == 4 assert len(pool._idle_cache) == 0 @@ -806,7 +807,7 @@ def test_maxconnections(dbapi, threadsafety): # noqa: F811, PLR0915 assert pool._maxconnections == 4 assert pool._connections == 0 assert len(pool._idle_cache) == 1 - for i in range(4): + for _i in range(4): cache.append(pool.connection()) assert len(pool._idle_cache) == 0 if shareable: @@ -826,7 +827,7 @@ def test_maxconnections(dbapi, threadsafety): # noqa: F811, PLR0915 assert pool._maxconnections == 3 assert pool._connections == 0 cache = [] - for i in range(3): + for _i in range(3): cache.append(pool.connection(False)) assert pool._connections == 3 with pytest.raises(TooManyConnections): @@ -835,11 +836,11 @@ def test_maxconnections(dbapi, threadsafety): # noqa: F811, PLR0915 pool.connection(True) cache = [] assert pool._connections == 0 - for i in range(3): + for _i in range(3): cache.append(pool.connection()) assert pool._connections == 3 if shareable: - for i in range(3): + for _i in range(3): cache.append(pool.connection()) assert pool._connections == 3 else: @@ -851,7 +852,7 @@ def test_maxconnections(dbapi, threadsafety): # noqa: F811, PLR0915 assert pool._maxconnections == 0 assert pool._connections == 0 cache = [] - for i in range(10): + for _i in range(10): cache.append(pool.connection(False)) cache.append(pool.connection()) if shareable: @@ -967,12 +968,12 @@ def test_one_thread_two_connections(dbapi, threadsafety): # noqa: F811 shareable = threadsafety > 1 pool = PooledDB(dbapi, 2) db1 = pool.connection() - for i in range(5): + for _i in range(5): db1.cursor().execute('select test') db2 = pool.connection() assert db1 != db2 assert db1._con != db2._con - for i in range(7): + for _i in range(7): db2.cursor().execute('select test') assert db1._con._con.num_queries == 5 assert db2._con._con.num_queries == 7 @@ -980,19 +981,19 @@ def test_one_thread_two_connections(dbapi, threadsafety): # noqa: F811 db1 = pool.connection() assert db1 != db2 assert db1._con != db2._con - for i in range(3): + for _i in range(3): db1.cursor().execute('select test') assert db1._con._con.num_queries == 8 db2.cursor().execute('select test') assert db2._con._con.num_queries == 8 pool = PooledDB(dbapi, 0, 0, 2) db1 = pool.connection() - for i in range(5): + for _i in range(5): db1.cursor().execute('select test') db2 = pool.connection() assert db1 != db2 assert db1._con != db2._con - for i in range(7): + for _i in range(7): db2.cursor().execute('select test') assert db1._con._con.num_queries == 5 assert db2._con._con.num_queries == 7 @@ -1000,7 +1001,7 @@ def test_one_thread_two_connections(dbapi, threadsafety): # noqa: F811 db1 = pool.connection() assert db1 != db2 assert db1._con != db2._con - for i in range(3): + for _i in range(3): db1.cursor().execute('select test') assert db1._con._con.num_queries == 8 db2.cursor().execute('select test') @@ -1028,7 +1029,7 @@ def test_three_threads_two_connections(dbapi, threadsafety): # noqa: F811 def connection(): queue.put(pool.connection(), timeout=1) - for i in range(3): + for _i in range(3): Thread(target=connection).start() db1 = queue.get(timeout=1) db2 = queue.get(timeout=1) diff --git a/tests/test_pooled_pg.py b/tests/test_pooled_pg.py index 8b25259..cc57277 100644 --- a/tests/test_pooled_pg.py +++ b/tests/test_pooled_pg.py @@ -111,13 +111,13 @@ def test_min_max_cached(): assert pool._cache.qsize() == 3 cache = [pool.connection() for _i in range(3)] assert pool._cache.qsize() == 0 - for i in range(3): + for _i in range(3): cache.pop().close() assert pool._cache.qsize() == 3 - for i in range(6): + for _i in range(6): cache.append(pool.connection()) assert pool._cache.qsize() == 0 - for i in range(6): + for _i in range(6): cache.pop().close() assert pool._cache.qsize() == 6 pool = PooledPg(3, 4) @@ -125,13 +125,13 @@ def test_min_max_cached(): assert pool._cache.qsize() == 3 cache = [pool.connection() for _i in range(3)] assert pool._cache.qsize() == 0 - for i in range(3): + for _i in range(3): cache.pop().close() assert pool._cache.qsize() == 3 - for i in range(6): + for _i in range(6): cache.append(pool.connection()) assert pool._cache.qsize() == 0 - for i in range(6): + for _i in range(6): cache.pop().close() assert pool._cache.qsize() == 4 pool = PooledPg(3, 2) @@ -139,7 +139,7 @@ def test_min_max_cached(): assert pool._cache.qsize() == 3 cache = [pool.connection() for _i in range(4)] assert pool._cache.qsize() == 0 - for i in range(4): + for _i in range(4): cache.pop().close() assert pool._cache.qsize() == 3 pool = PooledPg(2, 5) @@ -147,7 +147,7 @@ def test_min_max_cached(): assert pool._cache.qsize() == 2 cache = [pool.connection() for _i in range(10)] assert pool._cache.qsize() == 0 - for i in range(10): + for _i in range(10): cache.pop().close() assert pool._cache.qsize() == 5 @@ -215,12 +215,12 @@ def connection(): def test_one_thread_two_connections(): pool = PooledPg(2) db1 = pool.connection() - for i in range(5): + for _i in range(5): db1.query('select test') db2 = pool.connection() assert db1 != db2 assert db1._con != db2._con - for i in range(7): + for _i in range(7): db2.query('select test') assert db1.num_queries == 5 assert db2.num_queries == 7 @@ -229,7 +229,7 @@ def test_one_thread_two_connections(): assert db1 != db2 assert db1._con != db2._con assert hasattr(db1, 'query') - for i in range(3): + for _i in range(3): db1.query('select test') assert db1.num_queries == 8 db2.query('select test') @@ -243,7 +243,7 @@ def test_three_threads_two_connections(): def connection(): queue.put(pool.connection(), timeout=1) - for i in range(3): + for _i in range(3): Thread(target=connection).start() db1 = queue.get(timeout=1) db2 = queue.get(timeout=1) diff --git a/tests/test_simple_pooled_db.py b/tests/test_simple_pooled_db.py index b0bff30..bc2cf50 100644 --- a/tests/test_simple_pooled_db.py +++ b/tests/test_simple_pooled_db.py @@ -99,7 +99,7 @@ def test_two_connections(threadsafety): db1 = db_pool.connection() assert db1 != db2 assert hasattr(db1, 'cursor') - for i in range(3): + for _i in range(3): cursors1.append(db1.cursor()) assert db1.open_cursors == 8 cursors2.append(db2.cursor()) diff --git a/tests/test_simple_pooled_pg.py b/tests/test_simple_pooled_pg.py index c0856cc..dd2b988 100644 --- a/tests/test_simple_pooled_pg.py +++ b/tests/test_simple_pooled_pg.py @@ -66,12 +66,12 @@ def test_close_connection(): def test_two_connections(): db_pool = my_db_pool(2) db1 = db_pool.connection() - for i in range(5): + for _i in range(5): db1.query('select 1') db2 = db_pool.connection() assert db1 != db2 assert db1._con != db2._con - for i in range(7): + for _i in range(7): db2.query('select 1') assert db1.num_queries == 5 assert db2.num_queries == 7 @@ -80,7 +80,7 @@ def test_two_connections(): assert db1 != db2 assert db1._con != db2._con assert hasattr(db1, 'query') - for i in range(3): + for _i in range(3): db1.query('select 1') assert db1.num_queries == 8 db2.query('select 1') diff --git a/tests/test_steady_db.py b/tests/test_steady_db.py index dca75af..e5ce07a 100644 --- a/tests/test_steady_db.py +++ b/tests/test_steady_db.py @@ -41,7 +41,7 @@ def test_mocked_connection(): assert hasattr(db, 'valid') assert db.valid assert db.open_cursors == 0 - for i in range(3): + for _i in range(3): cursor = db.cursor() assert db.open_cursors == 1 cursor.close() @@ -67,7 +67,7 @@ def test_mocked_connection(): assert cursor.fetchone() == f'test{i}' assert cursor.valid assert db.open_cursors == 1 - for i in range(4): + for _i in range(4): cursor.callproc('test') cursor.close() assert not cursor.valid @@ -108,17 +108,17 @@ def test_broken_connection(): with pytest.raises(TypeError): SteadyDBCursor(None) db = steady_db_connect(dbapi, database='ok') - for i in range(3): + for _i in range(3): db.close() del db with pytest.raises(dbapi.OperationalError): steady_db_connect(dbapi, database='error') db = steady_db_connect(dbapi, database='ok') cursor = db.cursor() - for i in range(3): + for _i in range(3): cursor.close() cursor = db.cursor('ok') - for i in range(3): + for _i in range(3): cursor.close() with pytest.raises(dbapi.OperationalError): db.cursor('error') @@ -161,7 +161,7 @@ def test_connection(): # noqa: PLR0915 assert hasattr(db, 'cursor') assert hasattr(db, 'close') assert db._con.open_cursors == 0 - for i in range(3): + for _i in range(3): cursor = db.cursor() assert db._con.open_cursors == 1 cursor.close() @@ -188,7 +188,7 @@ def test_connection(): # noqa: PLR0915 assert cursor.fetchone() == f'test{i}' assert cursor.valid assert db._con.open_cursors == 1 - for i in range(4): + for _i in range(4): cursor.callproc('test') cursor.close() assert not cursor.valid @@ -400,7 +400,7 @@ def test_connection_setsession(): assert db._con.num_queries == 0 assert hasattr(db._con, 'session') assert tuple(db._con.session) == ('time zone', 'datestyle') - for i in range(11): + for _i in range(11): db.cursor().execute('select test') assert db._con.open_cursors == 0 assert db._usage == 2 diff --git a/tests/test_steady_pg.py b/tests/test_steady_pg.py index ef1659d..830c07c 100644 --- a/tests/test_steady_pg.py +++ b/tests/test_steady_pg.py @@ -82,7 +82,7 @@ def test_broken_connection(): SteadyPgConnection('wrong') db = SteadyPgConnection(dbname='ok') internal_error_cls = sys.modules[db._con.__module__].InternalError - for i in range(3): + for _i in range(3): db.close() del db with pytest.raises(internal_error_cls): @@ -259,7 +259,7 @@ def test_connection_setsession(): assert db.num_queries == 0 assert hasattr(db, 'session') assert tuple(db.session) == ('time zone', 'datestyle') - for i in range(11): + for _i in range(11): db.query('select test') assert db.num_queries == 2 assert db.session == ['time zone', 'datestyle'] From 9923fc2086a59e410efe98c2b4bcc5e5c375d1a1 Mon Sep 17 00:00:00 2001 From: Christian Clauss <cclauss@me.com> Date: Sat, 29 Apr 2023 23:15:56 +0200 Subject: [PATCH 65/84] Use pyproject.toml instead of setup.py (#48) --- pyproject.toml | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 58 -------------------------------------------------- 2 files changed, 56 insertions(+), 58 deletions(-) delete mode 100755 setup.py diff --git a/pyproject.toml b/pyproject.toml index e9a5f0a..b7cd471 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,59 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = [ + "setuptools>=61.2", +] + +[project] +name = "DBUtils" +version = "3.1.0b1" +description = "Database connections for multi-threaded environments." +license = {text = "MIT License"} +authors = [{name = "Christoph Zwerschke", email = "cito@online.de"}] +requires-python = ">3.7" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Database", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Software Development :: Libraries :: Python Modules", +] +[project.optional-dependencies] +pg = [ + "PyGreSQL>=5", +] +testing = [ + "pytest>=7", + "ruff", +] + +[project.readme] +file = "README.md" +content-type = "text/markdown" + +[project.urls] +Homepage = "https://webwareforpython.github.io/DBUtils/" +Download = "https://pypi.org/project/DBUtils/" +Documentation = "https://webwareforpython.github.io/DBUtils/main.html" +Changelog = "https://webwareforpython.github.io/DBUtils/changelog.html" +"Issue Tracker" = "https://github.com/WebwareForPython/DBUtils/issues" +"Source Code" = "https://github.com/WebwareForPython/DBUtils" + +[tool.setuptools] +packages = ["dbutils"] +platforms = ["any"] +include-package-data = false + [tool.ruff] select = [ "A", # flake8-builtins diff --git a/setup.py b/setup.py deleted file mode 100755 index 43d4ce6..0000000 --- a/setup.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python3 - -"""Setup Script for DBUtils""" - -try: - from setuptools import setup -except ImportError: - from distutils.core import setup - -__version__ = '3.1.0b1' - -readme = open('README.md').read() - -setup( - name='DBUtils', - version=__version__, - description='Database connections for multi-threaded environments.', - long_description=readme, - long_description_content_type='text/markdown', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'Topic :: Database', - 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Operating System :: OS Independent', - ], - author='Christoph Zwerschke', - author_email='cito@online.de', - url='https://webwareforpython.github.io/DBUtils/', - download_url="https://pypi.org/project/DBUtils/", - project_urls={ - 'Documentation': - 'https://webwareforpython.github.io/DBUtils/main.html', - 'Changelog': - 'https://webwareforpython.github.io/DBUtils/changelog.html', - 'Issue Tracker': - 'https://github.com/WebwareForPython/DBUtils/issues', - 'Source Code': - 'https://github.com/WebwareForPython/DBUtils'}, - platforms=['any'], - license='MIT License', - packages=['dbutils'], - python_requires='>3.7', - extras_require={ - "pg": ["PyGreSQL>=5"] - }, - tests_require=["pytest>=7", "ruff"] -) From 9980bf56bc7833ef1f73493bca02374acd4e0ff6 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 29 Apr 2023 23:20:54 +0200 Subject: [PATCH 66/84] Use from clause when re-raising exceptions --- dbutils/persistent_db.py | 5 +++-- dbutils/steady_db.py | 9 +++++---- pyproject.toml | 1 - 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/dbutils/persistent_db.py b/dbutils/persistent_db.py index 54ddfcc..7ca7f7c 100644 --- a/dbutils/persistent_db.py +++ b/dbutils/persistent_db.py @@ -210,10 +210,11 @@ def connection(self, shareable=False): # noqa: ARG002 """ try: con = self.thread.connection - except AttributeError: + except AttributeError as error: con = self.steady_connection() if not con.threadsafety(): - raise NotSupportedError("Database module is not thread-safe.") + raise NotSupportedError( + "Database module is not thread-safe.") from error self.thread.connection = con con._ping_check() return con diff --git a/dbutils/steady_db.py b/dbutils/steady_db.py index 667611e..24f953f 100644 --- a/dbutils/steady_db.py +++ b/dbutils/steady_db.py @@ -277,10 +277,11 @@ def _create(self): con.OperationalError, con.InterfaceError, con.InternalError) - except AttributeError: + except AttributeError as error: raise AttributeError( "Could not determine failure exceptions" - " (please set failures or creator.dbapi).") + " (please set failures or creator.dbapi)." + ) from error if isinstance(self._failures, tuple): self._failure = self._failures[0] else: @@ -535,8 +536,8 @@ def __init__(self, con, *args, **kwargs): self._clearsizes() try: self._cursor = con._cursor(*args, **kwargs) - except AttributeError: - raise TypeError(f"{con!r} is not a SteadyDBConnection.") + except AttributeError as error: + raise TypeError(f"{con!r} is not a SteadyDBConnection.") from error self._closed = False def __enter__(self): diff --git a/pyproject.toml b/pyproject.toml index b7cd471..a66bc58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,7 +106,6 @@ select = [ ] # You can use `ruff rule ...` to see what these ignored rules check ignore = [ - "B904", "E722", "N811", "N818", From df38633ad7432d67745a0bc4196a285c9a5e7203 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 29 Apr 2023 23:26:20 +0200 Subject: [PATCH 67/84] Check that no bare except is used --- dbutils/pooled_db.py | 6 +++--- dbutils/pooled_pg.py | 4 ++-- dbutils/steady_db.py | 4 ++-- dbutils/steady_pg.py | 2 +- pyproject.toml | 1 - 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/dbutils/pooled_db.py b/dbutils/pooled_db.py index 163ad9d..0e93d8a 100644 --- a/dbutils/pooled_db.py +++ b/dbutils/pooled_db.py @@ -386,7 +386,7 @@ def __del__(self): """Delete the pool.""" try: self.close() - except: # builtin Exceptions might not exist anymore + except: # noqa: E722 - builtin Exceptions might not exist anymore pass def _wait_lock(self): @@ -433,7 +433,7 @@ def __del__(self): """Delete the pooled connection.""" try: self.close() - except: # builtin Exceptions might not exist anymore + except: # noqa: E722 - builtin Exceptions might not exist anymore pass def __enter__(self): @@ -525,7 +525,7 @@ def __del__(self): """Delete the pooled connection.""" try: self.close() - except: # builtin Exceptions might not exist anymore + except: # noqa: E722 - builtin Exceptions might not exist anymore pass def __enter__(self): diff --git a/dbutils/pooled_pg.py b/dbutils/pooled_pg.py index 4b3a251..281d175 100644 --- a/dbutils/pooled_pg.py +++ b/dbutils/pooled_pg.py @@ -250,7 +250,7 @@ def __del__(self): """Delete the pool.""" try: self.close() - except: # builtin Exceptions might not exist anymore + except: # noqa: E722 - builtin Exceptions might not exist anymore pass @@ -296,7 +296,7 @@ def __del__(self): """Delete the pooled connection.""" try: self.close() - except: # builtin Exceptions might not exist anymore + except: # noqa: E722 - builtin Exceptions might not exist anymore pass def __enter__(self): diff --git a/dbutils/steady_db.py b/dbutils/steady_db.py index 24f953f..ed6fdda 100644 --- a/dbutils/steady_db.py +++ b/dbutils/steady_db.py @@ -518,7 +518,7 @@ def __del__(self): """Delete the steady connection.""" try: self._close() # make sure the connection is closed - except: # builtin Exceptions might not exist anymore + except: # noqa: E722 - builtin Exceptions might not exist anymore pass @@ -709,5 +709,5 @@ def __del__(self): """Delete the steady cursor.""" try: self.close() # make sure the cursor is closed - except: # builtin Exceptions might not exist anymore + except: # noqa: E722 - builtin Exceptions might not exist anymore pass diff --git a/dbutils/steady_pg.py b/dbutils/steady_pg.py index 6ccefe1..fb9d4bb 100644 --- a/dbutils/steady_pg.py +++ b/dbutils/steady_pg.py @@ -305,5 +305,5 @@ def __del__(self): """Delete the steady connection.""" try: self._close() # make sure the connection is closed - except: # builtin Exceptions might not exist anymore + except: # noqa: E722 - builtin Exceptions might not exist anymore pass diff --git a/pyproject.toml b/pyproject.toml index a66bc58..3edc6d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,7 +106,6 @@ select = [ ] # You can use `ruff rule ...` to see what these ignored rules check ignore = [ - "E722", "N811", "N818", "PIE790", From 6414e009945a129e6175b871e8136a5fc9e64070 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 29 Apr 2023 23:38:45 +0200 Subject: [PATCH 68/84] Exceptions classes should have Error suffix --- dbutils/pooled_db.py | 15 ++++++++---- dbutils/pooled_pg.py | 13 ++++++---- dbutils/simple_pooled_pg.py | 2 +- dbutils/steady_db.py | 8 +++++-- dbutils/steady_pg.py | 10 +++++--- pyproject.toml | 2 -- tests/test_pooled_db.py | 48 ++++++++++++++++++------------------- tests/test_pooled_pg.py | 20 +++++++++------- 8 files changed, 69 insertions(+), 49 deletions(-) diff --git a/dbutils/pooled_db.py b/dbutils/pooled_db.py index 0e93d8a..5b10aad 100644 --- a/dbutils/pooled_db.py +++ b/dbutils/pooled_db.py @@ -156,7 +156,7 @@ class PooledDBError(Exception): """General PooledDB error.""" -class InvalidConnection(PooledDBError): +class InvalidConnectionError(PooledDBError): """Database connection is invalid.""" @@ -164,10 +164,15 @@ class NotSupportedError(PooledDBError): """DB-API module not supported by PooledDB.""" -class TooManyConnections(PooledDBError): +class TooManyConnectionsError(PooledDBError): """Too many database connections were opened.""" +# deprecated alias names for error classes +InvalidConnection = InvalidConnectionError +TooManyConnections = TooManyConnectionsError + + class PooledDB: """Pool for DB-API 2 connections. @@ -392,7 +397,7 @@ def __del__(self): def _wait_lock(self): """Wait until notified or report an error.""" if not self._blocking: - raise TooManyConnections + raise TooManyConnectionsError self._lock.wait() @@ -427,7 +432,7 @@ def __getattr__(self, name): """Proxy all members of the class.""" if self._con: return getattr(self._con, name) - raise InvalidConnection + raise InvalidConnectionError def __del__(self): """Delete the pooled connection.""" @@ -519,7 +524,7 @@ def __getattr__(self, name): """Proxy all members of the class.""" if self._con: return getattr(self._con, name) - raise InvalidConnection + raise InvalidConnectionError def __del__(self): """Delete the pooled connection.""" diff --git a/dbutils/pooled_pg.py b/dbutils/pooled_pg.py index 281d175..c8b9aeb 100644 --- a/dbutils/pooled_pg.py +++ b/dbutils/pooled_pg.py @@ -126,14 +126,19 @@ class PooledPgError(Exception): """General PooledPg error.""" -class InvalidConnection(PooledPgError): +class InvalidConnectionError(PooledPgError): """Database connection is invalid.""" -class TooManyConnections(PooledPgError): +class TooManyConnectionsError(PooledPgError): """Too many database connections were opened.""" +# deprecated alias names for error classes +InvalidConnection = InvalidConnectionError +TooManyConnections = TooManyConnectionsError + + class PooledPg: """Pool for classic PyGreSQL connections. @@ -208,7 +213,7 @@ def connection(self): """Get a steady, cached PostgreSQL connection from the pool.""" if self._connections: if not self._connections.acquire(self._blocking): - raise TooManyConnections + raise TooManyConnectionsError try: con = self._cache.get_nowait() except Empty: @@ -290,7 +295,7 @@ def __getattr__(self, name): """Proxy all members of the class.""" if self._con: return getattr(self._con, name) - raise InvalidConnection + raise InvalidConnectionError def __del__(self): """Delete the pooled connection.""" diff --git a/dbutils/simple_pooled_pg.py b/dbutils/simple_pooled_pg.py index fe90d57..599213a 100644 --- a/dbutils/simple_pooled_pg.py +++ b/dbutils/simple_pooled_pg.py @@ -67,7 +67,7 @@ Licensed under the MIT license. """ -from pg import DB as PgConnection +from pg import DB as PgConnection # noqa: N811 from . import __version__ diff --git a/dbutils/steady_db.py b/dbutils/steady_db.py index ed6fdda..0cf846f 100644 --- a/dbutils/steady_db.py +++ b/dbutils/steady_db.py @@ -98,10 +98,14 @@ class SteadyDBError(Exception): """General SteadyDB error.""" -class InvalidCursor(SteadyDBError): +class InvalidCursorError(SteadyDBError): """Database cursor is invalid.""" +# deprecated alias names for error classes +InvalidCursor = InvalidCursorError + + def connect( creator, maxusage=None, setsession=None, failures=None, ping=1, closeable=True, *args, **kwargs): @@ -703,7 +707,7 @@ def __getattr__(self, name): # make execution methods "tough" return self._get_tough_method(name) return getattr(self._cursor, name) - raise InvalidCursor + raise InvalidCursorError def __del__(self): """Delete the steady cursor.""" diff --git a/dbutils/steady_pg.py b/dbutils/steady_pg.py index fb9d4bb..b847573 100644 --- a/dbutils/steady_pg.py +++ b/dbutils/steady_pg.py @@ -69,7 +69,7 @@ Licensed under the MIT license. """ -from pg import DB as PgConnection +from pg import DB as PgConnection # noqa: N811 from . import __version__ @@ -78,10 +78,14 @@ class SteadyPgError(Exception): """General SteadyPg error.""" -class InvalidConnection(SteadyPgError): +class InvalidConnectionError(SteadyPgError): """Database connection is invalid.""" +# deprecated alias names for error classes +InvalidConnection = InvalidConnectionError + + class SteadyPgConnection: """Class representing steady connections to a PostgreSQL database. @@ -299,7 +303,7 @@ def __getattr__(self, name): or name.startswith('get_')): attr = self._get_tough_method(attr) return attr - raise InvalidConnection + raise InvalidConnectionError def __del__(self): """Delete the steady connection.""" diff --git a/pyproject.toml b/pyproject.toml index 3edc6d4..daa7da3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,8 +106,6 @@ select = [ ] # You can use `ruff rule ...` to see what these ignored rules check ignore = [ - "N811", - "N818", "PIE790", "PLR5501", "PTH122", diff --git a/tests/test_pooled_db.py b/tests/test_pooled_db.py index 1b16322..48c8ce5 100644 --- a/tests/test_pooled_db.py +++ b/tests/test_pooled_db.py @@ -16,11 +16,11 @@ import pytest from dbutils.pooled_db import ( - InvalidConnection, + InvalidConnectionError, NotSupportedError, PooledDB, SharedDBConnection, - TooManyConnections, + TooManyConnectionsError, ) from dbutils.steady_db import SteadyDBConnection @@ -207,7 +207,7 @@ def test_close_connection(dbapi, threadsafety): # noqa: F811 if shareable: assert db._shared_con is None assert shared_con.shared == 0 - with pytest.raises(InvalidConnection): + with pytest.raises(InvalidConnectionError): assert db._usage assert not hasattr(db_con, '_num_queries') assert len(pool._idle_cache) == 1 @@ -692,9 +692,9 @@ def test_maxconnections(dbapi, threadsafety): # noqa: F811, PLR0915 assert len(pool._idle_cache) == 0 if shareable: assert len(pool._shared_cache) == 0 - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection(False) - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection() cache = [] assert pool._connections == 0 @@ -712,13 +712,13 @@ def test_maxconnections(dbapi, threadsafety): # noqa: F811, PLR0915 assert len(pool._shared_cache) == 2 else: assert pool._connections == 3 - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection(False) if shareable: cache.append(pool.connection(True)) assert pool._connections == 3 else: - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection() del cache assert pool._connections == 0 @@ -732,9 +732,9 @@ def test_maxconnections(dbapi, threadsafety): # noqa: F811, PLR0915 assert len(pool._idle_cache) == 0 if shareable: assert len(pool._shared_cache) == 0 - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection(False) - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection() assert db del db @@ -750,9 +750,9 @@ def test_maxconnections(dbapi, threadsafety): # noqa: F811, PLR0915 assert len(pool._shared_cache) == 1 assert pool._shared_cache[0].shared == 2 else: - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection() - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection(False) if shareable: cache.append(pool.connection(True)) @@ -760,7 +760,7 @@ def test_maxconnections(dbapi, threadsafety): # noqa: F811, PLR0915 assert len(pool._shared_cache) == 1 assert pool._shared_cache[0].shared == 3 else: - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection(True) del cache assert pool._connections == 0 @@ -786,9 +786,9 @@ def test_maxconnections(dbapi, threadsafety): # noqa: F811, PLR0915 assert len(pool._idle_cache) == 0 if shareable: assert len(pool._shared_cache) == 0 - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection(False) - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection() pool = PooledDB(dbapi, 4, 3, 2, 1, False) assert pool._maxconnections == 4 @@ -799,9 +799,9 @@ def test_maxconnections(dbapi, threadsafety): # noqa: F811, PLR0915 cache.append(pool.connection(False)) assert pool._connections == 4 assert len(pool._idle_cache) == 0 - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection(False) - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection() pool = PooledDB(dbapi, 1, 2, 3, 4, False) assert pool._maxconnections == 4 @@ -819,9 +819,9 @@ def test_maxconnections(dbapi, threadsafety): # noqa: F811, PLR0915 assert pool._connections == 4 else: assert pool._connections == 4 - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection() - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection(False) pool = PooledDB(dbapi, 0, 0, 3, 3, False) assert pool._maxconnections == 3 @@ -830,9 +830,9 @@ def test_maxconnections(dbapi, threadsafety): # noqa: F811, PLR0915 for _i in range(3): cache.append(pool.connection(False)) assert pool._connections == 3 - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection(False) - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection(True) cache = [] assert pool._connections == 0 @@ -844,9 +844,9 @@ def test_maxconnections(dbapi, threadsafety): # noqa: F811, PLR0915 cache.append(pool.connection()) assert pool._connections == 3 else: - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection() - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection(False) pool = PooledDB(dbapi, 0, 0, 3) assert pool._maxconnections == 0 @@ -1165,7 +1165,7 @@ def test_shared_in_transaction(dbapi): # noqa: F811 db = pool.connection() db.begin() pool.connection(False) - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection() pool = PooledDB(dbapi, 0, 2, 2) db1 = pool.connection() @@ -1183,7 +1183,7 @@ def test_shared_in_transaction(dbapi): # noqa: F811 db.close() db2.begin() pool.connection(False) - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection() db1.rollback() db = pool.connection() diff --git a/tests/test_pooled_pg.py b/tests/test_pooled_pg.py index cc57277..1bf3405 100644 --- a/tests/test_pooled_pg.py +++ b/tests/test_pooled_pg.py @@ -16,7 +16,11 @@ import pg # noqa: F401 import pytest -from dbutils.pooled_pg import InvalidConnection, PooledPg, TooManyConnections +from dbutils.pooled_pg import ( + InvalidConnectionError, + PooledPg, + TooManyConnectionsError, +) from dbutils.steady_pg import SteadyPgConnection @@ -88,7 +92,7 @@ def test_close_connection(): db.query('select test') assert db.num_queries == 1 db.close() - with pytest.raises(InvalidConnection): + with pytest.raises(InvalidConnectionError): assert db.num_queries db = pool.connection() assert hasattr(db, 'dbname') @@ -157,14 +161,14 @@ def test_max_connections(): assert pool._cache.qsize() == 1 cache = [pool.connection() for _i in range(3)] assert pool._cache.qsize() == 0 - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection() pool = PooledPg(0, 1, 1, False) assert pool._blocking == 0 assert pool._cache.qsize() == 0 db = pool.connection() assert pool._cache.qsize() == 0 - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection() assert db del db @@ -176,14 +180,14 @@ def test_max_connections(): assert pool._cache.qsize() == 0 cache.append(pool.connection()) assert pool._cache.qsize() == 0 - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection() pool = PooledPg(3, 2, 1, False) assert pool._cache.qsize() == 3 cache = [pool.connection() for _i in range(3)] assert len(cache) == 3 assert pool._cache.qsize() == 0 - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection() pool = PooledPg(1, 1, 1, True) assert pool._blocking == 1 @@ -307,11 +311,11 @@ def test_context_manager(): db_con = db._con._con db.query('select test') assert db_con.num_queries == 1 - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection() with pool.connection() as db: db_con = db._con._con db.query('select test') assert db_con.num_queries == 2 - with pytest.raises(TooManyConnections): + with pytest.raises(TooManyConnectionsError): pool.connection() From f33853853249becc323e6d8c5b3f99719e9cef60 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sat, 29 Apr 2023 23:52:00 +0200 Subject: [PATCH 69/84] Simplify control flow --- dbutils/pooled_pg.py | 11 +++++------ dbutils/simple_pooled_db.py | 2 +- pyproject.toml | 2 -- tests/test_persistent_db.py | 19 +++++++++---------- tests/test_persistent_pg.py | 13 ++++++------- 5 files changed, 21 insertions(+), 26 deletions(-) diff --git a/dbutils/pooled_pg.py b/dbutils/pooled_pg.py index c8b9aeb..df1c907 100644 --- a/dbutils/pooled_pg.py +++ b/dbutils/pooled_pg.py @@ -225,12 +225,11 @@ def cache(self, con): try: if self._reset == RESET_COMPLETELY: con.reset() # reset the connection completely - else: - if self._reset == RESET_ALWAYS_ROLLBACK or con._transaction: - try: - con.rollback() # rollback a possible transaction - except Exception: - pass + elif self._reset == RESET_ALWAYS_ROLLBACK or con._transaction: + try: + con.rollback() # rollback a possible transaction + except Exception: + pass self._cache.put_nowait(con) # and then put it back into the cache except Full: con.close() diff --git a/dbutils/simple_pooled_db.py b/dbutils/simple_pooled_db.py index d95f5d2..5e5c926 100644 --- a/dbutils/simple_pooled_db.py +++ b/dbutils/simple_pooled_db.py @@ -212,4 +212,4 @@ def _threadsafe_return_connection(self, con): In this case, the connections always stay in the pool, so there is no need to do anything here. """ - pass + # we don't need to do anything here diff --git a/pyproject.toml b/pyproject.toml index daa7da3..266480e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,8 +106,6 @@ select = [ ] # You can use `ruff rule ...` to see what these ignored rules check ignore = [ - "PIE790", - "PLR5501", "PTH122", "PTH123", "S110", diff --git a/tests/test_persistent_db.py b/tests/test_persistent_db.py index cd35127..32afd21 100644 --- a/tests/test_persistent_db.py +++ b/tests/test_persistent_db.py @@ -84,17 +84,16 @@ def run_queries(idx): db = persist.connection() if db != this_db: res = 'error - not persistent' + elif q == 'ping': + res = 'ok - thread alive' + elif q == 'close': + db.close() + res = 'ok - connection closed' else: - if q == 'ping': - res = 'ok - thread alive' - elif q == 'close': - db.close() - res = 'ok - connection closed' - else: - cursor = db.cursor() - cursor.execute(q) - res = cursor.fetchone() - cursor.close() + cursor = db.cursor() + cursor.execute(q) + res = cursor.fetchone() + cursor.close() res = f'{idx}({db._usage}): {res}' result_queue[idx].put(res, timeout=1) if db: diff --git a/tests/test_persistent_pg.py b/tests/test_persistent_pg.py index 6e1a258..189ca18 100644 --- a/tests/test_persistent_pg.py +++ b/tests/test_persistent_pg.py @@ -62,14 +62,13 @@ def run_queries(idx): db = persist.connection() if db.db != this_db: res = 'error - not persistent' + elif q == 'ping': + res = 'ok - thread alive' + elif q == 'close': + db.db.close() + res = 'ok - connection closed' else: - if q == 'ping': - res = 'ok - thread alive' - elif q == 'close': - db.db.close() - res = 'ok - connection closed' - else: - res = db.query(q) + res = db.query(q) res = f'{idx}({db._usage}): {res}' result_queue[idx].put(res, timeout=1) if db: From e8e73167e4f121ca4519f0f2b9f6284b9f389960 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sun, 30 Apr 2023 00:02:19 +0200 Subject: [PATCH 70/84] Modernize docs make script --- docs/make.py | 13 +++++++------ pyproject.toml | 2 -- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/make.py b/docs/make.py index ef88183..59e974b 100755 --- a/docs/make.py +++ b/docs/make.py @@ -3,26 +3,27 @@ """Build HTML from reST files.""" from glob import glob -from os.path import splitext +from pathlib import Path from docutils.core import publish_file print("Creating the documentation...") for rst_file in glob('*.rst'): - name = splitext(rst_file)[0] - lang = splitext(name)[1] + rst_path = Path(rst_file) + name = Path(rst_file).stem + lang = Path(name).suffix if lang.startswith('.'): lang = lang[1:] if lang == 'zh': lang = 'zh_cn' else: lang = 'en' - html_file = name + '.html' + html_path = Path(name + '.html') print(name, lang) - with open(rst_file, encoding='utf-8-sig') as source: - with open(html_file, 'w', encoding='utf-8') as destination: + with rst_path.open(encoding='utf-8-sig') as source: + with html_path.open('w', encoding='utf-8') as destination: output = publish_file( writer_name='html5', source=source, destination=destination, enable_exit_status=True, diff --git a/pyproject.toml b/pyproject.toml index 266480e..f6cc8d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,8 +106,6 @@ select = [ ] # You can use `ruff rule ...` to see what these ignored rules check ignore = [ - "PTH122", - "PTH123", "S110", ] line-length = 79 From c287b10843c91bd13cbd1dd37fab5006d194b263 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sun, 30 Apr 2023 00:14:27 +0200 Subject: [PATCH 71/84] Add and use extra requirements for docs and tests --- pyproject.toml | 5 ++++- tox.ini | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f6cc8d9..8c7613d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,10 @@ classifiers = [ pg = [ "PyGreSQL>=5", ] -testing = [ +docs = [ + "docutils", +] +tests = [ "pytest>=7", "ruff", ] diff --git a/tox.ini b/tox.ini index 812d428..529d75c 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ envlist = py3{7,8,9,10,11}, ruff, manifest, docs, spell [testenv] setenv = PYTHONPATH = {toxinidir} -deps = pytest>=7 +extras = tests commands = pytest {posargs} @@ -28,7 +28,7 @@ commands = [testenv:docs] basepython = python3.10 -deps = docutils +extras = docs changedir = docs commands = python make.py From f2ed0242b1e22374be049efc28419afe0ef5dce9 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sun, 30 Apr 2023 00:17:32 +0200 Subject: [PATCH 72/84] Allow silently catching errors --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8c7613d..6b68c26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,9 +107,9 @@ select = [ # "T20", # flake8-print # "TRY", # tryceratops ] -# You can use `ruff rule ...` to see what these ignored rules check +# Note: use `ruff rule ...` to see explanations of rules ignore = [ - "S110", + "S110", # allow silently catching errors ] line-length = 79 target-version = "py37" From 2aed93cf4b4760666ce8935f61436dec22159780 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sun, 30 Apr 2023 00:22:37 +0200 Subject: [PATCH 73/84] Check comma conventions --- dbutils/steady_db.py | 2 +- pyproject.toml | 2 +- tests/test_pooled_pg.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dbutils/steady_db.py b/dbutils/steady_db.py index 0cf846f..3360eb7 100644 --- a/dbutils/steady_db.py +++ b/dbutils/steady_db.py @@ -284,7 +284,7 @@ def _create(self): except AttributeError as error: raise AttributeError( "Could not determine failure exceptions" - " (please set failures or creator.dbapi)." + " (please set failures or creator.dbapi).", ) from error if isinstance(self._failures, tuple): self._failure = self._failures[0] diff --git a/pyproject.toml b/pyproject.toml index 6b68c26..67d7c58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,6 +64,7 @@ select = [ "B", # flake8-bugbear "C4", # flake8-comprehensions "C90", # McCabe cyclomatic complexity + "COM", # flake8-commas "DTZ", # flake8-datetimez "E", # pycodestyle "EXE", # flake8-executable @@ -93,7 +94,6 @@ select = [ "YTT", # flake8-2020 # "ANN", # flake8-annotations # "BLE", # flake8-blind-except - # "COM", # flake8-commas # "D", # pydocstyle # "DJ", # flake8-django # "EM", # flake8-errmsg diff --git a/tests/test_pooled_pg.py b/tests/test_pooled_pg.py index 1bf3405..0eed92b 100644 --- a/tests/test_pooled_pg.py +++ b/tests/test_pooled_pg.py @@ -70,7 +70,7 @@ def test_create_connection(): assert db.user is None assert hasattr(db, 'num_queries') assert db.num_queries == 0 - pool = PooledPg(0, 0, 0, False, 3, ('set datestyle',),) + pool = PooledPg(0, 0, 0, False, 3, ('set datestyle',)) assert pool._maxusage == 3 assert pool._setsession == ('set datestyle',) db = pool.connection() From 0e871d34ad0df608fd2c65c9ef0aa7cb3f23fcdb Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sun, 30 Apr 2023 00:52:54 +0200 Subject: [PATCH 74/84] Check and improve docstrings --- dbutils/__init__.py | 2 +- dbutils/pooled_db.py | 18 ++++-------------- dbutils/simple_pooled_db.py | 5 ++++- dbutils/simple_pooled_pg.py | 5 ++++- dbutils/steady_db.py | 13 +++++++++---- dbutils/steady_pg.py | 2 ++ pyproject.toml | 6 ++++-- tests/__init__.py | 2 +- 8 files changed, 29 insertions(+), 24 deletions(-) diff --git a/dbutils/__init__.py b/dbutils/__init__.py index f7a4ee8..9008cfc 100644 --- a/dbutils/__init__.py +++ b/dbutils/__init__.py @@ -1,4 +1,4 @@ -# DBUtils main package +"""The DBUtils main package.""" __all__ = [ '__version__', diff --git a/dbutils/pooled_db.py b/dbutils/pooled_db.py index 5b10aad..35d0bae 100644 --- a/dbutils/pooled_db.py +++ b/dbutils/pooled_db.py @@ -146,6 +146,7 @@ Licensed under the MIT license. """ +from functools import total_ordering from threading import Condition from . import __version__ @@ -450,6 +451,7 @@ def __exit__(self, *exc): self.close() +@total_ordering class SharedDBConnection: """Auxiliary class for shared connections.""" @@ -462,28 +464,16 @@ def __init__(self, con): self.shared = 1 def __lt__(self, other): + """Check whether this connection should come before the other one.""" if self.con._transaction == other.con._transaction: return self.shared < other.shared return not self.con._transaction - def __le__(self, other): - if self.con._transaction == other.con._transaction: - return self.shared <= other.shared - return not self.con._transaction - def __eq__(self, other): + """Check whether this connection is the same as the other one.""" return (self.con._transaction == other.con._transaction and self.shared == other.shared) - def __ne__(self, other): - return not self.__eq__(other) - - def __gt__(self, other): - return other.__lt__(self) - - def __ge__(self, other): - return other.__le__(self) - def share(self): """Increase the share of this connection.""" self.shared += 1 diff --git a/dbutils/simple_pooled_db.py b/dbutils/simple_pooled_db.py index 5e5c926..2558814 100644 --- a/dbutils/simple_pooled_db.py +++ b/dbutils/simple_pooled_db.py @@ -91,6 +91,7 @@ class PooledDBConnection: """ def __init__(self, pool, con): + """Initialize pooled connection.""" self._con = con self._pool = pool @@ -103,10 +104,12 @@ def close(self): self._con = None def __getattr__(self, name): - # All other members are the same. + """Get the attribute with the given name.""" + # All other attributes are the same. return getattr(self._con, name) def __del__(self): + """Delete the pooled connection.""" self.close() diff --git a/dbutils/simple_pooled_pg.py b/dbutils/simple_pooled_pg.py index 599213a..5997e27 100644 --- a/dbutils/simple_pooled_pg.py +++ b/dbutils/simple_pooled_pg.py @@ -80,6 +80,7 @@ class PooledPgConnection: """ def __init__(self, pool, con): + """Initialize pooled connection.""" self._con = con self._pool = pool @@ -92,10 +93,12 @@ def close(self): self._con = None def __getattr__(self, name): - # All other members are the same. + """Get the attribute with the given name.""" + # All other attributes are the same. return getattr(self._con, name) def __del__(self): + """Delete the pooled connection.""" self.close() diff --git a/dbutils/steady_db.py b/dbutils/steady_db.py index 3360eb7..bc7ea81 100644 --- a/dbutils/steady_db.py +++ b/dbutils/steady_db.py @@ -109,7 +109,9 @@ class InvalidCursorError(SteadyDBError): def connect( creator, maxusage=None, setsession=None, failures=None, ping=1, closeable=True, *args, **kwargs): - """A tough version of the connection constructor of a DB-API 2 module. + """Create a "tough" connection. + + A hardened version of the connection function of a DB-API 2 module. creator: either an arbitrary function returning new DB-API 2 compliant connection objects or a DB-API 2 compliant database module @@ -138,7 +140,7 @@ def connect( class SteadyDBConnection: - """A "tough" version of DB-API 2 connections.""" + """A hardened version of DB-API 2 connections.""" version = __version__ @@ -477,7 +479,10 @@ def ping(self, *args, **kwargs): return self._con.ping(*args, **kwargs) def _cursor(self, *args, **kwargs): - """A "tough" version of the method cursor().""" + """Create a "tough" cursor. + + This is a hardened version of the method cursor(). + """ # The args and kwargs are not part of the standard, # but some database modules seem to use these. transaction = self._transaction @@ -527,7 +532,7 @@ def __del__(self): class SteadyDBCursor: - """A "tough" version of DB-API 2 cursors.""" + """A hardened version of DB-API 2 cursors.""" def __init__(self, con, *args, **kwargs): """Create a "tough" DB-API 2 cursor.""" diff --git a/dbutils/steady_pg.py b/dbutils/steady_pg.py index b847573..fa67a83 100644 --- a/dbutils/steady_pg.py +++ b/dbutils/steady_pg.py @@ -105,6 +105,8 @@ def __init__( *args, **kwargs): """Create a "tough" PostgreSQL connection. + A hardened version of the DB wrapper class of PyGreSQL. + maxusage: maximum usage limit for the underlying PyGreSQL connection (number of uses, 0 or None means unlimited usage) When this limit is reached, the connection is automatically reset. diff --git a/pyproject.toml b/pyproject.toml index 67d7c58..1b249d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,7 @@ select = [ "C4", # flake8-comprehensions "C90", # McCabe cyclomatic complexity "COM", # flake8-commas + "D", # pydocstyle "DTZ", # flake8-datetimez "E", # pycodestyle "EXE", # flake8-executable @@ -94,8 +95,6 @@ select = [ "YTT", # flake8-2020 # "ANN", # flake8-annotations # "BLE", # flake8-blind-except - # "D", # pydocstyle - # "DJ", # flake8-django # "EM", # flake8-errmsg # "ERA", # eradicate # "FBT", # flake8-boolean-trap @@ -109,6 +108,8 @@ select = [ ] # Note: use `ruff rule ...` to see explanations of rules ignore = [ + "D203", # no blank linke before class docstring + "D213", # multi-line docstrings should not start at second line "S110", # allow silently catching errors ] line-length = 79 @@ -127,6 +128,7 @@ max-statements = 100 "INP001", # allow stand-alone scripts ] "tests/*" = [ + "D", # no docstrings necessary here "PLR2004", # allow magic values "S101", # allow assert statements ] diff --git a/tests/__init__.py b/tests/__init__.py index 49922b8..3c7c5ff 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,4 +1,4 @@ -"""DBUtils tests""" +"""The DBUtils tests package.""" # make sure the mock pg module is installed from . import mock_pg as pg # noqa: F401 From 236b58142e522c793d861bae3568e0432ce2bf8c Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sun, 30 Apr 2023 00:58:23 +0200 Subject: [PATCH 75/84] Fix typo --- pyproject.toml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1b249d9..c9f9fe8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,7 @@ select = [ "D", # pydocstyle "DTZ", # flake8-datetimez "E", # pycodestyle + "ERA", # eradicate "EXE", # flake8-executable "F", # Pyflakes "G", # flake8-logging-format @@ -96,10 +97,7 @@ select = [ # "ANN", # flake8-annotations # "BLE", # flake8-blind-except # "EM", # flake8-errmsg - # "ERA", # eradicate # "FBT", # flake8-boolean-trap - # "NPY", # NumPy-specific rules - # "PD", # pandas-vet # "Q", # flake8-quotes # "SIM", # flake8-simplify # "SLF", # flake8-self @@ -108,7 +106,7 @@ select = [ ] # Note: use `ruff rule ...` to see explanations of rules ignore = [ - "D203", # no blank linke before class docstring + "D203", # no blank line before class docstring "D213", # multi-line docstrings should not start at second line "S110", # allow silently catching errors ] From 13fffe30f9c68beea9bd5ca2e9d4adf826b7a598 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sun, 30 Apr 2023 01:37:25 +0200 Subject: [PATCH 76/84] Simplify and add checks for simple code --- dbutils/pooled_db.py | 33 ++++++++-------- dbutils/pooled_pg.py | 29 +++++++------- dbutils/steady_db.py | 83 +++++++++++++++-------------------------- dbutils/steady_pg.py | 23 +++++------- docs/make.py | 22 +++++------ pyproject.toml | 11 +++--- tests/test_pooled_db.py | 11 ++---- 7 files changed, 90 insertions(+), 122 deletions(-) diff --git a/dbutils/pooled_db.py b/dbutils/pooled_db.py index 35d0bae..be18c94 100644 --- a/dbutils/pooled_db.py +++ b/dbutils/pooled_db.py @@ -146,6 +146,7 @@ Licensed under the MIT license. """ +from contextlib import suppress from functools import total_ordering from threading import Condition @@ -349,11 +350,10 @@ def unshare(self, con): with self._lock: con.unshare() shared = con.shared - if not shared: # connection is idle, - try: # so try to remove it - self._shared_cache.remove(con) # from shared cache - except ValueError: - pass # pool has already been closed + if not shared: # connection is idle + # try to remove it from shared cache + with suppress(ValueError): # if pool has already been closed + self._shared_cache.remove(con) if not shared: # connection has become idle, self.cache(con.con) # so add it to the idle cache @@ -374,25 +374,22 @@ def close(self): with self._lock: while self._idle_cache: # close all idle connections con = self._idle_cache.pop(0) - try: + with suppress(Exception): con.close() - except Exception: - pass if self._maxshared: # close all shared connections while self._shared_cache: con = self._shared_cache.pop(0).con - try: + with suppress(Exception): con.close() - except Exception: - pass self._connections -= 1 self._lock.notify_all() def __del__(self): """Delete the pool.""" - try: + # builtins (including Exceptions) might not exist anymore + try: # noqa: SIM105 self.close() - except: # noqa: E722 - builtin Exceptions might not exist anymore + except: # noqa: E722, S110 pass def _wait_lock(self): @@ -437,9 +434,10 @@ def __getattr__(self, name): def __del__(self): """Delete the pooled connection.""" - try: + # builtins (including Exceptions) might not exist anymore + try: # noqa: SIM105 self.close() - except: # noqa: E722 - builtin Exceptions might not exist anymore + except: # noqa: E722, S110 pass def __enter__(self): @@ -518,9 +516,10 @@ def __getattr__(self, name): def __del__(self): """Delete the pooled connection.""" - try: + # builtins (including Exceptions) might not exist anymore + try: # noqa: SIM105 self.close() - except: # noqa: E722 - builtin Exceptions might not exist anymore + except: # noqa: E722, S110 pass def __enter__(self): diff --git a/dbutils/pooled_pg.py b/dbutils/pooled_pg.py index df1c907..eb73708 100644 --- a/dbutils/pooled_pg.py +++ b/dbutils/pooled_pg.py @@ -112,6 +112,7 @@ Licensed under the MIT license. """ +from contextlib import suppress from queue import Empty, Full, Queue from . import __version__ @@ -186,9 +187,8 @@ def __init__( maxcached = 0 if maxconnections is None: maxconnections = 0 - if maxcached: - if maxcached < mincached: - maxcached = mincached + if maxcached and maxcached < mincached: + maxcached = mincached if maxconnections: if maxconnections < maxcached: maxconnections = maxcached @@ -211,9 +211,8 @@ def steady_connection(self): def connection(self): """Get a steady, cached PostgreSQL connection from the pool.""" - if self._connections: - if not self._connections.acquire(self._blocking): - raise TooManyConnectionsError + if self._connections and not self._connections.acquire(self._blocking): + raise TooManyConnectionsError try: con = self._cache.get_nowait() except Empty: @@ -226,10 +225,8 @@ def cache(self, con): if self._reset == RESET_COMPLETELY: con.reset() # reset the connection completely elif self._reset == RESET_ALWAYS_ROLLBACK or con._transaction: - try: + with suppress(Exception): con.rollback() # rollback a possible transaction - except Exception: - pass self._cache.put_nowait(con) # and then put it back into the cache except Full: con.close() @@ -241,10 +238,8 @@ def close(self): while 1: try: con = self._cache.get_nowait() - try: + with suppress(Exception): con.close() - except Exception: - pass if self._connections: self._connections.release() except Empty: @@ -252,9 +247,10 @@ def close(self): def __del__(self): """Delete the pool.""" - try: + # builtins (including Exceptions) might not exist anymore + try: # noqa: SIM105 self.close() - except: # noqa: E722 - builtin Exceptions might not exist anymore + except: # noqa: E722, S110 pass @@ -298,9 +294,10 @@ def __getattr__(self, name): def __del__(self): """Delete the pooled connection.""" - try: + # builtins (including Exceptions) might not exist anymore + try: # noqa: SIM105 self.close() - except: # noqa: E722 - builtin Exceptions might not exist anymore + except: # noqa: E722, S110 pass def __enter__(self): diff --git a/dbutils/steady_db.py b/dbutils/steady_db.py index bc7ea81..52e1b60 100644 --- a/dbutils/steady_db.py +++ b/dbutils/steady_db.py @@ -90,6 +90,7 @@ """ import sys +from contextlib import suppress from . import __version__ @@ -232,10 +233,7 @@ def _create(self): else: break i = mod.rfind('.') - if i < 0: - mod = None - else: - mod = mod[:i] + mod = None if i < 0 else mod[:i] else: try: mod = con.OperationalError.__module__ @@ -251,20 +249,15 @@ def _create(self): else: break i = mod.rfind('.') - if i < 0: - mod = None - else: - mod = mod[:i] + mod = None if i < 0 else mod[:i] else: self._dbapi = None if self._threadsafety is None: try: self._threadsafety = self._dbapi.threadsafety except AttributeError: - try: + with suppress(AttributeError): self._threadsafety = con.threadsafety - except AttributeError: - pass if self._failures is None: try: self._failures = ( @@ -296,10 +289,8 @@ def _create(self): except Exception as error: # the database module could not be determined # or the session could not be prepared - try: # close the connection first - con.close() - except Exception: - pass + with suppress(Exception): + con.close() # close the connection first raise error # re-raise the original error again return con @@ -323,14 +314,12 @@ def _store(self, con): def _close(self): """Close the tough connection. - You can always close a tough connection with this method + You can always close a tough connection with this method, and it will not complain if you close it more than once. """ if not self._closed: - try: + with suppress(Exception): self._con.close() - except Exception: - pass self._transaction = False self._closed = True @@ -340,15 +329,13 @@ def _reset(self, force=False): Rollback if forced or the connection was in a transaction. """ if not self._closed and (force or self._transaction): - try: + with suppress(Exception): self.rollback() - except Exception: - pass def _ping_check(self, ping=1, reconnect=True): """Check whether the connection is still alive using ping(). - If the the underlying connection is not active and the ping + If the underlying connection is not active and the ping parameter is set accordingly, the connection will be recreated unless the connection is currently inside a transaction. """ @@ -372,7 +359,7 @@ def _ping_check(self, ping=1, reconnect=True): if reconnect and not self._transaction: try: # try to reopen the connection con = self._create() - except Exception: + except Exception: # noqa: S110 pass else: self._close() @@ -402,7 +389,7 @@ def threadsafety(self): def close(self): """Close the tough connection. - You are allowed to close a tough connection by default + You are allowed to close a tough connection by default, and it will not complain if you close it more than once. You can disallow closing connections by setting @@ -439,7 +426,7 @@ def commit(self): except self._failures as error: # cannot commit try: # try to reopen the connection con = self._create() - except Exception: + except Exception: # noqa: S110 pass else: self._close() @@ -454,7 +441,7 @@ def rollback(self): except self._failures as error: # cannot rollback try: # try to reopen the connection con = self._create() - except Exception: + except Exception: # noqa: S110 pass else: self._close() @@ -497,12 +484,12 @@ def _cursor(self, *args, **kwargs): except self._failures as error: # error in getting cursor try: # try to reopen the connection con = self._create() - except Exception: + except Exception: # noqa: S110 pass else: try: # and try one more time to get a cursor cursor = con.cursor(*args, **kwargs) - except Exception: + except Exception: # noqa: S110 pass else: self._close() @@ -510,10 +497,8 @@ def _cursor(self, *args, **kwargs): if transaction: raise error # re-raise the original error again return cursor - try: + with suppress(Exception): con.close() - except Exception: - pass if transaction: self._transaction = False raise error # re-raise the original error again @@ -525,9 +510,10 @@ def cursor(self, *args, **kwargs): def __del__(self): """Delete the steady connection.""" - try: + # builtins (including Exceptions) might not exist anymore + try: # noqa: SIM105 self._close() # make sure the connection is closed - except: # noqa: E722 - builtin Exceptions might not exist anymore + except: # noqa: E722, S110 pass @@ -596,10 +582,8 @@ def close(self): It will not complain if you close it more than once. """ if not self._closed: - try: + with suppress(Exception): self._cursor.close() - except Exception: - pass self._closed = True def _get_tough_method(self, name): @@ -626,7 +610,7 @@ def tough_method(*args, **kwargs): try: cursor2 = con._cursor( *self._args, **self._kwargs) # open new cursor - except Exception: + except Exception: # noqa: S110 pass else: try: # and try one more time to execute @@ -636,26 +620,24 @@ def tough_method(*args, **kwargs): result = method(*args, **kwargs) if execute: self._clearsizes() - except Exception: + except Exception: # noqa: S110 pass else: self.close() self._cursor = cursor2 con._usage += 1 return result - try: + with suppress(Exception): cursor2.close() - except Exception: - pass try: # try to reopen the connection con2 = con._create() - except Exception: + except Exception: # noqa: S110 pass else: try: cursor2 = con2.cursor( *self._args, **self._kwargs) # open new cursor - except Exception: + except Exception: # noqa: S110 pass else: if transaction: @@ -689,14 +671,10 @@ def tough_method(*args, **kwargs): if error2: raise error2 # raise the other error return result - try: + with suppress(Exception): cursor2.close() - except Exception: - pass - try: + with suppress(Exception): con2.close() - except Exception: - pass if transaction: self._transaction = False raise error # re-raise the original error again @@ -716,7 +694,8 @@ def __getattr__(self, name): def __del__(self): """Delete the steady cursor.""" - try: + # builtins (including Exceptions) might not exist anymore + try: # noqa: SIM105 self.close() # make sure the cursor is closed - except: # noqa: E722 - builtin Exceptions might not exist anymore + except: # noqa: E722, S110 pass diff --git a/dbutils/steady_pg.py b/dbutils/steady_pg.py index fa67a83..d17d3bb 100644 --- a/dbutils/steady_pg.py +++ b/dbutils/steady_pg.py @@ -69,6 +69,8 @@ Licensed under the MIT license. """ +from contextlib import suppress + from pg import DB as PgConnection # noqa: N811 from . import __version__ @@ -155,21 +157,19 @@ def _setsession(self): def _close(self): """Close the tough connection. - You can always close a tough connection with this method + You can always close a tough connection with this method, and it will not complain if you close it more than once. """ if not self._closed: - try: + with suppress(Exception): self._con.close() - except Exception: - pass self._transaction = False self._closed = True def close(self): """Close the tough connection. - You are allowed to close a tough connection by default + You are allowed to close a tough connection by default, and it will not complain if you close it more than once. You can disallow closing connections by setting @@ -191,10 +191,8 @@ def reopen(self): except Exception: if self._transaction: self._transaction = False - try: + with suppress(Exception): self._con.query('rollback') - except Exception: - pass else: self._transaction = False self._closed = False @@ -216,10 +214,8 @@ def reset(self): try: self.reopen() except Exception: - try: + with suppress(Exception): self.rollback() - except Exception: - pass def begin(self, sql=None): """Begin a transaction.""" @@ -309,7 +305,8 @@ def __getattr__(self, name): def __del__(self): """Delete the steady connection.""" - try: + # builtins (including Exceptions) might not exist anymore + try: # noqa: SIM105 self._close() # make sure the connection is closed - except: # noqa: E722 - builtin Exceptions might not exist anymore + except: # noqa: E722, S110 pass diff --git a/docs/make.py b/docs/make.py index 59e974b..c071071 100755 --- a/docs/make.py +++ b/docs/make.py @@ -22,16 +22,16 @@ html_path = Path(name + '.html') print(name, lang) - with rst_path.open(encoding='utf-8-sig') as source: - with html_path.open('w', encoding='utf-8') as destination: - output = publish_file( - writer_name='html5', source=source, destination=destination, - enable_exit_status=True, - settings_overrides={ - "stylesheet_path": 'doc.css', - "embed_stylesheet": False, - "toc_backlinks": False, - "language_code": lang, - "exit_status_level": 2}) + with rst_path.open(encoding='utf-8-sig') as source, \ + html_path.open('w', encoding='utf-8') as destination: + output = publish_file( + writer_name='html5', source=source, destination=destination, + enable_exit_status=True, + settings_overrides={ + "stylesheet_path": 'doc.css', + "embed_stylesheet": False, + "toc_backlinks": False, + "language_code": lang, + "exit_status_level": 2}) print("Done.") diff --git a/pyproject.toml b/pyproject.toml index c9f9fe8..f6d6772 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,6 +88,7 @@ select = [ "RSE", # flake8-raise "RUF", # Ruff-specific rules "S", # flake8-bandit + "SIM", # flake8-simplify "T10", # flake8-debugger "TCH", # flake8-type-checking "TID", # flake8-tidy-imports @@ -99,7 +100,6 @@ select = [ # "EM", # flake8-errmsg # "FBT", # flake8-boolean-trap # "Q", # flake8-quotes - # "SIM", # flake8-simplify # "SLF", # flake8-self # "T20", # flake8-print # "TRY", # tryceratops @@ -108,18 +108,17 @@ select = [ ignore = [ "D203", # no blank line before class docstring "D213", # multi-line docstrings should not start at second line - "S110", # allow silently catching errors ] line-length = 79 target-version = "py37" [tool.ruff.mccabe] -max-complexity = 32 +max-complexity = 30 [tool.ruff.pylint] -max-args = 15 -max-branches = 50 -max-statements = 100 +max-args = 12 +max-branches = 35 +max-statements = 95 [tool.ruff.per-file-ignores] "docs/*" = [ diff --git a/tests/test_pooled_db.py b/tests/test_pooled_db.py index 48c8ce5..b5ef7cf 100644 --- a/tests/test_pooled_db.py +++ b/tests/test_pooled_db.py @@ -922,10 +922,7 @@ def test_maxusage(dbapi, threadsafety, maxusage): # noqa: F811 assert r == f'test{i}' cursor.close() assert db._con._con.open_cursors == 0 - if maxusage: - j = i % maxusage + 1 - else: - j = i + 1 + j = i % maxusage + 1 if maxusage else i + 1 assert db._usage == j assert db._con._con.num_uses == j assert db._con._con.num_queries == j @@ -1269,18 +1266,18 @@ def test_shared_db_connection_compare(dbapi): # noqa: F811 assert con1 == con2 assert con1 <= con2 assert con1 >= con2 - assert not con1 != con2 + assert not con1 != con2 # noqa: SIM202 assert not con1 < con2 assert not con1 > con2 con2.share() - assert not con1 == con2 + assert not con1 == con2 # noqa: SIM201 assert con1 <= con2 assert not con1 >= con2 assert con1 != con2 assert con1 < con2 assert not con1 > con2 con1.con._transaction = True - assert not con1 == con2 + assert not con1 == con2 # noqa: SIM201 assert not con1 <= con2 assert con1 >= con2 assert con1 != con2 From 4d77b8db2ed2091d48f28cb17611a34b6c4a84b5 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sun, 30 Apr 2023 01:42:57 +0200 Subject: [PATCH 77/84] Check for print statements --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f6d6772..12b3d0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,6 +90,7 @@ select = [ "S", # flake8-bandit "SIM", # flake8-simplify "T10", # flake8-debugger + "T20", # flake8-print "TCH", # flake8-type-checking "TID", # flake8-tidy-imports "UP", # pyupgrade @@ -101,7 +102,6 @@ select = [ # "FBT", # flake8-boolean-trap # "Q", # flake8-quotes # "SLF", # flake8-self - # "T20", # flake8-print # "TRY", # tryceratops ] # Note: use `ruff rule ...` to see explanations of rules @@ -123,6 +123,7 @@ max-statements = 95 [tool.ruff.per-file-ignores] "docs/*" = [ "INP001", # allow stand-alone scripts + "T201", # allow print statements ] "tests/*" = [ "D", # no docstrings necessary here From 7f168ce3c9f267c3a23a5a0862928d0ad6fd216f Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sun, 30 Apr 2023 01:58:25 +0200 Subject: [PATCH 78/84] Adapt bumpversion config --- .bumpversion.cfg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 7d741e2..0c629a3 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,9 +1,9 @@ [bumpversion] current_version = 3.1.0b1 -[bumpversion:file:setup.py] -search = __version__ = '{current_version}' -replace = __version__ = '{new_version}' +[bumpversion:file:pyproject.toml] +search = version = "{current_version}" +replace = version = "{new_version}" [bumpversion:file:dbutils/__init__.py] search = __version__ = '{current_version}' From ddc86dc9f6f4fa3bf1de2b963e2fbd69face4776 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sun, 30 Apr 2023 02:01:56 +0200 Subject: [PATCH 79/84] Move codespell config to pyproject.toml --- .codespellrc | 3 --- MANIFEST.in | 1 - pyproject.toml | 4 ++++ 3 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 .codespellrc diff --git a/.codespellrc b/.codespellrc deleted file mode 100644 index ab81ffd..0000000 --- a/.codespellrc +++ /dev/null @@ -1,3 +0,0 @@ -[codespell] -skip = .git,.tox,.venv,*.de.html,*.de.rst,build,dist,local -quiet-level = 2 diff --git a/MANIFEST.in b/MANIFEST.in index 6408840..835317a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,7 +4,6 @@ include LICENSE include README.md include .bumpversion.cfg -include .codespellrc include pyproject.toml include tox.ini diff --git a/pyproject.toml b/pyproject.toml index 12b3d0f..3426c54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,3 +130,7 @@ max-statements = 95 "PLR2004", # allow magic values "S101", # allow assert statements ] + +[tool.codespell] +skip = '.git,.tox,.venv,*.de.html,*.de.rst,build,dist,local' +quiet-level = 2 From 4e2f3f18533d50991bc6d32bb3101dee288dcaed Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sun, 30 Apr 2023 02:16:19 +0200 Subject: [PATCH 80/84] Adapt GitHub action --- .github/workflows/publish_on_pypi.yml | 20 +++++++------------- .github/workflows/test_with_tox.yml | 2 +- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/.github/workflows/publish_on_pypi.yml b/.github/workflows/publish_on_pypi.yml index cd0df25..7ce0c8b 100644 --- a/.github/workflows/publish_on_pypi.yml +++ b/.github/workflows/publish_on_pypi.yml @@ -6,28 +6,22 @@ on: - 'Release-*' jobs: - build: + publish: runs-on: ubuntu-latest - strategy: - matrix: - python: ['3.10'] steps: - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python }} + - name: Set up Python uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python }} + python-version: "3.10" - - name: Build source tarball - if: matrix.python == 3.10 - run: python setup.py sdist + - name: Install build tool + run: python -m pip install build --user - - name: Build wheel - run: | - pip install wheel - python setup.py bdist_wheel + - name: Build source tarball and wheel + run: python -m build - name: Publish distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/test_with_tox.yml b/.github/workflows/test_with_tox.yml index dbc4ace..d829641 100644 --- a/.github/workflows/test_with_tox.yml +++ b/.github/workflows/test_with_tox.yml @@ -3,7 +3,7 @@ name: Test DBUtils using tox on: [push, pull_request] jobs: - build: + test: runs-on: ubuntu-latest strategy: matrix: From 06a7b9469969e484dd0caa8f207ea03a81119c26 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sun, 17 Mar 2024 13:55:31 +0100 Subject: [PATCH 81/84] Prepare for minor release --- .bumpversion.cfg | 2 +- .github/workflows/publish_on_pypi.yml | 6 ++-- .github/workflows/test_with_tox.yml | 6 ++-- LICENSE | 2 +- README.md | 2 +- dbutils/__init__.py | 2 +- docs/changelog.html | 47 ++++++++++++++++----------- docs/changelog.rst | 11 +++++++ docs/main.de.html | 8 ++--- docs/main.de.rst | 6 ++-- docs/main.html | 8 ++--- docs/main.rst | 6 ++-- docs/make.py | 5 ++- pyproject.toml | 34 +++++++++++-------- tests/test_threading_local.py | 2 +- tox.ini | 12 +++---- 16 files changed, 92 insertions(+), 67 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 0c629a3..ea56cf5 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.1.0b1 +current_version = 3.1.0 [bumpversion:file:pyproject.toml] search = version = "{current_version}" diff --git a/.github/workflows/publish_on_pypi.yml b/.github/workflows/publish_on_pypi.yml index 7ce0c8b..99cba8a 100644 --- a/.github/workflows/publish_on_pypi.yml +++ b/.github/workflows/publish_on_pypi.yml @@ -10,12 +10,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.11" - name: Install build tool run: python -m pip install build --user diff --git a/.github/workflows/test_with_tox.yml b/.github/workflows/test_with_tox.yml index d829641..5a0f068 100644 --- a/.github/workflows/test_with_tox.yml +++ b/.github/workflows/test_with_tox.yml @@ -7,10 +7,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Python ${{ matrix.python }} uses: actions/setup-python@v4 @@ -21,5 +21,5 @@ jobs: - run: tox -e py - - if: matrix.python == 3.10 + - if: matrix.python == 3.11 run: TOXENV=ruff,manifest,docs,spell tox diff --git a/LICENSE b/LICENSE index 15287a3..de120c4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2023 Christoph Zwerschke +Copyright (c) 2024 Christoph Zwerschke Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 09ccb37..fa3b0ef 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ to a database that can be used in all kinds of multi-threaded environments. The suite supports DB-API 2 compliant database interfaces and the classic PyGreSQL interface. -The current version 3.0.3 of DBUtils supports Python versions 3.6 to 3.11. +The current version 3.1.0 of DBUtils supports Python versions 3.7 to 3.12. **Please have a look at the [changelog](https://webwareforpython.github.io/DBUtils/changelog.html), because there were some breaking changes in version 2.0.** diff --git a/dbutils/__init__.py b/dbutils/__init__.py index 9008cfc..e503c5d 100644 --- a/dbutils/__init__.py +++ b/dbutils/__init__.py @@ -5,4 +5,4 @@ 'simple_pooled_pg', 'steady_pg', 'pooled_pg', 'persistent_pg', 'simple_pooled_db', 'steady_db', 'pooled_db', 'persistent_db'] -__version__ = '3.0.3' +__version__ = '3.1.0' diff --git a/docs/changelog.html b/docs/changelog.html index 1f5ce90..c30f65b 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -2,8 +2,8 @@ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta charset="utf-8" /> +<meta name="generator" content="Docutils 0.20.1: https://docutils.sourceforge.io/" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> -<meta name="generator" content="Docutils 0.19: https://docutils.sourceforge.io/" /> <title>Changelog for DBUtils</title> <link rel="stylesheet" href="doc.css" type="text/css" /> </head> @@ -12,6 +12,15 @@ <h1 class="title">Changelog for DBUtils</h1> <section id="section-1"> +<h2>3.1.0</h2> +<p>DBUtils 3.1.0 was released on March 17, 2024.</p> +<p>Changes:</p> +<ul class="simple"> +<li><p>Support Python version 3.12, cease support for Python 3.6.</p></li> +<li><p>Various small internal improvements and modernizations.</p></li> +</ul> +</section> +<section id="section-2"> <h2>3.0.3</h2> <p>DBUtils 3.0.3 was released on April 27, 2023.</p> <p>Changes:</p> @@ -21,19 +30,19 @@ <h2>3.0.3</h2> <li><p>Minor fixes and section an advanced usage in docs.</p></li> </ul> </section> -<section id="section-2"> +<section id="section-3"> <h2>3.0.2</h2> <p>DBUtils 3.0.2 was released on January 14, 2022.</p> <p>The optional iterator protocol on cursors is now supported.</p> </section> -<section id="section-3"> +<section id="section-4"> <h2>3.0.1</h2> <p>DBUtils 3.0.1 was released on December 22, 2021.</p> <p>It includes <span class="docutils literal">InterfaceError</span> to the default list of exceptions for which the connection failover mechanism is applied. You can override this with the <span class="docutils literal">failures</span> parameter.</p> </section> -<section id="section-4"> +<section id="section-5"> <h2>3.0.0</h2> <p>DBUtils 3.0.0 was released on November 26, 2021.</p> <p>It is intended to be used with Python versions 3.6 to 3.10.</p> @@ -42,7 +51,7 @@ <h2>3.0.0</h2> <li><p>Cease support for Python 2 and 3.5, minor optimizations.</p></li> </ul> </section> -<section id="section-5"> +<section id="section-6"> <h2>2.0.3</h2> <p>DBUtils 2.0.3 was released on November 26, 2021.</p> <p>Changes:</p> @@ -50,7 +59,7 @@ <h2>2.0.3</h2> <li><p>Support Python version 3.10.</p></li> </ul> </section> -<section id="section-6"> +<section id="section-7"> <h2>2.0.2</h2> <p>DBUtils 2.0.2 was released on June 8, 2021.</p> <p>Changes:</p> @@ -58,7 +67,7 @@ <h2>2.0.2</h2> <li><p>Allow using context managers for pooled connections.</p></li> </ul> </section> -<section id="section-7"> +<section id="section-8"> <h2>2.0.1</h2> <p>DBUtils 2.0.1 was released on April 8, 2021.</p> <p>Changes:</p> @@ -66,7 +75,7 @@ <h2>2.0.1</h2> <li><p>Avoid "name Exception is not defined" when exiting.</p></li> </ul> </section> -<section id="section-8"> +<section id="section-9"> <h2>2.0</h2> <p>DBUtils 2.0 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> @@ -82,7 +91,7 @@ <h2>2.0</h2> <li><p>This changelog has been compiled from the former release notes.</p></li> </ul> </section> -<section id="section-9"> +<section id="section-10"> <h2>1.4</h2> <p>DBUtils 1.4 was released on September 26, 2020.</p> <p>It is intended to be used with Python versions 2.7 and 3.5 to 3.9.</p> @@ -93,7 +102,7 @@ <h2>1.4</h2> inside a transaction.</p></li> </ul> </section> -<section id="section-10"> +<section id="section-11"> <h2>1.3</h2> <p>DBUtils 1.3 was released on March 3, 2018.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.4 to 3.7.</p> @@ -102,12 +111,12 @@ <h2>1.3</h2> <li><p>This version now supports context handlers for connections and cursors.</p></li> </ul> </section> -<section id="section-11"> +<section id="section-12"> <h2>1.2</h2> <p>DBUtils 1.2 was released on February 5, 2017.</p> <p>It is intended to be used with Python versions 2.6, 2.7 and 3.0 to 3.6.</p> </section> -<section id="section-12"> +<section id="section-13"> <h2>1.1.1</h2> <p>DBUtils 1.1.1 was released on February 4, 2017.</p> <p>It is intended to be used with Python versions 2.3 to 2.7.</p> @@ -121,7 +130,7 @@ <h2>1.1.1</h2> <li><p>Fixed a problem when running under Jython (reported by Vitaly Kruglikov).</p></li> </ul> </section> -<section id="section-13"> +<section id="section-14"> <h2>1.1</h2> <p>DBUtils 1.1 was released on August 14, 2011.</p> <p>Improvements:</p> @@ -150,7 +159,7 @@ <h2>1.1</h2> <li><p>Fixed some minor issues with the <span class="docutils literal">DBUtilsExample</span> for Webware.</p></li> </ul> </section> -<section id="section-14"> +<section id="section-15"> <h2>1.0</h2> <p>DBUtils 1.0 was released on November 29, 2008.</p> <p>It is intended to be used with Python versions 2.2 to 2.6.</p> @@ -183,7 +192,7 @@ <h2>1.0</h2> the MySQLdb module (problem reported by Gregory Pinero).</p></li> </ul> </section> -<section id="section-15"> +<section id="section-16"> <h2>0.9.4</h2> <p>DBUtils 0.9.4 was released on July 7, 2007.</p> <p>This release fixes a problem in the destructor code and has been supplemented @@ -192,7 +201,7 @@ <h2>0.9.4</h2> in the last release, since you can now pass custom creator functions for database connections instead of DB-API 2 modules.</p> </section> -<section id="section-16"> +<section id="section-17"> <h2>0.9.3</h2> <p>DBUtils 0.9.3 was released on May 21, 2007.</p> <p>Changes:</p> @@ -207,7 +216,7 @@ <h2>0.9.3</h2> Added Chinese translation of the User's Guide, kindly contributed by gashero.</p></li> </ul> </section> -<section id="section-17"> +<section id="section-18"> <h2>0.9.2</h2> <p>DBUtils 0.9.2 was released on September 22, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.5.</p> @@ -217,7 +226,7 @@ <h2>0.9.2</h2> storage engine. Accordingly, renamed <span class="docutils literal">SolidPg</span> to <span class="docutils literal">SteadyPg</span>.</p></li> </ul> </section> -<section id="section-18"> +<section id="section-19"> <h2>0.9.1</h2> <p>DBUtils 0.9.1 was released on May 8, 2006.</p> <p>It is intended to be used with Python versions 2.2 to 2.4.</p> @@ -231,7 +240,7 @@ <h2>0.9.1</h2> <li><p>Improved the documentation and added a User's Guide.</p></li> </ul> </section> -<section id="section-19"> +<section id="section-20"> <h2>0.8.1 - 2005-09-13</h2> <p>DBUtils 0.8.1 was released on September 13, 2005.</p> <p>It is intended to be used with Python versions 2.0 to 2.4.</p> diff --git a/docs/changelog.rst b/docs/changelog.rst index 6d0833f..796684e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,17 @@ Changelog for DBUtils +++++++++++++++++++++ +3.1.0 +===== + +DBUtils 3.1.0 was released on March 17, 2024. + +Changes: + +* Support Python version 3.12, cease support for Python 3.6. +* Various small internal improvements and modernizations. + + 3.0.3 ===== diff --git a/docs/main.de.html b/docs/main.de.html index 1a2dda7..e4f5834 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -2,8 +2,8 @@ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="de" lang="de"> <head> <meta charset="utf-8" /> +<meta name="generator" content="Docutils 0.20.1: https://docutils.sourceforge.io/" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> -<meta name="generator" content="Docutils 0.19: https://docutils.sourceforge.io/" /> <title>Benutzeranleitung für DBUtils</title> <link rel="stylesheet" href="doc.css" type="text/css" /> </head> @@ -12,7 +12,7 @@ <h1 class="title">Benutzeranleitung für DBUtils</h1> <dl class="docinfo simple"> <dt class="version">Version<span class="colon">:</span></dt> -<dd class="version">3.0.3</dd> +<dd class="version">3.1.0</dd> <dt class="translations">Translations<span class="colon">:</span></dt> <dd class="translations"><p><a class="reference external" href="main.html">English</a> | German</p> </dd> @@ -137,7 +137,7 @@ <h3>Installation</h3> </section> <section id="anforderungen"> <h2>Anforderungen</h2> -<p>DBUtils unterstützt die <a class="reference external" href="https://www.python.org">Python</a> Versionen 3.6 bis 3.11.</p> +<p>DBUtils unterstützt die <a class="reference external" href="https://www.python.org">Python</a> Versionen 3.7 bis 3.12.</p> <p>Die Module in der Variante für klassisches PyGreSQL benötigen <a class="reference external" href="https://www.pygresql.org/">PyGreSQL</a> Version 4.0 oder höher, während die Module in der allgemeinen Variante für DB-API 2 mit jedem beliebigen Python-Datenbankadapter-Modul zusammenarbeiten, @@ -536,7 +536,7 @@ <h2>Autoren</h2> </section> <section id="copyright-und-lizenz"> <h2>Copyright und Lizenz</h2> -<p>Copyright © 2005-2023 Christoph Zwerschke. +<p>Copyright © 2005-2024 Christoph Zwerschke. Alle Rechte vorbehalten.</p> <p>DBUtils ist freie und quelloffene Software, lizenziert unter der <a class="reference external" href="https://opensource.org/licenses/MIT">MIT-Lizenz</a>.</p> diff --git a/docs/main.de.rst b/docs/main.de.rst index aa800d6..a42853b 100644 --- a/docs/main.de.rst +++ b/docs/main.de.rst @@ -1,7 +1,7 @@ Benutzeranleitung für DBUtils +++++++++++++++++++++++++++++ -:Version: 3.0.3 +:Version: 3.1.0 :Translations: English_ | German .. _English: main.html @@ -98,7 +98,7 @@ herunterzuladen und zu installieren:: Anforderungen ============= -DBUtils unterstützt die Python_ Versionen 3.6 bis 3.11. +DBUtils unterstützt die Python_ Versionen 3.7 bis 3.12. Die Module in der Variante für klassisches PyGreSQL benötigen PyGreSQL_ Version 4.0 oder höher, während die Module in der allgemeinen Variante @@ -583,7 +583,7 @@ Autoren Copyright und Lizenz ==================== -Copyright © 2005-2023 Christoph Zwerschke. +Copyright © 2005-2024 Christoph Zwerschke. Alle Rechte vorbehalten. DBUtils ist freie und quelloffene Software, diff --git a/docs/main.html b/docs/main.html index 50c9ffd..9290bb2 100644 --- a/docs/main.html +++ b/docs/main.html @@ -2,8 +2,8 @@ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta charset="utf-8" /> +<meta name="generator" content="Docutils 0.20.1: https://docutils.sourceforge.io/" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> -<meta name="generator" content="Docutils 0.19: https://docutils.sourceforge.io/" /> <title>DBUtils User's Guide</title> <link rel="stylesheet" href="doc.css" type="text/css" /> </head> @@ -12,7 +12,7 @@ <h1 class="title">DBUtils User's Guide</h1> <dl class="docinfo simple"> <dt class="version">Version<span class="colon">:</span></dt> -<dd class="version">3.0.3</dd> +<dd class="version">3.1.0</dd> <dt class="translations">Translations<span class="colon">:</span></dt> <dd class="translations"><p>English | <a class="reference external" href="main.de.html">German</a></p> </dd> @@ -134,7 +134,7 @@ <h3>Installation</h3> </section> <section id="requirements"> <h2>Requirements</h2> -<p>DBUtils supports <a class="reference external" href="https://www.python.org">Python</a> versions 3.6 to 3.11.</p> +<p>DBUtils supports <a class="reference external" href="https://www.python.org">Python</a> versions 3.7 to 3.12.</p> <p>The modules in the classic PyGreSQL variant need <a class="reference external" href="https://www.pygresql.org/">PyGreSQL</a> version 4.0 or above, while the modules in the universal DB-API 2 variant run with any Python <a class="reference external" href="https://www.python.org/dev/peps/pep-0249/">DB-API 2</a> compliant database interface module.</p> @@ -494,7 +494,7 @@ <h2>Credits</h2> </section> <section id="copyright-and-license"> <h2>Copyright and License</h2> -<p>Copyright © 2005-2023 by Christoph Zwerschke. +<p>Copyright © 2005-2024 by Christoph Zwerschke. All Rights Reserved.</p> <p>DBUtils is free and open source software, licensed under the <a class="reference external" href="https://opensource.org/licenses/MIT">MIT license</a>.</p> diff --git a/docs/main.rst b/docs/main.rst index fa574b7..e2fdbd9 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -1,7 +1,7 @@ DBUtils User's Guide ++++++++++++++++++++ -:Version: 3.0.3 +:Version: 3.1.0 :Translations: English | German_ .. _German: main.de.html @@ -95,7 +95,7 @@ It is even easier to download and install the package in one go using `pip`_:: Requirements ============ -DBUtils supports Python_ versions 3.6 to 3.11. +DBUtils supports Python_ versions 3.7 to 3.12. The modules in the classic PyGreSQL variant need PyGreSQL_ version 4.0 or above, while the modules in the universal DB-API 2 variant run with @@ -543,7 +543,7 @@ Credits Copyright and License ===================== -Copyright © 2005-2023 by Christoph Zwerschke. +Copyright © 2005-2024 by Christoph Zwerschke. All Rights Reserved. DBUtils is free and open source software, diff --git a/docs/make.py b/docs/make.py index c071071..a00977b 100755 --- a/docs/make.py +++ b/docs/make.py @@ -1,15 +1,14 @@ -#!/usr/bin/python3.10 +#!/usr/bin/python3.11 """Build HTML from reST files.""" -from glob import glob from pathlib import Path from docutils.core import publish_file print("Creating the documentation...") -for rst_file in glob('*.rst'): +for rst_file in Path().glob('*.rst'): rst_path = Path(rst_file) name = Path(rst_file).stem lang = Path(name).suffix diff --git a/pyproject.toml b/pyproject.toml index 3426c54..d4f3aa4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [build-system] build-backend = "setuptools.build_meta" requires = [ - "setuptools>=61.2", + "setuptools>=69", ] [project] name = "DBUtils" -version = "3.1.0b1" +version = "3.1.0" description = "Database connections for multi-threaded environments." license = {text = "MIT License"} authors = [{name = "Christoph Zwerschke", email = "cito@online.de"}] @@ -24,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Database", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", @@ -58,19 +59,27 @@ platforms = ["any"] include-package-data = false [tool.ruff] +line-length = 79 +target-version = "py37" + +[tool.ruff.lint] select = [ "A", # flake8-builtins + # "ANN", # flake8-annotations "ARG", # flake8-unused-arguments "B", # flake8-bugbear + # "BLE", # flake8-blind-except "C4", # flake8-comprehensions "C90", # McCabe cyclomatic complexity "COM", # flake8-commas "D", # pydocstyle "DTZ", # flake8-datetimez "E", # pycodestyle + # "EM", # flake8-errmsg "ERA", # eradicate "EXE", # flake8-executable "F", # Pyflakes + # "FBT", # flake8-boolean-trap "G", # flake8-logging-format "I", # isort "ICN", # flake8-import-conventions @@ -84,43 +93,40 @@ select = [ "PT", # flake8-pytest-style "PTH", # flake8-use-pathlib "PYI", # flake8-pyi + # "Q", # flake8-quotes "RET", # flake8-return "RSE", # flake8-raise "RUF", # Ruff-specific rules "S", # flake8-bandit + # "SLF", # flake8-self "SIM", # flake8-simplify "T10", # flake8-debugger "T20", # flake8-print "TCH", # flake8-type-checking "TID", # flake8-tidy-imports + # "TRY", # tryceratops "UP", # pyupgrade "W", # pycodestyle "YTT", # flake8-2020 - # "ANN", # flake8-annotations - # "BLE", # flake8-blind-except - # "EM", # flake8-errmsg - # "FBT", # flake8-boolean-trap - # "Q", # flake8-quotes - # "SLF", # flake8-self - # "TRY", # tryceratops ] # Note: use `ruff rule ...` to see explanations of rules ignore = [ "D203", # no blank line before class docstring "D213", # multi-line docstrings should not start at second line ] -line-length = 79 -target-version = "py37" -[tool.ruff.mccabe] +[tool.ruff.lint.mccabe] max-complexity = 30 -[tool.ruff.pylint] +[tool.ruff.lint.flake8-quotes] +inline-quotes = "double" + +[tool.ruff.lint.pylint] max-args = 12 max-branches = 35 max-statements = 95 -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "docs/*" = [ "INP001", # allow stand-alone scripts "T201", # allow print statements diff --git a/tests/test_threading_local.py b/tests/test_threading_local.py index 2c86525..3871e6c 100644 --- a/tests/test_threading_local.py +++ b/tests/test_threading_local.py @@ -71,7 +71,7 @@ def f(): assert not hasattr(my_data, 'color') class MyLocal(local): - __slots__ = 'number' + __slots__ = ('number',) my_data = MyLocal() my_data.number = 42 diff --git a/tox.ini b/tox.ini index 529d75c..62aab9d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py3{7,8,9,10,11}, ruff, manifest, docs, spell +envlist = py3{7,8,9,10,11,12}, ruff, manifest, docs, spell [testenv] setenv = @@ -9,25 +9,25 @@ commands = pytest {posargs} [testenv:spell] -basepython = python3.10 +basepython = python3.11 deps = codespell commands = codespell . [testenv:ruff] -basepython = python3.10 +basepython = python3.11 deps = ruff commands = - ruff . + ruff check . [testenv:manifest] -basepython = python3.10 +basepython = python3.11 deps = check-manifest commands = check-manifest -v [testenv:docs] -basepython = python3.10 +basepython = python3.11 extras = docs changedir = docs commands = From 58b9fb4f1d0741929369a0f97e771aa45007baa8 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Sun, 17 Mar 2024 14:07:35 +0100 Subject: [PATCH 82/84] Allow setuptools compatible with Python 3.7 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d4f3aa4..02a8ff7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [build-system] build-backend = "setuptools.build_meta" requires = [ - "setuptools>=69", + "setuptools>=68", ] [project] From b01054ba3730af777fbd838a22cc78653c3e2699 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Fri, 8 Nov 2024 20:00:23 +0100 Subject: [PATCH 83/84] Make ruff happy --- dbutils/pooled_db.py | 9 +++------ dbutils/pooled_pg.py | 3 +-- docs/changelog.html | 2 +- docs/main.de.html | 2 +- docs/main.html | 2 +- tests/mock_db.py | 2 +- 6 files changed, 8 insertions(+), 12 deletions(-) diff --git a/dbutils/pooled_db.py b/dbutils/pooled_db.py index be18c94..ed19740 100644 --- a/dbutils/pooled_db.py +++ b/dbutils/pooled_db.py @@ -257,8 +257,7 @@ def __init__( if maxconnections is None: maxconnections = 0 if maxcached: - if maxcached < mincached: - maxcached = mincached + maxcached = max(maxcached, mincached) self._maxcached = maxcached else: self._maxcached = 0 @@ -268,10 +267,8 @@ def __init__( else: self._maxshared = 0 if maxconnections: - if maxconnections < maxcached: - maxconnections = maxcached - if maxconnections < maxshared: - maxconnections = maxshared + maxconnections = max(maxconnections, maxcached) + maxconnections = max(maxconnections, maxshared) self._maxconnections = maxconnections else: self._maxconnections = 0 diff --git a/dbutils/pooled_pg.py b/dbutils/pooled_pg.py index eb73708..18585df 100644 --- a/dbutils/pooled_pg.py +++ b/dbutils/pooled_pg.py @@ -190,8 +190,7 @@ def __init__( if maxcached and maxcached < mincached: maxcached = mincached if maxconnections: - if maxconnections < maxcached: - maxconnections = maxcached + maxconnections = max(maxconnections, maxcached) # Create semaphore for number of allowed connections generally: from threading import Semaphore self._connections = Semaphore(maxconnections) diff --git a/docs/changelog.html b/docs/changelog.html index c30f65b..0707d36 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -2,7 +2,7 @@ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta charset="utf-8" /> -<meta name="generator" content="Docutils 0.20.1: https://docutils.sourceforge.io/" /> +<meta name="generator" content="Docutils 0.21.2: https://docutils.sourceforge.io/" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Changelog for DBUtils</title> <link rel="stylesheet" href="doc.css" type="text/css" /> diff --git a/docs/main.de.html b/docs/main.de.html index e4f5834..632c0ff 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -2,7 +2,7 @@ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="de" lang="de"> <head> <meta charset="utf-8" /> -<meta name="generator" content="Docutils 0.20.1: https://docutils.sourceforge.io/" /> +<meta name="generator" content="Docutils 0.21.2: https://docutils.sourceforge.io/" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Benutzeranleitung für DBUtils</title> <link rel="stylesheet" href="doc.css" type="text/css" /> diff --git a/docs/main.html b/docs/main.html index 9290bb2..5650867 100644 --- a/docs/main.html +++ b/docs/main.html @@ -2,7 +2,7 @@ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta charset="utf-8" /> -<meta name="generator" content="Docutils 0.20.1: https://docutils.sourceforge.io/" /> +<meta name="generator" content="Docutils 0.21.2: https://docutils.sourceforge.io/" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>DBUtils User's Guide</title> <link rel="stylesheet" href="doc.css" type="text/css" /> diff --git a/tests/mock_db.py b/tests/mock_db.py index e3965a6..32ad498 100644 --- a/tests/mock_db.py +++ b/tests/mock_db.py @@ -10,7 +10,7 @@ threadsafety = 2 -@pytest.fixture() +@pytest.fixture def dbapi(): """Get mock DB API 2 module.""" mock_db = sys.modules[__name__] From c68e3c982a971f904bc69bd955fd1d98586be5b3 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke <cito@online.de> Date: Fri, 8 Nov 2024 21:22:35 +0100 Subject: [PATCH 84/84] Improve CSS style for the documentation --- dbutils/steady_db.py | 2 + docs/doc.css | 134 ++++++------------------------------------- docs/docutils.css | 4 +- docs/main.de.html | 6 ++ docs/main.de.rst | 25 ++++---- docs/main.html | 8 ++- docs/main.rst | 22 +++---- 7 files changed, 58 insertions(+), 143 deletions(-) diff --git a/dbutils/steady_db.py b/dbutils/steady_db.py index 52e1b60..8b6f11f 100644 --- a/dbutils/steady_db.py +++ b/dbutils/steady_db.py @@ -651,6 +651,8 @@ def tough_method(*args, **kwargs): if execute: self._setsizes(cursor2) method2 = getattr(cursor2, name) + # if the following call hangs, + # you may have forgotten to call begin() result = method2(*args, **kwargs) if execute: self._clearsizes() diff --git a/docs/doc.css b/docs/doc.css index c815371..c647434 100644 --- a/docs/doc.css +++ b/docs/doc.css @@ -1,39 +1,13 @@ /* - Webware for Python (https://webwareforpython.github.io/w4py/) - - Common style sheet for Webware's documentation pages + Style sheet for DBUtils documentation pages. */ /* First import default style for pages created with Docutils: */ @import url(docutils.css); -/* Customization for Webware goes here: */ +/* Customization for DBUtils goes here: */ -body { - background-color: #FFFFFF; - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 10pt; - padding: 12pt; -} -table { - empty-cells: show; - border-collapse: collapse; - margin: 0 auto; -} -table.doc { - border-spacing: 2px; - border-collapse: separate; - border-style: none; -} -td, th { - font-family: Verdana, Arial, Helvetica, sans-serif; - font-size: 10pt; -} -table.doc td, table.doc th { - padding: 4px; - border-style: none; -} p { margin-top: 6pt; margin-bottom: 6pt; @@ -43,11 +17,9 @@ li { margin-bottom: 6pt; } h1, h2 { - font-family: Verdana, Arial, Helvetica, sans-serif; color: #002352; } h3, h4 { - font-family: Verdana, Arial, Helvetica, sans-serif; color: #002352; } h1 { @@ -59,55 +31,13 @@ h2 { h3 { font-size: 14pt; } -h4 { - font-size: 12pt; -} -h5 { - font-size: 11pt; -} -h6 { - font-size: 10pt; -} -h1.titlebar { - padding: 4pt; - margin-bottom: 12pt; - text-align: center; - color: white; - background-color: #025; -} -h1.title, h1.header { +h1.title { padding: 4pt; margin-bottom: 12pt; text-align: center; border-bottom: 1pt solid #025; padding-bottom: 8pt; } -div.footer { - font-family: Tahoma, Arial, Helvetica, sans-serif; - font-size: 9pt; - text-align: center; - padding: 4pt; - margin-top: 16pt; - border-top: 1pt solid #025; -} -.left { - text-align: left; -} -.right { - text-align: right; -} -.center { - text-align: center; -} -.top { - vertical-align: top; -} -.nowrap { - white-space: nowrap; -} -.contents { - font-family: Tahoma, Arial, Helvetica, sans-serif; -} .contents ul { list-style: none; margin-bottom: 24pt; @@ -115,63 +45,31 @@ div.footer { margin-left: 2em; } .contents ul li { - font-size: 11pt; - margin-bottom: 3pt; + font-size: 14pt; + margin-bottom: 2pt; } .contents ul ul { list-style-type: none; - margin-top: 2pt; - margin-bottom: 2pt; + margin-top: 1pt; + margin-bottom: 1pt; padding-left: 0em; margin-left: 1.5em; } .contents ul ul li { - font-size: 10pt; + font-size: 13pt; margin-bottom: 1pt; } -.contents .topic-title { - font-size: 16pt; -} -span.name { - font-weight: bold; -} -span.filename { - font-family: Tahoma, Arial, Helvetica, sans-serif; - font-size: 9pt; -} -code, .literal, .literal-block, .pre, .py { - font-family: "Andale Mono", "Lucida Console", Monaco, "Courier New", Courier, monospace; - font-size: 10pt; - color: #052; -} -tt.literal, span.pre { - background-color: #FFFFFF; -} -pre.py, pre.literal-block { - margin: 0; - padding: 2pt 1pt 1pt 2pt; - background-color: #F0F0F8; -} -.typed { - font-weight: bold; -} -.error { - color: red; +.contents > p.topic-title { + font-size: 24pt; } .warning { color: brown; } - -/* Configuration documentation: */ - -dl.config { -} -dt.config { -} -dd.config { +.admonition-title { + background-color: #F5F5DC; + padding: 1pt 3pt; } -span.setting { - font-family: Tahoma, Arial, Helvetica, sans-serif; - font-size: 9pt; - font-weight: bold; +.admonition-title::before { + content: "⚠"; + margin-right: .5em; } diff --git a/docs/docutils.css b/docs/docutils.css index bebeb07..50d0088 100644 --- a/docs/docutils.css +++ b/docs/docutils.css @@ -1,2 +1,2 @@ -/* CSS 3.1 style sheet for the output of Docutils 0.17 HTML writer. */ -body{margin:0;background-color:#dbdbdb}main,footer,header{line-height:1.3;max-width:50rem;padding:1px 2%;margin:auto}main{counter-reset:table figure;background-color:white}footer,header{font-size:smaller;padding:.5em 2%;border:0}hr.docutils{width:80%;margin-top:1em;margin-bottom:1em;clear:both}p,ol,ul,dl,li,dd,div.line-block,div.topic,table{margin-top:.5em;margin-bottom:.5em}p:first-child{margin-top:0}p:last-child{margin-bottom:0}h1,h2,h3,h4,h5,h6,dl>dd{margin-bottom:.5em}dl>dd,ol>li,dd>ul:only-child,dd>ol:only-child{padding-left:1em}dl.description>dt{font-weight:bold;clear:left;float:left;margin:0;padding:0;padding-right:.5em}dl.field-list.narrow>dd{margin-left:5em}dl.field-list.run-in>dd p{display:block}div.abstract p.topic-title{text-align:center}div.dedication{margin:2em 5em;text-align:center;font-style:italic}div.dedication p.topic-title{font-style:normal}pre.literal-block,pre.doctest-block,pre.math,pre.code{font-family:monospace}blockquote>table,div.topic>table{margin-top:0;margin-bottom:0}blockquote p.attribution,div.topic p.attribution{text-align:right;margin-left:20%}table tr{text-align:left}table.booktabs{border:0;border-top:2px solid;border-bottom:2px solid;border-collapse:collapse}table.booktabs *{border:0}table.booktabs th{border-bottom:thin solid}table.numbered>caption:before{counter-increment:table;content:"Table " counter(table) ": ";font-weight:bold}dl.footnote{padding-left:1ex;border-left:solid;border-left-width:thin}figure.align-left,img.align-left,video.align-left,object.align-left{clear:left;float:left;margin-right:1em}figure.align-right,img.align-right,video.align-right,object.align-right{clear:right;float:right;margin-left:1em}h1,h2,h3,h4,footer,header{clear:both}figure.numbered>figcaption>p:before{counter-increment:figure;content:"Figure " counter(figure) ": ";font-weight:bold}.caution p.admonition-title,.attention p.admonition-title,.danger p.admonition-title,.error p.admonition-title,.warning p.admonition-title,div.error{color:red}aside.sidebar{width:30%;max-width:26em;margin-left:1em;margin-right:-2%;background-color:#ffe}pre.code{padding:.7ex}pre.code,code{background-color:#eee}pre.code .comment,code .comment{color:#5c6576}pre.code .keyword,code .keyword{color:#3b0d06;font-weight:bold}pre.code .literal.string,code .literal.string{color:#0c5404}pre.code .name.builtin,code .name.builtin{color:#352b84}pre.code .deleted,code .deleted{background-color:#deb0a1}pre.code .inserted,code .inserted{background-color:#a3d289}a{text-decoration:none} \ No newline at end of file +/* CSS 3 style sheet for the output of Docutils 0.21 HTML writer. */ +div.dedication,nav.contents{padding:0;font-style:italic}h1.title,table tr{text-align:left}.footnote,pre.code,pre.doctest-block,pre.literal-block,pre.math{overflow:auto}body{font-family:Georgia,serif;background-color:#fafaf6;font-size:1.2em;line-height:1.4;margin:auto}main{counter-reset:figure table}footer,header,main{padding:.5em 5%;background-color:#fefef8;max-width:100rem}.citation,.footnote,.topic,div.line-block,dl,h1,h2,h3,h4,h5,h6,li,ol,p,table,ul{margin-top:.5em;margin-bottom:.5em}.topic,blockquote,figure{margin:.5em 2%;padding-left:1em}dl>dd{margin-bottom:.5em}p:first-child{margin-top:0}p:last-child{margin-bottom:0}div.line-block div.line-block,dl.option-list,figure>img,pre.code,pre.doctest-block,pre.literal-block,pre.math{margin-left:calc(2% + 1em)}footer,header{font-size:smaller}h2,h3,h4,p.section-subtitle,p.sidebar-subtitle,p.sidebar-title,p.subtitle,p.topic-title{font-weight:400;font-style:italic;text-align:left}.sectnum{font-style:normal}h1.title{margin-top:2.4em;margin-bottom:2em;font-size:2.4em}h1+p.subtitle{margin-top:-2em;margin-bottom:2em;font-size:2em}section{margin-top:2em}.contents>p.topic-title,h2{font-size:2.2em}h2+p.section-subtitle{font-size:1.6em}h3{font-size:1.2em}h3+p.section-subtitle{font-size:1.1em}figure.marginal>figcaption,h4,p.section-subtitle{font-size:1em}div.dedication{margin-left:0;font-size:1.2em}div.dedication p.topic-title{display:none}.topic p.attribution,blockquote p.attribution{text-align:right}ul.auto-toc>li>p{padding-left:1em;text-indent:-1em}nav.contents ul{padding-left:1em}hr{border:0;border-top:1px solid #ccc;margin:1em 10%}ol,ul{padding-left:1.1em}dd{margin-left:1.5em}dd>dl:first-child,dd>ol:first-child,dd>ul:first-child{clear:left}dl.docinfo>dd,dl.field-list>dd,dl.option-list>dd{margin-left:4em}dl.field-list.narrow>dd{margin-left:3em}dl.field-list.run-in>dd p{display:block}dl.description>dt,dl.docinfo>dt,dl.field-list>dt{font-weight:400;font-style:italic}dl.description>dt{clear:left;float:left;margin:0;padding:0 .5em 0 0}dl.description>dd:after{display:block;content:"";clear:both}.citation-list,.footnote-list{display:contents}.citation{padding-left:1.5em}.citation .label{margin-left:-1.5em}figure{display:flex;flex-wrap:wrap;align-items:flex-start}figure.fullwidth>img,figure>img{margin:0 .5em .5em 0;padding:0}figcaption{font-size:.8em}.fullwidth>figcaption{font-size:inherit}figure.numbered>figcaption>p:before{counter-increment:figure;content:"Figure " counter(figure) ": "}table.booktabs{border-top:2px solid;border-bottom:2px solid}table.booktabs *{border:0}table.booktabs th{border-bottom:thin solid}table.numbered>caption:before{counter-increment:table;content:"Table " counter(table) ": "}.admonition,.system-message{border-style:solid;border-color:silver;border-width:thin;margin:1em 0;padding:.5em}.attention p.admonition-title,.caution p.admonition-title,.danger p.admonition-title,.warning p.admonition-title,div.error{color:maroon}code .comment,pre.code .comment{color:#5c6576}code .keyword,pre.code .keyword{color:#3b0d06;font-weight:700}code .literal.string,pre.code .literal.string{color:#0c5404}code .name.builtin,pre.code .name.builtin{color:#352b84}code .deleted,pre.code .deleted{background-color:#deb0a1}code .inserted,pre.code .inserted{background-color:#a3d289}.sans{font-family:"Gill Sans","Gill Sans MT",Calibri,"Lucida Sans","Noto Sans",sans-serif;letter-spacing:.02em}a{color:inherit}a:link,a:link:hover{text-decoration:underline}.backrefs a:link,.contents a:link,a.citation-reference:link,a.image-reference:link,a.toc-backref:link,a[href^="#system-message"],a[role=doc-backlink]:link,a[role=doc-noteref]:link{text-decoration:none}.contents>p.topic-title,.fullwidth,footer,h1,h2,h3,header,hr.docutils{clear:both}div.align-left,figure.align-left,img.align-left,svg.align-left,table.align-left,video.align-left{margin-left:0;padding-left:0;padding-right:.5em;clear:left;float:left}figure.align-left>img{margin-left:0;padding-left:0}div.align-right,img.align-right,svg.align-right,video.align-right{padding-left:.5em;clear:right;float:right}figure.align-right{clear:right;float:right}figure.align-right>img{justify-self:right;padding:0}table.align-right{margin-right:2.5%}figure.align-center{align-content:center;justify-content:center}figure.align-center>img{padding-left:0;justify-self:center}.admonition.marginal,.marginal,.topic.marginal,aside.sidebar{background-color:#efefea;box-sizing:border-box;margin-left:2%;margin-right:0;padding:.5em;font-size:.8em}aside.sidebar{background-color:inherit}.footnote{font-size:smaller}@media (min-width:35em){footer,header,main{padding:.5em calc(15% - 3rem);line-height:1.6}.admonition.marginal,.marginal,.topic.marginal,aside.sidebar{max-width:45%;float:right;clear:right}dl.docinfo>dd,dl.field-list>dd,dl.option-list>dd{margin-left:6em}}@media (min-width:65em){main,section{display:grid;grid-template-columns:[content] minmax(0,6fr) [margin] 3fr [end];grid-column-gap:calc(3em + 1%)}main>section,section>section{grid-column:1/end}footer,header,main{padding-right:5%}section>figure{display:contents}.citation.align-left,.footnote.align-left,figure>img,main>*,section>*{grid-column:content}.citation.align-left{font-size:1em;padding-left:1.5em}.citation.align-left .label{margin-left:-1.5em}figure>img{margin:.5em 2%;padding-left:1em}.admonition.marginal,.citation,.footnote,.marginal,.topic.marginal,aside.sidebar,figcaption{grid-column:margin;width:auto;max-width:55em;margin:.5em 0;border:none;padding:0;font-size:.8em;text-align:initial;background-color:inherit}.admonition.marginal{padding:.5em}figure.marginal{display:block;margin:.5em 0}.citation,.footnote{padding-left:0}.citation .label,.footnote .label{margin-left:0}.fullwidth,.fullwidth figcaption,.fullwidth img,aside.system-message,div.abstract,div.dedication,dl.docinfo,h1.title,nav.contents,p.subtitle,pre{grid-column:content/end;margin-right:calc(10% - 3rem);max-width:55em}}@media (min-width:100em){footer,header,main{padding-left:30%}main>nav.contents{position:fixed;top:0;left:0;box-sizing:border-box;width:25%;height:100vh;margin:0;background-color:#fafaf6;padding:5.5em 2%;overflow:auto}main>nav.contents>*{padding-left:0}} \ No newline at end of file diff --git a/docs/main.de.html b/docs/main.de.html index 632c0ff..3627cf0 100644 --- a/docs/main.de.html +++ b/docs/main.de.html @@ -321,11 +321,14 @@ <h3>PersistentDB (persistent_db)</h3> Stattdessen wird die Verbindung automatisch dann geschlossen, wenn der Thread endet. Sie können dieses Verhalten ändern, indem Sie den Parameter namens <span class="docutils literal">closeable</span> setzen.</p> +<aside class="admonition warning"> +<p class="admonition-title">Warnung</p> <p>Bitte beachten Sie, dass Transaktionen explizit durch Aufruf der Methode <span class="docutils literal">begin()</span> eingeleitet werden müssen. Hierdurch wird sichergestellt, dass das transparente Neueröffnen von Verbindungen bis zum Ende der Transaktion ausgesetzt wird, und dass die Verbindung zurückgerollt wird, before sie vom gleichen Thread erneut benutzt wird.</p> +</aside> <p>Das Holen einer Verbindung kann etwas beschleunigt werden, indem man den Parameter <span class="docutils literal">threadlocal</span> auf <span class="docutils literal">threading.local</span> setzt; dies könnte aber in einigen Umgebungen nicht funktionieren (es ist zum Beispiel bekannt, dass @@ -428,12 +431,15 @@ <h3>PooledDB (pooled_db)</h3> with db.cursor() as cur: cur.execute(...) res = cur.fetchone()</pre> +<aside class="admonition warning"> +<p class="admonition-title">Warnung</p> <p>Bitte beachten Sie, dass Transaktionen explizit durch Aufruf der Methode <span class="docutils literal">begin()</span> eingeleitet werden müssen. Hierdurch wird sichergestellt, dass die Verbindung nicht mehr mit anderen Threads geteilt wird, dass das transparente Neueröffnen von Verbindungen bis zum Ende der Transaktion ausgesetzt wird, und dass die Verbindung zurückgerollt wird, bevor sie wieder an den Verbindungspool zurückgegeben wird.</p> +</aside> </section> </section> <section id="besonderheiten-bei-der-benutzung"> diff --git a/docs/main.de.rst b/docs/main.de.rst index a42853b..ce97364 100644 --- a/docs/main.de.rst +++ b/docs/main.de.rst @@ -317,11 +317,12 @@ Stattdessen wird die Verbindung automatisch dann geschlossen, wenn der Thread endet. Sie können dieses Verhalten ändern, indem Sie den Parameter namens ``closeable`` setzen. -Bitte beachten Sie, dass Transaktionen explizit durch Aufruf der Methode -``begin()`` eingeleitet werden müssen. Hierdurch wird sichergestellt, dass -das transparente Neueröffnen von Verbindungen bis zum Ende der Transaktion -ausgesetzt wird, und dass die Verbindung zurückgerollt wird, before sie vom -gleichen Thread erneut benutzt wird. +.. warning:: + Bitte beachten Sie, dass Transaktionen explizit durch Aufruf der Methode + ``begin()`` eingeleitet werden müssen. Hierdurch wird sichergestellt, dass + das transparente Neueröffnen von Verbindungen bis zum Ende der Transaktion + ausgesetzt wird, und dass die Verbindung zurückgerollt wird, before sie vom + gleichen Thread erneut benutzt wird. Das Holen einer Verbindung kann etwas beschleunigt werden, indem man den Parameter ``threadlocal`` auf ``threading.local`` setzt; dies könnte aber in @@ -452,13 +453,13 @@ Sie können dies auch durch Verwendung von Kontext-Managern vereinfachen:: cur.execute(...) res = cur.fetchone() - -Bitte beachten Sie, dass Transaktionen explizit durch Aufruf der Methode -``begin()`` eingeleitet werden müssen. Hierdurch wird sichergestellt, -dass die Verbindung nicht mehr mit anderen Threads geteilt wird, dass das -transparente Neueröffnen von Verbindungen bis zum Ende der Transaktion -ausgesetzt wird, und dass die Verbindung zurückgerollt wird, bevor sie -wieder an den Verbindungspool zurückgegeben wird. +.. warning:: + Bitte beachten Sie, dass Transaktionen explizit durch Aufruf der Methode + ``begin()`` eingeleitet werden müssen. Hierdurch wird sichergestellt, + dass die Verbindung nicht mehr mit anderen Threads geteilt wird, dass das + transparente Neueröffnen von Verbindungen bis zum Ende der Transaktion + ausgesetzt wird, und dass die Verbindung zurückgerollt wird, bevor sie + wieder an den Verbindungspool zurückgegeben wird. Besonderheiten bei der Benutzung diff --git a/docs/main.html b/docs/main.html index 5650867..dede0dc 100644 --- a/docs/main.html +++ b/docs/main.html @@ -298,10 +298,13 @@ <h3>PersistentDB (persistent_db)</h3> contrary to the intent of having persistent connections. Instead, the connection will be automatically closed when the thread dies. You can change this behavior by setting the <span class="docutils literal">closeable</span> parameter.</p> +<aside class="admonition warning"> +<p class="admonition-title">Warning</p> <p>Note that you need to explicitly start transactions by calling the <span class="docutils literal">begin()</span> method. This ensures that the transparent reopening will be suspended until the end of the transaction, and that the connection will be rolled back before being reused by the same thread.</p> +</aside> <p>By setting the <span class="docutils literal">threadlocal</span> parameter to <span class="docutils literal">threading.local</span>, getting connections may become a bit faster, but this may not work in all environments (for instance, <span class="docutils literal">mod_wsgi</span> is known to cause problems @@ -374,7 +377,7 @@ <h3>PooledDB (pooled_db)</h3> <pre class="literal-block">db = pool.dedicated_connection()</pre> <p>If you don't need it anymore, you should immediately return it to the pool with <span class="docutils literal">db.close()</span>. You can get another connection in the same way.</p> -<p><em>Warning:</em> In a threaded environment, never do the following:</p> +<p>⚠ Warning: In a threaded environment, never do the following:</p> <pre class="literal-block">pool.connection().cursor().execute(...)</pre> <p>This would release the connection too early for reuse which may be fatal if the connections are not thread-safe. Make sure that the connection @@ -390,11 +393,14 @@ <h3>PooledDB (pooled_db)</h3> with db.cursor() as cur: cur.execute(...) res = cur.fetchone()</pre> +<aside class="admonition warning"> +<p class="admonition-title">Warning</p> <p>Note that you need to explicitly start transactions by calling the <span class="docutils literal">begin()</span> method. This ensures that the connection will not be shared with other threads, that the transparent reopening will be suspended until the end of the transaction, and that the connection will be rolled back before being given back to the connection pool.</p> +</aside> </section> </section> <section id="advanced-usage"> diff --git a/docs/main.rst b/docs/main.rst index e2fdbd9..816ae80 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -294,10 +294,11 @@ contrary to the intent of having persistent connections. Instead, the connection will be automatically closed when the thread dies. You can change this behavior by setting the ``closeable`` parameter. -Note that you need to explicitly start transactions by calling the -``begin()`` method. This ensures that the transparent reopening will be -suspended until the end of the transaction, and that the connection -will be rolled back before being reused by the same thread. +.. warning:: + Note that you need to explicitly start transactions by calling the + ``begin()`` method. This ensures that the transparent reopening will be + suspended until the end of the transaction, and that the connection + will be rolled back before being reused by the same thread. By setting the ``threadlocal`` parameter to ``threading.local``, getting connections may become a bit faster, but this may not work in all @@ -392,7 +393,7 @@ Instead of this, you can also get a dedicated connection as follows:: If you don't need it anymore, you should immediately return it to the pool with ``db.close()``. You can get another connection in the same way. -*Warning:* In a threaded environment, never do the following:: +⚠ Warning: In a threaded environment, never do the following:: pool.connection().cursor().execute(...) @@ -414,11 +415,12 @@ You can also use context managers for simpler code:: cur.execute(...) res = cur.fetchone() -Note that you need to explicitly start transactions by calling the -``begin()`` method. This ensures that the connection will not be shared -with other threads, that the transparent reopening will be suspended -until the end of the transaction, and that the connection will be rolled -back before being given back to the connection pool. +.. warning:: + Note that you need to explicitly start transactions by calling the + ``begin()`` method. This ensures that the connection will not be shared + with other threads, that the transparent reopening will be suspended + until the end of the transaction, and that the connection will be rolled + back before being given back to the connection pool. Advanced Usage