Source code for pytest_exploratory.ipython
"""Integration with IPython/Jupyter."""
import atexit
import shlex
from tempfile import TemporaryDirectory
from IPython.core.magic import Magics, magics_class, line_magic
from IPython.core.error import UsageError
from typing import Optional, Callable, Any
import warnings
from pytest_exploratory.interactive import InteractiveSession
sphinxify: Optional[Callable[[Any], Any]]
try:
import docrepr.sphinxify as sphx
def sphinxify(doc):
with TemporaryDirectory() as dirname:
return {
'text/html': sphx.sphinxify(doc, dirname),
'text/plain': doc
}
except ImportError:
sphinxify = None
config = None
# HACK for pytest
magics = None
[docs]@magics_class
class PytestMagics(Magics):
"""IPython magics to control a pytest test session."""
# TODO have smart autocompletion
def __init__(self, shell):
global magics
super().__init__(shell)
self.shell = shell
self._session = InteractiveSession()
self._in_pytest = False
if config is not None:
self._session.config = config
self._in_pytest = True
magics = self
[docs] @line_magic
def pytest_session(self, data):
"""Start a pytest session.
This sets the ``pytest_session`` variable to an :class:`.interactive.InteractiveSession` instance.
"""
if data == "":
args = None
else:
args = shlex.split(data)
self._session.start(args)
self._session.session_start()
self.shell.push({"pytest_session": self._session})
[docs] @line_magic
def pytest_context(self, context):
"""Get into the given pytest context.
If the context is a full test name, the fixtures are setup and put into corresponding variables.
"""
context = context.strip()
variables = self._session.context(context)
self.shell.push(variables)
[docs] @line_magic
def pytest_contextinfo(self, level):
"""Show information about the current test context (currently just code).
Pass a number to show information about a parent context.
"""
if level == "":
level = 0
else:
level = int(level)
docformat = sphinxify if self.shell.sphinxify_docstring else None
context_item = self._session.context_item
for _ in range(level):
context_item = context_item.parent
self.shell.inspector.pinfo(context_item.obj, detail_level=2, formatter=docformat)
[docs] @line_magic
def pytest_fixture(self, fixturenames):
"""Load the given fixture(s) and add them to the variables.
For parametrized fixtures, you can give the parameter id between brackets, e.g. ``fixture_name[param_id]``.
"""
for fixturename in fixturenames.split():
name, value = self._session.fixture_with_name(fixturename)
self.shell.push({name: value})
[docs] @line_magic
def pytest_fixtureinfo(self, fixturename):
"""Show information about the fixture.
E.g.: ``%pytest_fixtureinfo tmpdir``
"""
fixturename = fixturename.strip()
try:
definition = self._session.fixture_definition(fixturename)
except KeyError:
print(f"No fixture named {fixturename}")
return
docformat = sphinxify if self.shell.sphinxify_docstring else None
self.shell.inspector.pinfo(definition.func, formatter=docformat)
[docs] @line_magic
def pytest_fixtureinfodetail(self, fixturename):
"""Show detailed information about the fixture.
E.g.: ``%pytest_fixtureinfodetail tmpdir``
"""
fixturename = fixturename.strip()
try:
definition = self._session.fixture_definition(fixturename)
except KeyError:
print(f"No fixture named {fixturename}")
return
docformat = sphinxify if self.shell.sphinxify_docstring else None
self.shell.inspector.pinfo(definition.func, detail_level=2, formatter=docformat)
[docs] def pytest_fixture_completer(self, ipython, event):
return self._session.fixturenames
[docs] @line_magic
def pytest_runtests(self, dummy=""):
"""Run the tests in the current context."""
assert dummy == ""
self._session.runtests()
def _try_pytest_session_stop(self):
if self._session.session is None:
return
self._session.session_stop()
if not self._in_pytest:
self._session.stop()
[docs] def shutdown_hook(self):
self._try_pytest_session_stop()
[docs] @line_magic
def pytest_session_stop(self, data=""):
"""Stop the pytest session.
This ensures that all fixtures are torn down.
"""
if self._session.session is None:
raise UsageError("Pytest session not started")
self._try_pytest_session_stop()
def _shell_initialized(ipython):
# TODO remove this when it's fixed in IPython
warnings.filterwarnings('ignore', module=r'^jedi\.cache')
[docs]def load_ipython_extension(ipython):
console = PytestMagics(ipython)
ipython.register_magics(console)
atexit.register(console.shutdown_hook)
ipython.set_hook('complete_command', console.pytest_fixture_completer, re_key='%pytest_fixture')
# TODO autocomplete for pytest_context
ipython.events.register('shell_initialized', _shell_initialized)
[docs]def unload_ipython_extension(ipython):
pass