Skip to content

Commit 2617fde

Browse files
committed
-SSCursor no longer attempts to expire un-collected rows within __del__,
delaying termination of an interrupted program; cleanup of uncollected rows is left to the Connection on next execute, which emits a warning at that time. (fixes #287)
1 parent 6b40bea commit 2617fde

7 files changed

Lines changed: 135 additions & 8 deletions

File tree

CHANGELOG

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ Changes
44
0.6.4 -Support "LOAD LOCAL INFILE". Thanks @wraziens
55
-Show MySQL warnings after execute query.
66
-Fix MySQLError may be wrapped with OperationalError while connectiong. (#274)
7+
-SSCursor no longer attempts to expire un-collected rows within __del__,
8+
delaying termination of an interrupted program; cleanup of uncollected
9+
rows is left to the Connection on next execute, which emits a
10+
warning at that time. (#287)
711

812
0.6.3 -Fixed multiple result sets with SSCursor.
913
-Fixed connection timeout.
@@ -47,7 +51,7 @@ Changes
4751
-Removed DeprecationWarnings
4852
-Ran against the MySQLdb unit tests to check for bugs
4953
-Added support for client_flag, charset, sql_mode, read_default_file,
50-
use_unicode, cursorclass, init_command, and connect_timeout.
54+
use_unicode, cursorclass, init_command, and connect_timeout.
5155
-Refactoring for some more compatibility with MySQLdb including a fake
5256
pymysql.version_info attribute.
5357
-Now runs with no warnings with the -3 command-line switch

pymysql/_compat.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
PYPY = hasattr(sys, 'pypy_translation_info')
55
JYTHON = sys.platform.startswith('java')
66
IRONPYTHON = sys.platform == 'cli'
7+
CPYTHON = not PYPY and not JYTHON and not IRONPYTHON
78

89
if PY2:
910
range_type = xrange

pymysql/connections.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import socket
1515
import struct
1616
import sys
17+
import warnings
1718

1819
try:
1920
import ssl
@@ -923,6 +924,7 @@ def _execute_command(self, command, sql):
923924
# If the last query was unbuffered, make sure it finishes before
924925
# sending new commands
925926
if self._result is not None and self._result.unbuffered_active:
927+
warnings.warn("Previous unbuffered result was left incomplete")
926928
self._result._finish_unbuffered_query()
927929

928930
if isinstance(sql, text_type):

pymysql/cursors.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,6 @@ def __init__(self, connection):
4040
self._result = None
4141
self._rows = None
4242

43-
def __del__(self):
44-
'''
45-
When this gets GC'd close it.
46-
'''
47-
self.close()
48-
4943
def close(self):
5044
'''
5145
Closing a cursor just exhausts all remaining data.

pymysql/tests/base.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
import gc
12
import os
23
import json
34
import pymysql
5+
6+
from .._compat import CPYTHON
7+
8+
49
try:
510
import unittest2 as unittest
611
except ImportError:
712
import unittest
13+
import warnings
814

915
class PyMySQLTestCase(unittest.TestCase):
1016
# You can specify your test environment creating a file named
@@ -23,7 +29,46 @@ def setUp(self):
2329
self.connections = []
2430
for params in self.databases:
2531
self.connections.append(pymysql.connect(**params))
32+
self.addCleanup(self._teardown_connections)
2633

27-
def tearDown(self):
34+
def _teardown_connections(self):
2835
for connection in self.connections:
2936
connection.close()
37+
38+
def safe_create_table(self, connection, tablename, ddl, cleanup=False):
39+
"""create a table.
40+
41+
Ensures any existing version of that table
42+
is first dropped.
43+
44+
Also adds a cleanup rule to drop the table after the test
45+
completes.
46+
47+
"""
48+
49+
cursor = connection.cursor()
50+
51+
with warnings.catch_warnings():
52+
warnings.simplefilter("ignore")
53+
cursor.execute("drop table if exists test")
54+
cursor.execute("create table test (data varchar(10))")
55+
cursor.close()
56+
if cleanup:
57+
self.addCleanup(self.drop_table, connection, tablename)
58+
59+
def drop_table(self, connection, tablename):
60+
cursor = connection.cursor()
61+
with warnings.catch_warnings():
62+
warnings.simplefilter("ignore")
63+
cursor.execute("drop table if exists %s" % tablename)
64+
cursor.close()
65+
66+
def safe_gc_collect(self):
67+
"""Ensure cycles are collected via gc.
68+
69+
Runs additional times on non-CPython platforms.
70+
71+
"""
72+
gc.collect()
73+
if not CPYTHON:
74+
gc.collect()

pymysql/tests/test_DictCursor.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ def tearDown(self):
3232
c.execute("drop table dictcursor")
3333
super(TestDictCursor, self).tearDown()
3434

35+
def _ensure_cursor_expired(self, cursor):
36+
pass
37+
3538
def test_DictCursor(self):
3639
bob, jim, fred = self.bob.copy(), self.jim.copy(), self.fred.copy()
3740
#all assert test compare to the structure as would come out from MySQLdb
@@ -45,6 +48,8 @@ def test_DictCursor(self):
4548
c.execute("SELECT * from dictcursor where name='bob'")
4649
r = c.fetchone()
4750
self.assertEqual(bob, r, "fetchone via DictCursor failed")
51+
self._ensure_cursor_expired(c)
52+
4853
# same again, but via fetchall => tuple)
4954
c.execute("SELECT * from dictcursor where name='bob'")
5055
r = c.fetchall()
@@ -65,6 +70,7 @@ def test_DictCursor(self):
6570
c.execute("SELECT * from dictcursor")
6671
r = c.fetchmany(2)
6772
self.assertEqual([bob, jim], r, "fetchmany failed via DictCursor")
73+
self._ensure_cursor_expired(c)
6874

6975
def test_custom_dict(self):
7076
class MyDict(dict): pass
@@ -81,6 +87,7 @@ class MyDictCursor(self.cursor_type):
8187
cur.execute("SELECT * FROM dictcursor WHERE name='bob'")
8288
r = cur.fetchone()
8389
self.assertEqual(bob, r, "fetchone() returns MyDictCursor")
90+
self._ensure_cursor_expired(cur)
8491

8592
cur.execute("SELECT * FROM dictcursor")
8693
r = cur.fetchall()
@@ -96,11 +103,14 @@ class MyDictCursor(self.cursor_type):
96103
r = cur.fetchmany(2)
97104
self.assertEqual([bob, jim], r,
98105
"list failed via MyDictCursor")
106+
self._ensure_cursor_expired(cur)
99107

100108

101109
class TestSSDictCursor(TestDictCursor):
102110
cursor_type = pymysql.cursors.SSDictCursor
103111

112+
def _ensure_cursor_expired(self, cursor):
113+
list(cursor.fetchall_unbuffered())
104114

105115
if __name__ == "__main__":
106116
import unittest

pymysql/tests/test_cursor.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import warnings
2+
3+
from pymysql.tests import base
4+
import pymysql.cursors
5+
6+
class CursorTest(base.PyMySQLTestCase):
7+
def setUp(self):
8+
super(CursorTest, self).setUp()
9+
10+
conn = self.connections[0]
11+
self.safe_create_table(
12+
conn,
13+
"test", "create table test (data varchar(10))",
14+
cleanup=True)
15+
cursor = conn.cursor()
16+
cursor.execute(
17+
"insert into test (data) values "
18+
"('row1'), ('row2'), ('row3'), ('row4'), ('row5')")
19+
cursor.close()
20+
self.test_connection = pymysql.connect(**self.databases[0])
21+
self.addCleanup(self.test_connection.close)
22+
23+
def test_cleanup_rows_unbuffered(self):
24+
conn = self.test_connection
25+
cursor = conn.cursor(pymysql.cursors.SSCursor)
26+
27+
cursor.execute("select * from test as t1, test as t2")
28+
for counter, row in enumerate(cursor):
29+
if counter > 10:
30+
break
31+
32+
del cursor
33+
self.safe_gc_collect()
34+
35+
c2 = conn.cursor()
36+
37+
with warnings.catch_warnings(record=True) as log:
38+
warnings.filterwarnings("always")
39+
40+
c2.execute("select 1")
41+
42+
self.assertGreater(len(log), 0)
43+
self.assertEqual(
44+
"Previous unbuffered result was left incomplete",
45+
str(log[-1].message))
46+
self.assertEqual(
47+
c2.fetchone(), (1,)
48+
)
49+
self.assertIsNone(c2.fetchone())
50+
51+
def test_cleanup_rows_buffered(self):
52+
conn = self.test_connection
53+
cursor = conn.cursor(pymysql.cursors.Cursor)
54+
55+
cursor.execute("select * from test as t1, test as t2")
56+
for counter, row in enumerate(cursor):
57+
if counter > 10:
58+
break
59+
60+
del cursor
61+
self.safe_gc_collect()
62+
63+
c2 = conn.cursor()
64+
65+
c2.execute("select 1")
66+
67+
self.assertEqual(
68+
c2.fetchone(), (1,)
69+
)
70+
self.assertIsNone(c2.fetchone())
71+

0 commit comments

Comments
 (0)