Skip to content

Commit 5175b26

Browse files
author
James William Pye
committed
Implement Nested as it was deprecated in 3.1.
Alter the dependencies to reference the new implementation. It may be wise to just import the Python 3 version sans the warning in order to warrant exception __context__ preservation--currently Nested() uses __cause__ as a final out-of-context raise wrecks the context chain.
1 parent a5565bd commit 5175b26

5 files changed

Lines changed: 236 additions & 7 deletions

File tree

postgresql/bin/pg_python.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
import optparse
1313
import contextlib
1414
from .. import clientparameters
15-
from ..resolved import pythoncommand as pycmd
15+
from ..python import command as pycmd
16+
from ..python.contextlib import Nested, NoCM
1617
from .. import __version__
1718

1819
from ..driver import default as pg_driver
@@ -110,7 +111,7 @@ def command(argv = sys.argv):
110111
if trace_file is not None:
111112
connection.tracer = trace_file.write
112113
context = [connection]
113-
with contextlib.nested(*context):
114+
with Nested(*context):
114115
rv = pythonexec(
115116
context = pycmd.postmortem(os.environ.get('PYTHON_POSTMORTEM'))
116117
)

postgresql/python/contextlib.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
##
2+
# copyright 2009, James William Pye
3+
# http://python.projects.postgresql.org
4+
##
5+
import sys
6+
7+
class NoCM(object):
8+
"""
9+
CM that does nothing. Useful for parameterized CMs.
10+
"""
11+
__slots__ = ()
12+
13+
def __new__(typ):
14+
return typ
15+
16+
@staticmethod
17+
def __enter__():
18+
pass
19+
20+
@classmethod
21+
def __context__(typ):
22+
return typ
23+
24+
@staticmethod
25+
def __exit__(typ, val, tb):
26+
pass
27+
28+
class Nested(object):
29+
"""
30+
cause they deprecated it in 3.1
31+
32+
Implemented with a class instead of a contextlib.contextmanager. A generator
33+
CM is probably a better choice as it would likely make it easier to properly
34+
preserve __context__.
35+
36+
WARNING: Uses __cause__ to reference exception contexts when possible.
37+
"""
38+
__slots__ = ('cm', 'exits')
39+
40+
def __init__(self, *cm):
41+
self.cm = tuple([
42+
x.__context__() if hasattr(x, '__context__') else x for x in cm
43+
])
44+
45+
def __enter__(self):
46+
if hasattr(self, 'exits'):
47+
raise RuntimeError("context manager already ran")
48+
self.exits = []
49+
r = []
50+
try:
51+
for x in self.cm:
52+
r.append(x.__enter__())
53+
self.exits.append(x.__exit__)
54+
except:
55+
if self.__exit__(*sys.exc_info()):
56+
raise RuntimeError("cannot suppress exceptions raised during entry")
57+
raise
58+
return tuple(r)
59+
60+
def __exit__(self, typ, val, tb):
61+
# if there are no exits, there's nothing to be done.
62+
oval = val
63+
for x in reversed(self.exits):
64+
try:
65+
if x(typ, val, tb):
66+
typ = val = tb = None
67+
except:
68+
newtyp, newval, newtb = sys.exc_info()
69+
if getattr(newval,'__cause__') is None:
70+
newval.__cause__ = getattr(newval, '__context__', val)
71+
if newval.__cause__ is newval.__context__:
72+
newval.__context__ = None
73+
typ = newtyp
74+
val = newval
75+
tb = newtb
76+
self.exits = ()
77+
if val is not None and val is not oval:
78+
raise val
79+
return val is None
80+
81+
def __context__(self):
82+
return self

postgresql/test/test_dbapi20.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import unittest
55
from ..unittest import TestCaseWithCluster
66
import time
7-
from contextlib import nested
7+
from ..python.contextlib import NoCM
88

99
##
1010
# Various Adjustments for pg.driver.dbapi20
@@ -110,7 +110,7 @@ def tearDown(self):
110110
con.close()
111111

112112
def connection(self):
113-
return nested()
113+
return NoCM
114114

115115
def _connect(self):
116116
host, port = self.cluster.address()
@@ -807,21 +807,21 @@ def test_Date(self):
807807
d1 = self.driver.Date(2002,12,25)
808808
d2 = self.driver.DateFromTicks(time.mktime((2002,12,25,0,0,0,0,0,0)))
809809
# Can we assume this? API doesn't specify, but it seems implied
810-
# self.assertEqual(str(d1),str(d2))
810+
self.assertEqual(str(d1),str(d2))
811811

812812
def test_Time(self):
813813
t1 = self.driver.Time(13,45,30)
814814
t2 = self.driver.TimeFromTicks(time.mktime((2001,1,1,13,45,30,0,0,0)))
815815
# Can we assume this? API doesn't specify, but it seems implied
816-
# self.assertEqual(str(t1),str(t2))
816+
self.assertEqual(str(t1),str(t2))
817817

818818
def test_Timestamp(self):
819819
t1 = self.driver.Timestamp(2002,12,25,13,45,30)
820820
t2 = self.driver.TimestampFromTicks(
821821
time.mktime((2002,12,25,13,45,30,0,0,0))
822822
)
823823
# Can we assume this? API doesn't specify, but it seems implied
824-
# self.assertEqual(str(t1),str(t2))
824+
#self.assertEqual(str(t1),str(t2))
825825

826826
def test_Binary(self):
827827
b = self.driver.Binary(b'Something')

postgresql/test/test_python.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import errno
88
from itertools import chain
99
from operator import methodcaller
10+
from contextlib import contextmanager
1011

12+
from ..python.contextlib import *
1113
from ..python import functools
1214
from ..python import itertools
1315
from ..python.socket import find_available_port
@@ -148,6 +150,150 @@ def testFindAvailable(self):
148150
else:
149151
self.fail("got a connection to an available port: " + str(portnum))
150152

153+
class contextlib(unittest.TestCase):
154+
def testNoCM(self):
155+
with NoCM as foo:
156+
pass
157+
self.failUnlessEqual(foo, None)
158+
self.failUnlessEqual(NoCM(), NoCM)
159+
# has no state, may be used repeatedly
160+
with NoCM as foo:
161+
pass
162+
self.failUnlessEqual(foo, None)
163+
164+
def testNested(self):
165+
class SomeExc(Exception):
166+
pass
167+
@contextmanager
168+
def just_one():
169+
yield 1
170+
@contextmanager
171+
def just_two():
172+
try:
173+
yield 2
174+
except:
175+
print("wtf")
176+
@contextmanager
177+
def raise_exc():
178+
raise SomeExc("foo")
179+
yield 1
180+
@contextmanager
181+
def finally_exc():
182+
try:
183+
yield 1
184+
finally:
185+
raise SomeExc("foo")
186+
@contextmanager
187+
def suppress():
188+
try:
189+
yield None
190+
except SomeExc:
191+
pass
192+
@contextmanager
193+
def expect_and_raise(exc, rexc):
194+
try:
195+
yield None
196+
except exc:
197+
raise rexc("bleh")
198+
199+
with Nested() as foo:
200+
pass
201+
self.failUnlessEqual(foo, ())
202+
try:
203+
with Nested() as foo:
204+
self.failUnlessEqual(foo, ())
205+
raise SomeExc("bar")
206+
except SomeExc:
207+
pass
208+
else:
209+
self.fail("empty Nested did not raise exception")
210+
211+
with Nested(just_one()) as foo:
212+
pass
213+
self.failUnlessEqual(foo, (1,))
214+
215+
with Nested(just_one(), just_one()) as foo:
216+
pass
217+
self.failUnlessEqual(foo, (1,1))
218+
219+
# NoCM won't raise a RuntimeError on re-use.
220+
N=Nested(NoCM)
221+
with N:
222+
pass
223+
try:
224+
with N:
225+
pass
226+
except RuntimeError:
227+
pass
228+
else:
229+
self.fail("exhausted Nested() failed to raise RuntimeError")
230+
231+
# unsuppressed exceptions on entry
232+
try:
233+
with Nested(raise_exc()):
234+
pass
235+
except SomeExc:
236+
pass
237+
else:
238+
self.fail("nested didn't raise SomeExc")
239+
try:
240+
with Nested(just_one(), raise_exc()):
241+
pass
242+
except SomeExc:
243+
pass
244+
else:
245+
self.fail("nested didn't raise SomeExc")
246+
247+
# suppressed SomeExc during entry--disallowed.
248+
try:
249+
with Nested(suppress(), raise_exc()):
250+
pass
251+
except RuntimeError:
252+
pass
253+
else:
254+
self.fail("nested didn't raise RuntimeError on suppressed partial entry")
255+
256+
# suppress should stop it, and that's okay because we're already
257+
# inside the block.
258+
with Nested(suppress(), finally_exc()):
259+
raise SomeExc("foo")
260+
261+
# This test case validates that the context is being
262+
# properly set.
263+
class ThisExc(Exception):
264+
pass
265+
try:
266+
with Nested(expect_and_raise(ThisExc, SomeExc)):
267+
raise ThisExc("FOO")
268+
except SomeExc as e:
269+
self.failUnlessEqual(type(e.__context__), ThisExc)
270+
else:
271+
self.fail("failed to raise exception")
272+
273+
class ThatExc(Exception):
274+
pass
275+
try:
276+
with Nested(expect_and_raise(ThisExc, SomeExc), expect_and_raise(ThatExc, ThisExc)):
277+
raise ThatExc("BAFOON")
278+
except SomeExc as e:
279+
# ThatExc -> ThisExc -> SomeExc
280+
self.failUnlessEqual(type(e.__cause__), ThisExc)
281+
self.failUnlessEqual(type(e.__cause__.__cause__), ThatExc)
282+
283+
try:
284+
with Nested(just_one()) as foo:
285+
self.failUnlessEqual(foo, (1,))
286+
raise SomeExc("inside the block")
287+
except SomeExc as e:
288+
pass
289+
else:
290+
self.fail("Nested didn't pass up exception raised in block")
291+
292+
# Slightly more complex suppression.
293+
with Nested(just_two(), suppress(), expect_and_raise(ThisExc, SomeExc), expect_and_raise(ThatExc, ThisExc)) as foo:
294+
raise ThatExc("MOOF")
295+
self.failUnlessEqual(foo[0], 2)
296+
151297
if __name__ == '__main__':
152298
from types import ModuleType
153299
this = ModuleType("this")

0 commit comments

Comments
 (0)