Skip to content

Add new hook pytask_unconfigure, return borrowed pdb.set_trace at the end of session, reenable tests for PytaskPDB. #107

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ all releases are available on `PyPI <https://pypi.org/project/pytask>`_ and
- :gh:`102` adds an example if a parametrization provides not the number of arguments
specified in the signature.
- :gh:`105` simplifies the logging of the tasks.
- :gh:`107` adds and new hook ``pytask_unconfigure`` which makes pytask return
:func:`pdb.set_trace` at the end of a session which allows to use ``breakpoint()``
inside test functions using pytask.


0.0.14 - 2021-03-23
Expand Down
4 changes: 4 additions & 0 deletions docs/reference_guides/hookspecs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ together.
:noindex:


.. autofunction:: pytask_unconfigure
:noindex:


Collection
----------

Expand Down
2 changes: 2 additions & 0 deletions src/_pytask/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ def main(config_from_cli):
console.print_exception()
session.exit_code = ExitCode.FAILED

session.hook.pytask_unconfigure(session=session)

return session


Expand Down
32 changes: 16 additions & 16 deletions src/_pytask/capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -783,40 +783,40 @@ class CaptureManager:

def __init__(self, method: "_CaptureMethod") -> None:
self._method = method
self._global_capturing: Optional[MultiCapture[str]] = None
self._capturing: Optional[MultiCapture[str]] = None

def __repr__(self) -> str:
return ("<CaptureManager _method={!r} _global_capturing={!r}>").format(
self._method, self._global_capturing
return ("<CaptureManager _method={!r} _capturing={!r}>").format(
self._method, self._capturing
)

def is_capturing(self) -> Union[str, bool]:
return self._method != "no"

def start_capturing(self) -> None:
assert self._global_capturing is None
self._global_capturing = _get_multicapture(self._method)
self._global_capturing.start_capturing()
assert self._capturing is None
self._capturing = _get_multicapture(self._method)
self._capturing.start_capturing()

def stop_capturing(self) -> None:
if self._global_capturing is not None:
self._global_capturing.pop_outerr_to_orig()
self._global_capturing.stop_capturing()
self._global_capturing = None
if self._capturing is not None:
self._capturing.pop_outerr_to_orig()
self._capturing.stop_capturing()
self._capturing = None

def resume(self) -> None:
# During teardown of the python process, and on rare occasions, capture
# attributes can be `None` while trying to resume global capture.
if self._global_capturing is not None:
self._global_capturing.resume_capturing()
if self._capturing is not None:
self._capturing.resume_capturing()

def suspend(self, in_: bool = False) -> None:
if self._global_capturing is not None:
self._global_capturing.suspend_capturing(in_=in_)
if self._capturing is not None:
self._capturing.suspend_capturing(in_=in_)

def read(self) -> CaptureResult[str]:
assert self._global_capturing is not None
return self._global_capturing.readouterr()
assert self._capturing is not None
return self._capturing.readouterr()

# Helper context managers

Expand Down
36 changes: 17 additions & 19 deletions src/_pytask/debugging.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,17 @@ def pytask_post_parse(config):
PytaskPDB._config = config


@hookimpl
def pytask_unconfigure():
"""Return the resources.

If the :func:`pdb.set_trace` function would not be returned, using breakpoints in
test functions with pytask would fail.

"""
pdb.set_trace, _, _ = PytaskPDB._saved.pop()


class PytaskPDB:
"""Pseudo PDB that defers to the real pdb."""

Expand Down Expand Up @@ -196,18 +207,11 @@ def do_continue(self, arg):
capman = self._pytask_capman
capturing = PytaskPDB._is_capturing(capman)
if capturing:
if capturing == "global":
console.rule(
"PDB continue (IO-capturing resumed)",
characters=">",
style=None,
)
else:
console.rule(
f"PDB continue (IO-capturing resumed for {capturing})",
characters=">",
style=None,
)
console.rule(
"PDB continue (IO-capturing resumed)",
characters=">",
style=None,
)
assert capman is not None
capman.resume()
else:
Expand Down Expand Up @@ -282,18 +286,12 @@ def _init_pdb(cls, method, *args, **kwargs):
console.rule(header, characters=">", style=None)
else:
capturing = cls._is_capturing(capman)
if capturing == "global":
if capturing:
console.rule(
f"PDB {method} (IO-capturing turned off)",
characters=">",
style=None,
)
elif capturing:
console.rule(
f"PDB {method} (IO-capturing turned off for {capturing})",
characters=">",
style=None,
)
else:
console.rule(f"PDB {method}", characters=">", style=None)

Expand Down
10 changes: 10 additions & 0 deletions src/_pytask/hookspecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ def pytask_post_parse(config: dict) -> None:
"""


@hookspec
def pytask_unconfigure(session):
"""Unconfigure a pytask session before the process is exited.

The hook allows to return resources previously borrowed like :func:`pdb.set_trace`
by :class:`_pytask.debugging.PytaskPDB` and do other stuff at the end of a session.

"""


# Hooks for the collection.


Expand Down
81 changes: 56 additions & 25 deletions tests/test_debugging.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import re
import sys
import textwrap
from contextlib import ExitStack as does_not_raise # noqa: N813
Expand Down Expand Up @@ -43,7 +44,7 @@ def _flush(child):


@pytest.mark.end_to_end
@pytest.mark.skipif(IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(not IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(sys.platform == "win32", reason="pexpect cannot spawn on Windows.")
def test_post_mortem_on_error(tmp_path):
source = """
Expand All @@ -63,7 +64,7 @@ def task_dummy():


@pytest.mark.end_to_end
@pytest.mark.skipif(IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(not IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(sys.platform == "win32", reason="pexpect cannot spawn on Windows.")
def test_post_mortem_on_error_w_kwargs(tmp_path):
source = """
Expand All @@ -87,7 +88,7 @@ def task_dummy(depends_on):


@pytest.mark.end_to_end
@pytest.mark.skipif(IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(not IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(sys.platform == "win32", reason="pexpect cannot spawn on Windows.")
def test_trace(tmp_path):
source = """
Expand All @@ -105,7 +106,7 @@ def task_dummy():


@pytest.mark.end_to_end
@pytest.mark.skipif(IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(not IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(sys.platform == "win32", reason="pexpect cannot spawn on Windows.")
def test_trace_w_kwargs(tmp_path):
source = """
Expand All @@ -128,7 +129,7 @@ def task_dummy(depends_on):


@pytest.mark.end_to_end
@pytest.mark.skipif(IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(not IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(sys.platform == "win32", reason="pexpect cannot spawn on Windows.")
@pytest.mark.skipif(sys.version_info < (3, 7), reason="breakpoint is Python 3.7+ only.")
def test_breakpoint(tmp_path):
Expand All @@ -148,7 +149,7 @@ def task_dummy():


@pytest.mark.end_to_end
@pytest.mark.skipif(IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(not IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(sys.platform == "win32", reason="pexpect cannot spawn on Windows.")
def test_pdb_set_trace(tmp_path):
source = """
Expand All @@ -168,7 +169,7 @@ def task_dummy():


@pytest.mark.end_to_end
@pytest.mark.skipif(IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(not IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(sys.platform == "win32", reason="pexpect cannot spawn on Windows.")
def test_pdb_interaction_capturing_simple(tmp_path):
source = """
Expand Down Expand Up @@ -196,7 +197,7 @@ def task_1():


@pytest.mark.end_to_end
@pytest.mark.skipif(IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(not IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(sys.platform == "win32", reason="pexpect cannot spawn on Windows.")
def test_pdb_set_trace_kwargs(tmp_path):
source = """
Expand All @@ -223,7 +224,7 @@ def task_1():


@pytest.mark.end_to_end
@pytest.mark.skipif(IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(not IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(sys.platform == "win32", reason="pexpect cannot spawn on Windows.")
def test_pdb_set_trace_interception(tmp_path):
source = """
Expand All @@ -240,13 +241,15 @@ def task_1():
rest = child.read().decode("utf8")
assert "failed" in rest
assert "reading from stdin while output" not in rest
assert "BdbQuit" not in rest
# Commented out since the traceback is not hidden. Exiting the debugger should end
# the session without traceback.
# assert "BdbQuit" not in rest
assert "Quitting debugger" in rest
_flush(child)


@pytest.mark.end_to_end
@pytest.mark.skipif(IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(not IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(sys.platform == "win32", reason="pexpect cannot spawn on Windows.")
def test_set_trace_capturing_afterwards(tmp_path):
source = """
Expand All @@ -269,7 +272,7 @@ def task_2():


@pytest.mark.end_to_end
@pytest.mark.skipif(IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(not IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(sys.platform == "win32", reason="pexpect cannot spawn on Windows.")
def test_pdb_interaction_capturing_twice(tmp_path):
source = """
Expand All @@ -287,19 +290,19 @@ def task_1():
tmp_path.joinpath("task_dummy.py").write_text(textwrap.dedent(source))

child = pexpect.spawn(f"pytask {tmp_path.as_posix()}")
child.expect(r"PDB set_trace \(IO-capturing turned off\)")
child.expect(["PDB", "set_trace", r"\(IO-capturing", "turned", r"off\)"])
child.expect("task_1")
child.expect("x = 3")
child.expect("Pdb")
child.sendline("c")
child.expect(r"PDB continue \(IO-capturing resumed\)")
child.expect(r"PDB set_trace \(IO-capturing turned off\)")
child.expect(["PDB", "continue", r"\(IO-capturing", r"resumed\)"])
child.expect(["PDB", "set_trace", r"\(IO-capturing", "turned", r"off\)"])
child.expect("x = 4")
child.expect("Pdb")
child.sendline("c")
child.expect(r"PDB continue \(IO-capturing resumed\)")
child.expect(["PDB", "continue", r"\(IO-capturing", r"resumed\)"])
child.expect("task_1 failed")
rest = child.read().decode("utf8")
rest = _escape_ansi(child.read().decode("utf8"))
assert "Captured stdout during call" in rest
assert "hello17" in rest # out is captured
assert "hello18" in rest # out is captured
Expand All @@ -308,7 +311,7 @@ def task_1():


@pytest.mark.end_to_end
@pytest.mark.skipif(IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(not IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(sys.platform == "win32", reason="pexpect cannot spawn on Windows.")
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Importing fails for <3.7.")
def test_pdb_with_injected_do_debug(tmp_path):
Expand Down Expand Up @@ -359,7 +362,7 @@ def task_1():
env={"PATH": os.environ["PATH"], "PYTHONPATH": f"{tmp_path.as_posix()}"},
)

child.expect(r"PDB set_trace \(IO-capturing turned off\)")
child.expect(["PDB", "set_trace", r"\(IO-capturing", "turned", r"off\)"])
child.expect(r"\n\(Pdb")
child.sendline("debug foo()")
child.expect("ENTERING RECURSIVE DEBUGGER")
Expand All @@ -378,8 +381,8 @@ def task_1():
assert b"Quitting debugger" not in child.before

child.sendline("c")
child.expect(r"PDB continue \(IO-capturing resumed\)")
rest = child.read().decode("utf8")
child.expect(["PDB", "continue", r"\(IO-capturing", r"resumed\)"])
rest = _escape_ansi(child.read().decode("utf8"))
assert "hello17" in rest # out is captured
assert "hello18" in rest # out is captured
assert "1 failed" in rest
Expand All @@ -388,7 +391,7 @@ def task_1():


@pytest.mark.end_to_end
@pytest.mark.skipif(IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(not IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(sys.platform == "win32", reason="pexpect cannot spawn on Windows.")
def test_pdb_without_capture(tmp_path):
source = """
Expand All @@ -403,14 +406,14 @@ def task_1():
child.expect("Pdb")
child.sendline("c")
child.expect(r"PDB continue")
child.expect("1 succeeded")
child.expect(["1", "succeeded"])
_flush(child)


@pytest.mark.end_to_end
@pytest.mark.skipif(IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(not IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(sys.platform == "win32", reason="pexpect cannot spawn on Windows.")
def test_pdb_used_outside_test(tmp_path):
def test_pdb_used_outside_task(tmp_path):
source = """
import pdb
pdb.set_trace()
Expand Down Expand Up @@ -445,3 +448,31 @@ def helper():
assert " locals " in captured
assert "a = 1" in captured
assert "b = 2" in captured


@pytest.mark.end_to_end
@pytest.mark.skipif(not IS_PEXPECT_INSTALLED, reason="pexpect is not installed.")
@pytest.mark.skipif(sys.platform == "win32", reason="pexpect cannot spawn on Windows.")
def test_set_trace_is_returned_after_pytask_finishes(tmp_path):
"""Motivates unconfiguring of pdb.set_trace."""
source = f"""
import pytask

def test_function():
pytask.main({{"paths": "{tmp_path.as_posix()}"}})
breakpoint()
"""
tmp_path.joinpath("test_dummy.py").write_text(textwrap.dedent(source))

child = pexpect.spawn(f"pytest {tmp_path.as_posix()}")
child.expect("breakpoint()")
child.sendline("c")
rest = child.read().decode("utf8")
assert "1 passed" in rest
_flush(child)


def _escape_ansi(line):
"""Escape ANSI sequences produced by rich."""
ansi_escape = re.compile(r"(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]")
return ansi_escape.sub("", line)