Skip to content

Commit

Permalink
Improve, test, and document capture_stdout option (#188)
Browse files Browse the repository at this point in the history
- Add validations for capture_stdout option
- Make capture_stdout resolve relative to project_root
- Substitute environment variables in the capture_stdout option
- Add two relevant test cases
- Add documentation for the option
  • Loading branch information
nat-n committed Nov 18, 2023
1 parent ed8e89d commit 72ec368
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 15 deletions.
29 changes: 26 additions & 3 deletions docs/tasks/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,16 @@ The following options can be configured on your tasks and are not specific to an
Allows this task to use the output of other tasks which are executed first.
The value is a map where the values are invocations of the other tasks, and the keys are environment variables by which the results of those tasks will be accessible in this task.

**capture_stdout** : ``str`` :ref:`📖<Redirect task output to a file>`
Causes the task output to be redirected to a file with the given path.

**use_exec** : ``bool`` :ref:`📖<Defining tasks that run via exec instead of a subprocess>`
Specify that this task should be executed in the same process, instead of as a subprocess.

.. attention::

This option is only applicable to **cmd**, **script**, and **expr** tasks, and it implies the task in question cannot be referenced by another task.


Setting task specific environment variables
-------------------------------------------

Expand Down Expand Up @@ -76,7 +78,7 @@ It is also possible to reference existing environment variables when defining a
.. _envfile_option:

Loading environment variables from an env file
----------------------------------------------
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

You can also specify one or more env files (with bash-like syntax) to load per task like so:

Expand Down Expand Up @@ -126,8 +128,28 @@ Poe provides its own :sh:`$POE_PWD` variable that is by default set to the direc
cwd = "${POE_PWD}"
Redirect task output to a file
------------------------------

You can configure poe to redirect the standard output of a task to a file on disk by providing the ``capture_stdout`` option like so.

.. code-block:: toml
[tool.poe.tasks.serve]
cmd = "gunicorn ./my_app:run"
capture_stdout = "gunicorn_log.txt"
If a relative path is provided, as in the example above, then it will be resolved relative to the project root directory.

The ``capture_stdout`` option supports referencing environment variables. For example setting ``capture_stdout = "${POE_PWD}/output.txt"`` will cause the output file to be created within the current working directory of the parent process.

.. warning::

The ``capture_stdout`` is incompatible with the ``use_exec`` option, and tasks that declare it cannot be referenced by another task via the ``uses`` option.


Defining tasks that run via exec instead of a subprocess
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--------------------------------------------------------

Normally tasks are executed as subprocesses of the ``poe`` executable. This makes it possible for poe to run multiple tasks, for example within a sequence task or task graph.

Expand All @@ -145,3 +167,4 @@ However in certain situations it can be desirable to define a task that is inste

1. a task configured in this way may not be referenced by another task
2. this does not work on windows becuase of `this issue <https://bugs.python.org/issue19066>`_. On windows a subprocess is always created.

10 changes: 8 additions & 2 deletions poethepoet/executor/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,13 @@ def __init__(
self.working_dir = working_dir
self.env = env
self.dry = dry
self.capture_stdout = capture_stdout
self.capture_stdout = (
Path(self.context.config.project_dir).joinpath(
self.env.fill_template(capture_stdout)
)
if isinstance(capture_stdout, str)
else capture_stdout
)
self._is_windows = sys.platform == "win32"

@classmethod
Expand Down Expand Up @@ -227,7 +233,7 @@ def _exec_via_subproc(
if input is not None:
popen_kwargs["stdin"] = PIPE
if self.capture_stdout:
if isinstance(self.capture_stdout, str):
if isinstance(self.capture_stdout, Path):
# ruff: noqa: SIM115
popen_kwargs["stdout"] = open(self.capture_stdout, "wb")
else:
Expand Down
27 changes: 19 additions & 8 deletions poethepoet/task/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,12 @@ def validate_def(
"line breaks"
)

if task_def.get("use_exec") and task_def.get("capture_stdout"):
return (
f"Invalid task: {task_name!r}, 'use_exec' and 'capture_stdout'"
" options cannot be both provided on the same task."
)

all_task_names = set(config.tasks.keys())

if "deps" in task_def:
Expand Down Expand Up @@ -488,14 +494,19 @@ def validate_def(
)

referenced_task = config.tasks[dep_task_name]
if isinstance(referenced_task, dict) and referenced_task.get(
"use_exec"
):
return (
f"Invalid task: {task_name!r}. uses options contains "
"reference to task with use_exec set to true: "
f"{dep_task_name!r}"
)
if isinstance(referenced_task, dict):
if referenced_task.get("use_exec"):
return (
f"Invalid task: {task_name!r}, 'uses' option references"
" task with 'use_exec' set to true: "
f"{dep_task_name!r}"
)
if referenced_task.get("capture_stdout"):
return (
f"Invalid task: {task_name!r}, 'uses' option references"
" task with 'capture_stdout' option set: "
f"{dep_task_name!r}"
)

elif isinstance(task_def, list):
task_type_key = config.default_array_task_type
Expand Down
1 change: 0 additions & 1 deletion poethepoet/task/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ def __init__(
capture_stdout: bool = False,
inheritance: Optional[TaskInheritance] = None,
):
assert capture_stdout is False
super().__init__(
name, content, options, ui, config, invocation, False, inheritance
)
Expand Down
4 changes: 4 additions & 0 deletions tests/fixtures/cmds_project/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@ cmd = "poe_test_echo \"first: ${first} second: ${second}\""
positional = true
multiple = true
type = "integer"

[tool.poe.tasks.meeseeks]
cmd = """poe_test_echo "I'm Mr. Meeseeks! Look at me!" """
capture_stdout = "${POE_PWD}/message.txt"
14 changes: 13 additions & 1 deletion tests/fixtures/switch_project/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,20 @@ control.expr = "42"
cmd = "echo 'matched'"

[[tool.poe.tasks.switcher.switch]]
cmd = "echo other"
cmd = "echo default"

[tool.poe.tasks.switcher_user]
uses = { switched = "switcher" }
cmd = "echo switched=$switched"


[tool.poe.tasks.capture_out]
control.expr = "43"
capture_stdout = "./out.txt"

[[tool.poe.tasks.capture_out.switch]]
case = "42"
cmd = "echo 'matched'"

[[tool.poe.tasks.capture_out.switch]]
cmd = "echo default"
26 changes: 26 additions & 0 deletions tests/test_cmd_tasks.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from pathlib import Path


def test_call_echo_task(run_poe_subproc, projects, esc_prefix, is_windows):
result = run_poe_subproc("echo", "foo", "!", project="cmds")
if is_windows:
Expand Down Expand Up @@ -166,3 +169,26 @@ def test_cmd_task_with_with_glob_arg_and_cwd(
assert result.capture.endswith("foo\n")
assert result.stdout == "bar.txt\n"
assert result.stderr == ""


def test_cmd_with_capture_stdout(run_poe_subproc, projects, poe_project_path):
result = run_poe_subproc(
"--root",
str(projects["cmds"]),
"meeseeks",
project="cmds",
cwd=poe_project_path,
)
assert (
result.capture
== """Poe <= poe_test_echo 'I'"'"'m Mr. Meeseeks! Look at me!'\n"""
)
assert result.stdout == ""
assert result.stderr == ""

output_path = Path("message.txt")
try:
with output_path.open("r") as output_file:
assert output_file.read() == "I'm Mr. Meeseeks! Look at me!\n"
finally:
output_path.unlink()
14 changes: 14 additions & 0 deletions tests/test_switch_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,17 @@ def test_switch_multivalue_case(run_poe_subproc):
)
assert result.stdout == "It is not in 1-6\n"
assert result.stderr == ""


def test_switch_capture_out(run_poe_subproc, projects):
result = run_poe_subproc("capture_out", project="switch")
assert result.capture == ("Poe <= 43\n" "Poe <= echo default\n")
assert result.stdout == ""
assert result.stderr == ""

output_path = projects["switch"].joinpath("out.txt")
try:
with output_path.open("r") as output_file:
assert output_file.read() == "default\n"
finally:
output_path.unlink()

0 comments on commit 72ec368

Please sign in to comment.