diff --git a/docs/guides/global_tasks.rst b/docs/guides/global_tasks.rst new file mode 100644 index 00000000..76efd608 --- /dev/null +++ b/docs/guides/global_tasks.rst @@ -0,0 +1,41 @@ +Global tasks +============ + +This guide covers how to use poethepoet as a global task runner, for private user level tasks instead of shared project level tasks. Global tasks are available anywhere, and serve a similar purpose to shell aliases or scripts on the ``PATH`` — but as poe tasks. + +There are two steps required to make this work: + +1. Create a project somewhere central such as ``~/.poethepoet`` where you define tasks that you want to have globally accessible +2. Configure an alias in your shell's startup script such as ``alias goe="poe -C ~/.poethepoet"``. + +The project at ``~/.poethepoet`` can be a regular poetry project including dependencies or just a file with tasks. + +You can choose any location to define the tasks, and whatever name you like for the global poe alias. + +.. warning:: + + For this to work Poe the Poet must be installed globally such as via pipx or homebrew. + + +Shell completions for global tasks +---------------------------------- + +If you uze zsh or fish then the usual completion script should just work with your alias (as long as it was created with poethepoet >=0.28.0). + +However for bash you'll need to generate a new completion script for the alias specifying the alias and the path to you global tasks like so: + +.. code-block:: bash + + # System bash + poe _bash_completion goe ~/.poethepoet > /etc/bash_completion.d/goe.bash-completion + + # Homebrew bash + poe _bash_completion goe ~/.poethepoet > $(brew --prefix)/etc/bash_completion.d/goe.bash-completion + +.. note:: + + These examples assume your global poe alias is ``goe``, and your global tasks live at ``~/.poethepoet``. + +How to ensure installed bash completions are enabled may vary depending on your system. + + diff --git a/docs/guides/index.rst b/docs/guides/index.rst index 3e2cf09c..d4e230e4 100644 --- a/docs/guides/index.rst +++ b/docs/guides/index.rst @@ -11,4 +11,5 @@ This section contains guides for using the various features of Poe the Poet. args_guide composition_guide include_guide + global_tasks library_guide diff --git a/docs/index.rst b/docs/index.rst index 494dbe61..7fb2575b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -122,7 +122,6 @@ By default poe will detect when you're inside a project with a pyproject.toml in 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 poetry @@ -131,3 +130,4 @@ In all cases the path to project root (where the pyproject.toml resides) will be pipx +Using this feature you can also define :doc:`global tasks<./guides/global_tasks>` that are not associated with any particular project. diff --git a/docs/installation.rst b/docs/installation.rst index ade55b96..653e4804 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -107,7 +107,6 @@ Fish poe _fish_completion > (brew --prefix)/share/fish/vendor_completions.d/poe.fish - Supported python versions ------------------------- diff --git a/poethepoet/__init__.py b/poethepoet/__init__.py index 029df2a8..2be2e984 100644 --- a/poethepoet/__init__.py +++ b/poethepoet/__init__.py @@ -5,30 +5,46 @@ def main(): import sys + from pathlib import Path - if len(sys.argv) == 2 and sys.argv[1].startswith("_"): + if len(sys.argv) > 1 and sys.argv[1].startswith("_"): first_arg = sys.argv[1] + second_arg = next(iter(sys.argv[2:]), "") + third_arg = next(iter(sys.argv[3:]), "") + if first_arg in ("_list_tasks", "_describe_tasks"): - _list_tasks() + _list_tasks(target_path=str(Path(second_arg).expanduser().resolve())) return + + target_path = "" + if second_arg: + if not second_arg.isalnum(): + raise ValueError(f"Invalid alias: {second_arg!r}") + + if third_arg: + if not Path(third_arg).expanduser().resolve().exists(): + raise ValueError(f"Invalid path: {third_arg!r}") + + target_path = str(Path(third_arg).resolve()) + if first_arg == "_zsh_completion": from .completion.zsh import get_zsh_completion_script - print(get_zsh_completion_script()) + print(get_zsh_completion_script(name=second_arg)) return + if first_arg == "_bash_completion": from .completion.bash import get_bash_completion_script - print(get_bash_completion_script()) + print(get_bash_completion_script(name=second_arg, target_path=target_path)) return + if first_arg == "_fish_completion": from .completion.fish import get_fish_completion_script - print(get_fish_completion_script()) + print(get_fish_completion_script(name=second_arg)) return - from pathlib import Path - from .app import PoeThePoet app = PoeThePoet(cwd=Path().resolve(), output=sys.stdout) @@ -37,7 +53,7 @@ def main(): raise SystemExit(result) -def _list_tasks(): +def _list_tasks(target_path: str = ""): """ A special task accessible via `poe _list_tasks` for use in shell completion @@ -48,7 +64,7 @@ def _list_tasks(): from .config import PoeConfig config = PoeConfig() - config.load(strict=False) + config.load(target_path, strict=False) task_names = (task for task in config.task_names if task and task[0] != "_") print(" ".join(task_names)) except Exception: diff --git a/poethepoet/completion/bash.py b/poethepoet/completion/bash.py index 01f0129a..d9cb084d 100644 --- a/poethepoet/completion/bash.py +++ b/poethepoet/completion/bash.py @@ -1,4 +1,4 @@ -def get_bash_completion_script() -> str: +def get_bash_completion_script(name: str = "", target_path: str = "") -> str: """ A special task accessible via `poe _bash_completion` that prints a basic bash completion script for the presently available poe tasks @@ -7,14 +7,18 @@ def get_bash_completion_script() -> str: # TODO: see if it's possible to support completion of global options anywhere as # nicely as for zsh + name = name or "poe" + func_name = f"_{name}_complete" + return "\n".join( ( - "_poe_complete() {", + func_name + "() {", " local cur", ' cur="${COMP_WORDS[COMP_CWORD]}"', - ' COMPREPLY=( $(compgen -W "$(poe _list_tasks)" -- ${cur}) )', + f" COMPREPLY=( $(compgen -W \"$(poe _list_tasks '{target_path}')\"" + + " -- ${cur}) )", " return 0", "}", - "complete -o default -F _poe_complete poe", + f"complete -o default -F {func_name} {name}", ) ) diff --git a/poethepoet/completion/fish.py b/poethepoet/completion/fish.py index fea6b426..d6447183 100644 --- a/poethepoet/completion/fish.py +++ b/poethepoet/completion/fish.py @@ -1,4 +1,4 @@ -def get_fish_completion_script() -> str: +def get_fish_completion_script(name: str = "") -> str: """ A special task accessible via `poe _fish_completion` that prints a basic fish completion script for the presently available poe tasks @@ -8,16 +8,28 @@ def get_fish_completion_script() -> str: # - support completion of global options (with help) only if no task provided # without having to call poe for every option which would be too slow # - include task help in (dynamic) task completions - # - maybe just use python for the whole of the __list_poe_tasks logic? + + name = name or "poe" + func_name = f"__list_{name}_tasks" return "\n".join( ( - "function __list_poe_tasks", + "function " + func_name, + " # Check if `-C target_path` have been provided", + " set target_path ''", " set prev_args (commandline -pco)", - ' set tasks (poe _list_tasks | string split " ")', + " for i in (seq (math (count $prev_args) - 1))", + " set j (math $i + 1)", + " set k (math $i + 2)", + ' if test "$prev_args[$j]" = "-C" && test "$prev_args[$k]" != ""', + ' set target_path "$prev_args[$k]"', + " break", + " end", + " end", + " set tasks (poe _list_tasks $target_path | string split ' ')", " set arg (commandline -ct)", " for task in $tasks", - ' if test "$task" != poe && contains $task $prev_args', + f' if test "$task" != {name} && contains $task $prev_args', # TODO: offer $task specific options ' complete -C "ls $arg"', " return 0", @@ -27,6 +39,6 @@ def get_fish_completion_script() -> str: " echo $task", " end", "end", - "complete -c poe --no-files -a '(__list_poe_tasks)'", + f"complete -c {name} --no-files -a '({func_name})'", ) ) diff --git a/poethepoet/completion/zsh.py b/poethepoet/completion/zsh.py index 29f934f4..c528eb92 100644 --- a/poethepoet/completion/zsh.py +++ b/poethepoet/completion/zsh.py @@ -1,7 +1,7 @@ from typing import Any, Iterable, Set -def get_zsh_completion_script() -> str: +def get_zsh_completion_script(name: str = "") -> str: """ A special task accessible via `poe _zsh_completion` that prints a zsh completion script for poe generated from the argparses config @@ -10,6 +10,8 @@ def get_zsh_completion_script() -> str: from ..app import PoeThePoet + name = name or "poe" + # build and interogate the argument parser as the normal cli would app = PoeThePoet(cwd=Path().resolve()) parser = app.ui.build_parser() @@ -25,6 +27,9 @@ def format_exclusions(excl_option_strings): # format the zsh completion script args_lines = [" _arguments -C"] for option in global_options: + if option.help == "==SUPPRESS==": + continue + # help and version are special cases that dont go with other args if option.dest in ["help", "version"]: options_part = ( @@ -67,20 +72,58 @@ def format_exclusions(excl_option_strings): args_lines.append(f'"{options_part}[{option.help}]"') - args_lines.append('"1: :($TASKS)"') + args_lines.append('"1: :($tasks)"') + args_lines.append('": :($tasks)"') # needed to complete task after global options args_lines.append('"*::arg:->args"') + target_path_logic = """ + local DIR_ARGS=("-C" "--directory" "--root") + + local target_path="" + local tasks=() + + for ((i=2; i<${#words[@]}; i++)); do + # iter arguments passed so far + if (( $DIR_ARGS[(Ie)${words[i]}] )); then + # arg is one of DIR_ARGS, so the next arg should be the target_path + if (( ($i+1) >= ${#words[@]} )); then + # this is the last arg, the next one should be path + _files + return + fi + target_path="${words[i+1]}" + tasks=($(poe _list_tasks $target_path)) + i=$i+1 + elif [[ "${words[i]}" != -* ]] then + if (( ${#tasks[@]}<1 )); then + # get the list of tasks if we didn't already + tasks=($(poe _list_tasks $target_path)) + fi + if (( $tasks[(Ie)${words[i]}] )); then + # a task has been given so complete with files + _files + return + fi + fi + done + + if (( ${#tasks[@]}<1 )); then + # get the list of tasks if we didn't already + tasks=($(poe _list_tasks $target_path)) + fi + """ + return "\n".join( [ - "#compdef _poe poe\n", - "function _poe {", + f"#compdef _{name} {name}\n", + f"function _{name} " "{", + target_path_logic, ' local ALL_EXLC=("-h" "--help" "--version")', - " local TASKS=($(poe _list_tasks))", "", " \\\n ".join(args_lines), "", # Only offer filesystem based autocompletions after a task is specified - " if (($TASKS[(Ie)$line[1]])); then", + " if (($tasks[(Ie)$line[1]])); then", " _files", " fi", "}", diff --git a/poethepoet/config.py b/poethepoet/config.py index f865a66f..6a1562f5 100644 --- a/poethepoet/config.py +++ b/poethepoet/config.py @@ -372,7 +372,7 @@ def load(self, target_path: Optional[Union[Path, str]] = None, strict: bool = Tr config_path = self.find_config_file( target_path=Path(target_path) if target_path else None, - search_parent=target_path is None, + search_parent=not target_path, ) self._project_dir = config_path.parent diff --git a/pyproject.toml b/pyproject.toml index b9f8711b..07320b6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -198,5 +198,5 @@ fixable = ["E", "F", "I"] [build-system] -requires = ["poetry-core"] +requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api"