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

Issue #184 and #239 Added types and defaults to function help text. #251

Closed
wants to merge 3 commits into from
Closed
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
123 changes: 111 additions & 12 deletions fire/helptext.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import sys

import itertools

Expand All @@ -44,6 +45,7 @@

LINE_LENGTH = 80
SECTION_INDENTATION = 4
SUBSECTION_INDENTATION = 4


def HelpText(component, trace=None, verbose=False):
Expand Down Expand Up @@ -171,7 +173,7 @@ def _DescriptionSection(component, info):

def _CreateKeywordOnlyFlagItem(flag, docstring_info, spec):
return _CreateFlagItem(
flag, docstring_info, required=flag not in spec.kwonlydefaults)
flag, docstring_info, spec, required=flag not in spec.kwonlydefaults)


def _ArgsAndFlagsSections(info, spec, metadata):
Expand All @@ -188,13 +190,13 @@ def _ArgsAndFlagsSections(info, spec, metadata):
docstring_info = info['docstring_info']

arg_items = [
_CreateArgItem(arg, docstring_info)
_CreateArgItem(arg, docstring_info, spec)
for arg in args_with_no_defaults
]

if spec.varargs:
arg_items.append(
_CreateArgItem(spec.varargs, docstring_info)
_CreateArgItem(spec.varargs, docstring_info, spec)
)

if arg_items:
Expand All @@ -207,7 +209,7 @@ def _ArgsAndFlagsSections(info, spec, metadata):
)

positional_flag_items = [
_CreateFlagItem(flag, docstring_info, required=False)
_CreateFlagItem(flag, docstring_info, spec, required=False)
for flag in args_with_defaults
]
kwonly_flag_items = [
Expand Down Expand Up @@ -365,43 +367,140 @@ def _CreateOutputSection(name, content):
content=formatting.Indent(content, SECTION_INDENTATION))


def _CreateArgItem(arg, docstring_info):
def _CreateArgItem(arg, docstring_info, spec):
"""Returns a string describing a positional argument.

Args:
arg: The name of the positional argument.
docstring_info: A docstrings.DocstringInfo namedtuple with information about
the containing function's docstring.
spec: An instance of fire.inspectutils.FullArgSpec, containing type and
default information about the arguments to a callable.

Returns:
A string to be used in constructing the help screen for the function.
"""

# The help string is indented, so calculate the maximum permitted length
# before indentation to avoid exceeding the maximum line length.
max_str_length = LINE_LENGTH - SECTION_INDENTATION - SUBSECTION_INDENTATION

description = _GetArgDescription(arg, docstring_info)

arg = arg.upper()
return _CreateItem(formatting.BoldUnderline(arg), description, indent=4)
arg_string = formatting.BoldUnderline(arg.upper())

arg_type = _GetArgType(arg, spec)
arg_type = 'Type: {}'.format(arg_type) if arg_type else ''
available_space = max_str_length - len(arg_type)
arg_type = \
formatting.EllipsisTruncate(arg_type, available_space, max_str_length)

description = '\n'.join(part for part in (arg_type, description) if part)

return _CreateItem(arg_string, description, indent=SUBSECTION_INDENTATION)

def _CreateFlagItem(flag, docstring_info, required=False):
"""Returns a string describing a flag using information from the docstring.

def _CreateFlagItem(flag, docstring_info, spec, required=False):
"""Returns a string describing a flag using docstring and FullArgSpec info.

Args:
flag: The name of the flag.
docstring_info: A docstrings.DocstringInfo namedtuple with information about
the containing function's docstring.
spec: An instance of fire.inspectutils.FullArgSpec, containing type and
default information about the arguments to a callable.
required: Whether the flag is required.
Returns:
A string to be used in constructing the help screen for the function.
"""
# TODO(MichaelCG8): In future it would be good to be able to get type and
# default information out of docstrings if it is not available in
# FullArgSpec. This would require updating fire.docstrings.parser() and a
# decision would need to be made about which takes priority if the docstrings
# and function definition disagree.

# The help string is indented, so calculate the maximum permitted length
# before indentation to avoid exceeding the maximum line length.
max_str_length = LINE_LENGTH - SECTION_INDENTATION - SUBSECTION_INDENTATION

description = _GetArgDescription(flag, docstring_info)

flag_string_template = '--{flag_name}={flag_name_upper}'
flag = flag_string_template.format(
flag_string = flag_string_template.format(
flag_name=flag,
flag_name_upper=formatting.Underline(flag.upper()))
if required:
flag += ' (required)'
return _CreateItem(flag, description, indent=4)
flag_string += ' (required)'

arg_type = _GetArgType(flag, spec)
arg_default = _GetArgDefault(flag, spec)

# We need to handle the case where there is a default
# of None, but otherwise the argument has another type.
if arg_default == 'None':
arg_type = 'Optional[{}]'.format(arg_type)

arg_type = 'Type: {}'.format(arg_type) if arg_type else ''
available_space = max_str_length - len(arg_type)
arg_type = \
formatting.EllipsisTruncate(arg_type, available_space, max_str_length)

arg_default = 'Default: {}'.format(arg_default) if arg_default else ''
available_space = max_str_length - len(arg_default)
arg_default = \
formatting.EllipsisTruncate(arg_default, available_space, max_str_length)

description = '\n'.join(
part for part in (arg_type, arg_default, description) if part
)

return _CreateItem(flag_string, description, indent=SUBSECTION_INDENTATION)


def _GetArgType(arg, spec):
"""Returns a string describing the type of an argument.

Args:
arg: The name of the argument.
spec: An instance of fire.inspectutils.FullArgSpec, containing type and
default information about the arguments to a callable.
Returns:
A string to be used in constructing the help screen for the function, the
empty string if the argument type is not available.
"""
if arg in spec.annotations:
arg_type = spec.annotations[arg]
try:
if sys.version_info[0:2] >= (3, 3):
return arg_type.__qualname__
return arg_type.__name__
except AttributeError:
# Some typing objects, such as typing.Union do not have either a __name__
# or __qualname__ attribute.
# repr(typing.Union[int, str]) will return ': typing.Union[int, str]'
return repr(arg_type)
return ""


def _GetArgDefault(flag, spec):
"""Returns a string describing a flag's default value.

Args:
flag: The name of the flag.
spec: An instance of fire.inspectutils.FullArgSpec, containing type and
default information about the arguments to a callable.
Returns:
A string to be used in constructing the help screen for the function, the
empty string if the flag does not have a default or the default is not
available.
"""
num_defaults = len(spec.defaults)
args_with_defaults = spec.args[-num_defaults:]

for arg, default in zip(args_with_defaults, spec.defaults):
if arg == flag:
return repr(default)
return ''


def _CreateItem(name, description, indent=2):
Expand Down
90 changes: 89 additions & 1 deletion fire/helptext_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from __future__ import print_function

import os
import sys
import textwrap

from fire import formatting
Expand Down Expand Up @@ -80,9 +81,95 @@ def testHelpTextFunctionWithDefaults(self):
self.assertIn('NAME\n triple', help_screen)
self.assertIn('SYNOPSIS\n triple <flags>', help_screen)
self.assertNotIn('DESCRIPTION', help_screen)
self.assertIn('FLAGS\n --count=COUNT', help_screen)
self.assertIn('FLAGS\n --count=COUNT\n Default: 0', help_screen)
self.assertNotIn('NOTES', help_screen)

def testHelpTextFunctionWithLongDefaults(self):
component = tc.WithDefaults().text
help_screen = helptext.HelpText(
component=component,
trace=trace.FireTrace(component, name='text'))
self.assertIn('NAME\n text', help_screen)
self.assertIn('SYNOPSIS\n text <flags>', help_screen)
self.assertNotIn('DESCRIPTION', help_screen)
self.assertIn(
'FLAGS\n --string=STRING\n'
' Default: \'0001020304050607080910'
'1112131415161718192021222324252627282...',
help_screen)
self.assertNotIn('NOTES', help_screen)

@testutils.skipIf(
sys.version_info[0:2] < (3, 5),
'Python < 3.5 does not support type hints.')
def testHelpTextFunctionWithDefaultsAndTypes(self):
component = tc.py3p5.WithDefaultsAndTypes().double
help_screen = helptext.HelpText(
component=component,
trace=trace.FireTrace(component, name='double'))
self.assertIn('NAME\n double', help_screen)
self.assertIn('SYNOPSIS\n double <flags>', help_screen)
self.assertIn('DESCRIPTION', help_screen)
self.assertIn(
'FLAGS\n --count=COUNT\n Type: float\n Default: 0',
help_screen)
self.assertNotIn('NOTES', help_screen)

@testutils.skipIf(
sys.version_info[0:2] < (3, 5),
'Python < 3.5 does not support type hints.')
def testHelpTextFunctionWithTypesAndDefaultNone(self):
component = tc.py3p5.WithDefaultsAndTypes().get_int
help_screen = helptext.HelpText(
component=component,
trace=trace.FireTrace(component, name='get_int'))
self.assertIn('NAME\n get_int', help_screen)
self.assertIn('SYNOPSIS\n get_int <flags>', help_screen)
self.assertNotIn('DESCRIPTION', help_screen)
self.assertIn(
'FLAGS\n --value=VALUE\n'
' Type: Optional[int]\n Default: None',
help_screen)
self.assertNotIn('NOTES', help_screen)

@testutils.skipIf(
sys.version_info[0:2] < (3, 5),
'Python < 3.5 does not support type hints.')
def testHelpTextFunctionWithTypes(self):
component = tc.py3p5.WithTypes().double
help_screen = helptext.HelpText(
component=component,
trace=trace.FireTrace(component, name='double'))
self.assertIn('NAME\n double', help_screen)
self.assertIn('SYNOPSIS\n double COUNT', help_screen)
self.assertIn('DESCRIPTION', help_screen)
self.assertIn(
'POSITIONAL ARGUMENTS\n COUNT\n Type: float',
help_screen)
self.assertIn(
'NOTES\n You can also use flags syntax for POSITIONAL ARGUMENTS',
help_screen)

@testutils.skipIf(
sys.version_info[0:2] < (3, 5),
'Python < 3.5 does not support type hints.')
def testHelpTextFunctionWithLongTypes(self):
component = tc.py3p5.WithTypes().long_type
help_screen = helptext.HelpText(
component=component,
trace=trace.FireTrace(component, name='long_type'))
self.assertIn('NAME\n long_type', help_screen)
self.assertIn('SYNOPSIS\n long_type LONG_OBJ', help_screen)
self.assertNotIn('DESCRIPTION', help_screen)
self.assertIn(
'POSITIONAL ARGUMENTS\n LONG_OBJ\n'
' Type: typing.Tuple[typing.Tuple['
'typing.Tuple[typing.Tuple[typing.Tupl...',
help_screen)
self.assertIn(
'NOTES\n You can also use flags syntax for POSITIONAL ARGUMENTS',
help_screen)

def testHelpTextFunctionWithBuiltin(self):
component = 'test'.upper
help_screen = helptext.HelpText(
Expand Down Expand Up @@ -244,6 +331,7 @@ def testHelpScreenForFunctionFunctionWithDefaultArgs(self):

FLAGS
--count=COUNT
Default: 0
Input number that you want to double."""
self.assertEqual(textwrap.dedent(expected_output).strip(),
help_output.strip())
Expand Down
10 changes: 10 additions & 0 deletions fire/test_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@

import enum
import six
import sys

if six.PY3:
from fire import test_components_py3 as py3 # pylint: disable=unused-import,no-name-in-module,g-import-not-at-top
if sys.version_info[0:2] >= (3, 5):
from fire import test_components_py3p5 as py3p5 # pylint: disable=unused-import,no-name-in-module


def identity(arg1, arg2, arg3=10, arg4=20, *arg5, **arg6): # pylint: disable=keyword-arg-before-vararg
Expand Down Expand Up @@ -105,6 +108,13 @@ def double(self, count=0):
def triple(self, count=0):
return 3 * count

def text(
self,
string=('0001020304050607080910111213141516171819'
'2021222324252627282930313233343536373839')
):
return string


class OldStyleWithDefaults: # pylint: disable=old-style-class,no-init

Expand Down
58 changes: 58 additions & 0 deletions fire/test_components_py3p5.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Lint as: python3
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've copied this directive from fire/test_components_py3.py, but haven't come across it before and can't find mention of it in pylint's documentation. If there is a more specific option such as # Lint as: >python3.5 then that would be better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a Google-internal directive.

We'll modify our codebase syncing so its removed from the public codebase, but for now let's keep it so things work internally too.

"""This module has components that use Python 3.5 specific syntax."""


from typing import Tuple

class WithTypes(object):
"""Class with functions that have default arguments and types."""

def double(self, count: float) -> float:
"""Returns the input multiplied by 2.

Args:
count: Input number that you want to double.

Returns:
A number that is the double of count.s
"""
return 2 * count

def long_type(
self,
long_obj: (Tuple[Tuple[Tuple[Tuple[Tuple[Tuple[Tuple[
Tuple[Tuple[Tuple[Tuple[Tuple[int]]]]]]]]]]]])
):
return long_obj


class WithDefaultsAndTypes(object):
"""Class with functions that have default arguments and types."""

def double(self, count: float = 0) -> float:
"""Returns the input multiplied by 2.

Args:
count: Input number that you want to double.

Returns:
A number that is the double of count.s
"""
return 2 * count

def get_int(self, value: int = None):
return 0 if value is None else value
Loading