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
27 changes: 19 additions & 8 deletions fire/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ def main(argv):
from fire import value_types
from fire.console import console_io

# Single-character aliases for Fire's own flags (defined in parser.CreateParser).
# Shortcut expansion must not map these characters to user-defined arguments,
# because doing so would silently shadow Fire's built-in options.
_FIRE_RESERVED_SINGLE_CHAR_FLAGS = frozenset({'h', 'i', 'v', 't'})


def Fire(component=None, command=None, name=None, serialize=None):
"""This function, Fire, is the main entrypoint for Python Fire.
Expand Down Expand Up @@ -890,14 +895,20 @@ def _ParseKeywordArgs(args, fn_spec):
keyword = key
elif len(key) == 1:
# This may be a shortcut flag.
matching_fn_args = [arg for arg in fn_args if arg[0] == key]
if len(matching_fn_args) == 1:
keyword = matching_fn_args[0]
elif len(matching_fn_args) > 1:
raise FireError(
f"The argument '{argument}' is ambiguous as it could "
f"refer to any of the following arguments: {matching_fn_args}"
)
# Do not expand single-character flags that collide with Fire's own
# reserved flags (-h/--help, -i/--interactive, -v/--verbose,
# -t/--trace). Those should only be consumed via the '-- --flag'
# separator syntax so that they are not silently hijacked by a user
# function argument whose name starts with the same letter.
if key not in _FIRE_RESERVED_SINGLE_CHAR_FLAGS:
matching_fn_args = [arg for arg in fn_args if arg[0] == key]
if len(matching_fn_args) == 1:
keyword = matching_fn_args[0]
elif len(matching_fn_args) > 1:
raise FireError(
f"The argument '{argument}' is ambiguous as it could "
f"refer to any of the following arguments: {matching_fn_args}"
)

# Determine the value.
if not keyword:
Expand Down
7 changes: 7 additions & 0 deletions fire/core_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ def testHelpWithNamespaceCollision(self):
with self.assertOutputMatches(stdout='False', stderr=None):
core.Fire(tc.function_with_help, command=['False'])

def testHelpFlagNotConsumedAsShortcutForHArg(self):
# -h must show help, not be silently expanded to --headless=True.
# Before the fix, running with '-h' would set headless=True instead of
# showing help because 'headless' starts with 'h'.
with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*headless'):
core.Fire(tc.function_with_headless, command=['-h'])

def testInvalidParameterRaisesFireExit(self):
with self.assertRaisesFireExit(2, 'runmisspelled'):
core.Fire(tc.Kwargs, command=['props', '--a=1', '--b=2', 'runmisspelled'])
Expand Down
5 changes: 5 additions & 0 deletions fire/test_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ def function_with_help(help=True): # pylint: disable=redefined-builtin
return help


def function_with_headless(headless=False):
"""A function whose argument starts with 'h', used to test -h collision."""
return headless


class Empty:
pass

Expand Down