Skip to content

Commit d0fc5c9

Browse files
authored
Make debugger compatible with capturing. (#36)
1 parent f602462 commit d0fc5c9

13 files changed

+803
-74
lines changed

.github/workflows/continuous-integration-workflow.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535

3636
- name: Run unit tests and doctests.
3737
shell: bash -l {0}
38-
run: tox -e pytest -- src tests -m "unit or (not integration and not end_to_end)" --cov=./src --cov-report=xml -n auto
38+
run: tox -e pytest -- src tests -m "unit or (not integration and not end_to_end)" --cov=./ --cov-report=xml -n auto
3939

4040
- name: Upload coverage report for unit tests and doctests.
4141
if: runner.os == 'Linux' && matrix.python-version == '3.8'
@@ -44,7 +44,7 @@ jobs:
4444

4545
- name: Run integration tests.
4646
shell: bash -l {0}
47-
run: tox -e pytest -- src tests -m integration --cov=./src --cov-report=xml -n auto
47+
run: tox -e pytest -- src tests -m integration --cov=./ --cov-report=xml -n auto
4848

4949
- name: Upload coverage reports of integration tests.
5050
if: runner.os == 'Linux' && matrix.python-version == '3.8'
@@ -53,7 +53,7 @@ jobs:
5353

5454
- name: Run end-to-end tests.
5555
shell: bash -l {0}
56-
run: tox -e pytest -- src tests -m end_to_end --cov=./src --cov-report=xml -n auto
56+
run: tox -e pytest -- src tests -m end_to_end --cov=./ --cov-report=xml -n auto
5757

5858
- name: Upload coverage reports of end-to-end tests.
5959
if: runner.os == 'Linux' && matrix.python-version == '3.8'

codecov.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,3 @@ coverage:
2525
ignore:
2626
- ".tox/**/*"
2727
- "setup.py"
28-
- "tests/**/*"

docs/changes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ all releases are available on `Anaconda.org <https://anaconda.org/pytask/pytask>
1414
- :gh:`33` adds a module to apply common parameters to the command line interface.
1515
- :gh:`34` skips ``pytask_collect_task_teardown`` if task is None.
1616
- :gh:`35` adds the ability to capture stdout and stderr with the CaptureManager.
17+
- :gh:`36` reworks the debugger to make it work with the CaptureManager.
1718

1819

1920
0.0.8 - 2020-10-04

docs/tutorials/how_to_debug.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,11 @@ If you want to enter the debugger at the start of every task, use
2020
.. code-block:: console
2121
2222
$ pytask --trace
23+
24+
If you want to use your custom debugger, make sure it is importable and use
25+
:option:`pytask build --pdbcls`. Here, we change from the standard ``pdb`` debugger to
26+
IPython's implementation.
27+
28+
.. code-block:: console
29+
30+
$ pytask --pdbcls=IPython.terminal.debugger:TerminalPdb

src/_pytask/capture.py

Lines changed: 68 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ def pytask_extend_command_line_interface(cli):
5858
help="Per task capturing method. [default: fd]",
5959
),
6060
click.Option(["-s"], is_flag=True, help="Shortcut for --capture=no."),
61+
click.Option(
62+
["--show-capture"],
63+
type=click.Choice(["no", "stdout", "stderr", "all"]),
64+
help=(
65+
"Choose which captured output should be shown for failed tasks. "
66+
"[default: all]"
67+
),
68+
),
6169
]
6270
cli.commands["build"].params.extend(additional_parameters)
6371

@@ -80,6 +88,14 @@ def pytask_parse_config(config, config_from_cli, config_from_file):
8088
callback=_capture_callback,
8189
)
8290

91+
config["show_capture"] = get_first_non_none_value(
92+
config_from_cli,
93+
config_from_file,
94+
key="show_capture",
95+
default="all",
96+
callback=_show_capture_callback,
97+
)
98+
8399

84100
@hookimpl
85101
def pytask_post_parse(config):
@@ -106,6 +122,22 @@ def _capture_callback(x):
106122
else:
107123
raise ValueError("'capture' can only be one of ['fd', 'no', 'sys', 'tee-sys'].")
108124

125+
return x
126+
127+
128+
def _show_capture_callback(x):
129+
"""Validate the passed options for showing captured output."""
130+
if x in [None, "None", "none"]:
131+
x = None
132+
elif x in ["no", "stdout", "stderr", "all"]:
133+
pass
134+
else:
135+
raise ValueError(
136+
"'show_capture' must be one of ['no', 'stdout', 'stderr', 'all']."
137+
)
138+
139+
return x
140+
109141

110142
# Copied from pytest.
111143

@@ -126,21 +158,22 @@ def _colorama_workaround() -> None:
126158

127159

128160
def _readline_workaround() -> None:
129-
"""Ensure readline is imported so that it attaches to the correct stdio
130-
handles on Windows.
161+
"""Ensure readline is imported so that it attaches to the correct stdio handles on
162+
Windows.
131163
132-
Pdb uses readline support where available--when not running from the Python
133-
prompt, the readline module is not imported until running the pdb REPL. If
134-
running pytest with the --pdb option this means the readline module is not
135-
imported until after I/O capture has been started.
164+
Pdb uses readline support where available--when not running from the Python prompt,
165+
the readline module is not imported until running the pdb REPL. If running pytest
166+
with the --pdb option this means the readline module is not imported until after I/O
167+
capture has been started.
136168
137-
This is a problem for pyreadline, which is often used to implement readline
138-
support on Windows, as it does not attach to the correct handles for stdout
139-
and/or stdin if they have been redirected by the FDCapture mechanism. This
140-
workaround ensures that readline is imported before I/O capture is setup so
141-
that it can attach to the actual stdin/out for the console.
169+
This is a problem for pyreadline, which is often used to implement readline support
170+
on Windows, as it does not attach to the correct handles for stdout and/or stdin if
171+
they have been redirected by the FDCapture mechanism. This workaround ensures that
172+
readline is imported before I/O capture is setup so that it can attach to the actual
173+
stdin/out for the console.
142174
143175
See https://github.com/pytest-dev/pytest/pull/1281.
176+
144177
"""
145178
if sys.platform.startswith("win32"):
146179
try:
@@ -152,19 +185,17 @@ def _readline_workaround() -> None:
152185
def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
153186
"""Workaround for Windows Unicode console handling on Python>=3.6.
154187
155-
Python 3.6 implemented Unicode console handling for Windows. This works
156-
by reading/writing to the raw console handle using
157-
``{Read,Write}ConsoleW``.
188+
Python 3.6 implemented Unicode console handling for Windows. This works by
189+
reading/writing to the raw console handle using ``{Read,Write}ConsoleW``.
158190
159-
The problem is that we are going to ``dup2`` over the stdio file
160-
descriptors when doing ``FDCapture`` and this will ``CloseHandle`` the
161-
handles used by Python to write to the console. Though there is still some
162-
weirdness and the console handle seems to only be closed randomly and not
163-
on the first call to ``CloseHandle``, or maybe it gets reopened with the
164-
same handle value when we suspend capturing.
191+
The problem is that we are going to ``dup2`` over the stdio file descriptors when
192+
doing ``FDCapture`` and this will ``CloseHandle`` the handles used by Python to
193+
write to the console. Though there is still some weirdness and the console handle
194+
seems to only be closed randomly and not on the first call to ``CloseHandle``, or
195+
maybe it gets reopened with the same handle value when we suspend capturing.
165196
166-
The workaround in this case will reopen stdio with a different fd which
167-
also means a different handle by replicating the logic in
197+
The workaround in this case will reopen stdio with a different fd which also means a
198+
different handle by replicating the logic in
168199
"Py_lifecycle.c:initstdio/create_stdio".
169200
170201
:param stream:
@@ -384,6 +415,7 @@ class FDCaptureBinary:
384415
"""Capture IO to/from a given OS-level file descriptor.
385416
386417
snap() produces `bytes`.
418+
387419
"""
388420

389421
EMPTY_BUFFER = b""
@@ -394,17 +426,17 @@ def __init__(self, targetfd: int) -> None:
394426
try:
395427
os.fstat(targetfd)
396428
except OSError:
397-
# FD capturing is conceptually simple -- create a temporary file,
398-
# redirect the FD to it, redirect back when done. But when the
399-
# target FD is invalid it throws a wrench into this loveley scheme.
400-
#
401-
# Tests themselves shouldn't care if the FD is valid, FD capturing
402-
# should work regardless of external circumstances. So falling back
403-
# to just sys capturing is not a good option.
404-
#
429+
# FD capturing is conceptually simple -- create a temporary file, redirect
430+
# the FD to it, redirect back when done. But when the target FD is invalid
431+
# it throws a wrench into this loveley scheme.
432+
433+
# Tests themselves shouldn't care if the FD is valid, FD capturing should
434+
# work regardless of external circumstances. So falling back to just sys
435+
# capturing is not a good option.
436+
405437
# Further complications are the need to support suspend() and the
406-
# possibility of FD reuse (e.g. the tmpfile getting the very same
407-
# target FD). The following approach is robust, I believe.
438+
# possibility of FD reuse (e.g. the tmpfile getting the very same target
439+
# FD). The following approach is robust, I believe.
408440
self.targetfd_invalid: Optional[int] = os.open(os.devnull, os.O_RDWR)
409441
os.dup2(self.targetfd_invalid, targetfd)
410442
else:
@@ -524,11 +556,10 @@ def writeorg(self, data):
524556
# MultiCapture
525557

526558

527-
# This class was a namedtuple, but due to mypy limitation[0] it could not be
528-
# made generic, so was replaced by a regular class which tries to emulate the
529-
# pertinent parts of a namedtuple. If the mypy limitation is ever lifted, can
530-
# make it a namedtuple again.
531-
# [0]: https://github.com/python/mypy/issues/685
559+
# This class was a namedtuple, but due to mypy limitation[0] it could not be made
560+
# generic, so was replaced by a regular class which tries to emulate the pertinent parts
561+
# of a namedtuple. If the mypy limitation is ever lifted, can make it a namedtuple
562+
# again. [0]: https://github.com/python/mypy/issues/685
532563
@final
533564
@functools.total_ordering
534565
class CaptureResult(Generic[AnyStr]):

src/_pytask/cli.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def _prepare_plugin_manager():
2727

2828

2929
def _sort_options_for_each_command_alphabetically(cli):
30+
"""Sort command line options and arguments for each command alphabetically."""
3031
for command in cli.commands:
3132
cli.commands[command].params = sorted(
3233
cli.commands[command].params, key=lambda x: x.name

src/_pytask/collect.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
def pytask_collect(session):
2222
"""Collect tasks."""
2323
reports = _collect_from_paths(session)
24-
tasks = _extract_tasks_from_reports(reports)
24+
tasks = _extract_successful_tasks_from_reports(reports)
2525

2626
try:
2727
session.hook.pytask_collect_modify_tasks(session=session, tasks=tasks)
@@ -91,6 +91,7 @@ def pytask_collect_file_protocol(session, path, reports):
9191

9292
@hookimpl
9393
def pytask_collect_file(session, path, reports):
94+
"""Collect a file."""
9495
if any(path.match(pattern) for pattern in session.config["task_files"]):
9596
spec = importlib.util.spec_from_file_location(path.stem, str(path))
9697

@@ -121,6 +122,7 @@ def pytask_collect_file(session, path, reports):
121122

122123
@hookimpl
123124
def pytask_collect_task_protocol(session, reports, path, name, obj):
125+
"""Start protocol for collecting a task."""
124126
try:
125127
session.hook.pytask_collect_task_setup(
126128
session=session, reports=reports, path=path, name=name, obj=obj
@@ -204,12 +206,14 @@ def valid_paths(paths, session):
204206
yield path
205207

206208

207-
def _extract_tasks_from_reports(reports):
209+
def _extract_successful_tasks_from_reports(reports):
210+
"""Extract successful tasks from reports."""
208211
return [i.task for i in reports if i.successful]
209212

210213

211214
@hookimpl
212215
def pytask_collect_log(session, reports, tasks):
216+
"""Log collection."""
213217
tm_width = session.config["terminal_width"]
214218

215219
message = f"Collected {len(tasks)} task(s)."

src/_pytask/collect_command.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
"""This module contains the implementation of ``pytask collect``."""
12
import pdb
23
import sys
34
import traceback
@@ -18,6 +19,7 @@ def pytask_extend_command_line_interface(cli: click.Group):
1819

1920
@hookimpl
2021
def pytask_parse_config(config, config_from_cli):
22+
"""Parse configuration."""
2123
config["nodes"] = config_from_cli.get("nodes", False)
2224

2325

src/_pytask/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@
3030
".gitignore",
3131
".pre-commit-config.yaml",
3232
".readthedocs.yml",
33+
".readthedocs.yaml",
34+
"readthedocs.yml",
35+
"readthedocs.yaml",
3336
"environment.yml",
37+
"pytask.ini",
38+
"setup.cfg",
39+
"tox.ini",
3440
]
3541

3642
IGNORED_FILES_AND_FOLDERS = IGNORED_FILES + IGNORED_FOLDERS

0 commit comments

Comments
 (0)