diff --git a/poethepoet/app.py b/poethepoet/app.py index 47035836..afb0b6ac 100644 --- a/poethepoet/app.py +++ b/poethepoet/app.py @@ -58,7 +58,7 @@ class PoeThePoet: def __init__( self, - cwd: Optional[Path] = None, + cwd: Optional[Union[Path, str]] = None, config: Optional[Union[Mapping[str, Any], "PoeConfig"]] = None, output: IO = sys.stdout, poetry_env_path: Optional[str] = None, @@ -68,11 +68,16 @@ def __init__( from .config import PoeConfig from .ui import PoeUi - self.cwd = cwd or Path().resolve() + self.cwd = Path(cwd) if cwd else Path().resolve() + + if self.cwd and self.cwd.is_file(): + config_name = self.cwd.name + self.cwd = self.cwd.parent + self.config = ( config if isinstance(config, PoeConfig) - else PoeConfig(cwd=cwd, table=config, config_name=config_name) + else PoeConfig(cwd=self.cwd, table=config, config_name=config_name) ) self.ui = PoeUi(output=output, program_name=program_name) self._poetry_env_path = poetry_env_path diff --git a/poethepoet/config.py b/poethepoet/config.py index 5ffa7e7d..1ed996da 100644 --- a/poethepoet/config.py +++ b/poethepoet/config.py @@ -202,8 +202,8 @@ def find_config_file(self, target_dir: Optional[str] = None) -> Path: """ Resolve a path to a self._config_name using one of two strategies: 1. If target_dir is provided then only look there, (accept path to config file - or to a directory dir). - 2. Otherwise look for the self._config_name is the current working directory, + or to a directory). + 2. Otherwise look for the self._config_name in the current working directory, following by all parent directories in ascending order. Both strategies result in an Exception on failure. diff --git a/poethepoet/context.py b/poethepoet/context.py index aaaec6cd..685ad9fa 100644 --- a/poethepoet/context.py +++ b/poethepoet/context.py @@ -98,20 +98,13 @@ def get_task_output(self, invocation: Tuple[str, ...]): """ return re.sub(r"\s+", " ", self.captured_stdout[invocation].strip("\r\n")) - def get_working_dir(self, env: "EnvVarsManager", task_options: Dict[str, Any]): - 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 working_dir - def get_executor( self, invocation: Tuple[str, ...], env: "EnvVarsManager", - task_options: Dict[str, Any], + working_dir: Path, + executor_config: Optional[Mapping[str, str]] = None, + capture_stdout: bool = False, ) -> "PoeExecutor": from .executor import PoeExecutor @@ -119,8 +112,8 @@ def get_executor( invocation=invocation, context=self, env=env, - working_dir=self.get_working_dir(env, task_options), + working_dir=working_dir, dry=self.dry, - executor_config=task_options.get("executor"), - capture_stdout=task_options.get("capture_stdout", False), + executor_config=executor_config, + capture_stdout=capture_stdout, ) diff --git a/poethepoet/executor/base.py b/poethepoet/executor/base.py index 1594da86..ed465a9c 100644 --- a/poethepoet/executor/base.py +++ b/poethepoet/executor/base.py @@ -163,7 +163,14 @@ def _execute_cmd( return self._exec_via_subproc(cmd, input=input, env=env, shell=shell) except FileNotFoundError as error: - return self._handle_file_not_found(cmd, error) + if error.filename == cmd[0]: + return self._handle_file_not_found(cmd, error) + if error.filename == self.working_dir: + raise PoeException( + "The specified working directory does not exists " + f"'{self.working_dir}'" + ) + raise def _handle_file_not_found( self, cmd: Sequence[str], error: FileNotFoundError diff --git a/poethepoet/task/base.py b/poethepoet/task/base.py index f101b410..147ec5e2 100644 --- a/poethepoet/task/base.py +++ b/poethepoet/task/base.py @@ -8,6 +8,7 @@ Dict, Iterator, List, + NamedTuple, Optional, Sequence, Tuple, @@ -47,10 +48,23 @@ def __init__(cls, *args): TaskContent = Union[str, List[Union[str, Dict[str, Any]]]] +class TaskInheritance(NamedTuple): + """ + Collection of inheritanced config from a parent task to a child task + """ + + cwd: str + + @classmethod + def from_task(cls, parent_task: "PoeTask"): + return cls(cwd=str(parent_task.options.get("cwd", parent_task.inheritance.cwd))) + + class PoeTask(metaclass=MetaPoeTask): name: str content: TaskContent options: Dict[str, Any] + inheritance: TaskInheritance named_args: Optional[Dict[str, str]] = None __options__: Dict[str, Union[Type, Tuple[Type, ...]]] = {} @@ -81,6 +95,7 @@ def __init__( config: "PoeConfig", invocation: Tuple[str, ...], capture_stdout: bool = False, + inheritance: Optional[TaskInheritance] = None, ): self.name = name self.content = content @@ -89,6 +104,7 @@ def __init__( self._config = config self._is_windows = sys.platform == "win32" self.invocation = invocation + self.inheritance = inheritance or TaskInheritance(cwd=str(config.cwd)) @classmethod def from_config( @@ -98,6 +114,7 @@ def from_config( ui: "PoeUi", invocation: Tuple[str, ...], capture_stdout: Optional[bool] = None, + inheritance: Optional[TaskInheritance] = None, ) -> "PoeTask": task_def = config.tasks.get(task_name) if not task_def: @@ -109,6 +126,7 @@ def from_config( ui, invocation=invocation, capture_stdout=capture_stdout, + inheritance=inheritance, ) @classmethod @@ -121,6 +139,7 @@ def from_def( invocation: Tuple[str, ...], array_item: Union[bool, str] = False, capture_stdout: Optional[bool] = None, + inheritance: Optional[TaskInheritance] = None, ) -> "PoeTask": task_type = cls.resolve_task_type(task_def, config, array_item) if task_type is None: @@ -148,6 +167,7 @@ def from_def( ui=ui, config=config, invocation=invocation, + inheritance=inheritance, ) @classmethod @@ -268,6 +288,31 @@ def _handle_run( """ raise NotImplementedError + def _get_executor( + self, + context: "RunContext", + env: "EnvVarsManager", + ): + return context.get_executor( + self.invocation, + env, + working_dir=self.get_working_dir(env), + executor_config=self.options.get("executor"), + capture_stdout=self.options.get("capture_stdout", False), + ) + + def get_working_dir( + self, + env: "EnvVarsManager", + ) -> Path: + cwd_option = env.fill_template(self.options.get("cwd", self.inheritance.cwd)) + working_dir = Path(cwd_option) + + if not working_dir.is_absolute(): + working_dir = self._config.project_dir / working_dir + + return working_dir + def iter_upstream_tasks( self, context: "RunContext" ) -> Iterator[Tuple[str, "PoeTask"]]: diff --git a/poethepoet/task/cmd.py b/poethepoet/task/cmd.py index fed74905..7db949a0 100644 --- a/poethepoet/task/cmd.py +++ b/poethepoet/task/cmd.py @@ -44,7 +44,7 @@ def _handle_run( self._print_action(shlex.join(cmd), context.dry) - return context.get_executor(self.invocation, env, self.options).execute( + return self._get_executor(context, env).execute( cmd, use_exec=self.options.get("use_exec", False) ) @@ -68,7 +68,7 @@ def _resolve_args(self, context: "RunContext", env: "EnvVarsManager"): f"Invalid cmd task {self.name!r} includes multiple command lines" ) - working_dir = context.get_working_dir(env, self.options) + working_dir = self.get_working_dir(env) result = [] for cmd_token, has_glob in resolve_command_tokens( diff --git a/poethepoet/task/expr.py b/poethepoet/task/expr.py index 3b9a327f..450e020c 100644 --- a/poethepoet/task/expr.py +++ b/poethepoet/task/expr.py @@ -73,7 +73,7 @@ def _handle_run( cmd = ("python", "-c", "".join(script)) self._print_action(self.content.strip(), context.dry) - return context.get_executor(self.invocation, env, self.options).execute( + return self._get_executor(context, env).execute( cmd, use_exec=self.options.get("use_exec", False) ) diff --git a/poethepoet/task/ref.py b/poethepoet/task/ref.py index ae3a1954..0612ba82 100644 --- a/poethepoet/task/ref.py +++ b/poethepoet/task/ref.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Tuple, Type, Union -from .base import PoeTask +from .base import PoeTask, TaskInheritance if TYPE_CHECKING: from ..config import PoeConfig @@ -31,7 +31,13 @@ def _handle_run( invocation = tuple(shlex.split(env.fill_template(self.content.strip()))) extra_args = [*invocation[1:], *extra_args] - task = self.from_config(invocation[0], self._config, self._ui, invocation) + task = self.from_config( + invocation[0], + self._config, + self._ui, + invocation, + inheritance=TaskInheritance.from_task(self), + ) if task.has_deps(): return self._run_task_graph(task, context, extra_args, env) diff --git a/poethepoet/task/script.py b/poethepoet/task/script.py index 774f4a3e..54e5bc42 100644 --- a/poethepoet/task/script.py +++ b/poethepoet/task/script.py @@ -67,7 +67,7 @@ def _handle_run( cmd = ("python", "-c", "".join(script)) self._print_action(shlex.join(argv), context.dry) - return context.get_executor(self.invocation, env, self.options).execute( + return self._get_executor(context, env).execute( cmd, use_exec=self.options.get("use_exec", False) ) diff --git a/poethepoet/task/sequence.py b/poethepoet/task/sequence.py index 49c6df49..09999e2c 100644 --- a/poethepoet/task/sequence.py +++ b/poethepoet/task/sequence.py @@ -11,7 +11,7 @@ ) from ..exceptions import ExecutionError, PoeException -from .base import PoeTask, TaskContent +from .base import PoeTask, TaskContent, TaskInheritance if TYPE_CHECKING: from ..config import PoeConfig @@ -43,9 +43,12 @@ def __init__( config: "PoeConfig", invocation: Tuple[str, ...], capture_stdout: bool = False, + inheritance: Optional[TaskInheritance] = None, ): assert capture_stdout is False - super().__init__(name, content, options, ui, config, invocation) + super().__init__( + name, content, options, ui, config, invocation, False, inheritance + ) self.subtasks = [ self.from_def( @@ -55,6 +58,7 @@ def __init__( invocation=(task_name,), ui=ui, array_item=self.options.get("default_item_type", True), + inheritance=TaskInheritance.from_task(self), ) for index, item in enumerate(self.content) for task_name in ( diff --git a/poethepoet/task/shell.py b/poethepoet/task/shell.py index 959c51d7..f447737f 100644 --- a/poethepoet/task/shell.py +++ b/poethepoet/task/shell.py @@ -60,7 +60,7 @@ def _handle_run( self._print_action(content, context.dry) - return context.get_executor(self.invocation, env, self.options).execute( + return self._get_executor(context, env).execute( interpreter_cmd, input=content.encode() ) diff --git a/poethepoet/task/switch.py b/poethepoet/task/switch.py index e6dd145f..adaf39db 100644 --- a/poethepoet/task/switch.py +++ b/poethepoet/task/switch.py @@ -13,7 +13,7 @@ ) from ..exceptions import ExecutionError, PoeException -from .base import PoeTask, TaskContent +from .base import PoeTask, TaskContent, TaskInheritance if TYPE_CHECKING: from ..config import PoeConfig @@ -49,8 +49,12 @@ def __init__( config: "PoeConfig", invocation: Tuple[str, ...], capture_stdout: bool = False, + inheritance: Optional[TaskInheritance] = None, ): - super().__init__(name, content, options, ui, config, invocation) + assert capture_stdout is False + super().__init__( + name, content, options, ui, config, invocation, False, inheritance + ) control_task_name = f"{name}[control]" control_invocation: Tuple[str, ...] = (control_task_name,) @@ -65,6 +69,7 @@ def __init__( invocation=control_invocation, ui=ui, capture_stdout=True, + inheritance=TaskInheritance.from_task(self), ) self.switch_tasks = {} @@ -83,6 +88,7 @@ def __init__( config=config, invocation=task_invocation, ui=ui, + inheritance=TaskInheritance.from_task(self), ) def _handle_run( diff --git a/pyproject.toml b/pyproject.toml index c5fe867d..bfca9e01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -145,6 +145,13 @@ _clean_docs.script = "shutil:rmtree('docs/_build', ignore_errors=1)" help = "Execute poe from this repo (useful for testing)" script = "poethepoet:main" + [tool.poe.tasks.y] + cmd = "pwd" + + [tool.poe.tasks.x] + sequence = ["y", {cmd = "pwd"}] + cwd = "tests" + [tool.rstcheck] ignore_messages = [ diff --git a/tests/fixtures/sequences_project/pyproject.toml b/tests/fixtures/sequences_project/pyproject.toml index 81e9249b..f0c8b66c 100644 --- a/tests/fixtures/sequences_project/pyproject.toml +++ b/tests/fixtures/sequences_project/pyproject.toml @@ -44,3 +44,20 @@ env = {thing = "Done."} type = "integer" +[tool.poe.tasks.cwd_once] +expr = "os.getcwd()" +imports = ["os"] + +[tool.poe.tasks.cwd_elsewhere] +expr = "os.getcwd()" +imports = ["os"] +cwd = "${POE_ROOT}" + +[tool.poe.tasks.all_cwd] +sequence = [ + "cwd_once", + "cwd_elsewhere", + {script = "os:getcwd()", print_result = true}, + {script = "os:getcwd()", cwd = ".", print_result = true} +] +cwd = "my_package" diff --git a/tests/test_sequence_tasks.py b/tests/test_sequence_tasks.py index 4aa1f80b..618bcd4c 100644 --- a/tests/test_sequence_tasks.py +++ b/tests/test_sequence_tasks.py @@ -42,3 +42,32 @@ def test_sequence_task_with_multiple_value_arg(run_poe_subproc): ) assert result.stdout == "first: hey\nsecond: 1 2 3\nDone.\n" assert result.stderr == "" + + +def test_subtasks_inherit_cwd_option_as_default(run_poe_subproc, is_windows): + result = run_poe_subproc("all_cwd", project="sequences") + assert result.capture == ( + "Poe => os.getcwd()\n" + "Poe => os.getcwd()\n" + "Poe => 'all_cwd[2]'\n" + "Poe => 'all_cwd[3]'\n" + ) + if is_windows: + assert result.stdout.split()[0].endswith( + "tests\\fixtures\\sequences_project\\my_package" + ) + assert result.stdout.split()[1].endswith("tests\\fixtures\\sequences_project") + assert result.stdout.split()[2].endswith( + "tests\\fixtures\\sequences_project\\my_package" + ) + assert result.stdout.split()[3].endswith("tests\\fixtures\\sequences_project") + else: + assert result.stdout.split()[0].endswith( + "tests/fixtures/sequences_project/my_package" + ) + assert result.stdout.split()[1].endswith("tests/fixtures/sequences_project") + assert result.stdout.split()[2].endswith( + "tests/fixtures/sequences_project/my_package" + ) + assert result.stdout.split()[3].endswith("tests/fixtures/sequences_project") + assert result.stderr == ""