From 3c994684db00ebbe900248bf93419399249bc777 Mon Sep 17 00:00:00 2001 From: Nat Noordanus Date: Sat, 3 Dec 2022 00:02:33 +0100 Subject: [PATCH] Add support for interpolating env vars into task arg default values --- README.rst | 21 ++++++++++++++-- poethepoet/task/args.py | 8 ++++++- poethepoet/task/base.py | 24 +++++++++++-------- poethepoet/task/cmd.py | 5 ++-- poethepoet/task/sequence.py | 5 ++-- poethepoet/task/shell.py | 5 ++-- tests/fixtures/scripts_project/pyproject.toml | 8 ++++--- 7 files changed, 54 insertions(+), 22 deletions(-) diff --git a/README.rst b/README.rst index 334abaa0..e5d18976 100644 --- a/README.rst +++ b/README.rst @@ -641,8 +641,25 @@ Task argument options Named arguments support the following configuration options: - **default** : Union[str, int, float, bool] - The value to use if the argument is not provided. This option has no effect if the - required option is set to true. + The value to use if the argument is not provided. This option has no significance if + the required option is set to true. + + For string values, environment variables can be referenced using the usual templating + syntax as in the following example. + + .. code-block:: toml + + [[tool.poe.tasks.deploy.args]] + name = "region" + help = "The region to deploy to" + default = "${AWS_REGION}" + + This can be combined with setting an env values on the task with the default + specifier to get the following precendence of values for the arg: + + 1. the value passed on the command line + 2. the value of the variable set on the environment + 3. the default value for the environment variable configured on the task - **help** : str A short description of the argument to include in the documentation of the task. diff --git a/poethepoet/task/args.py b/poethepoet/task/args.py index 29934565..7036d3a3 100644 --- a/poethepoet/task/args.py +++ b/poethepoet/task/args.py @@ -12,6 +12,8 @@ Union, ) +from ..env.manager import EnvVarsManager + if TYPE_CHECKING: from ..config import PoeConfig @@ -39,9 +41,10 @@ class PoeTaskArgs: _args: Tuple[ArgParams, ...] - def __init__(self, args_def: ArgsDef, task_name: str): + def __init__(self, args_def: ArgsDef, task_name: str, env: EnvVarsManager): self._args = self._normalize_args_def(args_def) self._task_name = task_name + self._env = env @classmethod def _normalize_args_def(cls, args_def: ArgsDef) -> Tuple[ArgParams, ...]: @@ -244,6 +247,9 @@ def build_parser(self) -> argparse.ArgumentParser: def _get_argument_params(self, arg: ArgParams): default = arg.get("default") + if isinstance(default, str): + default = self._env.fill_template(default) + result = { "default": default, "help": arg.get("help", ""), diff --git a/poethepoet/task/base.py b/poethepoet/task/base.py index 987bc794..e72ef421 100644 --- a/poethepoet/task/base.py +++ b/poethepoet/task/base.py @@ -8,7 +8,6 @@ Dict, Iterator, List, - Mapping, Optional, Sequence, Tuple, @@ -54,6 +53,7 @@ class PoeTask(metaclass=MetaPoeTask): name: str content: TaskContent options: Dict[str, Any] + named_args: Optional[Dict[str, str]] = None __options__: Dict[str, Union[Type, Tuple[Type, ...]]] = {} __content_type__: Type = str @@ -94,7 +94,6 @@ def __init__( self._config = config self._is_windows = sys.platform == "win32" self.invocation = invocation - self.named_args = self._parse_named_args(invocation[1:]) @classmethod def from_config( @@ -202,18 +201,23 @@ def resolve_task_type( return None - def _parse_named_args(self, extra_args: Sequence[str]) -> Optional[Dict[str, str]]: + def _parse_named_args( + self, extra_args: Sequence[str], env: EnvVarsManager + ) -> Optional[Dict[str, str]]: args_def = self.options.get("args") if args_def: - return PoeTaskArgs(args_def, self.name).parse(extra_args) + return PoeTaskArgs(args_def, self.name, env).parse(extra_args) return None - @property - def has_named_args(self): - return bool(self.named_args) + # @property + # def has_named_args(self): + # return bool(self.named_args) - def get_named_arg_values(self) -> Mapping[str, str]: - result = {} + def get_named_arg_values(self, env: EnvVarsManager) -> Dict[str, str]: + result: Dict[str, str] = {} + + if self.named_args is None: + self.named_args = self._parse_named_args(self.invocation[1:], env) if not self.named_args: return {} @@ -279,7 +283,7 @@ def _get_upstream_invocations(self, context: "RunContext"): env = context.get_task_env( None, self.options.get("envfile"), self.options.get("env") ) - env.update(self.get_named_arg_values()) + env.update(self.get_named_arg_values(env)) self.__upstream_invocations = { "deps": [ diff --git a/poethepoet/task/cmd.py b/poethepoet/task/cmd.py index a656be50..a88ec61a 100644 --- a/poethepoet/task/cmd.py +++ b/poethepoet/task/cmd.py @@ -32,9 +32,10 @@ def _handle_run( extra_args: Sequence[str], env: EnvVarsManager, ) -> int: - env.update(self.get_named_arg_values()) + named_arg_values = self.get_named_arg_values(env) + env.update(named_arg_values) - if self.has_named_args: + if named_arg_values: # If named arguments are defined then it doesn't make sense to pass extra # args to the command, because they've already been parsed cmd = self._resolve_args(context, env) diff --git a/poethepoet/task/sequence.py b/poethepoet/task/sequence.py index 85e979f9..50cf9731 100644 --- a/poethepoet/task/sequence.py +++ b/poethepoet/task/sequence.py @@ -69,9 +69,10 @@ def _handle_run( extra_args: Sequence[str], env: EnvVarsManager, ) -> int: - env.update(self.get_named_arg_values()) + named_arg_values = self.get_named_arg_values(env) + env.update(named_arg_values) - if not self.has_named_args and any(arg.strip() for arg in extra_args): + if not named_arg_values and any(arg.strip() for arg in extra_args): raise PoeException(f"Sequence task {self.name!r} does not accept arguments") if len(self.subtasks) > 1: diff --git a/poethepoet/task/shell.py b/poethepoet/task/shell.py index 939142e3..5472679e 100644 --- a/poethepoet/task/shell.py +++ b/poethepoet/task/shell.py @@ -38,9 +38,10 @@ def _handle_run( extra_args: Sequence[str], env: EnvVarsManager, ) -> int: - env.update(self.get_named_arg_values()) + named_arg_values = self.get_named_arg_values(env) + env.update(named_arg_values) - if not self.has_named_args and any(arg.strip() for arg in extra_args): + if not named_arg_values and any(arg.strip() for arg in extra_args): raise PoeException(f"Shell task {self.name!r} does not accept arguments") interpreter_cmd = self.resolve_interpreter_cmd() diff --git a/tests/fixtures/scripts_project/pyproject.toml b/tests/fixtures/scripts_project/pyproject.toml index c9cf2fa9..314f9aba 100644 --- a/tests/fixtures/scripts_project/pyproject.toml +++ b/tests/fixtures/scripts_project/pyproject.toml @@ -77,10 +77,11 @@ greet = "pkg:greet" [tool.poe.tasks.greet-strict] script = "pkg:greet(greeting, user=name)" - help = "All arguments are required" + help = "All arguments are required" + env = { DOES = "does", STUFF = "matter" } [tool.poe.tasks.greet-strict.args.greeting] - default = "doesn't matter" + default = "${DOES}n't ${STUFF}" required = true help = "this one is required" @@ -90,11 +91,12 @@ greet = "pkg:greet" [tool.poe.tasks.greet-positional] script = "pkg:greet(greeting, user=username, upper=uppercase)" + env.DEFAULT_GREETING.default = "yo" [[tool.poe.tasks.greet-positional.args]] name = "greeting" positional = true - default = "yo" + default = "${DEFAULT_GREETING}" help = "this one is required" [[tool.poe.tasks.greet-positional.args]]