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

Show task help for InvalidUsageException and incorrect params #931

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions invoke/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
UnpicklableConfigMember,
WatcherError,
CommandTimedOut,
InvalidUsageException,
TaskInvalidUsageException,
)
from .executor import Executor # noqa
from .loader import FilesystemLoader # noqa
Expand Down
30 changes: 30 additions & 0 deletions invoke/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .parser import ParserContext
from .runners import Result
from .util import ExceptionWrapper
from .tasks import Task


class CollectionNotFound(Exception):
Expand Down Expand Up @@ -423,3 +424,32 @@ class SubprocessPipeError(Exception):
"""

pass


class InvalidUsageException(Exception):
"""
Some problem was encountered during parameter validation while in a task.

Should be raised by the task itself.

.. versionadded:: 2.1
"""

pass


class TaskInvalidUsageException(Exception):
"""
Wraper for InvalidUsageException when is's raised to throw it upper.

Should be raised by the executor.

.. versionadded:: 2.1
"""

def __init__(self, task: "Task", exception: InvalidUsageException) -> None:
self.task = task
self.exception = exception

def __str__(self) -> str:
return "Task {} usage error: {}".format(self.task.name, self.exception)
7 changes: 6 additions & 1 deletion invoke/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from .parser import ParserContext
from .util import debug
from .tasks import Call, Task
from .exceptions import InvalidUsageException, TaskInvalidUsageException


if TYPE_CHECKING:
from .collection import Collection
Expand Down Expand Up @@ -137,7 +139,10 @@ def execute(
# being parameterized), handing in this config for use there.
context = call.make_context(config)
args = (context, *call.args)
result = call.task(*args, **call.kwargs)
try:
result = call.task(*args, **call.kwargs)
except InvalidUsageException as e:
raise TaskInvalidUsageException(task=call.task, exception=e)
if autoprint:
print(result)
# TODO: handle the non-dedupe case / the same-task-different-args
Expand Down
22 changes: 20 additions & 2 deletions invoke/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@
from . import Collection, Config, Executor, FilesystemLoader
from .completion.complete import complete, print_completion_script
from .parser import Parser, ParserContext, Argument
from .exceptions import UnexpectedExit, CollectionNotFound, ParseError, Exit
from .exceptions import (
UnexpectedExit,
CollectionNotFound,
ParseError,
Exit,
TaskInvalidUsageException,
)
from .terminals import pty_size
from .util import debug, enable_logging, helpline

Expand Down Expand Up @@ -401,6 +407,14 @@ def run(self, argv: Optional[List[str]] = None, exit: bool = True) -> None:
# Print error messages from parser, runner, etc if necessary;
# prevents messy traceback but still clues interactive user into
# problems.
if (
isinstance(e, ParseError)
and e.context is not None
and hasattr(self, "collection")
):
name = self.collection.transform(e.context.name or "")
if e.context is not None and name in self.collection.tasks:
self.print_task_help(name)
if isinstance(e, ParseError):
print(e, file=sys.stderr)
if isinstance(e, Exit) and e.message:
Expand Down Expand Up @@ -580,7 +594,11 @@ def execute(self) -> None:
module = import_module(module_path)
klass = getattr(module, class_name)
executor = klass(self.collection, self.config, self.core)
executor.execute(*self.tasks)
try:
executor.execute(*self.tasks)
except TaskInvalidUsageException as e:
print("{}\n".format(e))
self.print_task_help(self.collection.transform(e.task.name))

def normalize_argv(self, argv: Optional[List[str]]) -> None:
"""
Expand Down
7 changes: 7 additions & 0 deletions tests/_support/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,10 @@ def iterable_values(c, mylist=None):
@task(incrementable=["verbose"])
def incrementable_values(c, verbose=None):
pass


@task
def invalid_usage_exception(c):
from invoke import InvalidUsageException

raise InvalidUsageException("Invalid task usage!")
45 changes: 45 additions & 0 deletions tests/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ def copying_from_task_context_does_not_set_empty_list_values(self):
# .value = <default value> actually ends up creating a
# list-of-lists.
p = Program()

# Set up core-args parser context with an iterable arg that hasn't
# seen any value yet
def filename_args():
Expand Down Expand Up @@ -647,6 +648,49 @@ def prints_help_for_task_only(self):
for flag in ["-h", "--help"]:
expect("-c decorators {} punch".format(flag), out=expected)

def prints_help_for_task_that_rises_invalid_usage_exception(self):
expected = """
Task invalid_usage_exception usage error: Invalid task usage!

Usage: invoke [--core-opts] invalid-usage-exception [other tasks here ...]

Docstring:
none

Options:
none

""".lstrip()

expect(
"-c decorators invalid-usage-exception",
out=expected,
)

def prints_help_for_with_invalid_parameters(self):
out_expected = """
Usage: invoke [--core-opts] two-positionals [--options] [other tasks here ...]

Docstring:
none

Options:
-n STRING, --nonpos=STRING
-o STRING, --pos2=STRING
-p STRING, --pos1=STRING

""".lstrip()

err_expected = """
'two-positionals' did not receive required positional arguments: 'pos1', 'pos2'
""".lstrip() # noqa

expect(
"-c decorators two-positionals",
out=out_expected,
err=err_expected,
)

def works_for_unparameterized_tasks(self):
expected = """
Usage: invoke [--core-opts] biz [other tasks here ...]
Expand Down Expand Up @@ -1374,6 +1418,7 @@ def env_vars_load_with_prefix(self, monkeypatch):

def env_var_prefix_can_be_overridden(self, monkeypatch):
monkeypatch.setenv("MYAPP_RUN_HIDE", "both")

# This forces the execution stuff, including Executor, to run
# NOTE: it's not really possible to rework the impl so this test is
# cleaner - tasks require per-task/per-collection config, which can
Expand Down