Pytest: Error when attempting to use IPython debugger

Created on 15 Nov 2016  Â·  11Comments  Â·  Source: pytest-dev/pytest

Problem description

From py.test help:

  --pdb                 start the interactive Python debugger on errors.
  --pdbcls=modulename:classname
                        start a custom interactive Python debugger on errors. For example:
                        --pdbcls=IPython.terminal.debugger:TerminalPdb

I created a simple test file containing just

def test_pdb():
    assert 0

And ran py.test as follows:

$ py.test  test_pdb.py \
--pdbcls=IPython.terminal.debugger:TerminalPdb \
--pdb

The result was an internal error with the following traceback:

Traceback (most recent call last):
  File ".../_pytest/main.py", line 96, in wrap_session
    session.exitstatus = doit(config, session) or 0
  ...
  File ".../IPython/terminal/debugger.py", line 38, in pt_init
    self.pt_cli = CommandLineInterface(self._pt_app, eventloop=self.shell._eventloop)
AttributeError: 'TerminalInteractiveShell' object has no attribute '_eventloop'

Installed packages

$ pip freeze
appnope==0.1.0
decorator==4.0.10
entrypoints==0.2.2
ipykernel==4.5.0
ipython==5.1.0
ipython-genutils==0.1.0
ipywidgets==5.2.2
Jinja2==2.8
jsonschema==2.5.1
jupyter==1.0.0
jupyter-client==4.4.0
jupyter-console==5.0.0
jupyter-core==4.2.0
MarkupSafe==0.23
mistune==0.7.3
nbconvert==4.2.0
nbformat==4.1.0
notebook==4.2.3
pexpect==4.2.1
pickleshare==0.7.4
pluggy==0.4.0
prompt-toolkit==1.0.9
ptyprocess==0.5.1
py==1.4.31
Pygments==2.1.3
pytest==3.0.4
pyzmq==16.0.1
qtconsole==4.2.1
simplegeneric==0.8.1
six==1.10.0
terminado==0.6
tornado==4.4.2
traitlets==4.3.1
wcwidth==0.1.7
widgetsnbextension==1.2.6

Pytest and operating system versions

I am using pytest 3.0.4 and

$ python
Python 3.5.2+ (3.5:da2ac103d326+, Nov 15 2016, 14:14:18)
[GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.42.1)] on darwin

Issue checklist

  • [x] Include a detailed description of the bug or suggestion
  • [x] pip list of the virtual environment you are using
  • [x] pytest and operating system versions
  • [x] Minimal example if possible
debugging bug

Most helpful comment

A similar issue has been reported for django-nose. See django-nose/django-nose#274. It was resolved there with a recommendation to use the "no capture" mode. This worked for py.test as well:

$ py.test test_pdb.py --pdbcls=IPython.terminal.debugger:TerminalPdb --pdb -s
=================================================== test session starts ====================================================
platform darwin -- Python 3.5.2+, pytest-3.0.5.dev0, py-1.4.31, pluggy-0.4.0
rootdir: /Users/a/Work/abalkin/pytest, inifile: tox.ini
plugins: leaks-0.1.0
collected 1 items

test_pdb.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    def test_pdb():
>       assert 0
E       assert 0

test_pdb.py:2: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /Users/a/Work/abalkin/pytest/test_pdb.py(2)test_pdb()
      1 def test_pdb():
----> 2     assert 0

ipdb>

While this is a decent work-around, I still believe raising an AttributeError is not acceptable.

All 11 comments

Thanks for the report!

A similar issue has been reported for django-nose. See django-nose/django-nose#274. It was resolved there with a recommendation to use the "no capture" mode. This worked for py.test as well:

$ py.test test_pdb.py --pdbcls=IPython.terminal.debugger:TerminalPdb --pdb -s
=================================================== test session starts ====================================================
platform darwin -- Python 3.5.2+, pytest-3.0.5.dev0, py-1.4.31, pluggy-0.4.0
rootdir: /Users/a/Work/abalkin/pytest, inifile: tox.ini
plugins: leaks-0.1.0
collected 1 items

test_pdb.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    def test_pdb():
>       assert 0
E       assert 0

test_pdb.py:2: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /Users/a/Work/abalkin/pytest/test_pdb.py(2)test_pdb()
      1 def test_pdb():
----> 2     assert 0

ipdb>

While this is a decent work-around, I still believe raising an AttributeError is not acceptable.

Trying this with IPython 5.4.1 and pytest 3.1.3, I'm getting this beauty:

Traceback (most recent call last):
  File "Lib\runpy.py", line 174, in _run_module_as_main
    "__main__", fname, loader, pkg_name)
  File "Lib\runpy.py", line 72, in _run_code
    exec code in run_globals
  File "Scripts\pytest.exe\__main__.py", line 9, in <module>
  File "lib\site-packages\_pytest\config.py", line 58, in main
    return config.hook.pytest_cmdline_main(config=config)
  File "lib\site-packages\_pytest\vendored_packages\pluggy.py", line 745, in __call__
    return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
  File "lib\site-packages\_pytest\vendored_packages\pluggy.py", line 339, in _hookexec
    return self._inner_hookexec(hook, methods, kwargs)
  File "lib\site-packages\_pytest\vendored_packages\pluggy.py", line 334, in <lambda>
    _MultiCall(methods, kwargs, hook.spec_opts).execute()
  File "lib\site-packages\_pytest\vendored_packages\pluggy.py", line 614, in execute
    res = hook_impl.function(*args)
  File "lib\site-packages\_pytest\main.py", line 134, in pytest_cmdline_main
    return wrap_session(config, _main)
  File "lib\site-packages\_pytest\main.py", line 117, in wrap_session
    config.notify_exception(excinfo, config.option)
  File "lib\site-packages\_pytest\config.py", line 947, in notify_exception
    excinfo=excinfo)
  File "lib\site-packages\_pytest\vendored_packages\pluggy.py", line 745, in __call__
    return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
  File "lib\site-packages\_pytest\vendored_packages\pluggy.py", line 339, in _hookexec
    return self._inner_hookexec(hook, methods, kwargs)
  File "lib\site-packages\_pytest\vendored_packages\pluggy.py", line 334, in <lambda>
    _MultiCall(methods, kwargs, hook.spec_opts).execute()
  File "lib\site-packages\_pytest\vendored_packages\pluggy.py", line 614, in execute
    res = hook_impl.function(*args)
  File "lib\site-packages\_pytest\debugging.py", line 79, in pytest_internalerror
    post_mortem(tb)
  File "lib\site-packages\_pytest\debugging.py", line 121, in post_mortem
    p = Pdb()
  File "lib\site-packages\IPython\terminal\debugger.py", line 26, in __init__
    self.pt_init()
  File "lib\site-packages\IPython\terminal\debugger.py", line 67, in pt_init
    style=self.shell.style
AttributeError: 'TerminalInteractiveShell' object has no attribute 'style'

This is caused by using TerminalPdb with simple_prompt which is auto detected here: IPython/terminal/interactiveshell.py:76-86 and here IPython/terminal/interactiveshell.py:113-115.

Obviously IPython's debuggers "API" is quite poor in this regard... ipdb handled this like this ipdb/__main__.py:25-35. Using simple_prompt is not desirable anyhow, it's really only meant for testing or for programs that wrap IPython.

From pytest's perspective we probably want to perform the import of the debugger class without IO redirection so that IPython correctly detects the tty (That assumes that the relevant internal IPython module gets imported when doing the import of the debugger class which smells like it can break really easily). We can also submit a patch to IPython to move that check to the class itself so it doesn't happen at import time, might be possible using this Traitlets - Dynamic default values. That way the check can happen when we first create the Pdb class with IO redirection off.

P.S. I guess there is no way to set a default pdbcls user-wide/system-wide... There is also no handling of calls to ipdb.set_trace (ipdb). All this nonsense can be handled by a plugin obviously, but the existing plugin is unmaintained and I don't think it supports IPython 5+ properly.

I am also getting AttributeError: 'TerminalInteractiveShell' object has no attribute 'style'.

My workaround is to add --capture=no to my pytest.ini configuration file:

[pytest]

addopts =
    --pdbcls=IPython.terminal.debugger:TerminalPdb
    --capture=no

Please post full stack traces..
In https://github.com/pytest-dev/pytest/issues/2064#issuecomment-318857657 it goes through post_mortem which might be not working correctly, as with e.g. using pdb.set_trace() directly.

There have been some fixes (e.g. for --trace), so is this still an issue?

520af9d76 might be related.

--pdbcls ipdb:__main__.debugger_cls works (see https://github.com/pytest-dev/pytest/issues/5039#issuecomment-479604656).

--pdbcls=IPython.terminal.debugger:TerminalPdb crashes:

    def test_foo():
        print(1)
>       __import__('pdb').set_trace()


t-pdb.py:7: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

cls = <class '_pytest.debugging.pytestPDB'>, args = (), kwargs = {}
frame = <frame at 0x7f9209c6cb70, file '…/Vcs/pytest/t-pdb.py', line 7, code test_foo>

    @classmethod
    def set_trace(cls, *args, **kwargs):
        """Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing."""
        frame = sys._getframe().f_back
>       _pdb = cls._init_pdb(*args, **kwargs)

args       = ()
cls        = <class '_pytest.debugging.pytestPDB'>
frame      = <frame at 0x7f9209c6cb70, file '…/Vcs/pytest/t-pdb.py', line 7, code test_foo>
kwargs     = {}

src/_pytest/debugging.py:225: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

cls = <class '_pytest.debugging.pytestPDB'>, args = (), kwargs = {}
tw = <py._io.terminalwriter.TerminalWriter object at 0x7f9209bf8710>, header = None
capturing = 'global'

    @classmethod
    def _init_pdb(cls, *args, **kwargs):
        """ Initialize PDB debugging, dropping any IO capturing. """
        import _pytest.config

        if cls._pluginmanager is not None:
            …

>           _pdb = PytestPdbWrapper(**kwargs)

PytestPdbWrapper = <class '_pytest.debugging.pytestPDB._init_pdb.<locals>.PytestPdbWrapper'>
_pytest    = <module '_pytest' from '…/Vcs/pytest/src/_pytest/__init__.py'>
args       = ()
capman     = <CaptureManager _method='fd' _global_capturing=<MultiCapture out=<FDCapture 1 oldfd=5 _state='suspended'> err=<FDCaptu... in_=<FDCapture 0 oldfd=3 _state='suspended'> _state='suspended' _in_suspended=True> _current_item=<Function test_foo>>
capturing  = 'global'
cls        = <class '_pytest.debugging.pytestPDB'>
header     = None
kwargs     = {}
tw         = <py._io.terminalwriter.TerminalWriter object at 0x7f9209bf8710>

src/_pytest/debugging.py:215: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <_pytest.debugging.pytestPDB._init_pdb.<locals>.PytestPdbWrapper object at 0x7f9209bf8860>
args = (), kwargs = {}

    def __init__(self, *args, **kwargs):
        Pdb.__init__(self, *args, **kwargs)
        self._ptcomp = None
>       self.pt_init()

args       = ()
kwargs     = {}
self       = <_pytest.debugging.pytestPDB._init_pdb.<locals>.PytestPdbWrapper object at 0x7f9209bf8860>

.venv/lib/python3.7/site-packages/IPython/terminal/debugger.py:25: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <_pytest.debugging.pytestPDB._init_pdb.<locals>.PytestPdbWrapper object at 0x7f9209bf8860>

    def pt_init(self):
        def get_prompt_tokens():
            return [(Token.Prompt, self.prompt)]

        if self._ptcomp is None:
            compl = IPCompleter(shell=self.shell,
                                        namespace={},
                                        global_namespace={},
                                        parent=self.shell,
                                       )
            self._ptcomp = IPythonPTCompleter(compl)

        kb = KeyBindings()
        supports_suspend = Condition(lambda: hasattr(signal, 'SIGTSTP'))
        kb.add('c-z', filter=supports_suspend)(suspend_to_bg)

        if self.shell.display_completions == 'readlinelike':
            kb.add('tab', filter=(has_focus(DEFAULT_BUFFER)
                                  & ~has_selection
                                  & vi_insert_mode | emacs_insert_mode
                                  & ~cursor_in_leading_ws
                              ))(display_completions_like_readline)

        self.pt_app = PromptSession(
                            message=(lambda: PygmentsTokens(get_prompt_tokens())),
                            editing_mode=getattr(EditingMode, self.shell.editing_mode.upper()),
                            key_bindings=kb,
                            history=self.shell.debugger_history,
                            completer=self._ptcomp,
                            enable_history_search=True,
                            mouse_support=self.shell.mouse_support,
                            complete_style=self.shell.pt_complete_style,
>                           style=self.shell.style,
                            inputhook=self.shell.inputhook,
                            color_depth=self.shell.color_depth,
        )
E       AttributeError: 'TerminalInteractiveShell' object has no attribute 'style'

compl      = <IPython.core.completer.IPCompleter object at 0x7f9209396e10>
get_prompt_tokens = <function TerminalPdb.pt_init.<locals>.get_prompt_tokens at 0x7f920938f378>
kb         = <prompt_toolkit.key_binding.key_bindings.KeyBindings object at 0x7f9209396ef0>
self       = <_pytest.debugging.pytestPDB._init_pdb.<locals>.PytestPdbWrapper object at 0x7f9209bf8860>
supports_suspend = Condition(<function TerminalPdb.pt_init.<locals>.<lambda> at 0x7f920938f2f0>)

.venv/lib/python3.7/site-packages/IPython/terminal/debugger.py:59: AttributeError

Just use the former I guess.. :)

Actually at least the help should be fixed then - wasn't aware that it's from there.
And then also investigate if it worked before, what broke it, and have a test for it.

Created https://github.com/ipython/ipython/issues/11745.

@segevfiner
Also thought about disabling capturing temporarily, or importing it only later altogether.

I have a plan for this, but it requires https://github.com/pytest-dev/pytest/pull/4908 to get merged first.

https://github.com/pytest-dev/pytest/pull/5307 should fix this, if you want to give it a try / review it.

Was this page helpful?
0 / 5 - 0 ratings