Skip to content

Commit

Permalink
Support configuring tasks to run in the original working directory (#161
Browse files Browse the repository at this point in the history
)

The EnvVarsManager now sets an new environment variable `"POE_PWD"` (if not already set) to the path of the current working directory of the process. The variable can then be referenced from the cwd option of a task using the usual variable templating syntax `${}` to make a task run in (or relative to) the user's current working directory instead of relative to the project root.

---------

Co-authored-by: Nat Noordanus <[email protected]>
  • Loading branch information
Spiffyk and nat-n committed Aug 13, 2023
1 parent 02c0b3b commit e185a56
Show file tree
Hide file tree
Showing 12 changed files with 144 additions and 11 deletions.
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ Run poe from anywhere

By default poe will detect when you're inside a project with a pyproject.toml in the root. However if you want to run it from elsewhere then that is supported by using the :sh:`--root` option to specify an alternate location for the toml file. The task will run with the given location as the current working directory.

In all cases the path to project root (where the pyproject.toml resides) will be available as :sh:`$POE_ROOT` within the command line and process.
In all cases the path to project root (where the pyproject.toml resides) will be available as :sh:`$POE_ROOT` within the command line and process. The variable :sh:`$POE_PWD` contains the original working directory from which poe was run.


.. |poetry_link| raw:: html
Expand Down
17 changes: 14 additions & 3 deletions docs/tasks/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ The following options can be configured on your tasks and are not specific to an
Provide one or more env files to be loaded before running this task.

**cwd** : ``str`` :ref:`📖<Running a task with a specific working directory>`
Specify the current working directory that this task should run with. The given path is resolved relative to the parent directory of the ``pyproject.toml``.
Specify the current working directory that this task should run with. The given path is resolved relative to the parent directory of the ``pyproject.toml``, or it may be absolute.
Resolves environment variables in the format ``${VAR_NAME}``.

**deps** : ``List[str]`` :doc:`📖<../guides/composition_guide>`
A list of task invocations that will be executed before this one.
Expand Down Expand Up @@ -104,15 +105,25 @@ In this case the referenced files will be loaded in the given order.
Running a task with a specific working directory
------------------------------------------------

By default tasks are run from the project root – that is the parent directory of the pyproject.toml file. However if a task needs to be run in another directory within the project then this can be accomplished by using the :toml:`cwd` option like so:
By default tasks are run from the project root – that is the parent directory of the pyproject.toml file. However if a task needs to be run in another directory then this can be accomplished by using the :toml:`cwd` option like so:

.. code-block:: toml
[tool.poe.tasks.build-client]
cmd = "npx ts-node -T ./build.ts"
cwd = "./client"
In this example, the npx executable is executed inside the :sh:`./client` subdirectory of the project, and will use the nodejs package.json configuration from that location and evaluate paths relative to that location.
In this example, the npx executable is executed inside the :sh:`./client` subdirectory of the project (when ``cwd`` is a relative path, it gets resolved relatively to the project root), and will use the nodejs package.json configuration from that location and evaluate paths relative to that location.

The ``cwd`` option also accepts absolute paths and resolves environment variables in the format ``${VAR_NAME}``.

Poe provides its own :sh:`$POE_PWD` variable that is by default set to the directory, from which poe was executed; this may be overridden by setting the variable to a different value beforehand. Using :sh:`$POE_PWD`, a task's working directory may be set to the one from which it was executed like so:

.. code-block:: toml
[tool.poe.tasks.convert]
script = "my_project.conversion_tool:main"
cwd = "${POE_PWD}"
Defining tasks that run via exec instead of a subprocess
Expand Down
1 change: 1 addition & 0 deletions poethepoet/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ def get_run_context(self, multistage: bool = False) -> "RunContext":
dry=self.ui["dry_run"],
poe_active=os.environ.get("POE_ACTIVE"),
multistage=multistage,
cwd=self.cwd,
)
if self._poetry_env_path:
# This allows the PoetryExecutor to use the venv from poetry directly
Expand Down
13 changes: 10 additions & 3 deletions poethepoet/context.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import re
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Mapping, Optional, Tuple
from typing import TYPE_CHECKING, Any, Dict, Mapping, Optional, Tuple, Union

if TYPE_CHECKING:
from .config import PoeConfig
Expand Down Expand Up @@ -28,6 +28,7 @@ def __init__(
dry: bool,
poe_active: Optional[str],
multistage: bool = False,
cwd: Optional[Union[Path, str]] = None,
):
from .env.manager import EnvVarsManager

Expand All @@ -39,7 +40,7 @@ def __init__(
self.multistage = multistage
self.exec_cache = {}
self.captured_stdout = {}
self.env = EnvVarsManager(self.config, self.ui, base_env=env)
self.env = EnvVarsManager(self.config, self.ui, base_env=env, cwd=cwd)

@property
def executor_type(self) -> Optional[str]:
Expand Down Expand Up @@ -105,11 +106,17 @@ def get_executor(
) -> "PoeExecutor":
from .executor import PoeExecutor

cwd_option = env.fill_template(task_options.get("cwd", "."))
working_dir = Path(cwd_option)

if not working_dir.is_absolute():
working_dir = self.project_dir / working_dir

return PoeExecutor.get(
invocation=invocation,
context=self,
env=env,
working_dir=self.project_dir / task_options.get("cwd", "."),
working_dir=working_dir,
dry=self.dry,
executor_config=task_options.get("executor"),
capture_stdout=task_options.get("capture_stdout", False),
Expand Down
13 changes: 12 additions & 1 deletion poethepoet/env/manager.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Mapping, Optional, Union

Expand All @@ -21,6 +22,7 @@ def __init__(
ui: Optional["PoeUi"],
parent_env: Optional["EnvVarsManager"] = None,
base_env: Optional[Mapping[str, str]] = None,
cwd: Optional[Union[Path, str]] = None,
):
from .cache import EnvFileCache

Expand Down Expand Up @@ -51,6 +53,10 @@ def __init__(

self._vars["POE_ROOT"] = str(self._config.project_dir)

self.cwd = str(cwd or os.getcwd())
if "POE_PWD" not in self._vars:
self._vars["POE_PWD"] = self.cwd

def get(self, key: str, default: Optional[str] = None) -> Optional[str]:
return self._vars.get(key, default)

Expand Down Expand Up @@ -81,7 +87,12 @@ def for_task(
"""
Create a copy of self and extend it to include vars for the task.
"""
result = EnvVarsManager(self._config, self._ui, parent_env=self)
result = EnvVarsManager(
self._config,
self._ui,
parent_env=self,
cwd=self.cwd,
)

# Include env vars from envfile(s) referenced in task options
if isinstance(task_envfile, str):
Expand Down
6 changes: 4 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,14 @@ def run_poe_subproc(projects, temp_file, tmp_path, is_windows):

def run_poe_subproc(
*run_args: str,
cwd: str = projects["example"],
cwd: Optional[str] = None,
config: Optional[Mapping[str, Any]] = None,
coverage: bool = not is_windows,
env: Dict[str, str] = None,
project: Optional[str] = None,
) -> PoeRunResult:
cwd = projects.get(project, cwd)
if cwd is None:
cwd = projects.get(project, projects["example"])
if config is not None:
config_path = tmp_path.joinpath("tmp_test_config_file")
with config_path.open("w+") as config_file:
Expand All @@ -141,6 +142,7 @@ def run_poe_subproc(

subproc_env = dict(os.environ)
subproc_env.pop("VIRTUAL_ENV", None)
subproc_env.pop("POE_PWD", None) # do not inherit this from the test
if env:
subproc_env.update(env)

Expand Down
13 changes: 13 additions & 0 deletions tests/fixtures/cwd_project/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
[tool.poe.tasks.cwd]
cmd = "poe_test_pwd"
cwd = "./subdir/foo"

[tool.poe.tasks.cwd_env]
cmd = "poe_test_pwd"
cwd = "./subdir/${BAR_ENV}"

[tool.poe.tasks.cwd_poe_pwd]
cmd = "poe_test_pwd"
cwd = "${POE_PWD}"

[tool.poe.tasks.cwd_arg]
cmd = "poe_test_pwd"
cwd = "./subdir/${foo_var}"
args = ["foo_var"]
Empty file.
2 changes: 2 additions & 0 deletions tests/fixtures/monorepo_project/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ path = "subproject_1/pyproject.toml"
[[tool.poe.include]]
path = "subproject_2/pyproject.toml"
cwd = "subproject_2"
[[tool.poe.include]]
path = "subproject_3/pyproject.toml"


[tool.poe.tasks.get_cwd_0]
Expand Down
7 changes: 7 additions & 0 deletions tests/fixtures/monorepo_project/subproject_3/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@



[tool.poe.tasks.get_cwd_3]
interpreter = "python"
shell = "import os; print(os.getcwd())"
cwd = "${POE_PWD}"
62 changes: 62 additions & 0 deletions tests/test_cmd_tasks.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import os


def test_call_echo_task(run_poe_subproc, projects, esc_prefix):
result = run_poe_subproc("echo", "foo", "!", project="cmds")
assert (
Expand Down Expand Up @@ -63,3 +66,62 @@ def test_cmd_task_with_cwd_option(run_poe_subproc, poe_project_path):
== f'{poe_project_path.joinpath("tests", "fixtures", "cwd_project", "subdir", "foo")}\n'
)
assert result.stderr == ""


def test_cmd_task_with_cwd_option_env(run_poe_subproc, poe_project_path):
result = run_poe_subproc("cwd_env", project="cwd", env={"BAR_ENV": "bar"})
assert result.capture == "Poe => poe_test_pwd\n"
assert (
result.stdout
== f'{poe_project_path.joinpath("tests", "fixtures", "cwd_project", "subdir", "bar")}\n'
)
assert result.stderr == ""


def test_cmd_task_with_cwd_option_pwd(run_poe_subproc, poe_project_path):
result = run_poe_subproc(
"cwd_poe_pwd",
project="cwd",
cwd=poe_project_path.joinpath(
"tests", "fixtures", "cwd_project", "subdir", "foo"
),
)
assert result.capture == "Poe => poe_test_pwd\n"
assert (
result.stdout
== f'{poe_project_path.joinpath("tests", "fixtures", "cwd_project", "subdir", "foo")}\n'
)
assert result.stderr == ""


def test_cmd_task_with_cwd_option_pwd_override(run_poe_subproc, poe_project_path):
result = run_poe_subproc(
"cwd_poe_pwd",
project="cwd",
env={
"POE_PWD": str(
poe_project_path.joinpath(
"tests", "fixtures", "cwd_project", "subdir", "bar"
)
)
},
cwd=poe_project_path.joinpath(
"tests", "fixtures", "cwd_project", "subdir", "foo"
),
)
assert result.capture == "Poe => poe_test_pwd\n"
assert (
result.stdout
== f'{poe_project_path.joinpath("tests", "fixtures", "cwd_project", "subdir", "bar")}\n'
)
assert result.stderr == ""


def test_cmd_task_with_cwd_option_arg(run_poe_subproc, poe_project_path):
result = run_poe_subproc("cwd_arg", "--foo_var", "foo", project="cwd")
assert result.capture == "Poe => poe_test_pwd\n"
assert (
result.stdout
== f'{poe_project_path.joinpath("tests", "fixtures", "cwd_project", "subdir", "foo")}\n'
)
assert result.stderr == ""
19 changes: 18 additions & 1 deletion tests/test_includes.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import os


def test_docs_for_include_toml_file(run_poe_subproc):
result = run_poe_subproc(project="includes")
assert (
Expand Down Expand Up @@ -89,7 +92,8 @@ def test_monorepo_contains_only_expected_tasks(run_poe_subproc, projects):
" get_cwd_0 \n"
" get_cwd_1 \n"
" add \n"
" get_cwd_2 \n\n\n"
" get_cwd_2 \n"
" get_cwd_3 \n\n\n"
)
assert result.stdout == ""
assert result.stderr == ""
Expand Down Expand Up @@ -153,3 +157,16 @@ def test_monorepo_runs_each_task_with_expected_cwd(
else:
assert result.stdout.endswith("/tests/fixtures/monorepo_project/subproject_2\n")
assert result.stderr == ""

result = run_poe_subproc(
"--root",
str(projects["monorepo/subproject_3"]),
"get_cwd_3",
cwd=projects["example"],
)
assert result.capture == "Poe => import os; print(os.getcwd())\n"
if is_windows:
assert result.stdout.endswith("\\tests\\fixtures\\example_project\n")
else:
assert result.stdout.endswith("/tests/fixtures/example_project\n")
assert result.stderr == ""

0 comments on commit e185a56

Please sign in to comment.