diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42deb91cc..a0b53a22e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,10 @@ jobs: matrix: os: [Ubuntu, MacOS, Windows] python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + exclude: # TODO: remove this when pip is fixed + - os: Windows + python-version: '3.11' + runs-on: ${{ matrix.os }}-latest steps: - uses: actions/checkout@v3 diff --git a/README.rst b/README.rst index 4d796f1f6..8d3cce3a8 100644 --- a/README.rst +++ b/README.rst @@ -234,6 +234,8 @@ If extra arguments are passed to task on the command line (and no CLI args are declared), then they will be available within the called python function via :python:`sys.argv`. +If the target python function is an async function then it will be exectued with :python:`asyncio.run`. + Calling standard library functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1064,6 +1066,34 @@ This works by setting the argument values as environment variables for the subta which can be read at runtime, but also referenced in the task definition as demonstrated in the above example for a *ref* task and *script* task. +Passing free arguments in addition to named arguments +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If no args are defined for a cmd task then any cli arguments that are provided are +simply appended to the command. If named arguments are defined then one can still +provide additional free arguments to the command by separating them from the defined +arguments with a double dash token :sh:`--`. + +For example given a task like: + +.. code-block:: toml + + [tool.poe.tasks.lint] + cmd = "ruff check ${target_dir}" + args = { target_dir = { options = ["--target", "-t"], default = "." }} + +calling the task like so: + +.. code-block:: sh + + poe lint -t tests -- --fix + +will result in poe parsing the target_dir cli option, but appending the :sh:`--fix` +flag to the ruff command without attempting to interpret it. + +Passing :sh:`--` in the arguments list to any other task type will simple result in any +subsequent arguments being ignored. + Project-wide configuration options ================================== @@ -1461,11 +1491,10 @@ macOS, linux and windows. Contributing ============ -There's plenty to do, come say hi in -`the issues `_! 👋 +There's plenty to do, come say hi in `the discussions `_ or +`open an issue `_! 👋 -Also check out the -`CONTRIBUTING.MD `_ 🤓 +Also check out the `CONTRIBUTING.MD `_ 🤓 Licence ======= diff --git a/poethepoet/__version__.py b/poethepoet/__version__.py index e9fa21e95..11ac8e1a9 100644 --- a/poethepoet/__version__.py +++ b/poethepoet/__version__.py @@ -1 +1 @@ -__version__ = "0.18.1" +__version__ = "0.19.0" diff --git a/poethepoet/app.py b/poethepoet/app.py index 058b9f9b0..9d64f9a2c 100644 --- a/poethepoet/app.py +++ b/poethepoet/app.py @@ -16,7 +16,6 @@ from .exceptions import ExecutionError, PoeException if TYPE_CHECKING: - from .config import PoeConfig from .context import RunContext from .task import PoeTask @@ -48,7 +47,13 @@ def __init__( self.ui = PoeUi(output=output) self.poetry_env_path = poetry_env_path - def __call__(self, cli_args: Sequence[str]) -> int: + def __call__(self, cli_args: Sequence[str], internal: bool = False) -> int: + """ + :param internal: + indicates that this is an internal call to run poe, e.g. from a + plugin hook. + """ + self.ui.parse_args(cli_args) if self.ui["version"]: @@ -71,7 +76,7 @@ def __call__(self, cli_args: Sequence[str]) -> int: self.print_help() return 0 - if not self.resolve_task(): + if not self.resolve_task(internal): return 1 assert self.task @@ -80,7 +85,7 @@ def __call__(self, cli_args: Sequence[str]) -> int: else: return self.run_task() or 0 - def resolve_task(self) -> bool: + def resolve_task(self, allow_hidden: bool = False) -> bool: from .task import PoeTask task = tuple(self.ui["task"]) @@ -93,7 +98,7 @@ def resolve_task(self) -> bool: self.print_help(error=PoeException(f"Unrecognised task {task_name!r}")) return False - if task_name.startswith("_"): + if task_name.startswith("_") and not allow_hidden: self.print_help( error=PoeException( "Tasks prefixed with `_` cannot be executed directly" @@ -111,7 +116,7 @@ def run_task(self, context: Optional["RunContext"] = None) -> Optional[int]: context = self.get_run_context() try: assert self.task - return self.task.run(context=context, extra_args=self.ui["task"][1:]) + return self.task.run(context=context, extra_args=self.task.invocation[1:]) except PoeException as error: self.print_help(error=error) return 1 @@ -173,8 +178,11 @@ def print_help( from .task.args import PoeTaskArgs if isinstance(error, str): - error == PoeException(error) - tasks_help: Dict[str, Tuple[str, Sequence[Tuple[Tuple[str, ...], str]]]] = { + error = PoeException(error) + + tasks_help: Dict[ + str, Tuple[str, Sequence[Tuple[Tuple[str, ...], str, str]]] + ] = { task_name: ( ( content.get("help", ""), @@ -185,4 +193,5 @@ def print_help( ) for task_name, content in self.config.tasks.items() } - self.ui.print_help(tasks=tasks_help, info=info, error=error) # type: ignore + + self.ui.print_help(tasks=tasks_help, info=info, error=error) diff --git a/poethepoet/env/cache.py b/poethepoet/env/cache.py index 21fe1f47a..3e914cc3b 100644 --- a/poethepoet/env/cache.py +++ b/poethepoet/env/cache.py @@ -24,7 +24,7 @@ def get(self, envfile_path_str: str) -> Dict[str, str]: result = {} - envfile_path = self._project_dir.joinpath(envfile_path_str) + envfile_path = self._project_dir.joinpath(Path(envfile_path_str).expanduser()) if envfile_path.is_file(): try: with envfile_path.open() as envfile: diff --git a/poethepoet/env/manager.py b/poethepoet/env/manager.py index 2886674ed..e84500260 100644 --- a/poethepoet/env/manager.py +++ b/poethepoet/env/manager.py @@ -38,7 +38,7 @@ def __init__( } if parent_env is None: - # Get env vars from envfile referenced in global options + # Get env vars from envfile(s) referenced in global options global_envfile = self._config.global_envfile if isinstance(global_envfile, str): self._vars.update(self.envfiles.get(global_envfile)) @@ -83,7 +83,7 @@ def for_task( """ result = EnvVarsManager(self._config, self._ui, parent_env=self) - # Include env vars from envfile referenced in task options + # Include env vars from envfile(s) referenced in task options if isinstance(task_envfile, str): result.update(self.envfiles.get(task_envfile)) elif isinstance(task_envfile, list): diff --git a/poethepoet/exceptions.py b/poethepoet/exceptions.py index 0312d99f1..68d916e1e 100644 --- a/poethepoet/exceptions.py +++ b/poethepoet/exceptions.py @@ -1,4 +1,9 @@ +from typing import Optional + + class PoeException(RuntimeError): + cause: Optional[str] + def __init__(self, msg, *args): self.msg = msg self.cause = args[0].args[0] if args else None @@ -14,6 +19,8 @@ class ExpressionParseError(PoeException): class ExecutionError(RuntimeError): + cause: Optional[str] + def __init__(self, msg, *args): self.msg = msg self.cause = args[0].args[0] if args else None diff --git a/poethepoet/executor/base.py b/poethepoet/executor/base.py index 5b4c63771..cd69bc3fd 100644 --- a/poethepoet/executor/base.py +++ b/poethepoet/executor/base.py @@ -1,4 +1,5 @@ import os +import shutil import sys from pathlib import Path from typing import ( @@ -126,8 +127,15 @@ def execute( self, cmd: Sequence[str], input: Optional[bytes] = None, use_exec: bool = False ) -> int: """ - Execute the given cmd + Execute the given cmd. """ + + # Attempt to explicitly resolve the target executable, because we can't count + # on the OS to do this consistently. + resolved_executable = shutil.which(cmd[0]) + if resolved_executable: + cmd = (resolved_executable, *cmd[1:]) + return self._execute_cmd(cmd, input=input, use_exec=use_exec) def _execute_cmd( diff --git a/poethepoet/executor/poetry.py b/poethepoet/executor/poetry.py index 89cacea62..9997992b1 100644 --- a/poethepoet/executor/poetry.py +++ b/poethepoet/executor/poetry.py @@ -85,7 +85,11 @@ def _get_poetry_virtualenv(self, force: bool = True): def _poetry_cmd(self): import shutil - return shutil.which("poetry") or "poetry" + from_path = shutil.which("poetry") + if from_path: + return str(Path(from_path).resolve()) + + return "poetry" def _virtualenv_creation_disabled(self): exec_cache = self.context.exec_cache diff --git a/poethepoet/helpers/python.py b/poethepoet/helpers/python.py index 1d514e657..6331100d6 100644 --- a/poethepoet/helpers/python.py +++ b/poethepoet/helpers/python.py @@ -301,7 +301,7 @@ def _get_name_source_segment(source: str, node: ast.Name): performant in common cases. """ if sys.version_info.minor >= 8: - return ast.get_source_segment(source, node) # type: ignore + return ast.get_source_segment(source, node) partial_result = ( re.split(r"(?:\r\n|\r|\n)", source)[node.lineno - 1] diff --git a/poethepoet/plugin.py b/poethepoet/plugin.py index dd2f91cda..ca31f7721 100644 --- a/poethepoet/plugin.py +++ b/poethepoet/plugin.py @@ -128,6 +128,8 @@ def _activate(self, application: Application) -> None: command_prefix, ) for task_name, task in poe_tasks.items(): + if task_name.startswith("_"): + continue self._register_command( application, task_name, task, f"{command_prefix} " ) @@ -220,7 +222,7 @@ def command_event_handler( import shlex task_status = PoeCommand.get_poe(application, event.io)( - cli_args=shlex.split(task) + cli_args=shlex.split(task), internal=True ) if task_status: @@ -235,7 +237,7 @@ def _monkey_patch_cleo(self, prefix: str, task_names: List[str]): """ Cleo is quite opinionated about CLI structure and loose about how options are used, and so doesn't currently support invidual commands having their own way of - interpreting arguments, and forces them in inherit certain options from the + interpreting arguments, and forces them to inherit certain options from the application. This is a problem for poe which requires that global options are provided before the task name, and everything after the task name is interpreted ONLY in terms of the task. @@ -245,7 +247,7 @@ def _monkey_patch_cleo(self, prefix: str, task_names: List[str]): option parsing are effectively disabled following any occurance of a "--" on the command line, but parsing of the command name still works! Thus the solution is to detect when it is a command from this plugin that is about to be executed and - insert the "--" token and the start of the tokens list of the ArgvInput instance + insert the "--" token at the start of the tokens list of the ArgvInput instance that the application is about to read the CLI options from. Hopefully this doesn't get broken by a future update to poetry or cleo :S diff --git a/poethepoet/task/args.py b/poethepoet/task/args.py index 400c1c386..916c7b47f 100644 --- a/poethepoet/task/args.py +++ b/poethepoet/task/args.py @@ -89,11 +89,18 @@ def _get_arg_options_list(arg: ArgParams, name: Optional[str] = None): @classmethod def get_help_content( cls, args_def: Optional[ArgsDef] - ) -> List[Tuple[Tuple[str, ...], str]]: + ) -> List[Tuple[Tuple[str, ...], str, str]]: if args_def is None: return [] + + def format_default(arg) -> str: + default = arg.get("default") + if default: + return f"[default: {default}]" + return "" + return [ - (arg["options"], arg.get("help", "")) + (arg["options"], arg.get("help", ""), format_default(arg)) for arg in cls._normalize_args_def(args_def) ] diff --git a/poethepoet/task/base.py b/poethepoet/task/base.py index ae4b3ad08..07b8fe3bd 100644 --- a/poethepoet/task/base.py +++ b/poethepoet/task/base.py @@ -84,10 +84,7 @@ def __init__( ): self.name = name self.content = content - if capture_stdout: - self.options = dict(options, capture_stdout=True) - else: - self.options = options + self.options = dict(options, capture_stdout=True) if capture_stdout else options self._ui = ui self._config = config self._is_windows = sys.platform == "win32" @@ -199,6 +196,21 @@ def resolve_task_type( return None + def get_named_arg_values(self, env: "EnvVarsManager") -> Dict[str, str]: + try: + split_index = self.invocation.index("--") + parse_args = self.invocation[1:split_index] + except ValueError: + parse_args = self.invocation[1:] + + if self.named_args is None: + self.named_args = self._parse_named_args(parse_args, env) + + if not self.named_args: + return {} + + return self.named_args + def _parse_named_args( self, extra_args: Sequence[str], env: "EnvVarsManager" ) -> Optional[Dict[str, str]]: @@ -209,15 +221,6 @@ def _parse_named_args( return PoeTaskArgs(args_def, self.name, env).parse(extra_args) return None - def get_named_arg_values(self, env: "EnvVarsManager") -> 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 {} - - return self.named_args - def run( self, context: "RunContext", diff --git a/poethepoet/task/cmd.py b/poethepoet/task/cmd.py index ab2c041e2..2767a2236 100644 --- a/poethepoet/task/cmd.py +++ b/poethepoet/task/cmd.py @@ -35,12 +35,18 @@ def _handle_run( env.update(named_arg_values) 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) - else: - cmd = (*self._resolve_args(context, env), *extra_args) + # If named arguments are defined then pass only arguments following a double + # dash token: `--` + try: + split_index = extra_args.index("--") + extra_args = extra_args[split_index + 1 :] + except ValueError: + extra_args = tuple() + + cmd = (*self._resolve_args(context, env), *extra_args) + self._print_action(" ".join(cmd), context.dry) + return context.get_executor(self.invocation, env, self.options).execute( cmd, use_exec=self.options.get("use_exec", False) ) diff --git a/poethepoet/task/ref.py b/poethepoet/task/ref.py index 4dfc97c8d..582897751 100644 --- a/poethepoet/task/ref.py +++ b/poethepoet/task/ref.py @@ -30,6 +30,7 @@ def _handle_run( import shlex 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) return task.run(context=context, extra_args=extra_args, parent_env=env) diff --git a/poethepoet/task/script.py b/poethepoet/task/script.py index eb503707b..bd5b38fd2 100644 --- a/poethepoet/task/script.py +++ b/poethepoet/task/script.py @@ -33,7 +33,10 @@ def _handle_run( named_arg_values = self.get_named_arg_values(env) env.update(named_arg_values) + target_module, function_call = self.parse_content(named_arg_values) + function_ref = function_call[: function_call.index("(")] + argv = [ self.name, *(env.fill_template(token) for token in extra_args), @@ -43,16 +46,19 @@ def _handle_run( # sys.path.append('src') if it doesn't script = [ - "import os,sys; ", - "from os import environ; ", - "from importlib import import_module; ", + "import asyncio,os,sys;", + "from inspect import iscoroutinefunction as ic;", + "from os import environ;", + "from importlib import import_module as im;", f"sys.argv = {argv!r}; sys.path.append('src');", f"{format_class(named_arg_values)}", - f"result = import_module('{target_module}').{function_call};", + f"_m = im('{target_module}');", + f"_r = asyncio.run(_m.{function_call}) if ic(_m.{function_ref})", + f" else _m.{function_call};", ] if self.options.get("print_result"): - script.append(f"result is not None and print(result);") + script.append(f"_r is not None and print(_r);") # Exactly which python executable to use is usually resolved by the executor # It's important that the script contains no line breaks to avoid issues on diff --git a/poethepoet/ui.py b/poethepoet/ui.py index ea11053a1..810b6434a 100644 --- a/poethepoet/ui.py +++ b/poethepoet/ui.py @@ -3,7 +3,7 @@ from typing import IO, TYPE_CHECKING, List, Mapping, Optional, Sequence, Tuple, Union from .__version__ import __version__ -from .exceptions import PoeException +from .exceptions import ExecutionError, PoeException if TYPE_CHECKING: from argparse import ArgumentParser, Namespace @@ -142,7 +142,9 @@ def set_default_verbosity(self, default_verbosity: int): def print_help( self, - tasks: Optional[Mapping[str, Tuple[str, Sequence[Tuple[str, str]]]]] = None, + tasks: Optional[ + Mapping[str, Tuple[str, Sequence[Tuple[Tuple[str, ...], str, str]]]] + ] = None, info: Optional[str] = None, error: Optional[PoeException] = None, ): @@ -165,9 +167,10 @@ def print_help( if error: # TODO: send this to stderr instead? - result.append([f"Error: {error.msg} "]) + error_line = [f"Error: {error.msg} "] if error.cause: - result[-1].append(f" From: {error.cause} ") # type: ignore + error_line.append(f" From: {error.cause} ") + result.append(error_line) if verbosity >= 0: # Use argparse for usage summary @@ -192,11 +195,13 @@ def print_help( max_task_len = max( max( len(task), - max([len(", ".join(opts)) for (opts, _) in args] or (0,)) + 2, + max([len(", ".join(opts)) for (opts, _, _) in args] or (0,)) + + 2, ) for task, (_, args) in tasks.items() ) col_width = max(13, min(30, max_task_len)) + tasks_section = ["

CONFIGURED TASKS

"] for task, (help_text, args_help) in tasks.items(): if task.startswith("_"): @@ -204,13 +209,19 @@ def print_help( tasks_section.append( f" {self._padr(task, col_width)} {help_text}" ) - for (options, arg_help_text) in args_help: - tasks_section.append( - " " - f"{self._padr(', '.join(options), col_width - 2)}" - f" {arg_help_text}" - ) + for (options, arg_help_text, default) in args_help: + task_arg_help = [ + " ", + f"{self._padr(', '.join(options), col_width-1)}", + ] + if arg_help_text: + task_arg_help.append(arg_help_text) + if default: + task_arg_help.append(default) + tasks_section.append(" ".join(task_arg_help)) + result.append(tasks_section) + else: result.append("NO TASKS CONFIGURED") @@ -233,10 +244,10 @@ def print_msg(self, message: str, verbosity=0, end="\n"): if verbosity <= self.verbosity: self._print(message, end=end) - def print_error(self, error: Exception): - self._print(f"Error: {error.msg} ") # type: ignore - if error.cause: # type: ignore - self._print(f" From: {error.cause} ") # type: ignore + def print_error(self, error: Union[PoeException, ExecutionError]): + self._print(f"Error: {error.msg} ") + if error.cause: + self._print(f" From: {error.cause} ") def print_version(self): if self.verbosity >= 0: diff --git a/poethepoet/virtualenv.py b/poethepoet/virtualenv.py index 4c8b35a54..11a48dac7 100644 --- a/poethepoet/virtualenv.py +++ b/poethepoet/virtualenv.py @@ -25,7 +25,8 @@ def bin_dir(self) -> Path: def resolve_executable(self, executable: str) -> str: """ - If the given executable can be found in the bin_dir then return its absolute path + If the given executable can be found in the bin_dir then return its absolute + path. Otherwise return the input. """ bin_dir = self.bin_dir() if bin_dir.joinpath(executable).is_file(): @@ -69,7 +70,9 @@ def valid(self) -> bool: and bin_dir.joinpath("python").is_file() and bool( next( - self.path.glob(os.path.sep.join(("lib", "python3*", "site-packages"))), # type: ignore + self.path.glob( + os.path.sep.join(("lib", "python3*", "site-packages")) + ), False, ) ) diff --git a/pyproject.toml b/pyproject.toml index d4d493d9f..aaeb8a103 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "poethepoet" -version = "0.18.1" +version = "0.19.0" description = "A task runner that works well with poetry." authors = ["Nat Noordanus "] readme = "README.rst" @@ -110,6 +110,7 @@ poethepoet = "poethepoet.plugin:PoetryPlugin" help = "Execute poe from this repo (useful for testing)" script = "poethepoet:main" + [tool.coverage.report] omit = ["**/site-packages/**", "poethepoet/completion/*", "poethepoet/plugin.py"] diff --git a/tests/fixtures/cmds_project/pyproject.toml b/tests/fixtures/cmds_project/pyproject.toml index 892f40dc7..fabe00c81 100644 --- a/tests/fixtures/cmds_project/pyproject.toml +++ b/tests/fixtures/cmds_project/pyproject.toml @@ -7,7 +7,7 @@ env = { BEST_PASSWORD = "Password1" } use_exec = true [tool.poe.tasks.greet] -shell = "poe_test_echo $formal_greeting $subject" +cmd = "poe_test_echo $formal_greeting $subject" args = ["formal-greeting", "subject"] [tool.poe.tasks.surfin-bird] diff --git a/tests/fixtures/refs_project/pyproject.toml b/tests/fixtures/refs_project/pyproject.toml new file mode 100644 index 000000000..a15407b5f --- /dev/null +++ b/tests/fixtures/refs_project/pyproject.toml @@ -0,0 +1,12 @@ +[tool.poe.tasks.greet] +cmd = "poe_test_echo hi" + +[tool.poe.tasks.greet-subject] +cmd = "poe_test_echo hi ${subject}" +args = ["subject"] + +[tool.poe.tasks.greet-funny] +ref = "greet lol!" + +[tool.poe.tasks.greet-dave] +ref = "greet-subject --subject dave" diff --git a/tests/fixtures/scripts_project/pkg/__init__.py b/tests/fixtures/scripts_project/pkg/__init__.py index bd8b4775b..3e74d71a1 100644 --- a/tests/fixtures/scripts_project/pkg/__init__.py +++ b/tests/fixtures/scripts_project/pkg/__init__.py @@ -59,3 +59,7 @@ class Scripts: class Deep: def fun(self): print("task!") + + +def async_task(*args, **kwargs): + print("I'm an async task!", args, kwargs) diff --git a/tests/fixtures/scripts_project/pyproject.toml b/tests/fixtures/scripts_project/pyproject.toml index cd5b093c9..d84cbfdf7 100644 --- a/tests/fixtures/scripts_project/pyproject.toml +++ b/tests/fixtures/scripts_project/pyproject.toml @@ -34,6 +34,8 @@ call_attrs.use_exec = true greet = "pkg:greet" +async-task.script = "pkg:async_task" +async-task.args = ["a","b"] [tool.poe.tasks.print-script-result] script = "pkg:get_random_number" diff --git a/tests/test_cli.py b/tests/test_cli.py index 29c047ff6..e1a491e73 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -78,28 +78,32 @@ def test_documentation_of_task_named_args(run_poe): assert ( "\nResult: No task specified.\n" in result.capture ), "Output should include status message" + assert re.search( r"CONFIGURED TASKS\n" - r" composite_task \s+\n" - r" echo-args \s+\n" - r" static-args-test \s+\n" - r" call_attrs \s+\n" - r" greet \s+\n" - r" print-script-result \s+\n" - r" dont-print-script-result \s+\n" - r" greet-passed-args \s+\n" - r" --greeting \s+\n" - r" --user \s+\n" - r" --optional \s+\n" - r" --upper \s+\n" - r" greet-full-args \s+\n" - r" --greeting, -g \s+\n" - r" --user \s+\n" - r" --upper \s+\n" - r" --age, -a \s+\n" - r" --height, -h \s+The user's height in meters\n" - r" greet-strict \s+All arguments are required\n" - r" --greeting \s+this one is required\n" - r" --name \s+and this one is required\n", + r" composite_task \s+\n" + r" echo-args \s+\n" + r" static-args-test \s+\n" + r" call_attrs \s+\n" + r" greet \s+\n" + r" async-task \s+\n" + r" --a \s+\n" + r" --b \s+\n" + r" print-script-result \s+\n" + r" dont-print-script-result\s+\n" + r" greet-passed-args \s+\n" + r" --greeting \s+\n" + r" --user \s+\n" + r" --optional \s+\n" + r" --upper \s+\n" + r" greet-full-args \s+\n" + r" --greeting, -g \s+\[default: hi\]\n" + r" --user \s+\n" + r" --upper \s+\n" + r" --age, -a \s+\n" + r" --height, -h \s+The user's height in meters\n" + r" greet-strict \s+All arguments are required\n" + r" --greeting \s+this one is required \[default: \$\{DOES\}n't \$\{STUFF\}\]\n" + r" --name \s+and this one is required\n", result.capture, ) diff --git a/tests/test_cmd_tasks.py b/tests/test_cmd_tasks.py index 9e1fb251e..120753ca1 100644 --- a/tests/test_cmd_tasks.py +++ b/tests/test_cmd_tasks.py @@ -19,11 +19,26 @@ def test_cmd_task_with_dash_case_arg(run_poe_subproc): result = run_poe_subproc( "greet", "--formal-greeting=hey", "--subject=you", project="cmds" ) - assert result.capture == f"Poe => poe_test_echo $formal_greeting $subject\n" + assert result.capture == f"Poe => poe_test_echo hey you\n" assert result.stdout == "hey you\n" assert result.stderr == "" +def test_cmd_task_with_args_and_extra_args(run_poe_subproc): + result = run_poe_subproc( + "greet", + "--formal-greeting=hey", + "--subject=you", + "--", + "guy", + "!", + project="cmds", + ) + assert result.capture == f"Poe => poe_test_echo hey you guy !\n" + assert result.stdout == "hey you guy !\n" + assert result.stderr == "" + + def test_cmd_alias_env_var(run_poe_subproc): result = run_poe_subproc( "surfin-bird", project="cmds", env={"SOME_INPUT_VAR": "BIRD"} diff --git a/tests/test_ref_task.py b/tests/test_ref_task.py new file mode 100644 index 000000000..7cb0d1afe --- /dev/null +++ b/tests/test_ref_task.py @@ -0,0 +1,12 @@ +def test_ref_passes_named_args_in_definition(run_poe_subproc): + result = run_poe_subproc("greet-dave", project="refs") + assert result.capture == "Poe => poe_test_echo hi dave\n" + assert result.stdout == "hi dave\n" + assert result.stderr == "" + + +def test_ref_passes_extra_args_in_definition(run_poe_subproc): + result = run_poe_subproc("greet-funny", project="refs") + assert result.capture == "Poe => poe_test_echo hi lol!\n" + assert result.stdout == "hi lol!\n" + assert result.stderr == "" diff --git a/tests/test_script_tasks.py b/tests/test_script_tasks.py index d89bc64fd..1cf64378f 100644 --- a/tests/test_script_tasks.py +++ b/tests/test_script_tasks.py @@ -390,3 +390,16 @@ def test_script_with_multi_value_args(run_poe_subproc): "poe multiple-value-args: error: argument second: invalid int value: 'wrong'" in result.stderr ) + + +def test_async_script_task(run_poe_subproc, projects): + result = run_poe_subproc( + "async-task", + "--a=foo", + "--b=bar", + project="scripts", + env=no_venv, + ) + assert result.capture == "Poe => async-task --a=foo --b=bar\n" + assert result.stdout == "I'm an async task! () {'a': 'foo', 'b': 'bar'}\n" + assert result.stderr == ""