Skip to content
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
6 changes: 3 additions & 3 deletions .github/workflows/continuous-integration-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:

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

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

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

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

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

- name: Upload coverage reports of end-to-end tests.
if: runner.os == 'Linux' && matrix.python-version == '3.8'
Expand Down
1 change: 0 additions & 1 deletion codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,3 @@ coverage:
ignore:
- ".tox/**/*"
- "setup.py"
- "tests/**/*"
1 change: 1 addition & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ all releases are available on `Anaconda.org <https://anaconda.org/pytask/pytask>
- :gh:`33` adds a module to apply common parameters to the command line interface.
- :gh:`34` skips ``pytask_collect_task_teardown`` if task is None.
- :gh:`35` adds the ability to capture stdout and stderr with the CaptureManager.
- :gh:`36` reworks the debugger to make it work with the CaptureManager.


0.0.8 - 2020-10-04
Expand Down
8 changes: 8 additions & 0 deletions docs/tutorials/how_to_debug.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,11 @@ If you want to enter the debugger at the start of every task, use
.. code-block:: console

$ pytask --trace

If you want to use your custom debugger, make sure it is importable and use
:option:`pytask build --pdbcls`. Here, we change from the standard ``pdb`` debugger to
IPython's implementation.

.. code-block:: console

$ pytask --pdbcls=IPython.terminal.debugger:TerminalPdb
105 changes: 68 additions & 37 deletions src/_pytask/capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ def pytask_extend_command_line_interface(cli):
help="Per task capturing method. [default: fd]",
),
click.Option(["-s"], is_flag=True, help="Shortcut for --capture=no."),
click.Option(
["--show-capture"],
type=click.Choice(["no", "stdout", "stderr", "all"]),
help=(
"Choose which captured output should be shown for failed tasks. "
"[default: all]"
),
),
]
cli.commands["build"].params.extend(additional_parameters)

Expand All @@ -80,6 +88,14 @@ def pytask_parse_config(config, config_from_cli, config_from_file):
callback=_capture_callback,
)

config["show_capture"] = get_first_non_none_value(
config_from_cli,
config_from_file,
key="show_capture",
default="all",
callback=_show_capture_callback,
)


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

return x


def _show_capture_callback(x):
"""Validate the passed options for showing captured output."""
if x in [None, "None", "none"]:
x = None
elif x in ["no", "stdout", "stderr", "all"]:
pass
else:
raise ValueError(
"'show_capture' must be one of ['no', 'stdout', 'stderr', 'all']."
)

return x


# Copied from pytest.

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


def _readline_workaround() -> None:
"""Ensure readline is imported so that it attaches to the correct stdio
handles on Windows.
"""Ensure readline is imported so that it attaches to the correct stdio handles on
Windows.

Pdb uses readline support where available--when not running from the Python
prompt, the readline module is not imported until running the pdb REPL. If
running pytest with the --pdb option this means the readline module is not
imported until after I/O capture has been started.
Pdb uses readline support where available--when not running from the Python prompt,
the readline module is not imported until running the pdb REPL. If running pytest
with the --pdb option this means the readline module is not imported until after I/O
capture has been started.

This is a problem for pyreadline, which is often used to implement readline
support on Windows, as it does not attach to the correct handles for stdout
and/or stdin if they have been redirected by the FDCapture mechanism. This
workaround ensures that readline is imported before I/O capture is setup so
that it can attach to the actual stdin/out for the console.
This is a problem for pyreadline, which is often used to implement readline support
on Windows, as it does not attach to the correct handles for stdout and/or stdin if
they have been redirected by the FDCapture mechanism. This workaround ensures that
readline is imported before I/O capture is setup so that it can attach to the actual
stdin/out for the console.

See https://github.com/pytest-dev/pytest/pull/1281.

"""
if sys.platform.startswith("win32"):
try:
Expand All @@ -152,19 +185,17 @@ def _readline_workaround() -> None:
def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
"""Workaround for Windows Unicode console handling on Python>=3.6.

Python 3.6 implemented Unicode console handling for Windows. This works
by reading/writing to the raw console handle using
``{Read,Write}ConsoleW``.
Python 3.6 implemented Unicode console handling for Windows. This works by
reading/writing to the raw console handle using ``{Read,Write}ConsoleW``.

The problem is that we are going to ``dup2`` over the stdio file
descriptors when doing ``FDCapture`` and this will ``CloseHandle`` the
handles used by Python to write to the console. Though there is still some
weirdness and the console handle seems to only be closed randomly and not
on the first call to ``CloseHandle``, or maybe it gets reopened with the
same handle value when we suspend capturing.
The problem is that we are going to ``dup2`` over the stdio file descriptors when
doing ``FDCapture`` and this will ``CloseHandle`` the handles used by Python to
write to the console. Though there is still some weirdness and the console handle
seems to only be closed randomly and not on the first call to ``CloseHandle``, or
maybe it gets reopened with the same handle value when we suspend capturing.

The workaround in this case will reopen stdio with a different fd which
also means a different handle by replicating the logic in
The workaround in this case will reopen stdio with a different fd which also means a
different handle by replicating the logic in
"Py_lifecycle.c:initstdio/create_stdio".

:param stream:
Expand Down Expand Up @@ -384,6 +415,7 @@ class FDCaptureBinary:
"""Capture IO to/from a given OS-level file descriptor.

snap() produces `bytes`.

"""

EMPTY_BUFFER = b""
Expand All @@ -394,17 +426,17 @@ def __init__(self, targetfd: int) -> None:
try:
os.fstat(targetfd)
except OSError:
# FD capturing is conceptually simple -- create a temporary file,
# redirect the FD to it, redirect back when done. But when the
# target FD is invalid it throws a wrench into this loveley scheme.
#
# Tests themselves shouldn't care if the FD is valid, FD capturing
# should work regardless of external circumstances. So falling back
# to just sys capturing is not a good option.
#
# FD capturing is conceptually simple -- create a temporary file, redirect
# the FD to it, redirect back when done. But when the target FD is invalid
# it throws a wrench into this loveley scheme.

# Tests themselves shouldn't care if the FD is valid, FD capturing should
# work regardless of external circumstances. So falling back to just sys
# capturing is not a good option.

# Further complications are the need to support suspend() and the
# possibility of FD reuse (e.g. the tmpfile getting the very same
# target FD). The following approach is robust, I believe.
# possibility of FD reuse (e.g. the tmpfile getting the very same target
# FD). The following approach is robust, I believe.
self.targetfd_invalid: Optional[int] = os.open(os.devnull, os.O_RDWR)
os.dup2(self.targetfd_invalid, targetfd)
else:
Expand Down Expand Up @@ -524,11 +556,10 @@ def writeorg(self, data):
# MultiCapture


# This class was a namedtuple, but due to mypy limitation[0] it could not be
# made generic, so was replaced by a regular class which tries to emulate the
# pertinent parts of a namedtuple. If the mypy limitation is ever lifted, can
# make it a namedtuple again.
# [0]: https://github.com/python/mypy/issues/685
# This class was a namedtuple, but due to mypy limitation[0] it could not be made
# generic, so was replaced by a regular class which tries to emulate the pertinent parts
# of a namedtuple. If the mypy limitation is ever lifted, can make it a namedtuple
# again. [0]: https://github.com/python/mypy/issues/685
@final
@functools.total_ordering
class CaptureResult(Generic[AnyStr]):
Expand Down
1 change: 1 addition & 0 deletions src/_pytask/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def _prepare_plugin_manager():


def _sort_options_for_each_command_alphabetically(cli):
"""Sort command line options and arguments for each command alphabetically."""
for command in cli.commands:
cli.commands[command].params = sorted(
cli.commands[command].params, key=lambda x: x.name
Expand Down
8 changes: 6 additions & 2 deletions src/_pytask/collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
def pytask_collect(session):
"""Collect tasks."""
reports = _collect_from_paths(session)
tasks = _extract_tasks_from_reports(reports)
tasks = _extract_successful_tasks_from_reports(reports)

try:
session.hook.pytask_collect_modify_tasks(session=session, tasks=tasks)
Expand Down Expand Up @@ -91,6 +91,7 @@ def pytask_collect_file_protocol(session, path, reports):

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

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

@hookimpl
def pytask_collect_task_protocol(session, reports, path, name, obj):
"""Start protocol for collecting a task."""
try:
session.hook.pytask_collect_task_setup(
session=session, reports=reports, path=path, name=name, obj=obj
Expand Down Expand Up @@ -204,12 +206,14 @@ def valid_paths(paths, session):
yield path


def _extract_tasks_from_reports(reports):
def _extract_successful_tasks_from_reports(reports):
"""Extract successful tasks from reports."""
return [i.task for i in reports if i.successful]


@hookimpl
def pytask_collect_log(session, reports, tasks):
"""Log collection."""
tm_width = session.config["terminal_width"]

message = f"Collected {len(tasks)} task(s)."
Expand Down
2 changes: 2 additions & 0 deletions src/_pytask/collect_command.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""This module contains the implementation of ``pytask collect``."""
import pdb
import sys
import traceback
Expand All @@ -18,6 +19,7 @@ def pytask_extend_command_line_interface(cli: click.Group):

@hookimpl
def pytask_parse_config(config, config_from_cli):
"""Parse configuration."""
config["nodes"] = config_from_cli.get("nodes", False)


Expand Down
6 changes: 6 additions & 0 deletions src/_pytask/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@
".gitignore",
".pre-commit-config.yaml",
".readthedocs.yml",
".readthedocs.yaml",
"readthedocs.yml",
"readthedocs.yaml",
"environment.yml",
"pytask.ini",
"setup.cfg",
"tox.ini",
]

IGNORED_FILES_AND_FOLDERS = IGNORED_FILES + IGNORED_FOLDERS
Expand Down
Loading