Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document support for global tasks and improve completion scripts #235

Merged
merged 1 commit into from
Aug 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Loading