Skip to content

Commit 9a4514d

Browse files
committed
Kill off get_cursor
Turns out we only need get_transaction.
1 parent 526400f commit 9a4514d

File tree

2 files changed

+46
-149
lines changed

2 files changed

+46
-149
lines changed

postgres.py

Lines changed: 23 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -66,28 +66,17 @@
6666
Eighty percent of your database usage should be covered by the simple
6767
:py:meth:`~postgres.Postgres.run`, :py:meth:`~postgres.Postgres.all`,
6868
:py:meth:`~postgres.Postgres.one_or_zero` API introduced above. For the other
69-
20%, :py:mod:`postgres` provides context managers for working at increasingly
70-
lower levels of abstraction. The lowest level of abstraction in
69+
20%, :py:mod:`postgres` provides two context managers for working at
70+
increasingly lower levels of abstraction. The lowest level of abstraction in
7171
:py:mod:`postgres` is a :py:mod:`psycopg2` `connection pool
7272
<http://initd.org/psycopg/docs/pool.html>`_ that we configure and manage for
7373
you. Everything in :py:mod:`postgres`, both the simple API and the context
7474
managers, uses this connection pool.
7575
76-
Here's how to work directly with a :py:mod:`psycogpg2` `cursor
76+
Use the :py:func:`~postgres.Postgres.get_transaction` context manager to work
77+
directly with a :py:mod:`psycogpg2` `cursor
7778
<http://initd.org/psycopg/docs/cursor.html>`_ while still taking advantage of
78-
connection pooling:
79-
80-
>>> with db.get_cursor() as cursor:
81-
... cursor.execute("SELECT * FROM foo ORDER BY bar")
82-
... cursor.fetchall()
83-
...
84-
[{'bar': 'baz'}, {'bar': 'buz'}]
85-
86-
A cursor you get from :py:func:`~postgres.Postgres.get_cursor` has
87-
:py:attr:`autocommit` turned on for its connection, so every call you make
88-
using such a cursor will be isolated in a separate transaction. Need to include
89-
multiple calls in a single transaction? Use the
90-
:py:func:`~postgres.Postgres.get_transaction` context manager:
79+
connection pooling and automatic transaction management:
9180
9281
>>> with db.get_transaction() as txn:
9382
... txn.execute("INSERT INTO foo VALUES ('blam')")
@@ -222,8 +211,6 @@ class Postgres(object):
222211
:param int minconn: The minimum size of the connection pool
223212
:param int maxconn: The maximum size of the connection pool
224213
:param cursor_factory: Defaults to :py:class:`~psycopg2.extras.RealDictCursor`
225-
:param strict_one: The default :py:attr:`strict` parameter for :py:meth:`~postgres.Postgres.one`
226-
:type strict_one: :py:class:`bool`
227214
228215
This is the main object that :py:mod:`postgres` provides, and you should
229216
have one instance per process for each PostgreSQL database your process
@@ -252,7 +239,7 @@ class Postgres(object):
252239
:py:meth:`~postgres.Postgres.all`, and
253240
:py:meth:`~postgres.Postgres.one_or_zero`, were chosen to be short and
254241
memorable, and to not conflict with the DB-API 2.0 :py:meth:`execute`,
255-
:py:meth:`fetchone`, and :py:meth:`fetchall` methods, which have slightly
242+
:py:meth:`fetchall`, and :py:meth:`fetchone` methods, which have slightly
256243
different semantics (under DB-API 2.0 you call :py:meth:`execute` on a
257244
cursor and then call one of the :py:meth:`fetch*` methods on the same
258245
cursor to retrieve rows; with our simple API there is no second
@@ -261,15 +248,15 @@ class Postgres(object):
261248
:py:meth:`get_` to set them apart from the simple-case API. Note that when
262249
working inside a block under one of the context managers, you're using
263250
DB-API 2.0 (:py:meth:`execute` + :py:meth:`fetch*`), not our simple API
264-
(:py:meth:`~postgres.Postgres.run` / :py:meth:`~postgres.Postgres.one` /
265-
:py:meth:`~postgres.Postgres.all`).
251+
(:py:meth:`~postgres.Postgres.run`, :py:meth:`~postgres.Postgres.all`,
252+
:py:meth:`~postgres.Postgres.one_or_zero`).
266253
267254
.. _this ticket: https://github.com/gittip/postgres.py/issues/16
268255
269256
"""
270257

271258
def __init__(self, url, minconn=1, maxconn=10, \
272-
cursor_factory=RealDictCursor, strict_one=None):
259+
cursor_factory=RealDictCursor):
273260
if url.startswith("postgres://"):
274261
dsn = url_to_dsn(url)
275262

@@ -281,10 +268,6 @@ def __init__(self, url, minconn=1, maxconn=10, \
281268
, connection_factory=Connection
282269
)
283270

284-
if strict_one not in (True, False, None):
285-
raise ValueError("strict_one must be True, False, or None.")
286-
self.strict_one = strict_one
287-
288271

289272
def run(self, sql, parameters=None):
290273
"""Execute a query and discard any results.
@@ -299,8 +282,8 @@ def run(self, sql, parameters=None):
299282
>>> db.run("INSERT INTO foo VALUES ('buz')")
300283
301284
"""
302-
with self.get_cursor() as cursor:
303-
cursor.execute(sql, parameters)
285+
with self.get_transaction() as txn:
286+
txn.execute(sql, parameters)
304287

305288

306289
def all(self, sql, parameters=None):
@@ -318,9 +301,9 @@ def all(self, sql, parameters=None):
318301
buz
319302
320303
"""
321-
with self.get_cursor() as cursor:
322-
cursor.execute(sql, parameters)
323-
return cursor.fetchall()
304+
with self.get_transaction() as txn:
305+
txn.execute(sql, parameters)
306+
return txn.fetchall()
324307

325308

326309
def one_or_zero(self, sql, parameters=None, zero=None):
@@ -366,25 +349,6 @@ def _some(self, sql, parameters=None, lo=0, hi=1):
366349

367350
return txn.fetchone()
368351

369-
370-
def get_cursor(self, *a, **kw):
371-
"""Return a :py:class:`~postgres.CursorContextManager` that uses our
372-
connection pool.
373-
374-
This gets you a cursor with :py:attr:`autocommit` turned on on its
375-
connection. The context manager closes the cursor when the block ends.
376-
377-
Use this when you want a simple cursor.
378-
379-
>>> with db.get_cursor() as cursor:
380-
... cursor.execute("SELECT * FROM foo")
381-
... cursor.rowcount
382-
...
383-
2
384-
385-
"""
386-
return CursorContextManager(self.pool, *a, **kw)
387-
388352
def get_transaction(self, *a, **kw):
389353
"""Return a :py:class:`~postgres.TransactionContextManager` that uses
390354
our connection pool.
@@ -431,11 +395,9 @@ class Connection(psycopg2.extensions.connection):
431395
"""This is a subclass of :py:class:`psycopg2.extensions.connection`.
432396
433397
:py:class:`Postgres` uses this class as the :py:attr:`connection_factory`
434-
for its connection pool. Here are the differences from the base class:
435-
436-
- We set :py:attr:`autocommit` to :py:const:`True`.
437-
- We set the client encoding to ``UTF-8``.
438-
- We use :py:attr:`self.cursor_factory`.
398+
for its connection pool. We use this subclass to support the
399+
:py:attr:`cursor_factory` parameter to the :py:class:`Postgres`
400+
constructor, and to ensure that the client encoding is ``UTF-8``.
439401
440402
"""
441403

@@ -444,7 +406,6 @@ class Connection(psycopg2.extensions.connection):
444406
def __init__(self, *a, **kw):
445407
psycopg2.extensions.connection.__init__(self, *a, **kw)
446408
self.set_client_encoding('UTF-8')
447-
self.autocommit = True
448409

449410
def cursor(self, *a, **kw):
450411
if 'cursor_factory' not in kw:
@@ -455,38 +416,6 @@ def cursor(self, *a, **kw):
455416
# Context Managers
456417
# ================
457418

458-
class CursorContextManager(object):
459-
"""Instantiated once per :py:func:`~postgres.Postgres.get_cursor` call.
460-
461-
The return value of :py:func:`CursorContextManager.__enter__` is a
462-
:py:class:`psycopg2.extras.RealDictCursor`. Any positional and keyword
463-
arguments to our constructor are passed through to the cursor constructor.
464-
The :py:class:`~postgres.Connection` underlying the cursor is checked
465-
out of the connection pool when the block starts, and checked back in when
466-
the block ends. Also when the block ends, the cursor is closed.
467-
468-
"""
469-
470-
def __init__(self, pool, *a, **kw):
471-
self.pool = pool
472-
self.a = a
473-
self.kw = kw
474-
self.conn = None
475-
476-
def __enter__(self):
477-
"""Get a connection from the pool.
478-
"""
479-
self.conn = self.pool.getconn()
480-
self.cursor = self.conn.cursor(*self.a, **self.kw)
481-
return self.cursor
482-
483-
def __exit__(self, *exc_info):
484-
"""Put our connection back in the pool.
485-
"""
486-
self.cursor.close()
487-
self.pool.putconn(self.conn)
488-
489-
490419
class TransactionContextManager(object):
491420
"""Instantiated once per :py:func:`~postgres.Postgres.get_transaction`
492421
call.
@@ -497,10 +426,9 @@ class TransactionContextManager(object):
497426
When the block starts, the :py:class:`~postgres.Connection` underlying the
498427
cursor is checked out of the connection pool and :py:attr:`autocommit` is
499428
set to :py:const:`False`. If the block raises an exception, the
500-
:py:class:`~postgres.Connection` is rolled back. Otherwise it's committed.
501-
In either case, the cursor is closed, :py:attr:`autocommit` is restored to
502-
:py:const:`True`, and the :py:class:`~postgres.Connection` is put back in
503-
the pool.
429+
:py:class:`~postgres.Connection` is rolled back. Otherwise, it's committed.
430+
In either case, the cursor is closed, and the
431+
:py:class:`~postgres.Connection` is put back in the pool.
504432
505433
"""
506434

@@ -526,7 +454,7 @@ def __exit__(self, *exc_info):
526454
else:
527455
self.conn.rollback()
528456
self.cursor.close()
529-
self.conn.autocommit = True
457+
self.conn.autocommit = False
530458
self.pool.putconn(self.conn)
531459

532460

@@ -536,8 +464,7 @@ class ConnectionContextManager(object):
536464
The return value of :py:func:`ConnectionContextManager.__enter__` is a
537465
:py:class:`postgres.Connection`. When the block starts, a
538466
:py:class:`~postgres.Connection` is checked out of the connection pool and
539-
:py:attr:`autocommit` is set to :py:const:`False`. When the block ends,
540-
:py:attr:`autocommit` is restored to :py:const:`True` and the
467+
:py:attr:`autocommit` is set to :py:const:`False`. When the block ends, the
541468
:py:class:`~postgres.Connection` is rolled back before being put back in
542469
the pool.
543470
@@ -558,5 +485,5 @@ def __exit__(self, *exc_info):
558485
"""Put our connection back in the pool.
559486
"""
560487
self.conn.rollback()
561-
self.conn.autocommit = True
488+
self.conn.autocommit = False
562489
self.pool.putconn(self.conn)

tests.py

Lines changed: 23 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def test_run_runs(self):
5050
def test_run_inserts(self):
5151
self.db.run("CREATE TABLE foo (bar text)")
5252
self.db.run("INSERT INTO foo VALUES ('baz')")
53-
actual = len(self.db.one("SELECT * FROM foo ORDER BY bar"))
53+
actual = len(self.db.one_or_zero("SELECT * FROM foo ORDER BY bar"))
5454
assert actual == 1
5555

5656

@@ -63,6 +63,14 @@ def test_rows_fetches_all_rows(self):
6363
actual = self.db.all("SELECT * FROM foo ORDER BY bar")
6464
assert actual == [{"bar": "baz"}, {"bar": "buz"}]
6565

66+
def test_rows_fetches_one_row(self):
67+
actual = self.db.all("SELECT * FROM foo WHERE bar='baz'")
68+
assert actual == [{"bar": "baz"}]
69+
70+
def test_rows_fetches_no_rows(self):
71+
actual = self.db.all("SELECT * FROM foo WHERE bar='blam'")
72+
assert actual == []
73+
6674
def test_bind_parameters_as_dict_work(self):
6775
params = {"bar": "baz"}
6876
actual = self.db.all("SELECT * FROM foo WHERE bar=%(bar)s", params)
@@ -73,45 +81,38 @@ def test_bind_parameters_as_tuple_work(self):
7381
assert actual == [{"bar": "baz"}]
7482

7583

76-
# db.one
77-
# ======
78-
# With all the combinations of strict_one and strict, we end up with a number
79-
# of tests here. Since the behavior of the one method with a strict parameter
80-
# of True or False is expected to be the same regardless of what strict_one is
81-
# set to, we can write those once and then use the TestOne TestCase as the base
82-
# class for other TestCases that vary the strict_one attribute. The TestOne
83-
# tests will be re-run in each new context.
84+
# db.one_or_zero
85+
# ==============
8486

8587
class TestWrongNumberException(WithData):
8688

8789
def test_TooFew_message_is_helpful(self):
8890
try:
89-
self.db.one("SELECT * FROM foo WHERE bar='blah'", strict=True)
91+
exc = self.db.one_or_zero("CREATE TABLE foux (baar text)")
9092
except TooFew as exc:
91-
actual = str(exc)
92-
assert actual == "Got 0 rows; expecting exactly 1."
93+
pass
94+
actual = str(exc)
95+
assert actual == "Got -1 rows; expecting 0 or 1."
9396

9497
def test_TooMany_message_is_helpful_for_two_options(self):
9598
try:
96-
self.db.one_or_zero("SELECT * FROM foo")
99+
exc = self.db._some("SELECT * FROM foo", lo=1, hi=1)
97100
except TooMany as exc:
98-
actual = str(exc)
99-
assert actual == "Got 2 rows; expecting 0 or 1."
101+
pass
102+
actual = str(exc)
103+
assert actual == "Got 2 rows; expecting exactly 1."
100104

101105
def test_TooMany_message_is_helpful_for_a_range(self):
102106
self.db.run("INSERT INTO foo VALUES ('blam')")
103107
self.db.run("INSERT INTO foo VALUES ('blim')")
104108
try:
105-
self.db._some("SELECT * FROM foo", lo=1, hi=3)
109+
exc = self.db._some("SELECT * FROM foo", lo=1, hi=3)
106110
except TooMany as exc:
107-
actual = str(exc)
108-
assert actual == \
109-
"Got 4 rows; expecting between 1 and 3 (inclusive)."
111+
pass
112+
actual = str(exc)
113+
assert actual == "Got 4 rows; expecting between 1 and 3 (inclusive)."
110114

111115

112-
# db.one_or_zero
113-
# ==============
114-
115116
class TestOneOrZero(WithData):
116117

117118
def test_one_or_zero_raises_TooFew(self):
@@ -149,37 +150,6 @@ def test_with_strict_True_one_raises_TooMany(self):
149150
self.assertRaises(TooMany, self.db.one_or_zero, "SELECT * FROM foo")
150151

151152

152-
# db.get_cursor
153-
# =============
154-
155-
class TestCursor(WithData):
156-
157-
def test_get_cursor_gets_a_cursor(self):
158-
with self.db.get_cursor() as cursor:
159-
cursor.execute("SELECT * FROM foo ORDER BY bar")
160-
actual = cursor.fetchall()
161-
assert actual == [{"bar": "baz"}, {"bar": "buz"}]
162-
163-
def test_we_can_use_cursor_rowcount(self):
164-
with self.db.get_cursor() as cursor:
165-
cursor.execute("SELECT * FROM foo ORDER BY bar")
166-
actual = cursor.rowcount
167-
assert actual == 2
168-
169-
def test_we_can_use_cursor_closed(self):
170-
with self.db.get_cursor() as cursor:
171-
cursor.execute("SELECT * FROM foo ORDER BY bar")
172-
actual = cursor.closed
173-
assert not actual
174-
175-
def test_we_close_the_cursor(self):
176-
with self.db.get_cursor() as cursor:
177-
cursor.execute("SELECT * FROM foo ORDER BY bar")
178-
self.assertRaises( InterfaceError
179-
, cursor.fetchall
180-
)
181-
182-
183153
# db.get_transaction
184154
# ==================
185155

0 commit comments

Comments
 (0)