Skip to content

Commit 4640f03

Browse files
committed
WIP: pdb: move/refactor initialization of PytestPdbWrapper
1 parent b10f289 commit 4640f03

File tree

2 files changed

+171
-130
lines changed

2 files changed

+171
-130
lines changed

src/_pytest/debugging.py

Lines changed: 132 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class pytestPDB(object):
8181
_config = None
8282
_saved = []
8383
_recursive_debug = 0
84+
_wrapped_pdb_cls = None
8485

8586
@classmethod
8687
def _is_capturing(cls, capman):
@@ -89,156 +90,167 @@ def _is_capturing(cls, capman):
8990
return False
9091

9192
@classmethod
92-
def _import_pdb_cls(cls):
93+
def _import_pdb_cls(cls, capman):
9394
if not cls._config:
9495
# Happens when using pytest.set_trace outside of a test.
9596
return pdb.Pdb
9697

97-
pdb_cls = cls._config.getvalue("usepdb_cls")
98-
if not pdb_cls:
99-
return pdb.Pdb
98+
usepdb_cls = cls._config.getvalue("usepdb_cls")
99+
100+
if cls._wrapped_pdb_cls and cls._wrapped_pdb_cls[0] == usepdb_cls:
101+
return cls._wrapped_pdb_cls[1]
100102

101-
modname, classname = pdb_cls
103+
if usepdb_cls:
104+
modname, classname = usepdb_cls
102105

103-
try:
104-
__import__(modname)
105-
mod = sys.modules[modname]
106+
try:
107+
__import__(modname)
108+
mod = sys.modules[modname]
106109

107-
# Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp).
108-
parts = classname.split(".")
109-
pdb_cls = getattr(mod, parts[0])
110-
for part in parts[1:]:
111-
pdb_cls = getattr(pdb_cls, part)
110+
# Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp).
111+
parts = classname.split(".")
112+
pdb_cls = getattr(mod, parts[0])
113+
for part in parts[1:]:
114+
pdb_cls = getattr(pdb_cls, part)
115+
except Exception as exc:
116+
value = ":".join((modname, classname))
117+
raise UsageError(
118+
"--pdbcls: could not import {!r}: {}".format(value, exc)
119+
)
120+
else:
121+
pdb_cls = pdb.Pdb
112122

113-
return pdb_cls
114-
except Exception as exc:
115-
value = ":".join((modname, classname))
116-
raise UsageError("--pdbcls: could not import {!r}: {}".format(value, exc))
123+
wrapped_cls = cls._get_pdb_wrapper_class(pdb_cls, capman)
124+
cls._wrapped_pdb_cls = (usepdb_cls, wrapped_cls)
125+
return wrapped_cls
117126

118127
@classmethod
119-
def _init_pdb(cls, *args, **kwargs):
128+
def _get_pdb_wrapper_class(cls, pdb_cls, capman):
129+
import _pytest.config
130+
131+
class PytestPdbWrapper(pdb_cls, object):
132+
_pytest_capman = capman
133+
_continued = False
134+
135+
def do_debug(self, arg):
136+
cls._recursive_debug += 1
137+
ret = super(PytestPdbWrapper, self).do_debug(arg)
138+
cls._recursive_debug -= 1
139+
return ret
140+
141+
def do_continue(self, arg):
142+
ret = super(PytestPdbWrapper, self).do_continue(arg)
143+
if cls._recursive_debug == 0:
144+
tw = _pytest.config.create_terminal_writer(cls._config)
145+
tw.line()
146+
147+
capman = self._pytest_capman
148+
capturing = pytestPDB._is_capturing(capman)
149+
if capturing:
150+
if capturing == "global":
151+
tw.sep(">", "PDB continue (IO-capturing resumed)")
152+
else:
153+
tw.sep(
154+
">",
155+
"PDB continue (IO-capturing resumed for %s)"
156+
% capturing,
157+
)
158+
capman.resume()
159+
else:
160+
tw.sep(">", "PDB continue")
161+
cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config, pdb=self)
162+
self._continued = True
163+
return ret
164+
165+
do_c = do_cont = do_continue
166+
167+
def do_quit(self, arg):
168+
"""Raise Exit outcome when quit command is used in pdb.
169+
170+
This is a bit of a hack - it would be better if BdbQuit
171+
could be handled, but this would require to wrap the
172+
whole pytest run, and adjust the report etc.
173+
"""
174+
ret = super(PytestPdbWrapper, self).do_quit(arg)
175+
176+
if cls._recursive_debug == 0:
177+
outcomes.exit("Quitting debugger")
178+
179+
return ret
180+
181+
do_q = do_quit
182+
do_exit = do_quit
183+
184+
def setup(self, f, tb):
185+
"""Suspend on setup().
186+
187+
Needed after do_continue resumed, and entering another
188+
breakpoint again.
189+
"""
190+
ret = super(PytestPdbWrapper, self).setup(f, tb)
191+
if not ret and self._continued:
192+
# pdb.setup() returns True if the command wants to exit
193+
# from the interaction: do not suspend capturing then.
194+
if self._pytest_capman:
195+
self._pytest_capman.suspend_global_capture(in_=True)
196+
return ret
197+
198+
def get_stack(self, f, t):
199+
stack, i = super(PytestPdbWrapper, self).get_stack(f, t)
200+
if f is None:
201+
# Find last non-hidden frame.
202+
i = max(0, len(stack) - 1)
203+
while i and stack[i][0].f_locals.get("__tracebackhide__", False):
204+
i -= 1
205+
return stack, i
206+
207+
return PytestPdbWrapper
208+
209+
@classmethod
210+
def _init_pdb(cls, method, *args, **kwargs):
120211
""" Initialize PDB debugging, dropping any IO capturing. """
121212
import _pytest.config
122213

123214
if cls._pluginmanager is not None:
124215
capman = cls._pluginmanager.getplugin("capturemanager")
125-
if capman:
126-
capman.suspend(in_=True)
216+
else:
217+
capman = None
218+
if capman:
219+
capman.suspend(in_=True)
220+
221+
if cls._config:
127222
tw = _pytest.config.create_terminal_writer(cls._config)
128223
tw.line()
224+
129225
if cls._recursive_debug == 0:
130226
# Handle header similar to pdb.set_trace in py37+.
131227
header = kwargs.pop("header", None)
132228
if header is not None:
133229
tw.sep(">", header)
134230
else:
135231
capturing = cls._is_capturing(capman)
136-
if capturing:
137-
if capturing == "global":
138-
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
139-
else:
140-
tw.sep(
141-
">",
142-
"PDB set_trace (IO-capturing turned off for %s)"
143-
% capturing,
144-
)
232+
if capturing == "global":
233+
tw.sep(">", "PDB %s (IO-capturing turned off)" % (method,))
234+
elif capturing:
235+
tw.sep(
236+
">",
237+
"PDB %s (IO-capturing turned off for %s)"
238+
% (method, capturing),
239+
)
145240
else:
146-
tw.sep(">", "PDB set_trace")
147-
148-
pdb_cls = cls._import_pdb_cls()
149-
150-
class PytestPdbWrapper(pdb_cls, object):
151-
_pytest_capman = capman
152-
_continued = False
153-
154-
def do_debug(self, arg):
155-
cls._recursive_debug += 1
156-
ret = super(PytestPdbWrapper, self).do_debug(arg)
157-
cls._recursive_debug -= 1
158-
return ret
159-
160-
def do_continue(self, arg):
161-
ret = super(PytestPdbWrapper, self).do_continue(arg)
162-
if cls._recursive_debug == 0:
163-
tw = _pytest.config.create_terminal_writer(cls._config)
164-
tw.line()
165-
166-
capman = self._pytest_capman
167-
capturing = pytestPDB._is_capturing(capman)
168-
if capturing:
169-
if capturing == "global":
170-
tw.sep(">", "PDB continue (IO-capturing resumed)")
171-
else:
172-
tw.sep(
173-
">",
174-
"PDB continue (IO-capturing resumed for %s)"
175-
% capturing,
176-
)
177-
capman.resume()
178-
else:
179-
tw.sep(">", "PDB continue")
180-
cls._pluginmanager.hook.pytest_leave_pdb(
181-
config=cls._config, pdb=self
182-
)
183-
self._continued = True
184-
return ret
185-
186-
do_c = do_cont = do_continue
187-
188-
def do_quit(self, arg):
189-
"""Raise Exit outcome when quit command is used in pdb.
190-
191-
This is a bit of a hack - it would be better if BdbQuit
192-
could be handled, but this would require to wrap the
193-
whole pytest run, and adjust the report etc.
194-
"""
195-
ret = super(PytestPdbWrapper, self).do_quit(arg)
196-
197-
if cls._recursive_debug == 0:
198-
outcomes.exit("Quitting debugger")
199-
200-
return ret
201-
202-
do_q = do_quit
203-
do_exit = do_quit
204-
205-
def setup(self, f, tb):
206-
"""Suspend on setup().
207-
208-
Needed after do_continue resumed, and entering another
209-
breakpoint again.
210-
"""
211-
ret = super(PytestPdbWrapper, self).setup(f, tb)
212-
if not ret and self._continued:
213-
# pdb.setup() returns True if the command wants to exit
214-
# from the interaction: do not suspend capturing then.
215-
if self._pytest_capman:
216-
self._pytest_capman.suspend_global_capture(in_=True)
217-
return ret
218-
219-
def get_stack(self, f, t):
220-
stack, i = super(PytestPdbWrapper, self).get_stack(f, t)
221-
if f is None:
222-
# Find last non-hidden frame.
223-
i = max(0, len(stack) - 1)
224-
while i and stack[i][0].f_locals.get(
225-
"__tracebackhide__", False
226-
):
227-
i -= 1
228-
return stack, i
229-
230-
_pdb = PytestPdbWrapper(**kwargs)
241+
tw.sep(">", "PDB %s" % (method,))
242+
243+
_pdb = cls._import_pdb_cls(capman)(**kwargs)
244+
245+
if cls._pluginmanager:
231246
cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb)
232-
else:
233-
pdb_cls = cls._import_pdb_cls()
234-
_pdb = pdb_cls(**kwargs)
235247
return _pdb
236248

237249
@classmethod
238250
def set_trace(cls, *args, **kwargs):
239251
"""Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing."""
240252
frame = sys._getframe().f_back
241-
_pdb = cls._init_pdb(*args, **kwargs)
253+
_pdb = cls._init_pdb("set_trace", *args, **kwargs)
242254
_pdb.set_trace(frame)
243255

244256

@@ -265,7 +277,7 @@ def pytest_pyfunc_call(self, pyfuncitem):
265277

266278

267279
def _test_pytest_function(pyfuncitem):
268-
_pdb = pytestPDB._init_pdb()
280+
_pdb = pytestPDB._init_pdb("runcall")
269281
testfunction = pyfuncitem.obj
270282
pyfuncitem.obj = _pdb.runcall
271283
if "func" in pyfuncitem._fixtureinfo.argnames: # pragma: no branch
@@ -315,7 +327,7 @@ def _postmortem_traceback(excinfo):
315327

316328

317329
def post_mortem(t):
318-
p = pytestPDB._init_pdb()
330+
p = pytestPDB._init_pdb("post_mortem")
319331
p.reset()
320332
p.interaction(None, t)
321333
if p.quitting:

testing/test_pdb.py

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -638,36 +638,35 @@ def test_1(monkeypatch):
638638
class pytestPDBTest(_pytest.debugging.pytestPDB):
639639
@classmethod
640640
def set_trace(cls, *args, **kwargs):
641-
# Init _PdbWrapper to handle capturing.
642-
_pdb = cls._init_pdb(*args, **kwargs)
641+
# Init PytestPdbWrapper to handle capturing.
642+
_pdb = cls._init_pdb("set_trace", *args, **kwargs)
643643
644644
# Mock out pdb.Pdb.do_continue.
645645
import pdb
646646
pdb.Pdb.do_continue = lambda self, arg: None
647647
648-
print("=== SET_TRACE ===")
648+
print("===" + " SET_TRACE ===")
649649
assert input() == "debug set_trace()"
650650
651-
# Simulate _PdbWrapper.do_debug
651+
# Simulate PytestPdbWrapper.do_debug
652652
cls._recursive_debug += 1
653653
print("ENTERING RECURSIVE DEBUGGER")
654-
print("=== SET_TRACE_2 ===")
654+
print("===" + " SET_TRACE_2 ===")
655655
656656
assert input() == "c"
657657
_pdb.do_continue("")
658-
print("=== SET_TRACE_3 ===")
658+
print("===" + " SET_TRACE_3 ===")
659659
660-
# Simulate _PdbWrapper.do_debug
660+
# Simulate PytestPdbWrapper.do_debug
661661
print("LEAVING RECURSIVE DEBUGGER")
662662
cls._recursive_debug -= 1
663663
664-
print("=== SET_TRACE_4 ===")
664+
print("===" + " SET_TRACE_4 ===")
665665
assert input() == "c"
666666
_pdb.do_continue("")
667667
668668
def do_continue(self, arg):
669669
print("=== do_continue")
670-
# _PdbWrapper.do_continue("")
671670
672671
monkeypatch.setattr(_pytest.debugging, "pytestPDB", pytestPDBTest)
673672
@@ -677,7 +676,7 @@ def do_continue(self, arg):
677676
set_trace()
678677
"""
679678
)
680-
child = testdir.spawn_pytest("%s %s" % (p1, capture_arg))
679+
child = testdir.spawn_pytest("--tb=short %s %s" % (p1, capture_arg))
681680
child.expect("=== SET_TRACE ===")
682681
before = child.before.decode("utf8")
683682
if not capture_arg:
@@ -1207,3 +1206,33 @@ def test(monkeypatch):
12071206
result = testdir.runpytest(str(p1))
12081207
result.stdout.fnmatch_lines(["E *BdbQuit", "*= 1 failed in*"])
12091208
assert result.ret == 1
1209+
1210+
1211+
def test_pdb_wrapper_class_is_reused(testdir):
1212+
p1 = testdir.makepyfile(
1213+
"""
1214+
def test():
1215+
__import__("pdb").set_trace()
1216+
__import__("pdb").set_trace()
1217+
1218+
import mypdb
1219+
instances = mypdb.instances
1220+
assert len(instances) == 2
1221+
assert instances[0].__class__ is instances[1].__class__
1222+
""",
1223+
mypdb="""
1224+
instances = []
1225+
1226+
class MyPdb:
1227+
def __init__(self, *args, **kwargs):
1228+
instances.append(self)
1229+
1230+
def set_trace(self, *args):
1231+
print("set_trace_called", args)
1232+
""",
1233+
)
1234+
result = testdir.runpytest(str(p1), "--pdbcls=mypdb:MyPdb", syspathinsert=True)
1235+
assert result.ret == 0
1236+
result.stdout.fnmatch_lines(
1237+
["*set_trace_called*", "*set_trace_called*", "* 1 passed in *"]
1238+
)

0 commit comments

Comments
 (0)