Skip to content

Commit

Permalink
Document support for global tasks and improve completion scripts
Browse files Browse the repository at this point in the history
Completion scripts and list_tasks are not configurable to respect the -C option
  • Loading branch information
nat-n committed Aug 18, 2024
1 parent 32689ca commit c90f8a7
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 29 deletions.
41 changes: 41 additions & 0 deletions docs/guides/global_tasks.rst
Original file line number Diff line number Diff line change
@@ -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.


1 change: 1 addition & 0 deletions docs/guides/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

<a href="https://python-poetry.org/" target="_blank">poetry</a>
Expand All @@ -131,3 +130,4 @@ In all cases the path to project root (where the pyproject.toml resides) will be

<a href="https://pypa.github.io/pipx/" target="_blank">pipx</a>

Using this feature you can also define :doc:`global tasks<./guides/global_tasks>` that are not associated with any particular project.
1 change: 0 additions & 1 deletion docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ Fish
poe _fish_completion > (brew --prefix)/share/fish/vendor_completions.d/poe.fish
Supported python versions
-------------------------

Expand Down
34 changes: 25 additions & 9 deletions poethepoet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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:
Expand Down
12 changes: 8 additions & 4 deletions poethepoet/completion/bash.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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}",
)
)
24 changes: 18 additions & 6 deletions poethepoet/completion/fish.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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",
Expand All @@ -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})'",
)
)
55 changes: 49 additions & 6 deletions poethepoet/completion/zsh.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
Expand All @@ -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 = (
Expand Down Expand Up @@ -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",
"}",
Expand Down
2 changes: 1 addition & 1 deletion poethepoet/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

0 comments on commit c90f8a7

Please sign in to comment.