Skip to content
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
1 change: 1 addition & 0 deletions invoke/parser/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ def __init__(
self.aliases = aliases
for arg in args:
self.add_arg(arg)
self.skip_checks = False

def __repr__(self) -> str:
aliases = ""
Expand Down
40 changes: 32 additions & 8 deletions invoke/parser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,10 @@ def handle(self, token: str) -> None:
# need a posarg and the user legitimately wants to give it a value that
# just happens to be a valid context name.)
elif self.context and self.context.missing_positional_args:
if self._is_token_help(token):
debug("--help passed, skip positional arg")
self._force_help()
return
msg = "Context {!r} requires positional args, eating {!r}"
debug(msg.format(self.context, token))
self.see_positional_arg(token)
Expand All @@ -305,12 +309,11 @@ def handle(self, token: str) -> None:
# Initial-context flag being given as per-task flag (e.g. --help)
elif self.initial and token in self.initial.flags:
debug("Saw (initial-context) flag {!r}".format(token))
flag = self.initial.flags[token]
# Special-case for core --help flag: context name is used as value.
if flag.name == "help":
flag.value = self.context.name
msg = "Saw --help in a per-task context, setting task name ({!r}) as its value" # noqa
debug(msg.format(flag.value))
if self._is_token_help(token):
debug("--help passed")
self._force_help()
return
# All others: just enter the 'switch to flag' parser state
else:
# TODO: handle inverse core flags too? There are none at the
Expand Down Expand Up @@ -338,15 +341,21 @@ def complete_context(self) -> None:
self.context.name if self.context else self.context
)
)
# Ensure all of context's positional args have been given.
if self.context and self.context.missing_positional_args:
if not self.context:
return

# Ensure all of context's positional args have been given
if (
not self.context.skip_checks
and self.context.missing_positional_args
):
err = "'{}' did not receive required positional arguments: {}"
names = ", ".join(
"'{}'".format(x.name)
for x in self.context.missing_positional_args
)
self.error(err.format(self.context.name, names))
if self.context and self.context not in self.result:
if self.context not in self.result:
self.result.append(self.context)

def switch_to_context(self, name: str) -> None:
Expand Down Expand Up @@ -453,3 +462,18 @@ def see_positional_arg(self, value: Any) -> None:

def error(self, msg: str) -> None:
raise ParseError(msg, self.context)

def _is_token_help(self, token: str) -> bool:
try:
flag = self.initial.flags[token]
if flag.name == "help":
return True
except KeyError:
pass
except AttributeError:
pass
return False

def _force_help(self) -> None:
self.initial.flags["--help"].value = self.context.name
self.context.skip_checks = True
2 changes: 2 additions & 0 deletions invoke/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@ def run(self, argv: Optional[List[str]] = None, exit: bool = True) -> None:
# problems.
if isinstance(e, ParseError):
print(e, file=sys.stderr)
if e.context and e.context.name:
self.print_task_help(e.context.name)
if isinstance(e, Exit) and e.message:
print(e.message, file=sys.stderr)
if isinstance(e, UnexpectedExit) and e.result.hide:
Expand Down
39 changes: 39 additions & 0 deletions tests/parser_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,45 @@ def core_bool_but_per_task_string(self):
assert result[0].args.hide.value is False
assert result[1].args.hide.value == "both"

def help_passed_when_task_expects_one_positional_arg(self):
init = Context(
args=[Argument(names=("help", "h"), optional=True)]
)
task1 = Context(
"mytask",
args=[
Argument(
names=("name", "n"), kind=str, positional=True
)
],
)
parser = Parser(initial=init, contexts=[task1])
result = parser.parse_argv(["mytask", "--help"])
assert result[0].flags["--help"].value == "mytask"

def help_passed_when_task_expects_multiple_positional_arg(self):
init = Context(
args=[Argument(names=("help", "h"), optional=True)]
)
task1 = Context(
"mytask",
args=[
Argument(
names=("pos_arg_one", "o"),
kind=str,
positional=True,
),
Argument(
names=("pos_arg_two", "t"),
kind=str,
positional=True,
),
],
)
parser = Parser(initial=init, contexts=[task1])
result = parser.parse_argv(["mytask", "--help"])
assert result[0].flags["--help"].value == "mytask"

class help_treats_context_name_as_its_value:
def by_itself_base_case(self):
task1 = Context("mytask")
Expand Down
17 changes: 17 additions & 0 deletions tests/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,23 @@ def prints_help_for_task_only(self):
for flag in ["-h", "--help"]:
expect("-c decorators {} punch".format(flag), out=expected)

def prints_help_if_no_mandatory_arg(self):
expected = """
Usage: invoke [--core-opts] punch [--options] [other tasks here ...]

Docstring:
none

Options:
-h STRING, --why=STRING Motive
-w STRING, --who=STRING Who to punch

""".lstrip()
expected_error = """
'punch' did not receive required positional arguments: 'who', 'why'
""".lstrip()
expect("-c decorators punch", out=expected, err=expected_error)

def works_for_unparameterized_tasks(self):
expected = """
Usage: invoke [--core-opts] biz [other tasks here ...]
Expand Down