From 8ea42297cae4289e9503efc667e56a0d3ae5fe5d Mon Sep 17 00:00:00 2001
From: Saurabh Patel <saurabh.ce1590@gmail.com>
Date: Fri, 7 Apr 2017 15:32:59 -0700
Subject: [PATCH 001/324] Added .gitignore file. (#50)

* Adds .gitignore file based on https://github.com/github/gitignore/blob/master/Python.gitignore.
---
 .gitignore | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 100 insertions(+)
 create mode 100644 .gitignore

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..a8c927df
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,100 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*,cover
+.hypothesis/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# dotenv
+.env
+
+# virtualenv
+.venv
+venv/
+ENV/
+
+# Spyder project settings
+.spyderproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# PyCharm IDE
+.idea/

From c1f045d9abaacc1f2a24ff78647be02ac1d277d2 Mon Sep 17 00:00:00 2001
From: Neal McBurnett <neal@mcburnett.org>
Date: Tue, 11 Apr 2017 11:30:47 -0600
Subject: [PATCH 002/324] ipython<6.0 for #57; fix broken links in guide.md
 (#58)

* Fix broken links to using-cli.md (extra "doc/")
* require ipython < 6.0 for python 2.7 compatibility
---
 doc/guide.md | 12 ++++++------
 setup.py     |  2 +-
 2 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/doc/guide.md b/doc/guide.md
index 84cf4ad8..78c9634a 100644
--- a/doc/guide.md
+++ b/doc/guide.md
@@ -699,12 +699,12 @@ The complete set of flags available is shown below, in the reference section.
 
 | Using a CLI    | Command                    | Notes
 | :------------- | :------------------------- | :---------
-| [Help](doc/using-cli.md#help-flag) | `command -- --help` |Show help and usage information for the command.
-| [REPL](doc/using-cli.md#interactive-flag) | `command -- --interactive` | Enter interactive mode.
-| [Separator](doc/using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
-| [Completion](doc/using-cli.md#completion-flag) | `command -- --completion` | Generate a completion script for the CLI.
-| [Trace](doc/using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
-| [Verbose](doc/using-cli.md#verbose-flag) | `command -- --verbose` | Include private members in the output.
+| [Help](using-cli.md#help-flag) | `command -- --help` |Show help and usage information for the command.
+| [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enter interactive mode.
+| [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
+| [Completion](using-cli.md#completion-flag) | `command -- --completion` | Generate a completion script for the CLI.
+| [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
+| [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` | Include private members in the output.
 _Note that flags are separated from the Fire command by an isolated `--` arg._
 
 
diff --git a/setup.py b/setup.py
index e2d7545e..842cd836 100644
--- a/setup.py
+++ b/setup.py
@@ -30,7 +30,7 @@
 A library for automatically generating command line interfaces.""".strip()
 
 DEPENDENCIES = [
-    'ipython',
+    'ipython<6.0',
     'six',
 ]
 

From 8eb6aa51a9681b62ab3763ee6e7ea4382f928ac3 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 10 Apr 2017 21:09:17 +0000
Subject: [PATCH 003/324] Clean up formatting in test and guide.

Copybara generated commit for Python Fire.

PiperOrigin-RevId: 152732270
Change-Id: Ib76f3c41cbd4a262232bd09cb786480c5e781db5
Reviewed-on: https://team-review.git.corp.google.com/67886
Reviewed-by: David Bieber <dbieber@google.com>
---
 doc/guide.md      | 3 ++-
 fire/core_test.py | 1 -
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/doc/guide.md b/doc/guide.md
index 78c9634a..ad49d3de 100644
--- a/doc/guide.md
+++ b/doc/guide.md
@@ -699,12 +699,13 @@ The complete set of flags available is shown below, in the reference section.
 
 | Using a CLI    | Command                    | Notes
 | :------------- | :------------------------- | :---------
-| [Help](using-cli.md#help-flag) | `command -- --help` |Show help and usage information for the command.
+| [Help](using-cli.md#help-flag) | `command -- --help` | Show help and usage information for the command.
 | [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enter interactive mode.
 | [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
 | [Completion](using-cli.md#completion-flag) | `command -- --completion` | Generate a completion script for the CLI.
 | [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
 | [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` | Include private members in the output.
+
 _Note that flags are separated from the Fire command by an isolated `--` arg._
 
 
diff --git a/fire/core_test.py b/fire/core_test.py
index 46727a95..755a2c45 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -33,7 +33,6 @@ def testOneLineResult(self):
     self.assertEqual(core._OneLineResult({}), '{}')  # pylint: disable=protected-access
     self.assertEqual(core._OneLineResult({'x': 'y'}), '{"x": "y"}')  # pylint: disable=protected-access
 
-
   def testOneLineResultCircularRef(self):
     circular_reference = tc.CircularReference()
     self.assertEqual(core._OneLineResult(circular_reference.create()),  # pylint: disable=protected-access

From b76f1c3deac6b6277ee621330556bf430252dc1c Mon Sep 17 00:00:00 2001
From: Jeff Tratner <jeffrey.tratner@gmail.com>
Date: Tue, 11 Apr 2017 10:44:51 -0700
Subject: [PATCH 004/324] Write help and trace messages to stderr (#54)

* Change test helper `assertOutputMatches` to track stdout and stderr + adds `assertRaisesRegex`
* Write help text to stderr
* Add some test cases for output matching
---
 fire/core.py           | 21 ++++++++++------
 fire/core_test.py      |  4 +--
 fire/testutils.py      | 57 ++++++++++++++++++++++++++++--------------
 fire/testutils_test.py | 55 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 109 insertions(+), 28 deletions(-)
 create mode 100644 fire/testutils_test.py

diff --git a/fire/core.py b/fire/core.py
index e411d59b..a1672b98 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -122,26 +122,33 @@ def Fire(component=None, command=None, name=None):
       if help_flag in component_trace.elements[-1].args:
         command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
         print(('WARNING: The proper way to show help is {cmd}.\n'
-               'Showing help anyway.\n').format(cmd=pipes.quote(command)))
+               'Showing help anyway.\n').format(cmd=pipes.quote(command)),
+              file=sys.stderr)
 
-    print('Fire trace:\n{trace}\n'.format(trace=component_trace))
+    print('Fire trace:\n{trace}\n'.format(trace=component_trace),
+          file=sys.stderr)
     result = component_trace.GetResult()
     print(
-        helputils.HelpString(result, component_trace, component_trace.verbose))
+        helputils.HelpString(result, component_trace, component_trace.verbose),
+        file=sys.stderr)
     raise FireExit(2, component_trace)
   elif component_trace.show_trace and component_trace.show_help:
-    print('Fire trace:\n{trace}\n'.format(trace=component_trace))
+    print('Fire trace:\n{trace}\n'.format(trace=component_trace),
+          file=sys.stderr)
     result = component_trace.GetResult()
     print(
-        helputils.HelpString(result, component_trace, component_trace.verbose))
+        helputils.HelpString(result, component_trace, component_trace.verbose),
+        file=sys.stderr)
     raise FireExit(0, component_trace)
   elif component_trace.show_trace:
-    print('Fire trace:\n{trace}'.format(trace=component_trace))
+    print('Fire trace:\n{trace}'.format(trace=component_trace),
+          file=sys.stderr)
     raise FireExit(0, component_trace)
   elif component_trace.show_help:
     result = component_trace.GetResult()
     print(
-        helputils.HelpString(result, component_trace, component_trace.verbose))
+        helputils.HelpString(result, component_trace, component_trace.verbose),
+        file=sys.stderr)
     raise FireExit(0, component_trace)
   else:
     _PrintResult(component_trace, verbose=component_trace.verbose)
diff --git a/fire/core_test.py b/fire/core_test.py
index 755a2c45..3033c3d3 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -99,9 +99,9 @@ def testFireErrorMultipleValues(self):
     self.assertIsNotNone(error)
 
   def testPrintEmptyDict(self):
-    with self.assertStdoutMatches('{}'):
+    with self.assertOutputMatches(stdout='{}', stderr=None):
       core.Fire(tc.EmptyDictOutput, 'totally_empty')
-    with self.assertStdoutMatches('{}'):
+    with self.assertOutputMatches(stdout='{}', stderr=None):
       core.Fire(tc.EmptyDictOutput, 'nothing_printable')
 
 
diff --git a/fire/testutils.py b/fire/testutils.py
index d4c4e93d..8520769a 100644
--- a/fire/testutils.py
+++ b/fire/testutils.py
@@ -34,17 +34,44 @@ class BaseTestCase(unittest.TestCase):
   """Shared test case for Python Fire tests."""
 
   @contextlib.contextmanager
-  def assertStdoutMatches(self, regexp):
-    """Asserts that the context generates stdout matching regexp."""
-    stdout = six.StringIO()
-    with mock.patch.object(sys, 'stdout', stdout):
-      yield
-    value = stdout.getvalue()
-    if not re.search(regexp, value, re.DOTALL | re.MULTILINE):
-      raise AssertionError('Expected %r to match %r' % (value, regexp))
+  def assertOutputMatches(self, stdout='.*', stderr='.*', capture=True):
+    """Asserts that the context generates stdout and stderr matching regexps.
+
+    Note: If wrapped code raises an exception, stdout and stderr will not be
+      checked.
+
+    Args:
+      stdout: (str) regexp to match against stdout (None will check no stdout)
+      stderr: (str) regexp to match against stderr (None will check no stderr)
+      capture: (bool, default True) do not bubble up stdout or stderr
+    Yields:
+      Yields to the wrapped context.
+    """
+    stdout_fp = six.StringIO()
+    stderr_fp = six.StringIO()
+    try:
+      with mock.patch.object(sys, 'stdout', stdout_fp):
+        with mock.patch.object(sys, 'stderr', stderr_fp):
+          yield
+    finally:
+      if not capture:
+        sys.stdout.write(stdout_fp.getvalue())
+        sys.stderr.write(stderr_fp.getvalue())
+
+    for name, regexp, fp in [('stdout', stdout, stdout_fp),
+                             ('stderr', stderr, stderr_fp)]:
+      value = fp.getvalue()
+      if regexp is None:
+        if value:
+          raise AssertionError('%s: Expected no output. Got: %r' %
+                               (name, value))
+      else:
+        if not re.search(regexp, value, re.DOTALL | re.MULTILINE):
+          raise AssertionError('%s: Expected %r to match %r' %
+                               (name, value, regexp))
 
   @contextlib.contextmanager
-  def assertRaisesFireExit(self, code, regexp=None):
+  def assertRaisesFireExit(self, code, regexp='.*'):
     """Asserts that a FireExit error is raised in the context.
 
     Allows tests to check that Fire's wrapper around SystemExit is raised
@@ -56,11 +83,8 @@ def assertRaisesFireExit(self, code, regexp=None):
     Yields:
       Yields to the wrapped context.
     """
-    if regexp is None:
-      regexp = '.*'
-    with self.assertRaises(core.FireExit):
-      stdout = six.StringIO()
-      with mock.patch.object(sys, 'stdout', stdout):
+    with self.assertOutputMatches(stderr=regexp):
+      with self.assertRaises(core.FireExit):
         try:
           yield
         except core.FireExit as exc:
@@ -68,11 +92,6 @@ def assertRaisesFireExit(self, code, regexp=None):
             raise AssertionError('Incorrect exit code: %r != %r' % (exc.code,
                                                                     code))
           self.assertIsInstance(exc.trace, trace.FireTrace)
-          stdout.flush()
-          stdout.seek(0)
-          value = stdout.getvalue()
-          if not re.search(regexp, value, re.DOTALL | re.MULTILINE):
-            raise AssertionError('Expected %r to match %r' % (value, regexp))
           raise
 
 
diff --git a/fire/testutils_test.py b/fire/testutils_test.py
new file mode 100644
index 00000000..409f3950
--- /dev/null
+++ b/fire/testutils_test.py
@@ -0,0 +1,55 @@
+# Copyright (C) 2017 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.
+
+"""Test the test utilities for Fire's tests."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import sys
+
+import six
+
+from fire import testutils
+
+
+class TestTestUtils(testutils.BaseTestCase):
+  """Let's get meta."""
+
+  def testNoCheckOnException(self):
+    with self.assertRaises(ValueError):
+      with self.assertOutputMatches(stdout='blah'):
+        raise ValueError()
+
+  def testCheckStdoutOrStderrNone(self):
+    with six.assertRaisesRegex(self, AssertionError, 'stdout:'):
+      with self.assertOutputMatches(stdout=None):
+        print('blah')
+
+    with six.assertRaisesRegex(self, AssertionError, 'stderr:'):
+      with self.assertOutputMatches(stderr=None):
+        print('blah', file=sys.stderr)
+
+    with six.assertRaisesRegex(self, AssertionError, 'stderr:'):
+      with self.assertOutputMatches(stdout='apple', stderr=None):
+        print('apple')
+        print('blah', file=sys.stderr)
+
+  def testCorrectOrderingOfAssertRaises(self):
+    # Check to make sure FireExit tests are correct
+    with self.assertOutputMatches(stdout='Yep.*first.*second'):
+      with self.assertRaises(ValueError):
+        print('Yep, this is the first line.\nIt should match to the second')
+        raise ValueError()

From 3b04b0edb969dcfa0b6af63fe347c551aaf9a42c Mon Sep 17 00:00:00 2001
From: Saurabh Patel <saurabh.ce1590@gmail.com>
Date: Thu, 13 Apr 2017 14:02:34 -0700
Subject: [PATCH 005/324] Fixes part of #51: uses os.path.basename(sys.argv[0])
 as default program name (#59)

* Uses os.path.basename(sys.argv[0]) for default program name
---
 fire/core.py      |  3 ++-
 fire/fire_test.py | 10 ++++++++++
 2 files changed, 12 insertions(+), 1 deletion(-)

diff --git a/fire/core.py b/fire/core.py
index a1672b98..39ba442a 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -54,6 +54,7 @@ def main(argv):
 
 import inspect
 import json
+import os
 import pipes
 import shlex
 import sys
@@ -100,7 +101,7 @@ def Fire(component=None, command=None, name=None):
   # Get args as a list.
   if command is None:
     # Use the command line args by default if no command is specified.
-    name = name or sys.argv[0]
+    name = name or os.path.basename(sys.argv[0])
     args = sys.argv[1:]
   else:
     # Otherwise use the specified command.
diff --git a/fire/fire_test.py b/fire/fire_test.py
index 9fb2dbde..b189cef4 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -18,6 +18,7 @@
 from __future__ import division
 from __future__ import print_function
 
+import os
 import sys
 import unittest
 
@@ -43,6 +44,15 @@ def testFire(self):
     self.assertEqual(fire.Fire(tc.OldStyleWithDefaults, 'double 2'), 4)
     self.assertEqual(fire.Fire(tc.OldStyleWithDefaults, 'triple 4'), 12)
 
+  def testFireDefaultBaseName(self):
+    with mock.patch.object(sys, 'argv',
+                           [os.path.join('python-fire', 'fire',
+                                         'base_filename.py')]):
+      # positive case
+      with self.assertOutputMatches(stdout='Usage:       base_filename.py',
+                                    stderr=None):
+        fire.Fire(tc.Empty)
+
   def testFireNoArgs(self):
     self.assertEqual(fire.Fire(tc.MixedDefaults, 'ten'), 10)
 

From d9982003c6c9a1b6fb3add26d26f6eaf1e007332 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 13 Apr 2017 14:27:50 -0700
Subject: [PATCH 006/324] Fix no-else-return lint violations.

Also cleans up some stylistic things.

PiperOrigin-RevId: 153106607
Change-Id: Id418ab8a2bd13cd983b6f2d9d285fa3e677fa650
Reviewed-on: https://team-review.git.corp.google.com/68304
Reviewed-by: David Bieber <dbieber@google.com>
---
 doc/using-cli.md       |  2 --
 fire/completion.py     | 17 ++++++++---------
 fire/fire_test.py      |  3 +--
 fire/helputils.py      | 16 +++++++---------
 fire/inspectutils.py   |  3 +--
 fire/testutils_test.py | 12 ++++++++----
 6 files changed, 25 insertions(+), 28 deletions(-)

diff --git a/doc/using-cli.md b/doc/using-cli.md
index ab4050bc..1c843f42 100644
--- a/doc/using-cli.md
+++ b/doc/using-cli.md
@@ -1,7 +1,5 @@
 # Using a Fire CLI
 
-
-
 ## Basic usage
 
 Every Fire command corresponds to a Python component.
diff --git a/fire/completion.py b/fire/completion.py
index 6d4c28f7..46010c07 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -160,18 +160,17 @@ def Completions(component, verbose=False):
     spec = inspectutils.GetFullArgSpec(component)
     return _CompletionsFromArgs(spec.args + spec.kwonlyargs)
 
-  elif isinstance(component, (tuple, list)):
+  if isinstance(component, (tuple, list)):
     return [str(index) for index in range(len(component))]
 
-  elif inspect.isgenerator(component):
+  if inspect.isgenerator(component):
     # TODO: There are currently no commands available for generators.
     return []
 
-  else:
-    return [
-        _FormatForCommand(member_name)
-        for member_name, unused_member in _Members(component, verbose)
-    ]
+  return [
+      _FormatForCommand(member_name)
+      for member_name, unused_member in _Members(component, verbose)
+  ]
 
 
 def _FormatForCommand(token):
@@ -192,8 +191,8 @@ def _FormatForCommand(token):
 
   if token.startswith('_'):
     return token
-  else:
-    return token.replace('_', '-')
+
+  return token.replace('_', '-')
 
 
 def _Commands(component, depth=3):
diff --git a/fire/fire_test.py b/fire/fire_test.py
index b189cef4..648c676b 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -44,11 +44,10 @@ def testFire(self):
     self.assertEqual(fire.Fire(tc.OldStyleWithDefaults, 'double 2'), 4)
     self.assertEqual(fire.Fire(tc.OldStyleWithDefaults, 'triple 4'), 12)
 
-  def testFireDefaultBaseName(self):
+  def testFireDefaultName(self):
     with mock.patch.object(sys, 'argv',
                            [os.path.join('python-fire', 'fire',
                                          'base_filename.py')]):
-      # positive case
       with self.assertOutputMatches(stdout='Usage:       base_filename.py',
                                     stderr=None):
         fire.Fire(tc.Empty)
diff --git a/fire/helputils.py b/fire/helputils.py
index 463ccfc8..5c23de3a 100644
--- a/fire/helputils.py
+++ b/fire/helputils.py
@@ -186,17 +186,15 @@ def UsageString(component, trace=None, verbose=False):
     spec = inspectutils.GetFullArgSpec(component)
     return _UsageStringFromFullArgSpec(command, spec)
 
-  elif isinstance(component, (list, tuple)):
+  if isinstance(component, (list, tuple)):
     length = len(component)
     if length == 0:
       return command
-    elif length == 1:
+    if length == 1:
       return command + '[0]'
-    else:
-      return command + '[0..{cap}]'.format(cap=length - 1)
+    return command + '[0..{cap}]'.format(cap=length - 1)
 
-  else:
-    completions = completion.Completions(component, verbose)
-    if command:
-      completions = [''] + completions
-    return '\n'.join(command + end for end in completions)
+  completions = completion.Completions(component, verbose)
+  if command:
+    completions = [''] + completions
+  return '\n'.join(command + end for end in completions)
diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index a16ae828..e8225c77 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -102,8 +102,7 @@ def GetFullArgSpec(fn):
     # Are there other cases?
     if inspect.isbuiltin(fn):
       return FullArgSpec(varargs='vars', varkw='kwargs')
-    else:
-      return FullArgSpec()
+    return FullArgSpec()
 
   if skip_arg and args:
     args.pop(0)  # Remove 'self' or 'cls' from the list of arguments.
diff --git a/fire/testutils_test.py b/fire/testutils_test.py
index 409f3950..1644ff50 100644
--- a/fire/testutils_test.py
+++ b/fire/testutils_test.py
@@ -20,10 +20,10 @@
 
 import sys
 
-import six
-
 from fire import testutils
 
+import six
+
 
 class TestTestUtils(testutils.BaseTestCase):
   """Let's get meta."""
@@ -48,8 +48,12 @@ def testCheckStdoutOrStderrNone(self):
         print('blah', file=sys.stderr)
 
   def testCorrectOrderingOfAssertRaises(self):
-    # Check to make sure FireExit tests are correct
+    # Check to make sure FireExit tests are correct.
     with self.assertOutputMatches(stdout='Yep.*first.*second'):
       with self.assertRaises(ValueError):
-        print('Yep, this is the first line.\nIt should match to the second')
+        print('Yep, this is the first line.\nThis is the second.')
         raise ValueError()
+
+
+if __name__ == '__main__':
+  testutils.main()

From 1f366f97602a1806f623ae73f07888abd61162ca Mon Sep 17 00:00:00 2001
From: Saurabh Patel <saurabh.ce1590@gmail.com>
Date: Wed, 19 Apr 2017 09:56:02 -0700
Subject: [PATCH 007/324] Bug Fix : name was not setting if command is set
 (#65)

* default name was not used when a command was provided
---
 fire/core.py      | 3 ++-
 fire/fire_test.py | 5 +++--
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index 39ba442a..3a0712e9 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -101,12 +101,13 @@ def Fire(component=None, command=None, name=None):
   # Get args as a list.
   if command is None:
     # Use the command line args by default if no command is specified.
-    name = name or os.path.basename(sys.argv[0])
     args = sys.argv[1:]
   else:
     # Otherwise use the specified command.
     args = shlex.split(command)
 
+  name = name or os.path.basename(sys.argv[0])
+
   # Determine the calling context.
   caller = inspect.stack()[1]
   caller_frame = caller[0]
diff --git a/fire/fire_test.py b/fire/fire_test.py
index 648c676b..38652a31 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -324,8 +324,9 @@ def testHelpFlagAndTraceFlag(self):
       fire.Fire(tc.BoolConverter, '-- -h --trace')
 
   def testTabCompletionNoName(self):
-    with self.assertRaises(ValueError):
-      fire.Fire(tc.NoDefaults, '-- --completion')
+    completion_script = fire.Fire(tc.NoDefaults, '-- --completion')
+    self.assertIn('double', completion_script)
+    self.assertIn('triple', completion_script)
 
   def testTabCompletion(self):
     completion_script = fire.Fire(tc.NoDefaults, '-- --completion', name='c')

From 3126e2cd70ba8f790aad7d2d32ca1cf1fb90b9bf Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Sat, 20 May 2017 23:42:35 -0700
Subject: [PATCH 008/324] Bump version from 0.1.0 to 0.1.1

---
 setup.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.py b/setup.py
index 842cd836..18a73e67 100644
--- a/setup.py
+++ b/setup.py
@@ -42,7 +42,7 @@
 
 setup(
     name='fire',
-    version='0.1.0',
+    version='0.1.1',
 
     description=SHORT_DESCRIPTION,
     long_description=LONG_DESCRIPTION,

From 952e20d1d5e0c264dd17f4f52ebf28f6a194b4c1 Mon Sep 17 00:00:00 2001
From: David Bieber <david810@gmail.com>
Date: Wed, 24 May 2017 17:02:57 -0700
Subject: [PATCH 009/324] Make ipython fully optional (#78)

* Fall back on custom info function if IPython's oinspect is not available, thereby making IPython an optional dependency.
* Test IPython if IPython available, otherwise test with 'code' module.
---
 .travis.yml            |  4 ++-
 fire/core.py           | 31 ++-----------------
 fire/helputils_test.py |  3 +-
 fire/inspectutils.py   | 67 ++++++++++++++++++++++++++++++++++++++++--
 fire/interact_test.py  | 23 ++++++++++-----
 setup.py               |  1 -
 6 files changed, 86 insertions(+), 43 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 57d0ae33..7f0f5195 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -10,5 +10,7 @@ before_install:
 install:
   - python setup.py develop
 script:
-  - python -m pytest
+  - python -m pytest  # Run the tests without IPython.
+  - pip install ipython
+  - python -m pytest  # Now run the tests with IPython.
   - if [[ $TRAVIS_PYTHON_VERSION != 3.6 ]]; then pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py; fi
diff --git a/fire/core.py b/fire/core.py
index 3a0712e9..37af3493 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -352,7 +352,7 @@ def _Fire(component, args, context, name=None):
 
       try:
         target = component.__name__
-        filename, lineno = _GetFileAndLine(component)
+        filename, lineno = inspectutils.GetFileAndLine(component)
 
         component, consumed_args, remaining_args, capacity = _CallCallable(
             component, remaining_args)
@@ -428,7 +428,7 @@ def _Fire(component, args, context, name=None):
         component, consumed_args, remaining_args = _GetMember(
             component, remaining_args)
 
-        filename, lineno = _GetFileAndLine(component)
+        filename, lineno = inspectutils.GetFileAndLine(component)
 
         component_trace.AddAccessedProperty(
             component, target, consumed_args, filename, lineno)
@@ -486,33 +486,6 @@ def _Fire(component, args, context, name=None):
   return component_trace
 
 
-def _GetFileAndLine(component):
-  """Returns the filename and line number of component.
-
-  Args:
-    component: A component to find the source information for, usually a class
-        or routine.
-  Returns:
-    filename: The name of the file where component is defined.
-    lineno: The line number where component is defined.
-  """
-  if inspect.isbuiltin(component):
-    return None, None
-
-  try:
-    filename = inspect.getsourcefile(component)
-  except TypeError:
-    return None, None
-
-  try:
-    unused_code, lineindex = inspect.findsource(component)
-    lineno = lineindex + 1
-  except IOError:
-    lineno = None
-
-  return filename, lineno
-
-
 def _GetMember(component, args):
   """Returns a subcomponent of component by consuming an arg from args.
 
diff --git a/fire/helputils_test.py b/fire/helputils_test.py
index 43c9ef50..cf8014f3 100644
--- a/fire/helputils_test.py
+++ b/fire/helputils_test.py
@@ -43,7 +43,8 @@ def testHelpStringObject(self):
     self.assertIn('Type:        NoDefaults', helpstring)
     self.assertIn('String form: <fire.test_components.NoDefaults object at ',
                   helpstring)
-    self.assertIn('test_components.py', helpstring)
+    # TODO: We comment this out since it only works with IPython:
+    # self.assertIn('test_components.py', helpstring)
     self.assertIn('Usage:       double\n'
                   '             triple', helpstring)
 
diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index e8225c77..40769fe3 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -111,6 +111,33 @@ def GetFullArgSpec(fn):
                      kwonlyargs, kwonlydefaults, annotations)
 
 
+def GetFileAndLine(component):
+  """Returns the filename and line number of component.
+
+  Args:
+    component: A component to find the source information for, usually a class
+        or routine.
+  Returns:
+    filename: The name of the file where component is defined.
+    lineno: The line number where component is defined.
+  """
+  if inspect.isbuiltin(component):
+    return None, None
+
+  try:
+    filename = inspect.getsourcefile(component)
+  except TypeError:
+    return None, None
+
+  try:
+    unused_code, lineindex = inspect.findsource(component)
+    lineno = lineindex + 1
+  except IOError:
+    lineno = None
+
+  return filename, lineno
+
+
 def Info(component):
   """Returns a dict with information about the given component.
 
@@ -130,9 +157,12 @@ def Info(component):
   Returns:
     A dict with information about the component.
   """
-  import IPython  # pylint: disable=g-import-not-at-top
-  inspector = IPython.core.oinspect.Inspector()
-  info = inspector.info(component)
+  try:
+    from IPython.core import oinspect  # pylint: disable=g-import-not-at-top
+    inspector = oinspect.Inspector()
+    info = inspector.info(component)
+  except ImportError:
+    info = _InfoBackup(component)
 
   try:
     unused_code, lineindex = inspect.findsource(component)
@@ -141,3 +171,34 @@ def Info(component):
     info['line'] = None
 
   return info
+
+
+def _InfoBackup(component):
+  """Returns a dict with information about the given component.
+
+  This function is to be called only in the case that IPython's
+  oinspect module is not available. The info dict it produces may
+  contain less information that contained in the info dict produced
+  by oinspect.
+
+  Args:
+    component: The component to analyze.
+  Returns:
+    A dict with information about the component.
+  """
+  info = {}
+
+  info['type_name'] = type(component).__name__
+  info['string_form'] = str(component)
+
+  filename, lineno = GetFileAndLine(component)
+  info['file'] = filename
+  info['line'] = lineno
+  info['docstring'] = inspect.getdoc(component)
+
+  try:
+    info['length'] = str(len(component))
+  except (TypeError, AttributeError):
+    pass
+
+  return info
diff --git a/fire/interact_test.py b/fire/interact_test.py
index 012011c2..dd3ed9cd 100644
--- a/fire/interact_test.py
+++ b/fire/interact_test.py
@@ -24,22 +24,29 @@
 import mock
 
 
+try:
+  import IPython  # pylint: disable=unused-import, g-import-not-at-top
+  INTERACT_METHOD = 'IPython.start_ipython'
+except ImportError:
+  INTERACT_METHOD = 'code.InteractiveConsole'
+
+
 class InteractTest(testutils.BaseTestCase):
 
-  @mock.patch('IPython.start_ipython')
-  def testInteract(self, mock_ipython):
-    self.assertFalse(mock_ipython.called)
+  @mock.patch(INTERACT_METHOD)
+  def testInteract(self, mock_interact_method):
+    self.assertFalse(mock_interact_method.called)
     interact.Embed({})
-    self.assertTrue(mock_ipython.called)
+    self.assertTrue(mock_interact_method.called)
 
-  @mock.patch('IPython.start_ipython')
-  def testInteractVariables(self, mock_ipython):
-    self.assertFalse(mock_ipython.called)
+  @mock.patch(INTERACT_METHOD)
+  def testInteractVariables(self, mock_interact_method):
+    self.assertFalse(mock_interact_method.called)
     interact.Embed({
         'count': 10,
         'mock': mock,
     })
-    self.assertTrue(mock_ipython.called)
+    self.assertTrue(mock_interact_method.called)
 
 if __name__ == '__main__':
   testutils.main()
diff --git a/setup.py b/setup.py
index 18a73e67..0036dc69 100644
--- a/setup.py
+++ b/setup.py
@@ -30,7 +30,6 @@
 A library for automatically generating command line interfaces.""".strip()
 
 DEPENDENCIES = [
-    'ipython<6.0',
     'six',
 ]
 

From c43c832e28a3cfb672f7ba5baf6a36d21fa1c05c Mon Sep 17 00:00:00 2001
From: "Christopher J. Wright" <cjwright4242gh@gmail.com>
Date: Mon, 17 Jul 2017 16:38:19 -0400
Subject: [PATCH 010/324] Add conda forge (#82)

Adds conda forge installation instructions to README.
---
 README.md | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/README.md b/README.md
index be63c103..c8e1f668 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,10 @@ modules and variables you'll need already imported and created. [[5]](doc/benefi
 
 `pip install fire`
 
+OR
+
+`conda install fire -c conda-forge`
+
 
 ## Basic Usage
 

From 84c5a46d94c91e64a9ccb6550e8ac8ce278fad25 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 13 Jul 2017 17:06:32 -0700
Subject: [PATCH 011/324] - Rename doc to docs in preparation for adding mkdocs
 support. - Adds additional public documentation to Python Fire.

Copybara generated commit for Python Fire.

PiperOrigin-RevId: 162004265
Change-Id: I356c966d74d2270cc59abaff7238913339d17609
Reviewed-on: https://team-review.git.corp.google.com/82464
Reviewed-by: David Bieber <dbieber@google.com>
---
 README.md                  | 41 +++++++++--------
 docs/api.md                | 20 ++++++++
 {doc => docs}/benefits.md  | 19 ++++----
 {doc => docs}/guide.md     |  2 +-
 docs/index.md              | 94 ++++++++++++++++++++++++++++++++++++++
 docs/installation.md       |  8 ++++
 docs/troubleshooting.md    | 13 ++++++
 {doc => docs}/using-cli.md |  0
 mkdocs.yml                 | 11 +++++
 9 files changed, 178 insertions(+), 30 deletions(-)
 create mode 100644 docs/api.md
 rename {doc => docs}/benefits.md (81%)
 rename {doc => docs}/guide.md (99%)
 create mode 100644 docs/index.md
 create mode 100644 docs/installation.md
 create mode 100644 docs/troubleshooting.md
 rename {doc => docs}/using-cli.md (100%)
 create mode 100644 mkdocs.yml

diff --git a/README.md b/README.md
index c8e1f668..7cd5feb2 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,24 @@
 # Python Fire
-_Python Fire is a library for creating command line interfaces (CLIs) from
-absolutely any Python object._
+_Python Fire is a library for automatically generating command line interfaces
+(CLIs) from absolutely any Python object._
 
-- Python Fire is a simple way to create a CLI in Python. [[1]](doc/benefits.md#simple-cli)
-- Python Fire is a helpful tool for developing and debugging Python code. [[2]](doc/benefits.md#debugging)
+- Python Fire is a simple way to create a CLI in Python. [[1]](docs/benefits.md#simple-cli)
+- Python Fire is a helpful tool for developing and debugging Python code. [[2]](docs/benefits.md#debugging)
 - Python Fire helps with exploring existing code or turning other people's code
-into a CLI. [[3]](doc/benefits.md#exploring)
-- Python Fire makes transitioning between Bash and Python easier. [[4]](doc/benefits.md#bash)
+into a CLI. [[3]](docs/benefits.md#exploring)
+- Python Fire makes transitioning between Bash and Python easier. [[4]](docs/benefits.md#bash)
 - Python Fire makes using a Python REPL easier by setting up the REPL with the
-modules and variables you'll need already imported and created. [[5]](doc/benefits.md#repl)
+modules and variables you'll need already imported and created. [[5]](docs/benefits.md#repl)
 
 
 ## Installation
 
-`pip install fire`
+To install Python Fire with pip, run: `pip install fire`
 
-OR
+To install Python Fire with conda, run: `conda install fire -c conda-forge`
 
-`conda install fire -c conda-forge`
+To install Python Fire from source, first clone the repository and then run:
+`python setup.py install`
 
 
 ## Basic Usage
@@ -26,7 +27,7 @@ You can call `Fire` on any Python object:<br>
 functions, classes, modules, objects, dictionaries, lists, tuples, etc.
 They all work!
 
-Here's a simple example.
+Here's an example of calling Fire on a class.
 
 ```python
 import fire
@@ -49,9 +50,9 @@ python calculator.py double --number=15  # 30
 ```
 
 To learn how Fire behaves on functions, objects, dicts, lists, etc, and to learn
-about Fire's other features, see the [Using a Fire CLI page](doc/using-cli.md).
+about Fire's other features, see the [Using a Fire CLI page](docs/using-cli.md).
 
-For additional examples, see [The Python Fire Guide](doc/guide.md).
+For additional examples, see [The Python Fire Guide](docs/guide.md).
 
 
 ## Why is it called Fire?
@@ -61,7 +62,7 @@ When you call `Fire`, it fires off (executes) your command.
 
 ## Where can I learn more?
 
-Please see [The Python Fire Guide](doc/guide.md).
+Please see [The Python Fire Guide](docs/guide.md).
 
 
 ## Reference
@@ -78,12 +79,12 @@ Please see [The Python Fire Guide](doc/guide.md).
 
 | Using a CLI    | Command                    | Notes
 | :------------- | :------------------------- | :---------
-| [Help](doc/using-cli.md#help-flag) | `command -- --help` |
-| [REPL](doc/using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
-| [Separator](doc/using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
-| [Completion](doc/using-cli.md#completion-flag) | `command -- --completion` | Generate a completion script for the CLI.
-| [Trace](doc/using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
-| [Verbose](doc/using-cli.md#verbose-flag) | `command -- --verbose` |
+| [Help](docs/using-cli.md#help-flag) | `command -- --help` |
+| [REPL](docs/using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
+| [Separator](docs/using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
+| [Completion](docs/using-cli.md#completion-flag) | `command -- --completion` | Generate a completion script for the CLI.
+| [Trace](docs/using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
+| [Verbose](docs/using-cli.md#verbose-flag) | `command -- --verbose` |
 
 _Note that flags are separated from the Fire command by an isolated `--` arg._
 
diff --git a/docs/api.md b/docs/api.md
new file mode 100644
index 00000000..f85d2065
--- /dev/null
+++ b/docs/api.md
@@ -0,0 +1,20 @@
+| Setup   | Command             | Notes
+| :------ | :------------------ | :---------
+| install | `pip install fire`  |
+
+| Creating a CLI | Command                | Notes
+| :--------------| :--------------------- | :---------
+| import         | `import fire`          |
+| Call           | `fire.Fire()`          | Turns the current module into a Fire CLI.
+| Call           | `fire.Fire(component)` | Turns `component` into a Fire CLI.
+
+| Using a CLI    | Command                    | Notes
+| :------------- | :------------------------- | :---------
+| [Help](using-cli.md#help-flag) | `command -- --help` |
+| [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
+| [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
+| [Completion](using-cli.md#completion-flag) | `command -- --completion` | Generate a completion script for the CLI.
+| [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
+| [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` |
+
+_Note that flags are separated from the Fire command by an isolated `--` arg._
diff --git a/doc/benefits.md b/docs/benefits.md
similarity index 81%
rename from doc/benefits.md
rename to docs/benefits.md
index 1ba95513..ac09f0be 100644
--- a/doc/benefits.md
+++ b/docs/benefits.md
@@ -1,13 +1,14 @@
 # Benefits of Python Fire
 
-## Python Fire is a simple way to create a CLI in Python. <a name="simple-cli"></a>
+<a name="simple-cli"></a>
+## Create CLIs in Python
 
 It's dead simple. Simply write the functionality you want exposed at the command
 line as a function / module / class, and then call Fire. With this addition of a
 single-line call to Fire, your CLI is ready to go.
 
-
-## Python Fire is a helpful tool for developing and debugging Python code. <a name="debugging"></a>
+<a name="debugging"></a>
+## Develop and debug Python code
 
 When you're writing a Python library, you probably want to try it out as you go.
 You could write a main method to check the functionality you're interested in,
@@ -23,8 +24,8 @@ a main method. And if you use the `--interactive` flag to enter an IPython REPL
 then you don't need to load the imports or create your variables; they'll
 already be ready for use as soon as you start the REPL.
 
-
-## Python Fire helps with exploring existing code or turning other people's code into a CLI. <a name="exploring"></a>
+<a name="exploring"></a>
+## Explore existing code; turn other people's code into a CLI
 
 You can take an existing module, maybe even one that you don't have access to
 the source code for, and call `Fire` on it. This lets you easily see what
@@ -40,8 +41,8 @@ The auto-generated help strings that Fire provides when you run a Fire CLI
 allow you to see all the functionality these modules provide in a concise
 manner.
 
-
-## Python Fire makes transitioning between Bash and Python easier. <a name="bash"></a>
+<a name="bash"></a>
+## Transition between Bash and Python
 
 Using Fire lets you call Python directly from Bash. So you can mix your Python
 functions with the unix tools you know and love, like `grep`, `xargs`, `wc`,
@@ -51,8 +52,8 @@ Additionally since writing CLIs in Python requires only a single call to Fire,
 it is now easy to write even one-off scripts that would previously have been in
 Bash, in Python.
 
-
-## Python Fire makes using a Python REPL easier by setting up the REPL with the modules and variables you'll need already imported and created. <a name="repl"></a>
+<a name="repl"></a>
+## Explore code in a Python REPL
 
 When you use the `--interactive` flag to enter an IPython REPL, it starts with
 variables and modules already defined for you. You don't need to waste time
diff --git a/doc/guide.md b/docs/guide.md
similarity index 99%
rename from doc/guide.md
rename to docs/guide.md
index ad49d3de..28d79a6c 100644
--- a/doc/guide.md
+++ b/docs/guide.md
@@ -489,7 +489,7 @@ $ python example.py --name="Sherrerd Hall" climb_stairs --stairs-per-story 10
 $ python example.py climb-stairs --stairs-per-story 10 --name="Sherrerd Hall"
 ```
 
-You'll notice that hyphens and underscores (`-` and `_`) are interchangable in
+You'll notice that hyphens and underscores (`-` and `_`) are interchangeable in
 member names and flag names.
 
 You'll also notice that the constructor's arguments can come after the
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..b1a00694
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,94 @@
+# Python Fire
+_Python Fire is a library for automatically generating command line interfaces
+(CLIs) from absolutely any Python object._
+
+- Python Fire is a simple way to create a CLI in Python. [[1]](benefits.md#simple-cli)
+- Python Fire is a helpful tool for developing and debugging Python code. [[2]](benefits.md#debugging)
+- Python Fire helps with exploring existing code or turning other people's code
+into a CLI. [[3]](benefits.md#exploring)
+- Python Fire makes transitioning between Bash and Python easier. [[4]](benefits.md#bash)
+- Python Fire makes using a Python REPL easier by setting up the REPL with the
+modules and variables you'll need already imported and created. [[5]](benefits.md#repl)
+
+
+## Installation
+
+To install Python Fire with pip, run: `pip install fire`
+
+To install Python Fire with conda, run: `conda install fire -c conda-forge`
+
+To install Python Fire from source, first clone the repository and then run:
+`python setup.py install`
+
+
+## Basic Usage
+
+You can call `Fire` on any Python object:<br>
+functions, classes, modules, objects, dictionaries, lists, tuples, etc.
+They all work!
+
+Here's an example of calling Fire on a class.
+
+```python
+import fire
+
+class Calculator(object):
+  """A simple calculator class."""
+
+  def double(self, number):
+    return 2 * number
+
+if __name__ == '__main__':
+  fire.Fire(Calculator)
+```
+
+Then, from the command line, you can run:
+
+```bash
+python calculator.py double 10  # 20
+python calculator.py double --number=15  # 30
+```
+
+To learn how Fire behaves on functions, objects, dicts, lists, etc, and to learn
+about Fire's other features, see the [Using a Fire CLI page](using-cli.md).
+
+For additional examples, see [The Python Fire Guide](guide.md).
+
+
+## Why is it called Fire?
+
+When you call `Fire`, it fires off (executes) your command.
+
+
+## Where can I learn more?
+
+Please see [The Python Fire Guide](guide.md).
+
+
+## Reference
+
+| Setup   | Command             | Notes
+| :------ | :------------------ | :---------
+| install | `pip install fire`  |
+
+| Creating a CLI | Command                | Notes
+| :--------------| :--------------------- | :---------
+| import         | `import fire`          |
+| Call           | `fire.Fire()`          | Turns the current module into a Fire CLI.
+| Call           | `fire.Fire(component)` | Turns `component` into a Fire CLI.
+
+| Using a CLI    | Command                    | Notes
+| :------------- | :------------------------- | :---------
+| [Help](using-cli.md#help-flag) | `command -- --help` |
+| [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
+| [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
+| [Completion](using-cli.md#completion-flag) | `command -- --completion` | Generate a completion script for the CLI.
+| [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
+| [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` |
+
+_Note that flags are separated from the Fire command by an isolated `--` arg._
+
+
+## Disclaimer
+
+This is not an official Google product.
diff --git a/docs/installation.md b/docs/installation.md
new file mode 100644
index 00000000..614243af
--- /dev/null
+++ b/docs/installation.md
@@ -0,0 +1,8 @@
+# Installation
+
+To install Python Fire with pip, run: `pip install fire`
+
+To install Python Fire with conda, run: `conda install fire -c conda-forge`
+
+To install Python Fire from source, first clone the repository and then run:
+`python setup.py install`
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
new file mode 100644
index 00000000..3ef6b548
--- /dev/null
+++ b/docs/troubleshooting.md
@@ -0,0 +1,13 @@
+# Troubleshooting
+
+This page describes known issues that users of Python Fire have run into. If you
+have an issue not resolved here, consider opening a
+[GitHub Issue](https://github.com/google/python-fire/issues).
+
+### Issue [#19](https://github.com/google/python-fire/issues/19): Don't name your module "cmd"
+
+If you have a module name that conflicts with the name of a builtin module, then
+when Fire goes to import the builtin module, it will import your module instead.
+This will result in an error, possibly an `AttributeError`. Specifically, do not
+name your module any of the following:
+sys, linecache, cmd, bdb, repr, os, re, pprint, traceback
diff --git a/doc/using-cli.md b/docs/using-cli.md
similarity index 100%
rename from doc/using-cli.md
rename to docs/using-cli.md
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 00000000..bb815e37
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,11 @@
+site_name: Python Fire
+theme: readthedocs
+markdown_extensions: [fenced_code]
+pages:
+    - Overview: index.md
+    - Installation: installation.md
+    - Benefits: benefits.md
+    - The Python Fire Guide: guide.md
+    - Using a CLI: using-cli.md
+    - Troubleshooting: troubleshooting.md
+    - Reference: api.md

From b5053bad7c92bb2ee73d99d924b09a48d1498d67 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 21 Jul 2017 13:52:58 -0700
Subject: [PATCH 012/324] Accept a list of arguments for the  argument (in
 addition to continuing to accept a string).

- Update tests to pass Fire args as a list instead of as a string.
- Also uses named argument for command= throughout unit tests.

    Copybara generated commit for Python Fire.

PiperOrigin-RevId: 162780662
Change-Id: I1cb0d32510fa5f43e50d85819ebe39f5fecaddce
Reviewed-on: https://team-review.git.corp.google.com/85622
Reviewed-by: David Bieber <dbieber@google.com>
---
 fire/core.py            |  19 +-
 fire/core_test.py       |  23 +-
 fire/decorators_test.py |  61 ++++--
 fire/fire_test.py       | 454 ++++++++++++++++++++++++++--------------
 4 files changed, 366 insertions(+), 191 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index 37af3493..bc7bc230 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -82,7 +82,8 @@ def Fire(component=None, command=None, name=None):
   Args:
     component: The initial target component.
     command: Optional. If supplied, this is the command executed. If not
-        supplied, then the command is taken from sys.argv instead.
+        supplied, then the command is taken from sys.argv instead. This can be
+        a string or a list of strings; a list of strings is preferred.
     name: Optional. The name of the command as entered at the command line.
         Used in interactive mode and for generating the completion script.
   Returns:
@@ -94,19 +95,25 @@ def Fire(component=None, command=None, name=None):
     to call or class left to instantiate, the resulting current component is
     the final result.
   Raises:
+    ValueError: If the command argument is supplied, but not a string or a
+        sequence of arguments.
     FireExit: When Fire encounters a FireError, Fire will raise a FireExit with
         code 2. When used with the help or trace flags, Fire will raise a
         FireExit with code 0 if successful.
   """
+  name = name or os.path.basename(sys.argv[0])
+
   # Get args as a list.
-  if command is None:
+  if isinstance(command, six.string_types):
+    args = shlex.split(command)
+  elif isinstance(command, (list, tuple)):
+    args = command
+  elif command is None:
     # Use the command line args by default if no command is specified.
     args = sys.argv[1:]
   else:
-    # Otherwise use the specified command.
-    args = shlex.split(command)
-
-  name = name or os.path.basename(sys.argv[0])
+    raise ValueError('The command argument must be a string or a sequence of '
+                     'arguments.')
 
   # Determine the calling context.
   caller = inspect.stack()[1]
diff --git a/fire/core_test.py b/fire/core_test.py
index 3033c3d3..235cb95d 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -40,19 +40,19 @@ def testOneLineResultCircularRef(self):
 
   @mock.patch('fire.interact.Embed')
   def testInteractiveMode(self, mock_embed):
-    core.Fire(tc.TypedProperties, 'alpha')
+    core.Fire(tc.TypedProperties, command=['alpha'])
     self.assertFalse(mock_embed.called)
-    core.Fire(tc.TypedProperties, 'alpha -- -i')
+    core.Fire(tc.TypedProperties, command=['alpha', '--', '-i'])
     self.assertTrue(mock_embed.called)
 
   @mock.patch('fire.interact.Embed')
   def testInteractiveModeFullArgument(self, mock_embed):
-    core.Fire(tc.TypedProperties, 'alpha -- --interactive')
+    core.Fire(tc.TypedProperties, command=['alpha', '--', '--interactive'])
     self.assertTrue(mock_embed.called)
 
   @mock.patch('fire.interact.Embed')
   def testInteractiveModeVariables(self, mock_embed):
-    core.Fire(tc.WithDefaults, 'double 2 -- -i')
+    core.Fire(tc.WithDefaults, command=['double', '2', '--', '-i'])
     self.assertTrue(mock_embed.called)
     (variables, verbose), unused_kwargs = mock_embed.call_args
     self.assertFalse(verbose)
@@ -62,7 +62,8 @@ def testInteractiveModeVariables(self, mock_embed):
 
   @mock.patch('fire.interact.Embed')
   def testInteractiveModeVariablesWithName(self, mock_embed):
-    core.Fire(tc.WithDefaults, 'double 2 -- -i -v', name='D')
+    core.Fire(tc.WithDefaults,
+              command=['double', '2', '--', '-i', '-v'], name='D')
     self.assertTrue(mock_embed.called)
     (variables, verbose), unused_kwargs = mock_embed.call_args
     self.assertTrue(verbose)
@@ -74,21 +75,21 @@ def testInteractiveModeVariablesWithName(self, mock_embed):
   def testImproperUseOfHelp(self):
     # This should produce a warning explaining the proper use of help.
     with self.assertRaisesFireExit(2, 'The proper way to show help.*Usage:'):
-      core.Fire(tc.TypedProperties, 'alpha --help')
+      core.Fire(tc.TypedProperties, command=['alpha', '--help'])
 
   def testProperUseOfHelp(self):
     with self.assertRaisesFireExit(0, 'Usage:.*upper'):
-      core.Fire(tc.TypedProperties, 'gamma -- --help')
+      core.Fire(tc.TypedProperties, command=['gamma', '--', '--help'])
 
   def testInvalidParameterRaisesFireExit(self):
     with self.assertRaisesFireExit(2, 'runmisspelled'):
-      core.Fire(tc.Kwargs, 'props --a=1 --b=2 runmisspelled')
+      core.Fire(tc.Kwargs, command=['props', '--a=1', '--b=2', 'runmisspelled'])
 
   def testErrorRaising(self):
     # Errors in user code should not be caught; they should surface as normal.
     # This will lead to exit status code 1 for the client program.
     with self.assertRaises(ValueError):
-      core.Fire(tc.ErrorRaiser, 'fail')
+      core.Fire(tc.ErrorRaiser, command=['fail'])
 
   def testFireError(self):
     error = core.FireError('Example error')
@@ -100,9 +101,9 @@ def testFireErrorMultipleValues(self):
 
   def testPrintEmptyDict(self):
     with self.assertOutputMatches(stdout='{}', stderr=None):
-      core.Fire(tc.EmptyDictOutput, 'totally_empty')
+      core.Fire(tc.EmptyDictOutput, command=['totally_empty'])
     with self.assertOutputMatches(stdout='{}', stderr=None):
-      core.Fire(tc.EmptyDictOutput, 'nothing_printable')
+      core.Fire(tc.EmptyDictOutput, command=['nothing_printable'])
 
 
 if __name__ == '__main__':
diff --git a/fire/decorators_test.py b/fire/decorators_test.py
index 99edfd53..91316fb0 100644
--- a/fire/decorators_test.py
+++ b/fire/decorators_test.py
@@ -92,14 +92,14 @@ def example7(self, arg1, arg2=None, *varargs, **kwargs):
 class FireDecoratorsTest(testutils.BaseTestCase):
 
   def testSetParseFnsNamedArgs(self):
-    self.assertEqual(core.Fire(NoDefaults, 'double 2'), 4)
-    self.assertEqual(core.Fire(NoDefaults, 'triple 4'), 12.0)
+    self.assertEqual(core.Fire(NoDefaults, command=['double', '2']), 4)
+    self.assertEqual(core.Fire(NoDefaults, command=['triple', '4']), 12.0)
 
   def testSetParseFnsPositionalArgs(self):
-    self.assertEqual(core.Fire(NoDefaults, 'quadruple 5'), 20)
+    self.assertEqual(core.Fire(NoDefaults, command=['quadruple', '5']), 20)
 
   def testSetParseFnsFnWithPositionalArgs(self):
-    self.assertEqual(core.Fire(double, '5'), 10)
+    self.assertEqual(core.Fire(double, command=['5']), 10)
 
   def testSetParseFnsDefaultsFromPython(self):
     # When called from Python, function should behave normally.
@@ -109,10 +109,13 @@ def testSetParseFnsDefaultsFromPython(self):
 
   def testSetParseFnsDefaultsFromFire(self):
     # Fire should use the decorator to know how to parse string arguments.
-    self.assertEqual(core.Fire(WithDefaults, 'example1'), (10, int))
-    self.assertEqual(core.Fire(WithDefaults, 'example1 10'), (10, float))
-    self.assertEqual(core.Fire(WithDefaults, 'example1 13'), (13, float))
-    self.assertEqual(core.Fire(WithDefaults, 'example1 14.0'), (14, float))
+    self.assertEqual(core.Fire(WithDefaults, command=['example1']), (10, int))
+    self.assertEqual(core.Fire(WithDefaults, command=['example1', '10']),
+                     (10, float))
+    self.assertEqual(core.Fire(WithDefaults, command=['example1', '13']),
+                     (13, float))
+    self.assertEqual(core.Fire(WithDefaults, command=['example1', '14.0']),
+                     (14, float))
 
   def testSetParseFnsNamedDefaultsFromPython(self):
     # When called from Python, function should behave normally.
@@ -122,33 +125,47 @@ def testSetParseFnsNamedDefaultsFromPython(self):
 
   def testSetParseFnsNamedDefaultsFromFire(self):
     # Fire should use the decorator to know how to parse string arguments.
-    self.assertEqual(core.Fire(WithDefaults, 'example2'), (10, int))
-    self.assertEqual(core.Fire(WithDefaults, 'example2 10'), (10, float))
-    self.assertEqual(core.Fire(WithDefaults, 'example2 13'), (13, float))
-    self.assertEqual(core.Fire(WithDefaults, 'example2 14.0'), (14, float))
+    self.assertEqual(core.Fire(WithDefaults, command=['example2']), (10, int))
+    self.assertEqual(core.Fire(WithDefaults, command=['example2', '10']),
+                     (10, float))
+    self.assertEqual(core.Fire(WithDefaults, command=['example2', '13']),
+                     (13, float))
+    self.assertEqual(core.Fire(WithDefaults, command=['example2', '14.0']),
+                     (14, float))
 
   def testSetParseFnsPositionalAndNamed(self):
-    self.assertEqual(core.Fire(MixedArguments, 'example3 10 10'), (10, '10'))
+    self.assertEqual(core.Fire(MixedArguments, ['example3', '10', '10']),
+                     (10, '10'))
 
   def testSetParseFnsOnlySomeTypes(self):
-    self.assertEqual(core.Fire(PartialParseFn, 'example4 10 10'), ('10', 10))
-    self.assertEqual(core.Fire(PartialParseFn, 'example5 10 10'), (10, '10'))
+    self.assertEqual(
+        core.Fire(PartialParseFn, command=['example4', '10', '10']), ('10', 10))
+    self.assertEqual(
+        core.Fire(PartialParseFn, command=['example5', '10', '10']), (10, '10'))
 
   def testSetParseFnsForKeywordArgs(self):
-    self.assertEqual(core.Fire(WithKwargs, 'example6'), ('default', 0))
     self.assertEqual(
-        core.Fire(WithKwargs, 'example6 --herring "red"'), ('default', 0))
+        core.Fire(WithKwargs, command=['example6']), ('default', 0))
+    self.assertEqual(
+        core.Fire(WithKwargs, command=['example6', '--herring', '"red"']),
+        ('default', 0))
     self.assertEqual(
-        core.Fire(WithKwargs, 'example6 --mode train'), ('train', 0))
-    self.assertEqual(core.Fire(WithKwargs, 'example6 --mode 3'), ('3', 0))
+        core.Fire(WithKwargs, command=['example6', '--mode', 'train']),
+        ('train', 0))
+    self.assertEqual(core.Fire(WithKwargs, command=['example6', '--mode', '3']),
+                     ('3', 0))
     self.assertEqual(
-        core.Fire(WithKwargs, 'example6 --mode -1 --count 10'), ('-1', 10))
+        core.Fire(WithKwargs,
+                  command=['example6', '--mode', '-1', '--count', '10']),
+        ('-1', 10))
     self.assertEqual(
-        core.Fire(WithKwargs, 'example6 --count -2'), ('default', -2))
+        core.Fire(WithKwargs, command=['example6', '--count', '-2']),
+        ('default', -2))
 
   def testSetParseFn(self):
     self.assertEqual(
-        core.Fire(WithVarArgs, 'example7 1 --arg2=2 3 4 --kwarg=5'),
+        core.Fire(WithVarArgs,
+                  command=['example7', '1', '--arg2=2', '3', '4', '--kwarg=5']),
         ('1', '2', ('3', '4'), {'kwarg': '5'}))
 
 
diff --git a/fire/fire_test.py b/fire/fire_test.py
index 38652a31..89ccb38a 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -37,12 +37,24 @@ def testFire(self):
       fire.Fire(tc.Empty)
       fire.Fire(tc.OldStyleEmpty)
       fire.Fire(tc.WithInit)
+    # Test both passing command as a sequence and as a string.
+    self.assertEqual(fire.Fire(tc.NoDefaults, command='triple 4'), 12)
+    self.assertEqual(fire.Fire(tc.WithDefaults, command=('double', '2')), 4)
+    self.assertEqual(fire.Fire(tc.WithDefaults, command=['triple', '4']), 12)
+    self.assertEqual(fire.Fire(tc.OldStyleWithDefaults,
+                               command=['double', '2']), 4)
+    self.assertEqual(fire.Fire(tc.OldStyleWithDefaults,
+                               command=['triple', '4']), 12)
+
+  def testFirePositionalCommand(self):
+    # Test passing command as a positional argument.
     self.assertEqual(fire.Fire(tc.NoDefaults, 'double 2'), 4)
-    self.assertEqual(fire.Fire(tc.NoDefaults, 'triple 4'), 12)
-    self.assertEqual(fire.Fire(tc.WithDefaults, 'double 2'), 4)
-    self.assertEqual(fire.Fire(tc.WithDefaults, 'triple 4'), 12)
-    self.assertEqual(fire.Fire(tc.OldStyleWithDefaults, 'double 2'), 4)
-    self.assertEqual(fire.Fire(tc.OldStyleWithDefaults, 'triple 4'), 12)
+    self.assertEqual(fire.Fire(tc.NoDefaults, ['double', '2']), 4)
+
+  def testFireInvalidCommandArg(self):
+    with self.assertRaises(ValueError):
+      # This is not a valid command.
+      fire.Fire(tc.WithDefaults, command=10)
 
   def testFireDefaultName(self):
     with mock.patch.object(sys, 'argv',
@@ -53,217 +65,319 @@ def testFireDefaultName(self):
         fire.Fire(tc.Empty)
 
   def testFireNoArgs(self):
-    self.assertEqual(fire.Fire(tc.MixedDefaults, 'ten'), 10)
+    self.assertEqual(fire.Fire(tc.MixedDefaults, command=['ten']), 10)
 
   def testFireExceptions(self):
     # Exceptions of Fire are printed to stderr and a FireExit is raised.
     with self.assertRaisesFireExit(2):
-      fire.Fire(tc.Empty, 'nomethod')  # Member doesn't exist.
+      fire.Fire(tc.Empty, command=['nomethod'])  # Member doesn't exist.
     with self.assertRaisesFireExit(2):
-      fire.Fire(tc.NoDefaults, 'double')  # Missing argument.
+      fire.Fire(tc.NoDefaults, command=['double'])  # Missing argument.
     with self.assertRaisesFireExit(2):
-      fire.Fire(tc.TypedProperties, 'delta x')  # Missing key.
+      fire.Fire(tc.TypedProperties, command=['delta', 'x'])  # Missing key.
 
     # Exceptions of the target components are still raised.
     with self.assertRaises(ZeroDivisionError):
-      fire.Fire(tc.NumberDefaults, 'reciprocal 0.0')
+      fire.Fire(tc.NumberDefaults, command=['reciprocal', '0.0'])
 
   def testFireNamedArgs(self):
-    self.assertEqual(fire.Fire(tc.WithDefaults, 'double --count 5'), 10)
-    self.assertEqual(fire.Fire(tc.WithDefaults, 'triple --count 5'), 15)
-    self.assertEqual(fire.Fire(tc.OldStyleWithDefaults, 'double --count 5'), 10)
-    self.assertEqual(fire.Fire(tc.OldStyleWithDefaults, 'triple --count 5'), 15)
+    self.assertEqual(fire.Fire(tc.WithDefaults,
+                               command=['double', '--count', '5']), 10)
+    self.assertEqual(fire.Fire(tc.WithDefaults,
+                               command=['triple', '--count', '5']), 15)
+    self.assertEqual(
+        fire.Fire(tc.OldStyleWithDefaults, command=['double', '--count', '5']),
+        10)
+    self.assertEqual(
+        fire.Fire(tc.OldStyleWithDefaults, command=['triple', '--count', '5']),
+        15)
 
   def testFireNamedArgsWithEquals(self):
-    self.assertEqual(fire.Fire(tc.WithDefaults, 'double --count=5'), 10)
-    self.assertEqual(fire.Fire(tc.WithDefaults, 'triple --count=5'), 15)
+    self.assertEqual(fire.Fire(tc.WithDefaults,
+                               command=['double', '--count=5']), 10)
+    self.assertEqual(fire.Fire(tc.WithDefaults,
+                               command=['triple', '--count=5']), 15)
 
   def testFireAllNamedArgs(self):
-    self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum 1 2'), 5)
-    self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum --alpha 1 2'), 5)
-    self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum --beta 1 2'), 4)
-    self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum 1 --alpha 2'), 4)
-    self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum 1 --beta 2'), 5)
-    self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum --alpha 1 --beta 2'), 5)
-    self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum --beta 1 --alpha 2'), 4)
+    self.assertEqual(fire.Fire(tc.MixedDefaults, command=['sum', '1', '2']), 5)
+    self.assertEqual(fire.Fire(tc.MixedDefaults,
+                               command=['sum', '--alpha', '1', '2']), 5)
+    self.assertEqual(fire.Fire(tc.MixedDefaults,
+                               command=['sum', '--beta', '1', '2']), 4)
+    self.assertEqual(fire.Fire(tc.MixedDefaults,
+                               command=['sum', '1', '--alpha', '2']), 4)
+    self.assertEqual(fire.Fire(tc.MixedDefaults,
+                               command=['sum', '1', '--beta', '2']), 5)
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['sum', '--alpha', '1', '--beta', '2']), 5)
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['sum', '--beta', '1', '--alpha', '2']), 4)
 
   def testFireAllNamedArgsOneMissing(self):
-    self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum'), 0)
-    self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum 1'), 1)
-    self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum --alpha 1'), 1)
-    self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum --beta 2'), 4)
+    self.assertEqual(fire.Fire(tc.MixedDefaults, command=['sum']), 0)
+    self.assertEqual(fire.Fire(tc.MixedDefaults, command=['sum', '1']), 1)
+    self.assertEqual(fire.Fire(tc.MixedDefaults,
+                               command=['sum', '--alpha', '1']), 1)
+    self.assertEqual(fire.Fire(tc.MixedDefaults,
+                               command=['sum', '--beta', '2']), 4)
 
   def testFirePartialNamedArgs(self):
-    self.assertEqual(fire.Fire(tc.MixedDefaults, 'identity 1 2'), (1, 2))
     self.assertEqual(
-        fire.Fire(tc.MixedDefaults, 'identity --alpha 1 2'), (1, 2))
-    self.assertEqual(fire.Fire(tc.MixedDefaults, 'identity --beta 1 2'), (2, 1))
+        fire.Fire(tc.MixedDefaults, command=['identity', '1', '2']), (1, 2))
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '--alpha', '1', '2']), (1, 2))
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '--beta', '1', '2']), (2, 1))
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '1', '--alpha', '2']), (2, 1))
     self.assertEqual(
-        fire.Fire(tc.MixedDefaults, 'identity 1 --alpha 2'), (2, 1))
-    self.assertEqual(fire.Fire(tc.MixedDefaults, 'identity 1 --beta 2'), (1, 2))
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '1', '--beta', '2']), (1, 2))
     self.assertEqual(
-        fire.Fire(tc.MixedDefaults, 'identity --alpha 1 --beta 2'), (1, 2))
+        fire.Fire(
+            tc.MixedDefaults,
+            command=['identity', '--alpha', '1', '--beta', '2']), (1, 2))
     self.assertEqual(
-        fire.Fire(tc.MixedDefaults, 'identity --beta 1 --alpha 2'), (2, 1))
+        fire.Fire(
+            tc.MixedDefaults,
+            command=['identity', '--beta', '1', '--alpha', '2']), (2, 1))
 
   def testFirePartialNamedArgsOneMissing(self):
     # Errors are written to standard out and a FireExit is raised.
     with self.assertRaisesFireExit(2):
-      fire.Fire(tc.MixedDefaults, 'identity')  # Identity needs an arg.
+      fire.Fire(tc.MixedDefaults,
+                command=['identity'])  # Identity needs an arg.
 
     with self.assertRaisesFireExit(2):
       # Identity needs a value for alpha.
-      fire.Fire(tc.MixedDefaults, 'identity --beta 2')
+      fire.Fire(tc.MixedDefaults, command=['identity', '--beta', '2'])
 
-    self.assertEqual(fire.Fire(tc.MixedDefaults, 'identity 1'), (1, '0'))
     self.assertEqual(
-        fire.Fire(tc.MixedDefaults, 'identity --alpha 1'), (1, '0'))
+        fire.Fire(tc.MixedDefaults, command=['identity', '1']), (1, '0'))
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults, command=['identity', '--alpha', '1']),
+        (1, '0'))
 
   def testFireAnnotatedArgs(self):
-    self.assertEqual(fire.Fire(tc.Annotations, 'double 5'), 10)
-    self.assertEqual(fire.Fire(tc.Annotations, 'triple 5'), 15)
+    self.assertEqual(fire.Fire(tc.Annotations, command=['double', '5']), 10)
+    self.assertEqual(fire.Fire(tc.Annotations, command=['triple', '5']), 15)
 
   @unittest.skipIf(six.PY2, 'Keyword-only arguments not in Python 2.')
   def testFireKeywordOnlyArgs(self):
     with self.assertRaisesFireExit(2):
       # Keyword arguments must be passed with flag syntax.
-      fire.Fire(tc.py3.KeywordOnly, 'double 5')
+      fire.Fire(tc.py3.KeywordOnly, command=['double', '5'])
 
-    self.assertEqual(fire.Fire(tc.py3.KeywordOnly, 'double --count 5'), 10)
-    self.assertEqual(fire.Fire(tc.py3.KeywordOnly, 'triple --count 5'), 15)
+    self.assertEqual(
+        fire.Fire(tc.py3.KeywordOnly, command=['double', '--count', '5']), 10)
+    self.assertEqual(
+        fire.Fire(tc.py3.KeywordOnly, command=['triple', '--count', '5']), 15)
 
   def testFireProperties(self):
-    self.assertEqual(fire.Fire(tc.TypedProperties, 'alpha'), True)
-    self.assertEqual(fire.Fire(tc.TypedProperties, 'beta'), (1, 2, 3))
+    self.assertEqual(fire.Fire(tc.TypedProperties, command=['alpha']), True)
+    self.assertEqual(fire.Fire(tc.TypedProperties, command=['beta']), (1, 2, 3))
 
   def testFireRecursion(self):
     self.assertEqual(
-        fire.Fire(tc.TypedProperties, 'charlie double hello'), 'hellohello')
-    self.assertEqual(fire.Fire(tc.TypedProperties, 'charlie triple w'), 'www')
+        fire.Fire(tc.TypedProperties,
+                  command=['charlie', 'double', 'hello']), 'hellohello')
+    self.assertEqual(fire.Fire(tc.TypedProperties,
+                               command=['charlie', 'triple', 'w']), 'www')
 
   def testFireVarArgs(self):
     self.assertEqual(
-        fire.Fire(tc.VarArgs, 'cumsums a b c d'), ['a', 'ab', 'abc', 'abcd'])
-    self.assertEqual(fire.Fire(tc.VarArgs, 'cumsums 1 2 3 4'), [1, 3, 6, 10])
+        fire.Fire(tc.VarArgs,
+                  command=['cumsums', 'a', 'b', 'c', 'd']),
+        ['a', 'ab', 'abc', 'abcd'])
+    self.assertEqual(
+        fire.Fire(tc.VarArgs, command=['cumsums', '1', '2', '3', '4']),
+        [1, 3, 6, 10])
 
   def testFireVarArgsWithNamedArgs(self):
-    self.assertEqual(fire.Fire(tc.VarArgs, 'varchars 1 2 c d'), (1, 2, 'cd'))
-    self.assertEqual(fire.Fire(tc.VarArgs, 'varchars 3 4 c d e'), (3, 4, 'cde'))
+    self.assertEqual(
+        fire.Fire(tc.VarArgs, command=['varchars', '1', '2', 'c', 'd']),
+        (1, 2, 'cd'))
+    self.assertEqual(
+        fire.Fire(tc.VarArgs, command=['varchars', '3', '4', 'c', 'd', 'e']),
+        (3, 4, 'cde'))
 
   def testFireKeywordArgs(self):
-    self.assertEqual(fire.Fire(tc.Kwargs, 'props --name David --age 24'),
-                     {'name': 'David', 'age': 24})
     self.assertEqual(
-        fire.Fire(tc.Kwargs,
-                  'props --message "This is a message it has -- in it"'),
+        fire.Fire(
+            tc.Kwargs,
+            command=['props', '--name', 'David', '--age', '24']),
+        {'name': 'David', 'age': 24})
+    # Run this test both with a list command and a string command.
+    self.assertEqual(
+        fire.Fire(
+            tc.Kwargs,
+            command=['props', '--message',
+                     '"This is a message it has -- in it"']),  # Quotes stripped
+        {'message': 'This is a message it has -- in it'})
+    self.assertEqual(
+        fire.Fire(
+            tc.Kwargs,
+            command=['props', '--message',
+                     'This is a message it has -- in it']),
         {'message': 'This is a message it has -- in it'})
-    self.assertEqual(fire.Fire(tc.Kwargs, 'upper --alpha A --beta B'),
-                     'ALPHA BETA')
-    self.assertEqual(fire.Fire(tc.Kwargs, 'upper --alpha A --beta B - lower'),
-                     'alpha beta')
+    self.assertEqual(
+        fire.Fire(
+            tc.Kwargs,
+            command='props --message "This is a message it has -- in it"'),
+        {'message': 'This is a message it has -- in it'})
+    self.assertEqual(
+        fire.Fire(tc.Kwargs,
+                  command=['upper', '--alpha', 'A', '--beta', 'B']),
+        'ALPHA BETA')
+    self.assertEqual(
+        fire.Fire(
+            tc.Kwargs,
+            command=['upper', '--alpha', 'A', '--beta', 'B', '-', 'lower']),
+        'alpha beta')
 
   def testFireKeywordArgsWithMissingPositionalArgs(self):
-    self.assertEqual(fire.Fire(tc.Kwargs, 'run Hello World --cell is'),
-                     ('Hello', 'World', {'cell': 'is'}))
-    self.assertEqual(fire.Fire(tc.Kwargs, 'run Hello --cell ok'),
-                     ('Hello', None, {'cell': 'ok'}))
+    self.assertEqual(
+        fire.Fire(tc.Kwargs, command=['run', 'Hello', 'World', '--cell', 'is']),
+        ('Hello', 'World', {'cell': 'is'}))
+    self.assertEqual(
+        fire.Fire(tc.Kwargs, command=['run', 'Hello', '--cell', 'ok']),
+        ('Hello', None, {'cell': 'ok'}))
 
   def testFireObject(self):
-    self.assertEqual(fire.Fire(tc.WithDefaults(), 'double --count 5'), 10)
-    self.assertEqual(fire.Fire(tc.WithDefaults(), 'triple --count 5'), 15)
+    self.assertEqual(
+        fire.Fire(tc.WithDefaults(), command=['double', '--count', '5']), 10)
+    self.assertEqual(
+        fire.Fire(tc.WithDefaults(), command=['triple', '--count', '5']), 15)
 
   def testFireDict(self):
     component = {
         'double': lambda x=0: 2 * x,
         'cheese': 'swiss',
     }
-    self.assertEqual(fire.Fire(component, 'double 5'), 10)
-    self.assertEqual(fire.Fire(component, 'cheese'), 'swiss')
+    self.assertEqual(fire.Fire(component, command=['double', '5']), 10)
+    self.assertEqual(fire.Fire(component, command=['cheese']), 'swiss')
 
   def testFireObjectWithDict(self):
-    self.assertEqual(fire.Fire(tc.TypedProperties, 'delta echo'), 'E')
-    self.assertEqual(fire.Fire(tc.TypedProperties, 'delta echo lower'), 'e')
-    self.assertIsInstance(fire.Fire(tc.TypedProperties, 'delta nest'), dict)
-    self.assertEqual(fire.Fire(tc.TypedProperties, 'delta nest 0'), 'a')
+    self.assertEqual(
+        fire.Fire(tc.TypedProperties, command=['delta', 'echo']), 'E')
+    self.assertEqual(
+        fire.Fire(tc.TypedProperties, command=['delta', 'echo', 'lower']), 'e')
+    self.assertIsInstance(
+        fire.Fire(tc.TypedProperties, command=['delta', 'nest']), dict)
+    self.assertEqual(
+        fire.Fire(tc.TypedProperties, command=['delta', 'nest', '0']), 'a')
 
   def testFireList(self):
     component = ['zero', 'one', 'two', 'three']
-    self.assertEqual(fire.Fire(component, '2'), 'two')
-    self.assertEqual(fire.Fire(component, '3'), 'three')
-    self.assertEqual(fire.Fire(component, '-1'), 'three')
+    self.assertEqual(fire.Fire(component, command=['2']), 'two')
+    self.assertEqual(fire.Fire(component, command=['3']), 'three')
+    self.assertEqual(fire.Fire(component, command=['-1']), 'three')
 
   def testFireObjectWithList(self):
-    self.assertEqual(fire.Fire(tc.TypedProperties, 'echo 0'), 'alex')
-    self.assertEqual(fire.Fire(tc.TypedProperties, 'echo 1'), 'bethany')
+    self.assertEqual(fire.Fire(tc.TypedProperties, command=['echo', '0']),
+                     'alex')
+    self.assertEqual(fire.Fire(tc.TypedProperties, command=['echo', '1']),
+                     'bethany')
 
   def testFireObjectWithTuple(self):
-    self.assertEqual(fire.Fire(tc.TypedProperties, 'fox 0'), 'carry')
-    self.assertEqual(fire.Fire(tc.TypedProperties, 'fox 1'), 'divide')
+    self.assertEqual(fire.Fire(tc.TypedProperties, command=['fox', '0']),
+                     'carry')
+    self.assertEqual(fire.Fire(tc.TypedProperties, command=['fox', '1']),
+                     'divide')
 
   def testFireNoComponent(self):
-    self.assertEqual(fire.Fire(command='tc WithDefaults double 10'), 20)
+    self.assertEqual(fire.Fire(command=['tc', 'WithDefaults', 'double', '10']),
+                     20)
     last_char = lambda text: text[-1]  # pylint: disable=unused-variable
-    self.assertEqual(fire.Fire(command='last_char "Hello"'), 'o')
-    self.assertEqual(fire.Fire(command='last-char "World"'), 'd')
+    self.assertEqual(fire.Fire(command=['last_char', '"Hello"']), 'o')
+    self.assertEqual(fire.Fire(command=['last-char', '"World"']), 'd')
     rset = lambda count=0: set(range(count))  # pylint: disable=unused-variable
-    self.assertEqual(fire.Fire(command='rset 5'), {0, 1, 2, 3, 4})
+    self.assertEqual(fire.Fire(command=['rset', '5']), {0, 1, 2, 3, 4})
 
   def testFireUnderscores(self):
     self.assertEqual(
-        fire.Fire(tc.Underscores, 'underscore-example'), 'fish fingers')
+        fire.Fire(tc.Underscores,
+                  command=['underscore-example']), 'fish fingers')
     self.assertEqual(
-        fire.Fire(tc.Underscores, 'underscore_example'), 'fish fingers')
+        fire.Fire(tc.Underscores,
+                  command=['underscore_example']), 'fish fingers')
 
   def testFireUnderscoresInArg(self):
     self.assertEqual(
-        fire.Fire(tc.Underscores, 'underscore-function example'), 'example')
+        fire.Fire(tc.Underscores,
+                  command=['underscore-function', 'example']), 'example')
     self.assertEqual(
-        fire.Fire(tc.Underscores, 'underscore_function --underscore-arg=score'),
+        fire.Fire(tc.Underscores,
+                  command=['underscore_function', '--underscore-arg=score']),
         'score')
     self.assertEqual(
-        fire.Fire(tc.Underscores, 'underscore_function --underscore_arg=score'),
+        fire.Fire(tc.Underscores,
+                  command=['underscore_function', '--underscore_arg=score']),
         'score')
 
   def testBoolParsing(self):
-    self.assertEqual(fire.Fire(tc.BoolConverter, 'as-bool True'), True)
-    self.assertEqual(fire.Fire(tc.BoolConverter, 'as-bool False'), False)
-    self.assertEqual(fire.Fire(tc.BoolConverter, 'as-bool --arg=True'), True)
-    self.assertEqual(fire.Fire(tc.BoolConverter, 'as-bool --arg=False'), False)
-    self.assertEqual(fire.Fire(tc.BoolConverter, 'as-bool --arg'), True)
-    self.assertEqual(fire.Fire(tc.BoolConverter, 'as-bool --noarg'), False)
+    self.assertEqual(fire.Fire(tc.BoolConverter, command=['as-bool', 'True']),
+                     True)
+    self.assertEqual(
+        fire.Fire(tc.BoolConverter, command=['as-bool', 'False']), False)
+    self.assertEqual(
+        fire.Fire(tc.BoolConverter, command=['as-bool', '--arg=True']), True)
+    self.assertEqual(
+        fire.Fire(tc.BoolConverter, command=['as-bool', '--arg=False']), False)
+    self.assertEqual(fire.Fire(tc.BoolConverter, command=['as-bool', '--arg']),
+                     True)
+    self.assertEqual(
+        fire.Fire(tc.BoolConverter, command=['as-bool', '--noarg']), False)
 
   def testBoolParsingContinued(self):
     self.assertEqual(
-        fire.Fire(tc.MixedDefaults, 'identity True False'), (True, False))
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', 'True', 'False']), (True, False))
     self.assertEqual(
-        fire.Fire(tc.MixedDefaults, 'identity --alpha=False 10'), (False, 10))
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '--alpha=False', '10']), (False, 10))
     self.assertEqual(
-        fire.Fire(tc.MixedDefaults, 'identity --alpha --beta 10'), (True, 10))
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '--alpha', '--beta', '10']), (True, 10))
     self.assertEqual(
-        fire.Fire(tc.MixedDefaults, 'identity --alpha --beta=10'), (True, 10))
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '--alpha', '--beta=10']), (True, 10))
     self.assertEqual(
-        fire.Fire(tc.MixedDefaults, 'identity --noalpha --beta'), (False, True))
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '--noalpha', '--beta']), (False, True))
     self.assertEqual(
-        fire.Fire(tc.MixedDefaults, 'identity 10 --beta'), (10, True))
+        fire.Fire(tc.MixedDefaults, command=['identity', '10', '--beta']),
+        (10, True))
 
   def testBoolParsingLessExpectedCases(self):
     # Note: Does not return (True, 10).
     self.assertEqual(
-        fire.Fire(tc.MixedDefaults, 'identity --alpha 10'), (10, '0'))
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '--alpha', '10']), (10, '0'))
     # To get (True, 10), use one of the following:
     self.assertEqual(
-        fire.Fire(tc.MixedDefaults, 'identity --alpha --beta=10'), (True, 10))
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '--alpha', '--beta=10']),
+        (True, 10))
     self.assertEqual(
-        fire.Fire(tc.MixedDefaults, 'identity True 10'), (True, 10))
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', 'True', '10']), (True, 10))
 
     # Note: Does not return ('--test', '0').
-    self.assertEqual(fire.Fire(tc.MixedDefaults, 'identity --alpha --test'),
+    self.assertEqual(fire.Fire(tc.MixedDefaults,
+                               command=['identity', '--alpha', '--test']),
                      (True, '--test'))
     # To get ('--test', '0'), use one of the following:
-    self.assertEqual(fire.Fire(tc.MixedDefaults, 'identity --alpha=--test'),
+    self.assertEqual(fire.Fire(tc.MixedDefaults,
+                               command=['identity', '--alpha=--test']),
                      ('--test', '0'))
     self.assertEqual(
-        fire.Fire(tc.MixedDefaults, r'identity --alpha \"--test\"'),
+        fire.Fire(tc.MixedDefaults, command=r'identity --alpha \"--test\"'),
         ('--test', '0'))
 
   def testBoolParsingWithNo(self):
@@ -271,158 +385,194 @@ def testBoolParsingWithNo(self):
     def fn1(thing, nothing):
       return thing, nothing
 
-    self.assertEqual(fire.Fire(fn1, '--thing --nothing'), (True, True))
-    self.assertEqual(fire.Fire(fn1, '--thing --nonothing'), (True, False))
+    self.assertEqual(fire.Fire(fn1, command=['--thing', '--nothing']),
+                     (True, True))
+    self.assertEqual(fire.Fire(fn1, command=['--thing', '--nonothing']),
+                     (True, False))
 
     with self.assertRaisesFireExit(2):
       # In this case nothing=False (since rightmost setting of a flag gets
       # precedence), but it errors because thing has no value.
-      fire.Fire(fn1, '--nothing --nonothing')
+      fire.Fire(fn1, command=['--nothing', '--nonothing'])
 
     # In these examples, --nothing sets thing=False:
     def fn2(thing, **kwargs):
       return thing, kwargs
-    self.assertEqual(fire.Fire(fn2, '--thing'), (True, {}))
-    self.assertEqual(fire.Fire(fn2, '--nothing'), (False, {}))
+    self.assertEqual(fire.Fire(fn2, command=['--thing']), (True, {}))
+    self.assertEqual(fire.Fire(fn2, command=['--nothing']), (False, {}))
     with self.assertRaisesFireExit(2):
       # In this case, nothing=True, but it errors because thing has no value.
-      fire.Fire(fn2, '--nothing=True')
-    self.assertEqual(fire.Fire(fn2, '--nothing --nothing=True'),
+      fire.Fire(fn2, command=['--nothing=True'])
+    self.assertEqual(fire.Fire(fn2, command=['--nothing', '--nothing=True']),
                      (False, {'nothing': True}))
 
     def fn3(arg, **kwargs):
       return arg, kwargs
-    self.assertEqual(fire.Fire(fn3, '--arg=value --thing'),
+    self.assertEqual(fire.Fire(fn3, command=['--arg=value', '--thing']),
                      ('value', {'thing': True}))
-    self.assertEqual(fire.Fire(fn3, '--arg=value --nothing'),
+    self.assertEqual(fire.Fire(fn3, command=['--arg=value', '--nothing']),
                      ('value', {'thing': False}))
-    self.assertEqual(fire.Fire(fn3, '--arg=value --nonothing'),
+    self.assertEqual(fire.Fire(fn3, command=['--arg=value', '--nonothing']),
                      ('value', {'nothing': False}))
 
   def testTraceFlag(self):
     with self.assertRaisesFireExit(0, 'Fire trace:\n'):
-      fire.Fire(tc.BoolConverter, 'as-bool True -- --trace')
+      fire.Fire(tc.BoolConverter, command=['as-bool', 'True', '--', '--trace'])
     with self.assertRaisesFireExit(0, 'Fire trace:\n'):
-      fire.Fire(tc.BoolConverter, 'as-bool True -- -t')
+      fire.Fire(tc.BoolConverter, command=['as-bool', 'True', '--', '-t'])
     with self.assertRaisesFireExit(0, 'Fire trace:\n'):
-      fire.Fire(tc.BoolConverter, '-- --trace')
+      fire.Fire(tc.BoolConverter, command=['--', '--trace'])
 
   def testHelpFlag(self):
     with self.assertRaisesFireExit(0):
-      fire.Fire(tc.BoolConverter, 'as-bool True -- --help')
+      fire.Fire(tc.BoolConverter, command=['as-bool', 'True', '--', '--help'])
     with self.assertRaisesFireExit(0):
-      fire.Fire(tc.BoolConverter, 'as-bool True -- -h')
+      fire.Fire(tc.BoolConverter, command=['as-bool', 'True', '--', '-h'])
     with self.assertRaisesFireExit(0):
-      fire.Fire(tc.BoolConverter, '-- --help')
+      fire.Fire(tc.BoolConverter, command=['--', '--help'])
 
   def testHelpFlagAndTraceFlag(self):
     with self.assertRaisesFireExit(0, 'Fire trace:\n.*Usage:'):
-      fire.Fire(tc.BoolConverter, 'as-bool True -- --help --trace')
+      fire.Fire(tc.BoolConverter,
+                command=['as-bool', 'True', '--', '--help', '--trace'])
     with self.assertRaisesFireExit(0, 'Fire trace:\n.*Usage:'):
-      fire.Fire(tc.BoolConverter, 'as-bool True -- -h -t')
+      fire.Fire(tc.BoolConverter, command=['as-bool', 'True', '--', '-h', '-t'])
     with self.assertRaisesFireExit(0, 'Fire trace:\n.*Usage:'):
-      fire.Fire(tc.BoolConverter, '-- -h --trace')
+      fire.Fire(tc.BoolConverter, command=['--', '-h', '--trace'])
 
   def testTabCompletionNoName(self):
-    completion_script = fire.Fire(tc.NoDefaults, '-- --completion')
+    completion_script = fire.Fire(tc.NoDefaults, command=['--', '--completion'])
     self.assertIn('double', completion_script)
     self.assertIn('triple', completion_script)
 
   def testTabCompletion(self):
-    completion_script = fire.Fire(tc.NoDefaults, '-- --completion', name='c')
+    completion_script = fire.Fire(
+        tc.NoDefaults, command=['--', '--completion'], name='c')
     self.assertIn('double', completion_script)
     self.assertIn('triple', completion_script)
 
   def testTabCompletionWithDict(self):
     actions = {'multiply': lambda a, b: a * b}
-    completion_script = fire.Fire(actions, '-- --completion', name='actCLI')
+    completion_script = fire.Fire(
+        actions, command=['--', '--completion'], name='actCLI')
     self.assertIn('actCLI', completion_script)
     self.assertIn('multiply', completion_script)
 
   def testBasicSeparator(self):
     # '-' is the default separator.
-    self.assertEqual(fire.Fire(tc.MixedDefaults, 'identity + _'), ('+', '_'))
-    self.assertEqual(fire.Fire(tc.MixedDefaults, 'identity _ + -'), ('_', '+'))
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '+', '_']), ('+', '_'))
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '_', '+', '-']), ('_', '+'))
 
     # If we change the separator we can use '-' as an argument.
     self.assertEqual(
-        fire.Fire(tc.MixedDefaults, 'identity - _ -- --separator &'),
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '-', '_', '--', '--separator', '&']),
         ('-', '_'))
 
     # The separator triggers a function call, but there aren't enough arguments.
     with self.assertRaisesFireExit(2):
-      fire.Fire(tc.MixedDefaults, 'identity - _ +')
+      fire.Fire(tc.MixedDefaults, command=['identity', '-', '_', '+'])
 
   def testNonComparable(self):
     """Fire should work with classes that disallow comparisons."""
-    self.assertIsInstance(fire.Fire(tc.NonComparable, ''), tc.NonComparable)
+    # Make sure this test passes both with a string command or a list command.
+    self.assertIsInstance(
+        fire.Fire(tc.NonComparable, command=''), tc.NonComparable)
+    self.assertIsInstance(
+        fire.Fire(tc.NonComparable, command=[]), tc.NonComparable)
 
     # The first separator instantiates the NonComparable object.
     # The second separator causes Fire to check if the separator was necessary.
-    self.assertIsInstance(fire.Fire(tc.NonComparable, '- -'), tc.NonComparable)
+    self.assertIsInstance(
+        fire.Fire(tc.NonComparable, command=['-', '-']), tc.NonComparable)
 
   def testExtraSeparators(self):
     self.assertEqual(
-        fire.Fire(tc.ReturnsObj, 'get-obj arg1 arg2 - - as-bool True'), True)
+        fire.Fire(
+            tc.ReturnsObj,
+            command=['get-obj', 'arg1', 'arg2', '-', '-', 'as-bool', 'True']),
+        True)
     self.assertEqual(
-        fire.Fire(tc.ReturnsObj, 'get-obj arg1 arg2 - - - as-bool True'), True)
+        fire.Fire(
+            tc.ReturnsObj,
+            command=['get-obj', 'arg1', 'arg2', '-', '-', '-', 'as-bool',
+                     'True']),
+        True)
 
   def testSeparatorForChaining(self):
     # Without a separator all args are consumed by get_obj.
     self.assertIsInstance(
-        fire.Fire(tc.ReturnsObj, 'get-obj arg1 arg2 as-bool True'),
+        fire.Fire(tc.ReturnsObj,
+                  command=['get-obj', 'arg1', 'arg2', 'as-bool', 'True']),
         tc.BoolConverter)
     # With a separator only the preceeding args are consumed by get_obj.
     self.assertEqual(
-        fire.Fire(tc.ReturnsObj, 'get-obj arg1 arg2 - as-bool True'), True)
+        fire.Fire(
+            tc.ReturnsObj,
+            command=['get-obj', 'arg1', 'arg2', '-', 'as-bool', 'True']), True)
     self.assertEqual(
         fire.Fire(tc.ReturnsObj,
-                  'get-obj arg1 arg2 & as-bool True -- --separator &'),
+                  command=['get-obj', 'arg1', 'arg2', '&', 'as-bool', 'True',
+                           '--', '--separator', '&']),
         True)
     self.assertEqual(
         fire.Fire(tc.ReturnsObj,
-                  'get-obj arg1 $$ as-bool True -- --separator $$'),
+                  command=['get-obj', 'arg1', '$$', 'as-bool', 'True', '--',
+                           '--separator', '$$']),
         True)
 
   def testFloatForExpectedInt(self):
     self.assertEqual(
-        fire.Fire(tc.MixedDefaults, 'sum --alpha 2.2 --beta 3.0'), 8.2)
+        fire.Fire(tc.MixedDefaults,
+                  command=['sum', '--alpha', '2.2', '--beta', '3.0']), 8.2)
     self.assertEqual(
-        fire.Fire(tc.NumberDefaults, 'integer_reciprocal --divisor 5.0'), 0.2)
+        fire.Fire(
+            tc.NumberDefaults,
+            command=['integer_reciprocal', '--divisor', '5.0']), 0.2)
     self.assertEqual(
-        fire.Fire(tc.NumberDefaults, 'integer_reciprocal 4.0'), 0.25)
+        fire.Fire(tc.NumberDefaults, command=['integer_reciprocal', '4.0']),
+        0.25)
 
   def testClassInstantiation(self):
-    self.assertIsInstance(fire.Fire(tc.InstanceVars, '--arg1=a1 --arg2=a2'),
+    self.assertIsInstance(fire.Fire(tc.InstanceVars,
+                                    command=['--arg1=a1', '--arg2=a2']),
                           tc.InstanceVars)
     with self.assertRaisesFireExit(2):
       # Cannot instantiate a class with positional args.
-      fire.Fire(tc.InstanceVars, 'a1 a2')
+      fire.Fire(tc.InstanceVars, command=['a1', 'a2'])
 
   def testTraceErrors(self):
     # Class needs additional value but runs out of args.
     with self.assertRaisesFireExit(2):
-      fire.Fire(tc.InstanceVars, 'a1')
+      fire.Fire(tc.InstanceVars, command=['a1'])
     with self.assertRaisesFireExit(2):
-      fire.Fire(tc.InstanceVars, '--arg1=a1')
+      fire.Fire(tc.InstanceVars, command=['--arg1=a1'])
 
     # Routine needs additional value but runs out of args.
     with self.assertRaisesFireExit(2):
-      fire.Fire(tc.InstanceVars, 'a1 a2 - run b1')
+      fire.Fire(tc.InstanceVars, command=['a1', 'a2', '-', 'run', 'b1'])
     with self.assertRaisesFireExit(2):
-      fire.Fire(tc.InstanceVars, '--arg1=a1 --arg2=a2 - run b1')
+      fire.Fire(tc.InstanceVars,
+                command=['--arg1=a1', '--arg2=a2', '-', 'run b1'])
 
     # Extra args cannot be consumed.
     with self.assertRaisesFireExit(2):
-      fire.Fire(tc.InstanceVars, 'a1 a2 - run b1 b2 b3')
+      fire.Fire(tc.InstanceVars,
+                command=['a1', 'a2', '-', 'run', 'b1', 'b2', 'b3'])
     with self.assertRaisesFireExit(2):
-      fire.Fire(tc.InstanceVars, '--arg1=a1 --arg2=a2 - run b1 b2 b3')
+      fire.Fire(
+          tc.InstanceVars,
+          command=['--arg1=a1', '--arg2=a2', '-', 'run', 'b1', 'b2', 'b3'])
 
     # Cannot find member to access.
     with self.assertRaisesFireExit(2):
-      fire.Fire(tc.InstanceVars, 'a1 a2 - jog')
+      fire.Fire(tc.InstanceVars, command=['a1', 'a2', '-', 'jog'])
     with self.assertRaisesFireExit(2):
-      fire.Fire(tc.InstanceVars, '--arg1=a1 --arg2=a2 - jog')
+      fire.Fire(tc.InstanceVars, command=['--arg1=a1', '--arg2=a2', '-', 'jog'])
 
 
 if __name__ == '__main__':

From 3e98ed525eced05a5e78418f92770da28c71cf8e Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 24 Jul 2017 08:54:10 -0700
Subject: [PATCH 013/324] Updates to setup.py in preparation for release 0.1.2.
 Copybara generated commit for Python Fire.

PiperOrigin-RevId: 162942210
Change-Id: I2b42bfc7ed249e2265fb3227230241b0bc19ab32
Reviewed-on: https://team-review.git.corp.google.com/85921
Reviewed-by: David Bieber <dbieber@google.com>
---
 setup.py | 14 +++++++++-----
 1 file changed, 9 insertions(+), 5 deletions(-)

diff --git a/setup.py b/setup.py
index 0036dc69..4914e242 100644
--- a/setup.py
+++ b/setup.py
@@ -39,18 +39,21 @@
     'python-Levenshtein',
 ]
 
+VERSION = '0.1.2'
+URL = 'https://github.com/google/python-fire'
+DOWNLOAD_URL = ('https://github.com/google/python-fire/archive/'
+                'v{version}.tar.gz').format(version=VERSION)
+
 setup(
     name='fire',
-    version='0.1.1',
-
+    version=VERSION,
     description=SHORT_DESCRIPTION,
     long_description=LONG_DESCRIPTION,
-
-    url='https://github.com/google/python-fire',
+    url=URL,
+    download_url=DOWNLOAD_URL,
 
     author='David Bieber',
     author_email='dbieber@google.com',
-
     license='Apache Software License',
 
     classifiers=[
@@ -81,4 +84,5 @@
 
     install_requires=DEPENDENCIES,
     tests_require=TEST_DEPENDENCIES,
+    data_files=[('', ['LICENSE'])],
 )

From 43c0b8cd3955110e1c79e55ad97ee64b30c79956 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 24 Jul 2017 10:48:22 -0700
Subject: [PATCH 014/324] Adds MANIFEST.in to Python Fire (properly including
 LICENSE file in sdist).

Copybara generated commit for Python Fire.

PiperOrigin-RevId: 162957262
Change-Id: I3f8ef57524d05554a85d0e5fd8c83aa14e0c6200
Reviewed-on: https://team-review.git.corp.google.com/86022
Reviewed-by: David Bieber <dbieber@google.com>
Reviewed-by: Peter Grabowski <petergrabowski@google.com>
---
 MANIFEST.in | 1 +
 setup.cfg   | 8 +++++++-
 setup.py    | 1 -
 3 files changed, 8 insertions(+), 2 deletions(-)
 create mode 100644 MANIFEST.in

diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 00000000..1aba38f6
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1 @@
+include LICENSE
diff --git a/setup.cfg b/setup.cfg
index f8baac0a..592eb0f9 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,11 @@
+[metadata]
+license-file = LICENSE
+
+[wheel]
+universal = 1
+
 [aliases]
-test=pytest
+test = pytest
 
 [tool:pytest]
 addopts = --ignore=fire/test_components_py3.py --ignore=fire/parser_fuzz_test.py
diff --git a/setup.py b/setup.py
index 4914e242..cc32d858 100644
--- a/setup.py
+++ b/setup.py
@@ -84,5 +84,4 @@
 
     install_requires=DEPENDENCIES,
     tests_require=TEST_DEPENDENCIES,
-    data_files=[('', ['LICENSE'])],
 )

From a944d2854f881469c0e099a1a029806bedd670dc Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 24 Jul 2017 11:52:19 -0700
Subject: [PATCH 015/324] Remove download_url from setup.py

---
 setup.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/setup.py b/setup.py
index cc32d858..efab8cbf 100644
--- a/setup.py
+++ b/setup.py
@@ -50,7 +50,6 @@
     description=SHORT_DESCRIPTION,
     long_description=LONG_DESCRIPTION,
     url=URL,
-    download_url=DOWNLOAD_URL,
 
     author='David Bieber',
     author_email='dbieber@google.com',

From 6c3331fb3e6197ac84320d7ddcf3c536e3b31c11 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 24 Jul 2017 14:14:29 -0700
Subject: [PATCH 016/324] Remove unused DOWNLOAD_URL from setup.py.

---
 setup.py | 2 --
 1 file changed, 2 deletions(-)

diff --git a/setup.py b/setup.py
index efab8cbf..d5bd6b08 100644
--- a/setup.py
+++ b/setup.py
@@ -41,8 +41,6 @@
 
 VERSION = '0.1.2'
 URL = 'https://github.com/google/python-fire'
-DOWNLOAD_URL = ('https://github.com/google/python-fire/archive/'
-                'v{version}.tar.gz').format(version=VERSION)
 
 setup(
     name='fire',

From 379dd0a497c9698c7cf0a6be98e58cc836b3a71c Mon Sep 17 00:00:00 2001
From: Aaron Giovannini <djtenzing@gmail.com>
Date: Mon, 21 Aug 2017 17:37:30 +0200
Subject: [PATCH 017/324] =?UTF-8?q?Disabled=20ASCII=20conversion=20in=20or?=
 =?UTF-8?q?der=20to=20improve=20console=20output=20encoding=E2=80=A6=20(#8?=
 =?UTF-8?q?7)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Disabled ASCII conversion from json.dumps in order to improve console output encoding/printing.
---
 fire/core.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/fire/core.py b/fire/core.py
index bc7bc230..9c723080 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -262,7 +262,8 @@ def _OneLineResult(result):
     return str(result).replace('\n', ' ')
 
   try:
-    return json.dumps(result)
+    # non-forced to ascii convert
+    return json.dumps(result, ensure_ascii=False)
   except (TypeError, ValueError):
     return str(result).replace('\n', ' ')
 

From 2c7b03954cbc42c83ab819ae530fd58d5feef145 Mon Sep 17 00:00:00 2001
From: Ajo John <ajojohn555@gmail.com>
Date: Tue, 29 Aug 2017 00:43:24 +0530
Subject: [PATCH 018/324] Document supported Python versions (#92)

* Document the supported python versions with a badge (#69)
---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 7cd5feb2..aedd1d00 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Python Fire
+# Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/Django.svg?style=plastic)](https://github.com/google/python-fire)
 _Python Fire is a library for automatically generating command line interfaces
 (CLIs) from absolutely any Python object._
 

From e76a10c0511ea4485b57543ccb966e7778213279 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Tue, 29 Aug 2017 10:16:20 -0700
Subject: [PATCH 019/324]   - Sync docs across readme and index.   - Clean up
 ascii comment

Copybara generated commit for Python Fire.

PiperOrigin-RevId: 166864447
Change-Id: Iafe0bb00bc44b49175c7f138debe89013f2787dd
Reviewed-on: https://team-review.git.corp.google.com/107481
Reviewed-by: David Bieber <dbieber@google.com>
---
 docs/index.md | 2 +-
 fire/core.py  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/index.md b/docs/index.md
index b1a00694..4b58e63c 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,4 +1,4 @@
-# Python Fire
+# Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/Django.svg?style=plastic)](https://github.com/google/python-fire)
 _Python Fire is a library for automatically generating command line interfaces
 (CLIs) from absolutely any Python object._
 
diff --git a/fire/core.py b/fire/core.py
index 9c723080..a7e3adaa 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -262,7 +262,7 @@ def _OneLineResult(result):
     return str(result).replace('\n', ' ')
 
   try:
-    # non-forced to ascii convert
+    # Don't force conversion to ascii.
     return json.dumps(result, ensure_ascii=False)
   except (TypeError, ValueError):
     return str(result).replace('\n', ' ')

From 7400ca845eb5fb41bc788e5b3052e860ab17e43b Mon Sep 17 00:00:00 2001
From: mbarkhau <mbarkhau@gmail.com>
Date: Wed, 11 Oct 2017 19:15:08 +0200
Subject: [PATCH 020/324] Fix for BinOp in args (#96)

* Always treat binary op args as string literals
* add test for binop
* add test for negative numbers
* raise ValueError for BinOp expressions
Fixes #95
---
 fire/parser.py      | 3 +++
 fire/parser_test.py | 6 +++++-
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/fire/parser.py b/fire/parser.py
index a0cef6d1..858201ff 100644
--- a/fire/parser.py
+++ b/fire/parser.py
@@ -94,6 +94,9 @@ def _LiteralEval(value):
     SyntaxError: If the value string has a syntax error.
   """
   root = ast.parse(value, mode='eval')
+  if isinstance(root.body, ast.BinOp):
+    raise ValueError(value)
+
   for node in ast.walk(root):
     for field, child in ast.iter_fields(node):
       if isinstance(child, list):
diff --git a/fire/parser_test.py b/fire/parser_test.py
index 2be6b83d..7fd9ca79 100644
--- a/fire/parser_test.py
+++ b/fire/parser_test.py
@@ -69,10 +69,12 @@ def testDefaultParseValueSpecialStrings(self):
 
   def testDefaultParseValueNumbers(self):
     self.assertEqual(parser.DefaultParseValue('23'), 23)
+    self.assertEqual(parser.DefaultParseValue('-23'), -23)
     self.assertEqual(parser.DefaultParseValue('23.0'), 23.0)
     self.assertIsInstance(parser.DefaultParseValue('23'), int)
     self.assertIsInstance(parser.DefaultParseValue('23.0'), float)
     self.assertEqual(parser.DefaultParseValue('23.5'), 23.5)
+    self.assertEqual(parser.DefaultParseValue('-23.5'), -23.5)
 
   def testDefaultParseValueStringNumbers(self):
     self.assertEqual(parser.DefaultParseValue("'23'"), '23')
@@ -127,13 +129,15 @@ def testDefaultParseValueBadLiteral(self):
     # If it can't be parsed, we treat it as a string. This behavior may change.
     self.assertEqual(
         parser.DefaultParseValue('[(A, 2, "3"), 5'), '[(A, 2, "3"), 5')
-
     self.assertEqual(parser.DefaultParseValue('x=10'), 'x=10')
 
   def testDefaultParseValueSyntaxError(self):
     # If it can't be parsed, we treat it as a string.
     self.assertEqual(parser.DefaultParseValue('"'), '"')
 
+  def testDefaultParseValueIgnoreBinOp(self):
+    self.assertEqual(parser.DefaultParseValue('2017-10-10'), '2017-10-10')
+    self.assertEqual(parser.DefaultParseValue('1+1'), '1+1')
 
 if __name__ == '__main__':
   testutils.main()

From aff56bb0dcc92acedd3ee5da0182bfe2fbb20138 Mon Sep 17 00:00:00 2001
From: David Bieber <david810@gmail.com>
Date: Wed, 11 Oct 2017 11:07:03 -0700
Subject: [PATCH 021/324] Use proper shields.io image.

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index aedd1d00..129f765e 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/Django.svg?style=plastic)](https://github.com/google/python-fire)
+# Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/fire.svg?style=plastic)](https://github.com/google/python-fire)
 _Python Fire is a library for automatically generating command line interfaces
 (CLIs) from absolutely any Python object._
 

From fa575938964a65e29d89f7f13244587602cf957d Mon Sep 17 00:00:00 2001
From: greenmoon55 <greenmoon55@gmail.com>
Date: Mon, 4 Dec 2017 17:01:10 -0500
Subject: [PATCH 022/324] Do not treat arguments that start with '--' as
 string. (#99)

* Do not treat arguments that start with '--' as strings / values.
---
 fire/core.py      | 110 +++++++++++++++++++++++++---------------------
 fire/fire_test.py |  13 ++++--
 2 files changed, 69 insertions(+), 54 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index a7e3adaa..5dc40d0a 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -565,7 +565,8 @@ def _MakeParseFn(fn):
 
   def _ParseFn(args):
     """Parses the list of `args` into (varargs, kwargs), remaining_args."""
-    kwargs, remaining_args = _ParseKeywordArgs(args, all_args, fn_spec.varkw)
+    kwargs, remaining_kwargs, remaining_args = \
+        _ParseKeywordArgs(args, all_args, fn_spec.varkw)
 
     # Note: _ParseArgs modifies kwargs.
     parsed_args, kwargs, remaining_args, capacity = _ParseArgs(
@@ -594,6 +595,7 @@ def _ParseFn(args):
       varargs[index] = _ParseValue(value, None, None, metadata)
 
     varargs = parsed_args + varargs
+    remaining_args += remaining_kwargs
 
     consumed_args = args[:len(args) - len(remaining_args)]
     return (varargs, kwargs), consumed_args, remaining_args, capacity
@@ -681,64 +683,72 @@ def _ParseKeywordArgs(args, fn_args, fn_keywords):
     fn_keywords: The argument name for **kwargs, or None if **kwargs not used
   Returns:
     kwargs: A dictionary mapping keywords to values.
+    remaining_kwargs: A list of the unused kwargs from the original args.
     remaining_args: A list of the unused arguments from the original args.
   """
   kwargs = {}
-  if args:
-    remaining_args = []
-    skip_argument = False
-
-    for index, argument in enumerate(args):
-      if skip_argument:
-        skip_argument = False
-        continue
-
-      arg_consumed = False
-      if argument.startswith('--'):
-        # This is a named argument; get its value from this arg or the next.
-        got_argument = False
-
-        keyword = argument[2:]
-        contains_equals = '=' in keyword
-        is_bool_syntax = (
-            not contains_equals and
-            (index + 1 == len(args) or args[index + 1].startswith('--')))
-        if contains_equals:
-          keyword, value = keyword.split('=', 1)
-          got_argument = True
-        elif is_bool_syntax:
-          # Since there's no next arg or the next arg is a Flag, we consider
-          # this flag to be a boolean.
-          got_argument = True
-          if keyword in fn_args:
-            value = 'True'
-          elif keyword.startswith('no'):
-            keyword = keyword[2:]
-            value = 'False'
-          else:
-            value = 'True'
+  remaining_kwargs = []
+  remaining_args = []
+
+  if not args:
+    return kwargs, remaining_kwargs, remaining_args
+
+  skip_argument = False
+
+  for index, argument in enumerate(args):
+    if skip_argument:
+      skip_argument = False
+      continue
+
+    arg_consumed = False
+    if argument.startswith('--'):
+      # This is a named argument; get its value from this arg or the next.
+      got_argument = False
+
+      keyword = argument[2:]
+      contains_equals = '=' in keyword
+      is_bool_syntax = (
+          not contains_equals and
+          (index + 1 == len(args) or args[index + 1].startswith('--')))
+      if contains_equals:
+        keyword, value = keyword.split('=', 1)
+        got_argument = True
+      elif is_bool_syntax:
+        # Since there's no next arg or the next arg is a Flag, we consider
+        # this flag to be a boolean.
+        got_argument = True
+        if keyword in fn_args:
+          value = 'True'
+        elif keyword.startswith('no'):
+          keyword = keyword[2:]
+          value = 'False'
         else:
-          if index + 1 < len(args):
-            value = args[index + 1]
-            got_argument = True
+          value = 'True'
+      else:
+        if index + 1 < len(args):
+          value = args[index + 1]
+          got_argument = True
 
-        keyword = keyword.replace('-', '_')
+      keyword = keyword.replace('-', '_')
 
-        # In order for us to consume the argument as a keyword arg, we either:
-        # Need to be explicitly expecting the keyword, or we need to be
-        # accepting **kwargs.
-        if got_argument and (keyword in fn_args or fn_keywords):
+      # In order for us to consume the argument as a keyword arg, we either:
+      # Need to be explicitly expecting the keyword, or we need to be
+      # accepting **kwargs.
+      if got_argument:
+        skip_argument = not contains_equals and not is_bool_syntax
+        arg_consumed = True
+        if keyword in fn_args or fn_keywords:
           kwargs[keyword] = value
-          skip_argument = not contains_equals and not is_bool_syntax
-          arg_consumed = True
+        else:
+          remaining_kwargs.append(argument)
+          if skip_argument:
+            remaining_kwargs.append(args[index + 1])
 
-      if not arg_consumed:
-        # The argument was not consumed, so it is still a remaining argument.
-        remaining_args.append(argument)
-  else:
-    remaining_args = args
+    if not arg_consumed:
+      # The argument was not consumed, so it is still a remaining argument.
+      remaining_args.append(argument)
 
-  return kwargs, remaining_args
+  return kwargs, remaining_kwargs, remaining_args
 
 
 def _ParseValue(value, index, arg, metadata):
diff --git a/fire/fire_test.py b/fire/fire_test.py
index 89ccb38a..8f1f53ec 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -368,10 +368,15 @@ def testBoolParsingLessExpectedCases(self):
         fire.Fire(tc.MixedDefaults,
                   command=['identity', 'True', '10']), (True, 10))
 
-    # Note: Does not return ('--test', '0').
-    self.assertEqual(fire.Fire(tc.MixedDefaults,
-                               command=['identity', '--alpha', '--test']),
-                     (True, '--test'))
+    # Note: Does not return (True, '--test') or ('--test', 0).
+    with self.assertRaisesFireExit(2):
+      fire.Fire(tc.MixedDefaults, command=['identity', '--alpha', '--test'])
+
+    self.assertEqual(
+        fire.Fire(
+            tc.MixedDefaults,
+            command=['identity', '--alpha', 'True', '"--test"']),
+        (True, '--test'))
     # To get ('--test', '0'), use one of the following:
     self.assertEqual(fire.Fire(tc.MixedDefaults,
                                command=['identity', '--alpha=--test']),

From 72604f40314008e562ba47936dcc183b51166b72 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 4 Dec 2017 14:30:37 -0800
Subject: [PATCH 023/324] Lint for continued line, fire.svg badge, and spelling
 fix.

PiperOrigin-RevId: 177869077
Change-Id: I2feed4959ce0842ef6152b7bfcf3539e9141b176
Reviewed-on: https://team-review.git.corp.google.com/172708
Reviewed-by: David Bieber <dbieber@google.com>
---
 docs/index.md | 2 +-
 fire/core.py  | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/docs/index.md b/docs/index.md
index 4b58e63c..e8bfb135 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,4 +1,4 @@
-# Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/Django.svg?style=plastic)](https://github.com/google/python-fire)
+# Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/fire.svg?style=plastic)](https://github.com/google/python-fire)
 _Python Fire is a library for automatically generating command line interfaces
 (CLIs) from absolutely any Python object._
 
diff --git a/fire/core.py b/fire/core.py
index 5dc40d0a..86c7d4ac 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -30,7 +30,7 @@ def main(argv):
 access a member of current component, call the current component (if it's a
 function), or instantiate the current component (if it's a class). The target
 component begins as Component, and at each operation the component becomes the
-result of the preceeding operation.
+result of the preceding operation.
 
 For example "command fn arg1 arg2" might access the "fn" property of the initial
 target component, and then call that function with arguments 'arg1' and 'arg2'.
@@ -565,8 +565,8 @@ def _MakeParseFn(fn):
 
   def _ParseFn(args):
     """Parses the list of `args` into (varargs, kwargs), remaining_args."""
-    kwargs, remaining_kwargs, remaining_args = \
-        _ParseKeywordArgs(args, all_args, fn_spec.varkw)
+    kwargs, remaining_kwargs, remaining_args = _ParseKeywordArgs(
+        args, all_args, fn_spec.varkw)
 
     # Note: _ParseArgs modifies kwargs.
     parsed_args, kwargs, remaining_args, capacity = _ParseArgs(

From 7f166e90890f18c77aee2ea5f2c24ba97b818f8e Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 8 Jan 2018 11:21:03 -0800
Subject: [PATCH 024/324] Consistent return statements,
 keyword-arg-before-vararg lint disables, docstrings, and import order
 pylintrc.

PiperOrigin-RevId: 181191467
Change-Id: I1e7670270b7ff94abaeec1294bdb57fdfcaf8e7e
Reviewed-on: https://team-review.git.corp.google.com/192187
Reviewed-by: David Bieber <dbieber@google.com>
---
 fire/decorators_test.py | 3 ++-
 fire/helputils.py       | 2 +-
 fire/test_components.py | 7 ++++---
 fire/trace.py           | 1 +
 pylintrc                | 2 +-
 5 files changed, 9 insertions(+), 6 deletions(-)

diff --git a/fire/decorators_test.py b/fire/decorators_test.py
index 91316fb0..372cee60 100644
--- a/fire/decorators_test.py
+++ b/fire/decorators_test.py
@@ -24,6 +24,7 @@
 
 
 class NoDefaults(object):
+  """A class for testing decorated functions without default values."""
 
   @decorators.SetParseFns(count=int)
   def double(self, count):
@@ -85,7 +86,7 @@ def example6(self, **kwargs):
 class WithVarArgs(object):
 
   @decorators.SetParseFn(str)
-  def example7(self, arg1, arg2=None, *varargs, **kwargs):
+  def example7(self, arg1, arg2=None, *varargs, **kwargs):  # pylint: disable=keyword-arg-before-vararg
     return arg1, arg2, varargs, kwargs
 
 
diff --git a/fire/helputils.py b/fire/helputils.py
index 5c23de3a..eecd25ca 100644
--- a/fire/helputils.py
+++ b/fire/helputils.py
@@ -54,7 +54,7 @@ def _DisplayValue(info, field, padding):
   value = info.get(field)
 
   if value is None:
-    return
+    return None
 
   skip_doc_types = ('dict', 'list', 'unicode', 'int', 'float', 'bool')
 
diff --git a/fire/test_components.py b/fire/test_components.py
index a2696d6a..c29b7ae4 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -24,7 +24,7 @@
   from fire import test_components_py3 as py3  # pylint: disable=unused-import,no-name-in-module,g-import-not-at-top
 
 
-def identity(arg1, arg2, arg3=10, arg4=20, *arg5, **arg6):
+def identity(arg1, arg2, arg3=10, arg4=20, *arg5, **arg6):  # pylint: disable=keyword-arg-before-vararg
   return arg1, arg2, arg3, arg4, arg5, arg6
 
 identity.__annotations__ = {'arg2': int, 'arg4': int}
@@ -96,6 +96,7 @@ def triple(self, count=0):
 
 
 class TypedProperties(object):
+  """Test class for testing Python Fire with properties of various types."""
 
   def __init__(self):
     self.alpha = True
@@ -114,7 +115,7 @@ def __init__(self):
 
 
 class VarArgs(object):
-  """Test class G for testing Python Fire."""
+  """Test class for testing Python Fire with a property with varargs."""
 
   def cumsums(self, *items):
     total = None
@@ -127,7 +128,7 @@ def cumsums(self, *items):
       sums.append(total)
     return sums
 
-  def varchars(self, alpha=0, beta=0, *chars):
+  def varchars(self, alpha=0, beta=0, *chars):  # pylint: disable=keyword-arg-before-vararg
     return alpha, beta, ''.join(chars)
 
 
diff --git a/fire/trace.py b/fire/trace.py
index a901fbd1..b1703250 100644
--- a/fire/trace.py
+++ b/fire/trace.py
@@ -76,6 +76,7 @@ def GetLastHealthyElement(self):
     for element in reversed(self.elements):
       if not element.HasError():
         return element
+    return None
 
   def HasError(self):
     """Returns whether the Fire execution encountered a Fire usage error."""
diff --git a/pylintrc b/pylintrc
index e42aeea3..e7425503 100644
--- a/pylintrc
+++ b/pylintrc
@@ -32,7 +32,7 @@ enable=indexing-exception,old-raise-syntax
 # Disable the message, report, category or checker with the given id(s). You
 # can either give multiple identifier separated by comma (,) or put this option
 # multiple time.
-disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored
+disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order
 
 
 [REPORTS]

From 9bff9d01ce16589201f57ffef27ea84744951c11 Mon Sep 17 00:00:00 2001
From: cclauss <cclauss@bluewin.ch>
Date: Thu, 11 Jan 2018 01:20:54 +0100
Subject: [PATCH 025/324] unicode() --> six.u() for Python 3 compatibility
 (#111)

__unicode()__ was removed from Python 3 because all strs are unicode.

Executing these lines in Python 3 would raise a NameError.

* https://pythonhosted.org/six/#six.u
---
 fire/parser_fuzz_test.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/fire/parser_fuzz_test.py b/fire/parser_fuzz_test.py
index bb8543e8..0c06a351 100644
--- a/fire/parser_fuzz_test.py
+++ b/fire/parser_fuzz_test.py
@@ -67,8 +67,8 @@ def testDefaultParseValueFuzz(self, value):
       raise
 
     try:
-      uvalue = unicode(value)
-      uresult = unicode(result)
+      uvalue = six.u(value)
+      uresult = six.u(result)
     except UnicodeDecodeError:
       # This is not what we're testing.
       return

From 6912ccd56f50e0f4bb30a0725d95858ef29f3bde Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 23 Feb 2018 10:08:24 -0800
Subject: [PATCH 026/324] - Prepare Fire for v0.1.3 release. by dbieber
 <dbieber@google.com>

PiperOrigin-RevId: 186785089
Change-Id: I9c0d87c78fc37c73768c72f9630a5f6d40023cf6
Reviewed-on: https://team-review.git.corp.google.com/221474
Reviewed-by: David Bieber <dbieber@google.com>
---
 fire/parser_fuzz_test.py | 3 ++-
 setup.py                 | 2 +-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/fire/parser_fuzz_test.py b/fire/parser_fuzz_test.py
index 0c06a351..e611b9ad 100644
--- a/fire/parser_fuzz_test.py
+++ b/fire/parser_fuzz_test.py
@@ -30,7 +30,8 @@
 
 class ParserFuzzTest(testutils.BaseTestCase):
 
-  @given(st.text(min_size=1), settings=settings.Settings(max_examples=10000))
+  @settings(max_examples=10000)
+  @given(st.text(min_size=1))
   @example('True')
   @example(r'"test\t\t\a\\a"')
   @example(r' "test\t\t\a\\a"   ')
diff --git a/setup.py b/setup.py
index d5bd6b08..99d44910 100644
--- a/setup.py
+++ b/setup.py
@@ -39,7 +39,7 @@
     'python-Levenshtein',
 ]
 
-VERSION = '0.1.2'
+VERSION = '0.1.3'
 URL = 'https://github.com/google/python-fire'
 
 setup(

From 83a8036ea9a3a4594ba89d0f48e48a2aed597a7e Mon Sep 17 00:00:00 2001
From: Isaac Ellmen <isaacellmen@gmail.com>
Date: Mon, 21 May 2018 16:41:04 -0400
Subject: [PATCH 027/324] Fish completion support (#122)

- Fish completion support (--completion fish)
---
 README.md               |  2 +-
 docs/api.md             |  2 +-
 docs/guide.md           |  2 +-
 docs/index.md           |  2 +-
 docs/using-cli.md       |  3 ++
 fire/completion.py      | 63 +++++++++++++++++++++++++++++++++++++++--
 fire/completion_test.py | 44 +++++++++++++++++++++++++---
 fire/core.py            | 13 +++++----
 fire/parser.py          |  2 +-
 9 files changed, 115 insertions(+), 18 deletions(-)

diff --git a/README.md b/README.md
index 129f765e..9c407ecc 100644
--- a/README.md
+++ b/README.md
@@ -82,7 +82,7 @@ Please see [The Python Fire Guide](docs/guide.md).
 | [Help](docs/using-cli.md#help-flag) | `command -- --help` |
 | [REPL](docs/using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
 | [Separator](docs/using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
-| [Completion](docs/using-cli.md#completion-flag) | `command -- --completion` | Generate a completion script for the CLI.
+| [Completion](docs/using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI.
 | [Trace](docs/using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
 | [Verbose](docs/using-cli.md#verbose-flag) | `command -- --verbose` |
 
diff --git a/docs/api.md b/docs/api.md
index f85d2065..c3bb2ef6 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -13,7 +13,7 @@
 | [Help](using-cli.md#help-flag) | `command -- --help` |
 | [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
 | [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
-| [Completion](using-cli.md#completion-flag) | `command -- --completion` | Generate a completion script for the CLI.
+| [Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI.
 | [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
 | [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` |
 
diff --git a/docs/guide.md b/docs/guide.md
index 28d79a6c..81e73539 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -702,7 +702,7 @@ The complete set of flags available is shown below, in the reference section.
 | [Help](using-cli.md#help-flag) | `command -- --help` | Show help and usage information for the command.
 | [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enter interactive mode.
 | [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
-| [Completion](using-cli.md#completion-flag) | `command -- --completion` | Generate a completion script for the CLI.
+| [Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI.
 | [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
 | [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` | Include private members in the output.
 
diff --git a/docs/index.md b/docs/index.md
index e8bfb135..9b7d2e3a 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -82,7 +82,7 @@ Please see [The Python Fire Guide](guide.md).
 | [Help](using-cli.md#help-flag) | `command -- --help` |
 | [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
 | [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
-| [Completion](using-cli.md#completion-flag) | `command -- --completion` | Generate a completion script for the CLI.
+| [Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI.
 | [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
 | [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` |
 
diff --git a/docs/using-cli.md b/docs/using-cli.md
index 1c843f42..236a8228 100644
--- a/docs/using-cli.md
+++ b/docs/using-cli.md
@@ -140,6 +140,9 @@ Call `widget -- --completion` to generate a completion script for the Fire CLI
 run `widget -- --completion > ~/.widget-completion`. You should then source this
 file; to get permanent completion, source this file from your .bashrc file.
 
+Call `widget -- --completion fish` to generate a completion script for the Fish
+shell. Source this file from your fish.config.
+
 If the commands available in the Fire CLI change, you'll have to regenerate the
 completion script and source it again.
 
diff --git a/fire/completion.py b/fire/completion.py
index 46010c07..acfa2fb6 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -26,11 +26,13 @@
 import six
 
 
-def Script(name, component, default_options=None):
-  return _Script(name, _Commands(component), default_options)
+def Script(name, component, default_options=None, shell='bash'):
+  if shell == 'fish':
+    return _FishScript(name, _Commands(component), default_options)
+  return _BashScript(name, _Commands(component), default_options)
 
 
-def _Script(name, commands, default_options=None):
+def _BashScript(name, commands, default_options=None):
   """Returns a Bash script registering a completion function for the commands.
 
   Args:
@@ -97,6 +99,61 @@ def _Script(name, commands, default_options=None):
   )
 
 
+def _FishScript(name, commands, default_options=None):
+  """Returns a Fish script registering a completion function for the commands.
+
+  Args:
+    name: The first token in the commands, also the name of the command.
+    commands: A list of all possible commands that tab completion can complete
+        to. Each command is a list or tuple of the string tokens that make up
+        that command.
+    default_options: A dict of options that can be used with any command. Use
+        this if there are flags that can always be appended to a command.
+  Returns:
+    A string which is the Fish script. Source the fish script to enable tab
+    completion in Fish.
+  """
+  default_options = default_options or set()
+  options_map = defaultdict(lambda: copy(default_options))
+  for command in commands:
+    start = (name + ' ' + ' '.join(command[:-1])).strip()
+    completion = _FormatForCommand(command[-1])
+    options_map[start].add(completion)
+    options_map[start.replace('_', '-')].add(completion)
+  fish_source = """function __fish_using_command
+    set cmd (commandline -opc)
+    if [ (count $cmd) -eq (count $argv) ]
+        for i in (seq (count $argv))
+            if [ $cmd[$i] != $argv[$i] ]
+                return 1
+            end
+        end
+        return 0
+    end
+    return 1
+end
+"""
+  subcommand_template = "complete -c {name} -n " \
+          "'__fish_using_command {start}' -f -a {subcommand}\n"
+  flag_template = "complete -c {name} -n " \
+          "'__fish_using_command {start}' -l {option}\n"
+  for start in options_map:
+    for option in sorted(options_map[start]):
+      if option.startswith('--'):
+        fish_source += flag_template.format(
+            name=name,
+            start=start,
+            option=option[2:]
+        )
+      else:
+        fish_source += subcommand_template.format(
+            name=name,
+            start=start,
+            subcommand=option
+        )
+  return fish_source
+
+
 def _IncludeMember(name, verbose):
   if verbose:
     return True
diff --git a/fire/completion_test.py b/fire/completion_test.py
index 7c2e139b..5da360a9 100644
--- a/fire/completion_test.py
+++ b/fire/completion_test.py
@@ -25,19 +25,32 @@
 
 class TabCompletionTest(testutils.BaseTestCase):
 
-  def testCompletionScript(self):
-    # A sanity check test to make sure the completion script satisfies some
-    # basic assumptions.
+  def testCompletionBashScript(self):
+    # A sanity check test to make sure the bash completion script satisfies
+    # some basic assumptions.
     commands = [
         ['run'],
         ['halt'],
         ['halt', '--now'],
     ]
-    script = completion._Script(name='command', commands=commands)  # pylint: disable=protected-access
+    script = completion._BashScript(name='command', commands=commands)  # pylint: disable=protected-access
     self.assertIn('command', script)
     self.assertIn('halt', script)
     self.assertIn('"$start" == "command"', script)
 
+  def testCompletionFishScript(self):
+    # A sanity check test to make sure the fish completion script satisfies
+    # some basic assumptions.
+    commands = [
+        ['run'],
+        ['halt'],
+        ['halt', '--now'],
+    ]
+    script = completion._FishScript(name='command', commands=commands)  # pylint: disable=protected-access
+    self.assertIn('command', script)
+    self.assertIn('halt', script)
+    self.assertIn('-l now', script)
+
   def testFnCompletions(self):
     def example(one, two, three):
       return one, two, three
@@ -113,6 +126,29 @@ def testClassScript(self):
     self.assertIn('--alpha', script)
     self.assertIn('--beta', script)
 
+  def testDeepDictFishScript(self):
+    deepdict = {'level1': {'level2': {'level3': {'level4': {}}}}}
+    script = completion.Script('deepdict', deepdict, shell='fish')
+    self.assertIn('level1', script)
+    self.assertIn('level2', script)
+    self.assertIn('level3', script)
+    self.assertNotIn('level4', script)  # The default depth is 3.
+
+  def testFnFishScript(self):
+    script = completion.Script('identity', tc.identity, shell='fish')
+    self.assertIn('arg1', script)
+    self.assertIn('arg2', script)
+    self.assertIn('arg3', script)
+    self.assertIn('arg4', script)
+
+  def testClassFishScript(self):
+    script = completion.Script('', tc.MixedDefaults, shell='fish')
+    self.assertIn('ten', script)
+    self.assertIn('sum', script)
+    self.assertIn('identity', script)
+    self.assertIn('alpha', script)
+    self.assertIn('beta', script)
+
   def testNonStringDictCompletions(self):
     completions = completion.Completions({
         10: 'green',
diff --git a/fire/core.py b/fire/core.py
index 86c7d4ac..e3ce2f7f 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -44,6 +44,7 @@ def main(argv):
   -h --help: Provide help and usage information for the command.
   -i --interactive: Drop into a Python REPL after running the command.
   --completion: Write the Bash completion script for the tool to stdout.
+  --completion fish: Write the Fish completion script for the tool to stdout.
   --separator SEPARATOR: Use SEPARATOR in place of the default separator, '-'.
   --trace: Get the Fire Trace for the command.
 """
@@ -165,9 +166,9 @@ def Fire(component=None, command=None, name=None):
     return result
 
 
-def CompletionScript(name, component):
-  """Returns the text of the Bash completion script for a Fire CLI."""
-  return completion.Script(name, component)
+def CompletionScript(name, component, shell):
+  """Returns the text of the completion script for a Fire CLI."""
+  return completion.Script(name, component, shell=shell)
 
 
 class FireError(Exception):
@@ -338,7 +339,7 @@ def _Fire(component, args, context, name=None):
     initial_args = remaining_args
 
     if not remaining_args and (show_help or interactive or show_trace
-                               or show_completion):
+                               or show_completion is not None):
       # Don't initialize the final class or call the final function unless
       # there's a separator after it, and instead process the current component.
       break
@@ -469,10 +470,10 @@ def _Fire(component, args, context, name=None):
         initial_args)
     return component_trace
 
-  if show_completion:
+  if show_completion is not None:
     if name is None:
       raise ValueError('Cannot make completion script without command name')
-    script = CompletionScript(name, initial_component)
+    script = CompletionScript(name, initial_component, shell=show_completion)
     component_trace.AddCompletionScript(script)
 
   if interactive:
diff --git a/fire/parser.py b/fire/parser.py
index 858201ff..e8a12dae 100644
--- a/fire/parser.py
+++ b/fire/parser.py
@@ -27,7 +27,7 @@ def CreateParser():
   parser.add_argument('--verbose', '-v', action='store_true')
   parser.add_argument('--interactive', '-i', action='store_true')
   parser.add_argument('--separator', default='-')
-  parser.add_argument('--completion', action='store_true')
+  parser.add_argument('--completion', nargs='?', const='bash', type=str)
   parser.add_argument('--help', '-h', action='store_true')
   parser.add_argument('--trace', '-t', action='store_true')
   # TODO: Consider allowing name to be passed as an argument.

From 021d627c92adead1aa8cc45a1308a6e4980dbcca Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Thu, 21 Jun 2018 16:02:57 -0700
Subject: [PATCH 028/324] Copybara generated commit for Python Fire.

  - Formatting for completion.
  - Preserving order of keys when printing result which is an OrderedDict.
  - Allow use of --help without --.

PiperOrigin-RevId: 201601732
Change-Id: Icba7e663634f5153283095d8b177d834af480b15
Reviewed-on: https://team-review.git.corp.google.com/278010
Reviewed-by: Joe Chen <zuhaochen@google.com>
---
 fire/completion.py       |  8 ++---
 fire/core.py             | 73 +++++++++++++++++++++++++++++++---------
 fire/core_test.py        | 47 ++++++++++++++++++++++----
 fire/parser_fuzz_test.py |  4 +--
 fire/test_components.py  | 33 ++++++++++++++++++
 5 files changed, 137 insertions(+), 28 deletions(-)

diff --git a/fire/completion.py b/fire/completion.py
index acfa2fb6..ff18e3e8 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -133,10 +133,10 @@ def _FishScript(name, commands, default_options=None):
     return 1
 end
 """
-  subcommand_template = "complete -c {name} -n " \
-          "'__fish_using_command {start}' -f -a {subcommand}\n"
-  flag_template = "complete -c {name} -n " \
-          "'__fish_using_command {start}' -l {option}\n"
+  subcommand_template = ("complete -c {name} -n '__fish_using_command {start}' "
+                         "-f -a {subcommand}\n")
+  flag_template = ("complete -c {name} -n "
+                   "'__fish_using_command {start}' -l {option}\n")
   for start in options_map:
     for option in sorted(options_map[start]):
       if option.startswith('--'):
diff --git a/fire/core.py b/fire/core.py
index e3ce2f7f..4083caa8 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -200,6 +200,41 @@ def __init__(self, code, component_trace):
     self.trace = component_trace
 
 
+def _IsHelpShortcut(component_trace, remaining_args):
+  """Determines if the user is trying to access help without '--' separator.
+
+  For example, mycmd.py --help instead of mycmd.py -- --help.
+
+  Args:
+    component_trace: (FireTrace) The trace for the Fire command.
+    remaining_args: List of remaining args that haven't been consumed yet.
+  Returns:
+    True if help is requested, False otherwise.
+  """
+  show_help = False
+  if remaining_args:
+    target = remaining_args[0]
+    if target == '-h':
+      show_help = True
+    elif target == '--help':
+      # Check if --help would be consumed as a keyword argument, or is a member.
+      component = component_trace.GetResult()
+      if inspect.isclass(component) or inspect.isroutine(component):
+        fn_spec = inspectutils.GetFullArgSpec(component)
+        _, remaining_kwargs, _ = _ParseKeywordArgs(remaining_args, fn_spec)
+        show_help = target in remaining_kwargs
+      else:
+        members = dict(inspect.getmembers(component))
+        show_help = target not in members
+
+  if show_help:
+    component_trace.show_help = True
+    command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
+    print('INFO: Showing help with the command {cmd}.\n'.format(
+        cmd=pipes.quote(command)), file=sys.stderr)
+  return show_help
+
+
 def _PrintResult(component_trace, verbose=False):
   """Prints the result of the Fire call to stdout in a human readable way."""
   # TODO: Design human readable deserializable serialization method
@@ -231,20 +266,25 @@ def _DictAsString(result, verbose=False):
   Returns:
     A string representing the dict
   """
-  result = {key: value for key, value in result.items()
-            if _ComponentVisible(key, verbose)}
 
-  if not result:
+  # We need to do 2 iterations over the items in the result dict
+  # 1) Getting visible items and the longest key for output formatting
+  # 2) Actually construct the output lines
+  result_visible = {key: value for key, value in result.items()
+                    if _ComponentVisible(key, verbose)}
+
+  if not result_visible:
     return '{}'
 
-  longest_key = max(len(str(key)) for key in result.keys())
+  longest_key = max(len(str(key)) for key in result_visible.keys())
   format_string = '{{key:{padding}s}} {{value}}'.format(padding=longest_key + 1)
 
   lines = []
   for key, value in result.items():
-    line = format_string.format(key=str(key) + ':',
-                                value=_OneLineResult(value))
-    lines.append(line)
+    if _ComponentVisible(key, verbose):
+      line = format_string.format(key=str(key) + ':',
+                                  value=_OneLineResult(value))
+      lines.append(line)
   return '\n'.join(lines)
 
 
@@ -344,6 +384,10 @@ def _Fire(component, args, context, name=None):
       # there's a separator after it, and instead process the current component.
       break
 
+    if _IsHelpShortcut(component_trace, remaining_args):
+      remaining_args = []
+      break
+
     saved_args = []
     used_separator = False
     if separator in remaining_args:
@@ -556,7 +600,6 @@ def _MakeParseFn(fn):
     the leftover args from the arguments to the parse function.
   """
   fn_spec = inspectutils.GetFullArgSpec(fn)
-  all_args = fn_spec.args + fn_spec.kwonlyargs
   metadata = decorators.GetMetadata(fn)
 
   # Note: num_required_args is the number of positional arguments without
@@ -566,8 +609,7 @@ def _MakeParseFn(fn):
 
   def _ParseFn(args):
     """Parses the list of `args` into (varargs, kwargs), remaining_args."""
-    kwargs, remaining_kwargs, remaining_args = _ParseKeywordArgs(
-        args, all_args, fn_spec.varkw)
+    kwargs, remaining_kwargs, remaining_args = _ParseKeywordArgs(args, fn_spec)
 
     # Note: _ParseArgs modifies kwargs.
     parsed_args, kwargs, remaining_args, capacity = _ParseArgs(
@@ -663,7 +705,7 @@ def _ParseArgs(fn_args, fn_defaults, num_required_args, kwargs,
   return parsed_args, kwargs, remaining_args, capacity
 
 
-def _ParseKeywordArgs(args, fn_args, fn_keywords):
+def _ParseKeywordArgs(args, fn_spec):
   """Parses the supplied arguments for keyword arguments.
 
   Given a list of arguments, finds occurences of --name value, and uses 'name'
@@ -677,11 +719,8 @@ def _ParseKeywordArgs(args, fn_args, fn_keywords):
   _ParseArgs, which converts them to the appropriate type.
 
   Args:
-    args: A list of arguments
-    fn_args: A list of argument names that the target function accepts,
-        including positional and named arguments, but not the varargs or kwargs
-        names.
-    fn_keywords: The argument name for **kwargs, or None if **kwargs not used
+    args: A list of arguments.
+    fn_spec: The inspectutils.FullArgSpec describing the given callable.
   Returns:
     kwargs: A dictionary mapping keywords to values.
     remaining_kwargs: A list of the unused kwargs from the original args.
@@ -690,6 +729,8 @@ def _ParseKeywordArgs(args, fn_args, fn_keywords):
   kwargs = {}
   remaining_kwargs = []
   remaining_args = []
+  fn_keywords = fn_spec.varkw
+  fn_args = fn_spec.args + fn_spec.kwonlyargs
 
   if not args:
     return kwargs, remaining_kwargs, remaining_args
diff --git a/fire/core_test.py b/fire/core_test.py
index 235cb95d..0af8f216 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -72,14 +72,43 @@ def testInteractiveModeVariablesWithName(self, mock_embed):
     self.assertEqual(variables['D'], tc.WithDefaults)
     self.assertIsInstance(variables['trace'], trace.FireTrace)
 
-  def testImproperUseOfHelp(self):
-    # This should produce a warning explaining the proper use of help.
-    with self.assertRaisesFireExit(2, 'The proper way to show help.*Usage:'):
-      core.Fire(tc.TypedProperties, command=['alpha', '--help'])
-
-  def testProperUseOfHelp(self):
+  # TODO: Use parameterized tests to break up repetitive tests.
+  def testHelpWithClass(self):
+    with self.assertRaisesFireExit(0, 'Usage:.*ARG1'):
+      core.Fire(tc.InstanceVars, command=['--', '--help'])
+    with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*ARG1'):
+      core.Fire(tc.InstanceVars, command=['--help'])
+    with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*ARG1'):
+      core.Fire(tc.InstanceVars, command=['-h'])
+
+  def testHelpWithMember(self):
     with self.assertRaisesFireExit(0, 'Usage:.*upper'):
       core.Fire(tc.TypedProperties, command=['gamma', '--', '--help'])
+    with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*upper'):
+      core.Fire(tc.TypedProperties, command=['gamma', '--help'])
+    with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*upper'):
+      core.Fire(tc.TypedProperties, command=['gamma', '-h'])
+    with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*delta'):
+      core.Fire(tc.TypedProperties, command=['delta', '--help'])
+    with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*echo'):
+      core.Fire(tc.TypedProperties, command=['echo', '--help'])
+
+  def testHelpOnErrorInConstructor(self):
+    with self.assertRaisesFireExit(0, 'Usage:.*[VALUE]'):
+      core.Fire(tc.ErrorInConstructor, command=['--', '--help'])
+    with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*[VALUE]'):
+      core.Fire(tc.ErrorInConstructor, command=['--help'])
+
+  def testHelpWithNamespaceCollision(self):
+    # Tests cases when calling the help shortcut should not show help.
+    with self.assertOutputMatches(stdout='Docstring.*', stderr=None):
+      core.Fire(tc.WithHelpArg, command=['--help', 'False'])
+    with self.assertOutputMatches(stdout='help in a dict', stderr=None):
+      core.Fire(tc.WithHelpArg, command=['dictionary', '__help'])
+    with self.assertOutputMatches(stdout='{}', stderr=None):
+      core.Fire(tc.WithHelpArg, command=['dictionary', '--help'])
+    with self.assertOutputMatches(stdout='False', stderr=None):
+      core.Fire(tc.function_with_help, command=['False'])
 
   def testInvalidParameterRaisesFireExit(self):
     with self.assertRaisesFireExit(2, 'runmisspelled'):
@@ -105,6 +134,12 @@ def testPrintEmptyDict(self):
     with self.assertOutputMatches(stdout='{}', stderr=None):
       core.Fire(tc.EmptyDictOutput, command=['nothing_printable'])
 
+  def testPrintDict(self):
+    with self.assertOutputMatches(stdout=r'A:\s+A\s+2:\s+2\s+', stderr=None):
+      core.Fire(tc.OrderedDictionary, command=['non_empty'])
+    with self.assertOutputMatches(stdout='{}'):
+      core.Fire(tc.OrderedDictionary, command=['empty'])
+
 
 if __name__ == '__main__':
   testutils.main()
diff --git a/fire/parser_fuzz_test.py b/fire/parser_fuzz_test.py
index e611b9ad..1cb87f5c 100644
--- a/fire/parser_fuzz_test.py
+++ b/fire/parser_fuzz_test.py
@@ -68,8 +68,8 @@ def testDefaultParseValueFuzz(self, value):
       raise
 
     try:
-      uvalue = six.u(value)
-      uresult = six.u(result)
+      uvalue = unicode(value)
+      uresult = unicode(result)
     except UnicodeDecodeError:
       # This is not what we're testing.
       return
diff --git a/fire/test_components.py b/fire/test_components.py
index c29b7ae4..948e2ec3 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -18,6 +18,8 @@
 from __future__ import division
 from __future__ import print_function
 
+import collections
+
 import six
 
 if six.PY3:
@@ -30,6 +32,10 @@ def identity(arg1, arg2, arg3=10, arg4=20, *arg5, **arg6):  # pylint: disable=ke
 identity.__annotations__ = {'arg2': int, 'arg4': int}
 
 
+def function_with_help(help=True):  # pylint: disable=redefined-builtin
+  return help
+
+
 class Empty(object):
   pass
 
@@ -44,6 +50,21 @@ def __init__(self):
     pass
 
 
+class ErrorInConstructor(object):
+
+  def __init__(self, value='value'):
+    self.value = value
+    raise ValueError('Error in constructor')
+
+
+class WithHelpArg(object):
+  """Test class for testing when class has a help= arg."""
+
+  def __init__(self, help=True):  # pylint: disable=redefined-builtin
+    self.has_help = help
+    self.dictionary = {'__help': 'help in a dict'}
+
+
 class NoDefaults(object):
 
   def double(self, count):
@@ -215,3 +236,15 @@ def create(self):
     x = {}
     x['y'] = x
     return x
+
+
+class OrderedDictionary(object):
+
+  def empty(self):
+    return collections.OrderedDict()
+
+  def non_empty(self):
+    ordered_dict = collections.OrderedDict()
+    ordered_dict['A'] = 'A'
+    ordered_dict[2] = 2
+    return ordered_dict

From eb8f053cee599b49a7ec4f313b6ed7d7fb33e907 Mon Sep 17 00:00:00 2001
From: cclauss <cclauss@bluewin.ch>
Date: Fri, 22 Jun 2018 06:05:15 +0200
Subject: [PATCH 029/324] =?UTF-8?q?Change=20unicode()=20=E2=80=94>=20six.t?=
 =?UTF-8?q?ext=5Ftype()=20for=20Python=203=20(#128)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

See #111
---
 fire/parser_fuzz_test.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/fire/parser_fuzz_test.py b/fire/parser_fuzz_test.py
index 1cb87f5c..13761247 100644
--- a/fire/parser_fuzz_test.py
+++ b/fire/parser_fuzz_test.py
@@ -68,8 +68,8 @@ def testDefaultParseValueFuzz(self, value):
       raise
 
     try:
-      uvalue = unicode(value)
-      uresult = unicode(result)
+      uvalue = six.text_type(value)
+      uresult = six.text_type(result)
     except UnicodeDecodeError:
       # This is not what we're testing.
       return

From b55cebd963e3edeb7ac477e0d3c612b78ce24970 Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Wed, 27 Jun 2018 10:53:52 -0700
Subject: [PATCH 030/324] Copybara generated commit for Python Fire.

  - Added support of calling callable to Python Fire.
  - Use six.text_type instead of unicode for Python 2/3 compatibility in parser_fuzz_test.

PiperOrigin-RevId: 202218864
Change-Id: I87a1a719c16c2162722f4bbee749eafb485b3f99
Reviewed-on: https://team-review.git.corp.google.com/280130
Reviewed-by: Joe Chen <zuhaochen@google.com>
---
 examples/cipher/cipher.py         |  2 +-
 examples/cipher/cipher_test.py    |  2 +-
 examples/diff/diff.py             |  2 +-
 examples/diff/diff_test.py        |  2 +-
 examples/diff/difffull.py         |  2 +-
 examples/identity/identity.py     |  2 +-
 examples/widget/collector.py      |  2 +-
 examples/widget/collector_test.py |  2 +-
 examples/widget/widget.py         |  2 +-
 examples/widget/widget_test.py    |  2 +-
 fire/__init__.py                  |  2 +-
 fire/completion.py                |  6 +--
 fire/completion_test.py           |  2 +-
 fire/core.py                      | 85 +++++++++++++++++++++++--------
 fire/core_test.py                 | 14 +++--
 fire/decorators.py                |  2 +-
 fire/decorators_test.py           |  2 +-
 fire/fire_import_test.py          |  2 +-
 fire/fire_test.py                 |  2 +-
 fire/helputils.py                 |  2 +-
 fire/helputils_test.py            |  4 +-
 fire/inspectutils.py              |  2 +-
 fire/inspectutils_test.py         |  2 +-
 fire/interact.py                  |  2 +-
 fire/interact_test.py             |  2 +-
 fire/parser.py                    |  4 +-
 fire/parser_fuzz_test.py          |  2 +-
 fire/parser_test.py               |  2 +-
 fire/test_components.py           | 13 ++++-
 fire/test_components_py3.py       |  2 +-
 fire/test_components_test.py      |  2 +-
 fire/testutils.py                 |  2 +-
 fire/testutils_test.py            |  2 +-
 fire/trace.py                     | 26 +++++++++-
 fire/trace_test.py                |  2 +-
 setup.py                          |  2 +-
 36 files changed, 149 insertions(+), 61 deletions(-)

diff --git a/examples/cipher/cipher.py b/examples/cipher/cipher.py
index 26a07756..83610a5d 100644
--- a/examples/cipher/cipher.py
+++ b/examples/cipher/cipher.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/examples/cipher/cipher_test.py b/examples/cipher/cipher_test.py
index 285c42cd..d2fb5c5f 100644
--- a/examples/cipher/cipher_test.py
+++ b/examples/cipher/cipher_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/examples/diff/diff.py b/examples/diff/diff.py
index be5b8dab..d321acec 100644
--- a/examples/diff/diff.py
+++ b/examples/diff/diff.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/examples/diff/diff_test.py b/examples/diff/diff_test.py
index f73732e2..81a513c3 100644
--- a/examples/diff/diff_test.py
+++ b/examples/diff/diff_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/examples/diff/difffull.py b/examples/diff/difffull.py
index 38918dee..b765e3f2 100644
--- a/examples/diff/difffull.py
+++ b/examples/diff/difffull.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/examples/identity/identity.py b/examples/identity/identity.py
index 1dec9032..45143883 100644
--- a/examples/identity/identity.py
+++ b/examples/identity/identity.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/examples/widget/collector.py b/examples/widget/collector.py
index dc66b690..f37ecc7a 100644
--- a/examples/widget/collector.py
+++ b/examples/widget/collector.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/examples/widget/collector_test.py b/examples/widget/collector_test.py
index c4c13e7d..274cf382 100644
--- a/examples/widget/collector_test.py
+++ b/examples/widget/collector_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/examples/widget/widget.py b/examples/widget/widget.py
index c4bb83d5..bf1cbeb2 100644
--- a/examples/widget/widget.py
+++ b/examples/widget/widget.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/examples/widget/widget_test.py b/examples/widget/widget_test.py
index 6e2188d6..a5cd7188 100644
--- a/examples/widget/widget_test.py
+++ b/examples/widget/widget_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/fire/__init__.py b/fire/__init__.py
index e9c87141..ab0874f8 100644
--- a/fire/__init__.py
+++ b/fire/__init__.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/fire/completion.py b/fire/completion.py
index ff18e3e8..62af5a44 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
@@ -221,7 +221,7 @@ def Completions(component, verbose=False):
     return [str(index) for index in range(len(component))]
 
   if inspect.isgenerator(component):
-    # TODO: There are currently no commands available for generators.
+    # TODO(dbieber): There are currently no commands available for generators.
     return []
 
   return [
@@ -276,7 +276,7 @@ def _Commands(component, depth=3):
     return
 
   for member_name, member in _Members(component):
-    # TODO: Also skip components we've already seen.
+    # TODO(dbieber): Also skip components we've already seen.
     member_name = _FormatForCommand(member_name)
 
     yield (member_name,)
diff --git a/fire/completion_test.py b/fire/completion_test.py
index 5da360a9..47d80b4a 100644
--- a/fire/completion_test.py
+++ b/fire/completion_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/fire/core.py b/fire/core.py
index 4083caa8..9784a596 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
@@ -237,7 +237,7 @@ def _IsHelpShortcut(component_trace, remaining_args):
 
 def _PrintResult(component_trace, verbose=False):
   """Prints the result of the Fire call to stdout in a human readable way."""
-  # TODO: Design human readable deserializable serialization method
+  # TODO(dbieber): Design human readable deserializable serialization method
   # and move serialization to it's own module.
   result = component_trace.GetResult()
 
@@ -298,7 +298,7 @@ def _ComponentVisible(component, verbose=False):
 
 def _OneLineResult(result):
   """Returns result serialized to a single line string."""
-  # TODO: Ensure line is fewer than eg 120 characters.
+  # TODO(dbieber): Ensure line is fewer than eg 120 characters.
   if isinstance(result, six.string_types):
     return str(result).replace('\n', ' ')
 
@@ -404,20 +404,12 @@ def _Fire(component, args, context, name=None):
       isclass = inspect.isclass(component)
 
       try:
-        target = component.__name__
-        filename, lineno = inspectutils.GetFileAndLine(component)
-
-        component, consumed_args, remaining_args, capacity = _CallCallable(
-            component, remaining_args)
-
-        # Update the trace.
-        if isclass:
-          component_trace.AddInstantiatedClass(
-              component, target, consumed_args, filename, lineno, capacity)
-        else:
-          component_trace.AddCalledRoutine(
-              component, target, consumed_args, filename, lineno, capacity)
-
+        component, remaining_args = _CallAndUpdateTrace(
+            component,
+            remaining_args,
+            component_trace,
+            treatment='class' if isclass else 'routine',
+            target=component.__name__)
       except FireError as error:
         component_trace.AddError(error, initial_args)
         return component_trace
@@ -454,7 +446,7 @@ def _Fire(component, args, context, name=None):
       else:
         # The target isn't present in the dict as a string, but maybe it is as
         # another type.
-        # TODO: Consider alternatives for accessing non-string keys.
+        # TODO(dbieber): Consider alternatives for accessing non-string keys.
         found_target = False
         for key, value in component.items():
           if target == str(key):
@@ -487,8 +479,19 @@ def _Fire(component, args, context, name=None):
             component, target, consumed_args, filename, lineno)
 
       except FireError as error:
-        component_trace.AddError(error, initial_args)
-        return component_trace
+        if not callable(component):
+          component_trace.AddError(error, initial_args)
+          return component_trace
+
+        # If we can't access the member, we try to treat component as a callable
+        try:
+          component, remaining_args = _CallAndUpdateTrace(component,
+                                                          remaining_args,
+                                                          component_trace,
+                                                          treatment='callable')
+        except FireError as error:
+          component_trace.AddError(error, initial_args)
+          return component_trace
 
     if used_separator:
       # Add back in the arguments from after the separator.
@@ -569,6 +572,48 @@ def _GetMember(component, args):
   raise FireError('Could not consume arg:', arg)
 
 
+def _CallAndUpdateTrace(component, args, component_trace, treatment='class',
+                        target=None):
+  """Call the component and update FireTrace.
+
+  The component could a class, a routine, or a callable object. This function
+  will attempt to call the callable and add corresponding action trace to
+  component_trace if the invocation is successful. If not, raise FireError.
+
+  Args:
+    component: The component to call
+    args: Args for calling the component
+    component_trace: FireTrace object that contains action trace
+    treatment: Type of treatment used. Indicating whether we treat the component
+               as a class, a routine, or a callable.
+    target: Target in FireTrace element, default is None. If the value is None,
+            the component itself will be used as target.
+  Returns:
+    component: The object that is the result of the callable call.
+    remaining_args: The remaining args that haven't been consumed yet.
+  """
+  if not target:
+    target = component
+  filename, lineno = inspectutils.GetFileAndLine(component)
+  component, consumed_args, remaining_args, capacity = _CallCallable(
+      component.__call__ if treatment == 'callable' else component, args)
+
+  # TODO(joejoevictor): Consolidate AddInstantiatedClass, AddCalledRoutine, and
+  # AddCalledCallable into one method since the only different between those
+  # methods is the 'action' attribute of the FireTraceElement they created.
+  if treatment == 'class':
+    component_trace.AddInstantiatedClass(
+        component, target, consumed_args, filename, lineno, capacity)
+  elif treatment == 'routine':
+    component_trace.AddCalledRoutine(
+        component, target, consumed_args, filename, lineno, capacity)
+  else:
+    component_trace.AddCalledCallable(
+        component, target, consumed_args, filename, lineno, capacity)
+
+  return component, remaining_args
+
+
 def _CallCallable(fn, args):
   """Calls the function fn by consuming args from args.
 
diff --git a/fire/core_test.py b/fire/core_test.py
index 0af8f216..adfdd514 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
@@ -72,7 +72,7 @@ def testInteractiveModeVariablesWithName(self, mock_embed):
     self.assertEqual(variables['D'], tc.WithDefaults)
     self.assertIsInstance(variables['trace'], trace.FireTrace)
 
-  # TODO: Use parameterized tests to break up repetitive tests.
+  # TODO(dbieber): Use parameterized tests to break up repetitive tests.
   def testHelpWithClass(self):
     with self.assertRaisesFireExit(0, 'Usage:.*ARG1'):
       core.Fire(tc.InstanceVars, command=['--', '--help'])
@@ -134,12 +134,20 @@ def testPrintEmptyDict(self):
     with self.assertOutputMatches(stdout='{}', stderr=None):
       core.Fire(tc.EmptyDictOutput, command=['nothing_printable'])
 
-  def testPrintDict(self):
+  def testPrintOrderedDict(self):
     with self.assertOutputMatches(stdout=r'A:\s+A\s+2:\s+2\s+', stderr=None):
       core.Fire(tc.OrderedDictionary, command=['non_empty'])
     with self.assertOutputMatches(stdout='{}'):
       core.Fire(tc.OrderedDictionary, command=['empty'])
 
+  def testCallable(self):
+    with self.assertOutputMatches(stdout=r'foo:\s+foo\s+', stderr=None):
+      core.Fire(tc.CallableWithKeywordArgument(), command=['--foo=foo'])
+    with self.assertOutputMatches(stdout=r'foo\s+', stderr=None):
+      core.Fire(tc.CallableWithKeywordArgument(), command=['print_msg', 'foo'])
+    with self.assertOutputMatches(stdout=r'', stderr=None):
+      core.Fire(tc.CallableWithKeywordArgument(), command=[])
+
 
 if __name__ == '__main__':
   testutils.main()
diff --git a/fire/decorators.py b/fire/decorators.py
index 168312d4..3fcc4b97 100644
--- a/fire/decorators.py
+++ b/fire/decorators.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/fire/decorators_test.py b/fire/decorators_test.py
index 372cee60..cc7d6203 100644
--- a/fire/decorators_test.py
+++ b/fire/decorators_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/fire/fire_import_test.py b/fire/fire_import_test.py
index 8589b79e..c5975681 100644
--- a/fire/fire_import_test.py
+++ b/fire/fire_import_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/fire/fire_test.py b/fire/fire_test.py
index 8f1f53ec..dd8527b5 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/fire/helputils.py b/fire/helputils.py
index eecd25ca..162ad382 100644
--- a/fire/helputils.py
+++ b/fire/helputils.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/fire/helputils_test.py b/fire/helputils_test.py
index cf8014f3..416ce7a7 100644
--- a/fire/helputils_test.py
+++ b/fire/helputils_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
@@ -43,7 +43,7 @@ def testHelpStringObject(self):
     self.assertIn('Type:        NoDefaults', helpstring)
     self.assertIn('String form: <fire.test_components.NoDefaults object at ',
                   helpstring)
-    # TODO: We comment this out since it only works with IPython:
+    # TODO(dbieber): We comment this out since it only works with IPython:
     # self.assertIn('test_components.py', helpstring)
     self.assertIn('Usage:       double\n'
                   '             triple', helpstring)
diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index 40769fe3..e7d6fc74 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/fire/inspectutils_test.py b/fire/inspectutils_test.py
index 23f86803..8c6dd9a3 100644
--- a/fire/inspectutils_test.py
+++ b/fire/inspectutils_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/fire/interact.py b/fire/interact.py
index b97f7f11..d7d458c8 100644
--- a/fire/interact.py
+++ b/fire/interact.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/fire/interact_test.py b/fire/interact_test.py
index dd3ed9cd..29fa7597 100644
--- a/fire/interact_test.py
+++ b/fire/interact_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/fire/parser.py b/fire/parser.py
index e8a12dae..1e524d05 100644
--- a/fire/parser.py
+++ b/fire/parser.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
@@ -30,7 +30,7 @@ def CreateParser():
   parser.add_argument('--completion', nargs='?', const='bash', type=str)
   parser.add_argument('--help', '-h', action='store_true')
   parser.add_argument('--trace', '-t', action='store_true')
-  # TODO: Consider allowing name to be passed as an argument.
+  # TODO(dbieber): Consider allowing name to be passed as an argument.
   return parser
 
 
diff --git a/fire/parser_fuzz_test.py b/fire/parser_fuzz_test.py
index 13761247..af0be038 100644
--- a/fire/parser_fuzz_test.py
+++ b/fire/parser_fuzz_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/fire/parser_test.py b/fire/parser_test.py
index 7fd9ca79..0257be28 100644
--- a/fire/parser_test.py
+++ b/fire/parser_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/fire/test_components.py b/fire/test_components.py
index 948e2ec3..7ce52e89 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
@@ -248,3 +248,14 @@ def non_empty(self):
     ordered_dict['A'] = 'A'
     ordered_dict[2] = 2
     return ordered_dict
+
+
+class CallableWithKeywordArgument(object):
+  """Test class for supporting callable."""
+
+  def __call__(self, **kwargs):
+    for key, value in kwargs.items():
+      print('%s: %s' % (key, value))
+
+  def print_msg(self, msg):
+    print(msg)
diff --git a/fire/test_components_py3.py b/fire/test_components_py3.py
index 479cbaf6..d705c43a 100644
--- a/fire/test_components_py3.py
+++ b/fire/test_components_py3.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/fire/test_components_test.py b/fire/test_components_test.py
index 1e52944c..f35d7ab5 100644
--- a/fire/test_components_test.py
+++ b/fire/test_components_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/fire/testutils.py b/fire/testutils.py
index 8520769a..08964e25 100644
--- a/fire/testutils.py
+++ b/fire/testutils.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/fire/testutils_test.py b/fire/testutils_test.py
index 1644ff50..ad604193 100644
--- a/fire/testutils_test.py
+++ b/fire/testutils_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/fire/trace.py b/fire/trace.py
index b1703250..29b22d71 100644
--- a/fire/trace.py
+++ b/fire/trace.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
@@ -34,6 +34,7 @@
 INITIAL_COMPONENT = 'Initial component'
 INSTANTIATED_CLASS = 'Instantiated class'
 CALLED_ROUTINE = 'Called routine'
+CALLED_CALLABLE = 'Called callable'
 ACCESSED_PROPERTY = 'Accessed property'
 COMPLETION_SCRIPT = 'Generated completion script'
 INTERACTIVE_MODE = 'Entered interactive mode'
@@ -93,6 +94,29 @@ def AddAccessedProperty(self, component, target, args, filename, lineno):
     )
     self.elements.append(element)
 
+  def AddCalledCallable(self, component, target, args, filename, lineno,
+                        capacity):
+    """Adds an element to the trace indicating that a callable was called.
+
+    Args:
+      component: The result of calling the callable.
+      target: The name of the callable.
+      args: The args consumed in order to call this callable.
+      filename: The file in which the callable is defined, or None if N/A.
+      lineno: The line number on which the callable is defined, or None if N/A.
+      capacity: (bool) Whether the callable could have accepted additional args.
+    """
+    element = FireTraceElement(
+        component=component,
+        action=CALLED_CALLABLE,
+        target=target,
+        args=args,
+        filename=filename,
+        lineno=lineno,
+        capacity=capacity,
+    )
+    self.elements.append(element)
+
   def AddCalledRoutine(self, component, target, args, filename, lineno,
                        capacity):
     """Adds an element to the trace indicating that a routine was called.
diff --git a/fire/trace_test.py b/fire/trace_test.py
index 5e244f8c..956ce94c 100644
--- a/fire/trace_test.py
+++ b/fire/trace_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.
diff --git a/setup.py b/setup.py
index 99d44910..bd802702 100644
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2017 Google Inc.
+# 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.

From 80ee2564ac50635d65851404f7721250d0149f9d Mon Sep 17 00:00:00 2001
From: Neal Fultz <nfultz@gmail.com>
Date: Mon, 2 Jul 2018 11:05:12 -0700
Subject: [PATCH 031/324] Fixes bug setting stories (#130)

The example wass slightly broken, and always sets stories=1. Also changed the phew to be after a whole floor instead of every single step.
---
 docs/guide.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/guide.md b/docs/guide.md
index 81e73539..7f610699 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -461,13 +461,13 @@ class Building(object):
 
   def __init__(self, name, stories=1):
     self.name = name
-    self.stories = 1
+    self.stories = stories
 
   def climb_stairs(self, stairs_per_story=10):
     for story in range(self.stories):
       for stair in range(1, stairs_per_story):
         yield stair
-        yield 'Phew!'
+      yield 'Phew!'
     yield 'Done!'
 
 if __name__ == '__main__':

From 625b305d9df719f9e75a37b05b56b900d387cebd Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 23 Jul 2018 13:56:13 -0700
Subject: [PATCH 032/324] Adds useless-object-inheritance and no-else-return to
 pylintrc disable

PiperOrigin-RevId: 205717397
Change-Id: I0aa6fd6be26b6629db5949515e984873175d861d
Reviewed-on: https://team-review.git.corp.google.com/291456
Reviewed-by: David Bieber <dbieber@google.com>
---
 pylintrc | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pylintrc b/pylintrc
index e7425503..37bfa447 100644
--- a/pylintrc
+++ b/pylintrc
@@ -32,7 +32,7 @@ enable=indexing-exception,old-raise-syntax
 # Disable the message, report, category or checker with the given id(s). You
 # can either give multiple identifier separated by comma (,) or put this option
 # multiple time.
-disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order
+disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return
 
 
 [REPORTS]

From 6e0ca736a9ab6ff586f6c60c7d6ab500e528f600 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 27 Jul 2018 09:09:53 -0700
Subject: [PATCH 033/324] Consolidate fire trace methods into
 AddCalledComponent

PiperOrigin-RevId: 206324964
Change-Id: I691b0882fc216221be122181eb80d3526e81a15c
Reviewed-on: https://team-review.git.corp.google.com/293492
Reviewed-by: David Bieber <dbieber@google.com>
---
 fire/core.py       | 15 +++++-------
 fire/trace.py      | 57 ++++++----------------------------------------
 fire/trace_test.py | 26 ++++++++++++++++-----
 3 files changed, 33 insertions(+), 65 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index 9784a596..07698e05 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -598,18 +598,15 @@ def _CallAndUpdateTrace(component, args, component_trace, treatment='class',
   component, consumed_args, remaining_args, capacity = _CallCallable(
       component.__call__ if treatment == 'callable' else component, args)
 
-  # TODO(joejoevictor): Consolidate AddInstantiatedClass, AddCalledRoutine, and
-  # AddCalledCallable into one method since the only different between those
-  # methods is the 'action' attribute of the FireTraceElement they created.
   if treatment == 'class':
-    component_trace.AddInstantiatedClass(
-        component, target, consumed_args, filename, lineno, capacity)
+    action = trace.INSTANTIATED_CLASS
   elif treatment == 'routine':
-    component_trace.AddCalledRoutine(
-        component, target, consumed_args, filename, lineno, capacity)
+    action = trace.CALLED_ROUTINE
   else:
-    component_trace.AddCalledCallable(
-        component, target, consumed_args, filename, lineno, capacity)
+    action = trace.CALLED_CALLABLE
+  component_trace.AddCalledComponent(
+      component, target, consumed_args, filename, lineno, capacity,
+      action=action)
 
   return component, remaining_args
 
diff --git a/fire/trace.py b/fire/trace.py
index 29b22d71..c2fc8bc5 100644
--- a/fire/trace.py
+++ b/fire/trace.py
@@ -94,9 +94,11 @@ def AddAccessedProperty(self, component, target, args, filename, lineno):
     )
     self.elements.append(element)
 
-  def AddCalledCallable(self, component, target, args, filename, lineno,
-                        capacity):
-    """Adds an element to the trace indicating that a callable was called.
+  def AddCalledComponent(self, component, target, args, filename, lineno,
+                         capacity, action=CALLED_CALLABLE):
+    """Adds an element to the trace indicating that a component was called.
+
+    Also applies to instantiating a class.
 
     Args:
       component: The result of calling the callable.
@@ -105,56 +107,11 @@ def AddCalledCallable(self, component, target, args, filename, lineno,
       filename: The file in which the callable is defined, or None if N/A.
       lineno: The line number on which the callable is defined, or None if N/A.
       capacity: (bool) Whether the callable could have accepted additional args.
+      action: The value to include as the action in the FireTraceElement.
     """
     element = FireTraceElement(
         component=component,
-        action=CALLED_CALLABLE,
-        target=target,
-        args=args,
-        filename=filename,
-        lineno=lineno,
-        capacity=capacity,
-    )
-    self.elements.append(element)
-
-  def AddCalledRoutine(self, component, target, args, filename, lineno,
-                       capacity):
-    """Adds an element to the trace indicating that a routine was called.
-
-    Args:
-      component: The result of calling the routine.
-      target: The name of the routine.
-      args: The args consumed in order to call this routine.
-      filename: The file in which the routine is defined, or None if N/A.
-      lineno: The line number on which the routine is defined, or None if N/A.
-      capacity: (bool) Whether the routine could have accepted additional args.
-    """
-    element = FireTraceElement(
-        component=component,
-        action=CALLED_ROUTINE,
-        target=target,
-        args=args,
-        filename=filename,
-        lineno=lineno,
-        capacity=capacity,
-    )
-    self.elements.append(element)
-
-  def AddInstantiatedClass(self, component, target, args, filename, lineno,
-                           capacity):
-    """Adds an element to the trace indicating that a class was instantiated.
-
-    Args:
-      component: The result of instantiating the class.
-      target: The name of the class.
-      args: The args consumed in order to instantiate the class.
-      filename: The file in which the class is defined, or None if N/A.
-      lineno: The line number on which the class is defined, or None if N/A.
-      capacity: (bool) Whether cls.__init__ could have accepted additional args.
-    """
-    element = FireTraceElement(
-        component=component,
-        action=INSTANTIATED_CLASS,
+        action=action,
         target=target,
         args=args,
         filename=filename,
diff --git a/fire/trace_test.py b/fire/trace_test.py
index 956ce94c..1621a593 100644
--- a/fire/trace_test.py
+++ b/fire/trace_test.py
@@ -51,10 +51,20 @@ def testAddAccessedProperty(self):
         str(t),
         '1. Initial component\n2. Accessed property "prop" (sample.py:12)')
 
+  def testAddCalledCallable(self):
+    t = trace.FireTrace('initial object')
+    args = ('example', 'args')
+    t.AddCalledComponent('result', 'cell', args, 'sample.py', 10, False,
+                         action=trace.CALLED_CALLABLE)
+    self.assertEqual(
+        str(t),
+        '1. Initial component\n2. Called callable "cell" (sample.py:10)')
+
   def testAddCalledRoutine(self):
     t = trace.FireTrace('initial object')
     args = ('example', 'args')
-    t.AddCalledRoutine('result', 'run', args, 'sample.py', 12, False)
+    t.AddCalledComponent('result', 'run', args, 'sample.py', 12, False,
+                         action=trace.CALLED_ROUTINE)
     self.assertEqual(
         str(t),
         '1. Initial component\n2. Called routine "run" (sample.py:12)')
@@ -62,8 +72,9 @@ def testAddCalledRoutine(self):
   def testAddInstantiatedClass(self):
     t = trace.FireTrace('initial object')
     args = ('example', 'args')
-    t.AddInstantiatedClass(
-        'Classname', 'classname', args, 'sample.py', 12, False)
+    t.AddCalledComponent(
+        'Classname', 'classname', args, 'sample.py', 12, False,
+        action=trace.INSTANTIATED_CLASS)
     target = """1. Initial component
 2. Instantiated class "classname" (sample.py:12)"""
     self.assertEqual(str(t), target)
@@ -85,19 +96,22 @@ def testAddInteractiveMode(self):
   def testGetCommand(self):
     t = trace.FireTrace('initial object')
     args = ('example', 'args')
-    t.AddCalledRoutine('result', 'run', args, 'sample.py', 12, False)
+    t.AddCalledComponent('result', 'run', args, 'sample.py', 12, False,
+                         action=trace.CALLED_ROUTINE)
     self.assertEqual(t.GetCommand(), 'example args')
 
   def testGetCommandWithQuotes(self):
     t = trace.FireTrace('initial object')
     args = ('example', 'spaced arg')
-    t.AddCalledRoutine('result', 'run', args, 'sample.py', 12, False)
+    t.AddCalledComponent('result', 'run', args, 'sample.py', 12, False,
+                         action=trace.CALLED_ROUTINE)
     self.assertEqual(t.GetCommand(), "example 'spaced arg'")
 
   def testGetCommandWithFlagQuotes(self):
     t = trace.FireTrace('initial object')
     args = ('--example=spaced arg',)
-    t.AddCalledRoutine('result', 'run', args, 'sample.py', 12, False)
+    t.AddCalledComponent('result', 'run', args, 'sample.py', 12, False,
+                         action=trace.CALLED_ROUTINE)
     self.assertEqual(t.GetCommand(), "--example='spaced arg'")
 
 

From 54f91a21c06a17003f93cf7c6e4657180cffefc2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= <contact@tiger-222.fr>
Date: Wed, 29 Aug 2018 01:21:50 +0200
Subject: [PATCH 034/324] Fix warnings in the diff example (#135)

Fix ResourceWarning (unclosed file) and DeprecationWarning ('U' mode is deprecated)
---
 examples/diff/diff.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/examples/diff/diff.py b/examples/diff/diff.py
index d321acec..f99e525e 100644
--- a/examples/diff/diff.py
+++ b/examples/diff/diff.py
@@ -74,8 +74,10 @@ def __init__(self, fromfile, tofile):
 
     self.fromdate = time.ctime(os.stat(fromfile).st_mtime)
     self.todate = time.ctime(os.stat(tofile).st_mtime)
-    self.fromlines = open(fromfile, 'U').readlines()
-    self.tolines = open(tofile, 'U').readlines()
+    with open(fromfile) as f:
+      self.fromlines = f.readlines()
+    with open(tofile) as f:
+      self.tolines = f.readlines()
 
   def unified_diff(self, lines=3):
     return difflib.unified_diff(

From 0a6bb9d75488751746aae7b4b54c3bab57408c4a Mon Sep 17 00:00:00 2001
From: Alex Shadley <shadleyalex@gmail.com>
Date: Fri, 5 Oct 2018 13:29:54 -0500
Subject: [PATCH 035/324] Issue #108: Shortcuts for boolean arguments (#141)

* adds support for parsing shortcuts for single-character arguments, along with tests
---
 fire/core.py            | 44 +++++++++++++++++++++++++++++++++++------
 fire/fire_test.py       | 25 +++++++++++++++++++++++
 fire/test_components.py |  6 ++++++
 3 files changed, 69 insertions(+), 6 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index 07698e05..5b3a068f 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -60,6 +60,7 @@ def main(argv):
 import shlex
 import sys
 import types
+import re
 
 from fire import completion
 from fire import decorators
@@ -750,7 +751,7 @@ def _ParseArgs(fn_args, fn_defaults, num_required_args, kwargs,
 def _ParseKeywordArgs(args, fn_spec):
   """Parses the supplied arguments for keyword arguments.
 
-  Given a list of arguments, finds occurences of --name value, and uses 'name'
+  Given a list of arguments, finds occurrences of --name value, and uses 'name'
   as the keyword and 'value' as the value. Constructs and returns a dictionary
   of these keyword arguments, and returns a list of the remaining arguments.
 
@@ -767,6 +768,9 @@ def _ParseKeywordArgs(args, fn_spec):
     kwargs: A dictionary mapping keywords to values.
     remaining_kwargs: A list of the unused kwargs from the original args.
     remaining_args: A list of the unused arguments from the original args.
+  Raises:
+    FireError: if a boolean shortcut arg is passed that could refer to multiple
+        args
   """
   kwargs = {}
   remaining_kwargs = []
@@ -785,15 +789,27 @@ def _ParseKeywordArgs(args, fn_spec):
       continue
 
     arg_consumed = False
-    if argument.startswith('--'):
+    if _IsFlag(argument):
       # This is a named argument; get its value from this arg or the next.
       got_argument = False
 
-      keyword = argument[2:]
+      keyword = ''
+      if _IsSingleCharFlag(argument):
+        keychar = argument[1]
+        potential_args = [arg for arg in fn_args if arg[0] == keychar]
+        if len(potential_args) == 1:
+          keyword = potential_args[0]
+        elif len(potential_args) > 1:
+          raise FireError("The argument '{}' is ambiguous as it could "
+                          "refer to any of the following arguments: {}".format(
+                              argument, potential_args))
+
+      else:
+        keyword = argument[2:]
+
       contains_equals = '=' in keyword
-      is_bool_syntax = (
-          not contains_equals and
-          (index + 1 == len(args) or args[index + 1].startswith('--')))
+      is_bool_syntax = (not contains_equals and
+                        (index + 1 == len(args) or _IsFlag(args[index + 1])))
       if contains_equals:
         keyword, value = keyword.split('=', 1)
         got_argument = True
@@ -828,6 +844,7 @@ def _ParseKeywordArgs(args, fn_spec):
           if skip_argument:
             remaining_kwargs.append(args[index + 1])
 
+
     if not arg_consumed:
       # The argument was not consumed, so it is still a remaining argument.
       remaining_args.append(argument)
@@ -835,6 +852,21 @@ def _ParseKeywordArgs(args, fn_spec):
   return kwargs, remaining_kwargs, remaining_args
 
 
+def _IsFlag(argument):
+  """Determines if the argument is a flag argument"""
+  return _IsSingleCharFlag(argument) or _IsMultiCharFlag(argument)
+
+
+def _IsSingleCharFlag(argument):
+  """Determines if the argument is a single char flag (e.g. '-a')"""
+  return re.match('^-[a-z]$', argument)
+
+
+def _IsMultiCharFlag(argument):
+  """Determines if the argument is a multi char flag (e.g. '--alpha')"""
+  return argument.startswith('--')
+
+
 def _ParseValue(value, index, arg, metadata):
   """Parses value, a string, into the appropriate type.
 
diff --git a/fire/fire_test.py b/fire/fire_test.py
index dd8527b5..267c6fa2 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -385,6 +385,31 @@ def testBoolParsingLessExpectedCases(self):
         fire.Fire(tc.MixedDefaults, command=r'identity --alpha \"--test\"'),
         ('--test', '0'))
 
+  def testBoolShortcutParsing(self):
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '-a']), (True, '0'))
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '-a', '--beta=10']), (True, 10))
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '-a', '-b']), (True, True))
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '-a', '42', '-b']), (42, True))
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '-a', '42', '-b', '10']), (42, 10))
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '--alpha', 'True', '-b', '10']),
+        (True, 10))
+    with self.assertRaisesFireExit(2):
+      # This test attempts to use a boolean shortcut on a function with
+      # a naming conflict for the shortcut, triggering a FireError
+      fire.Fire(tc.SimilarArgNames, command=['identity', '-b'])
+
   def testBoolParsingWithNo(self):
     # In these examples --nothing always refers to the nothing argument:
     def fn1(thing, nothing):
diff --git a/fire/test_components.py b/fire/test_components.py
index 7ce52e89..ee687abd 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -104,6 +104,12 @@ def identity(self, alpha, beta='0'):
     return alpha, beta
 
 
+class SimilarArgNames(object):
+
+  def identity(self, bool_one=False, bool_two=False):
+    return bool_one, bool_two
+
+
 class Annotations(object):
 
   def double(self, count=0):

From f37dd2b8a52c597c8860de941326143709d5a219 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 5 Oct 2018 13:27:28 -0700
Subject: [PATCH 036/324] Refactoring of some helputils, lint clean up, and a
 spelling fix.

Copybara generated commit for Python Fire.

PiperOrigin-RevId: 215941316
Change-Id: I32a36103b5f4de2dc396a11465baf3a3a95734e0
Reviewed-on: https://team-review.git.corp.google.com/c/330453
Reviewed-by: David Bieber <dbieber@google.com>
---
 fire/core.py      |  33 +++++++-------
 fire/fire_test.py |   2 +-
 fire/helputils.py | 107 ++++++++++++++++++++++++++++++++++++++--------
 3 files changed, 107 insertions(+), 35 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index 5b3a068f..a9d71055 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -57,10 +57,10 @@ def main(argv):
 import json
 import os
 import pipes
+import re
 import shlex
 import sys
 import types
-import re
 
 from fire import completion
 from fire import decorators
@@ -135,13 +135,7 @@ def Fire(component=None, command=None, name=None):
         print(('WARNING: The proper way to show help is {cmd}.\n'
                'Showing help anyway.\n').format(cmd=pipes.quote(command)),
               file=sys.stderr)
-
-    print('Fire trace:\n{trace}\n'.format(trace=component_trace),
-          file=sys.stderr)
-    result = component_trace.GetResult()
-    print(
-        helputils.HelpString(result, component_trace, component_trace.verbose),
-        file=sys.stderr)
+    _PrintError(component_trace)
     raise FireExit(2, component_trace)
   elif component_trace.show_trace and component_trace.show_help:
     print('Fire trace:\n{trace}\n'.format(trace=component_trace),
@@ -258,6 +252,15 @@ def _PrintResult(component_trace, verbose=False):
     print(helputils.HelpString(result, component_trace, verbose))
 
 
+def _PrintError(component_trace):
+  """Prints the Fire trace and the error to stdout."""
+  print('Fire trace:\n{trace}\n'.format(trace=component_trace), file=sys.stderr)
+  result = component_trace.GetResult()
+  print(
+      helputils.HelpString(result, component_trace, component_trace.verbose),
+      file=sys.stderr)
+
+
 def _DictAsString(result, verbose=False):
   """Returns a dict as a string.
 
@@ -713,7 +716,7 @@ def _ParseArgs(fn_args, fn_defaults, num_required_args, kwargs,
     remaining_args: A list of the supplied args that have not been used yet.
     capacity: Whether the call could have taken args in place of defaults.
   Raises:
-    FireError: if additional positional arguments are expected, but none are
+    FireError: If additional positional arguments are expected, but none are
         available.
   """
   accepts_positional_args = metadata.get(decorators.ACCEPTS_POSITIONAL_ARGS)
@@ -769,8 +772,8 @@ def _ParseKeywordArgs(args, fn_spec):
     remaining_kwargs: A list of the unused kwargs from the original args.
     remaining_args: A list of the unused arguments from the original args.
   Raises:
-    FireError: if a boolean shortcut arg is passed that could refer to multiple
-        args
+    FireError: If a single-character flag is passed that could refer to multiple
+        possible args.
   """
   kwargs = {}
   remaining_kwargs = []
@@ -803,7 +806,6 @@ def _ParseKeywordArgs(args, fn_spec):
           raise FireError("The argument '{}' is ambiguous as it could "
                           "refer to any of the following arguments: {}".format(
                               argument, potential_args))
-
       else:
         keyword = argument[2:]
 
@@ -844,7 +846,6 @@ def _ParseKeywordArgs(args, fn_spec):
           if skip_argument:
             remaining_kwargs.append(args[index + 1])
 
-
     if not arg_consumed:
       # The argument was not consumed, so it is still a remaining argument.
       remaining_args.append(argument)
@@ -853,17 +854,17 @@ def _ParseKeywordArgs(args, fn_spec):
 
 
 def _IsFlag(argument):
-  """Determines if the argument is a flag argument"""
+  """Determines if the argument is a flag argument."""
   return _IsSingleCharFlag(argument) or _IsMultiCharFlag(argument)
 
 
 def _IsSingleCharFlag(argument):
-  """Determines if the argument is a single char flag (e.g. '-a')"""
+  """Determines if the argument is a single char flag (e.g. '-a')."""
   return re.match('^-[a-z]$', argument)
 
 
 def _IsMultiCharFlag(argument):
-  """Determines if the argument is a multi char flag (e.g. '--alpha')"""
+  """Determines if the argument is a multi char flag (e.g. '--alpha')."""
   return argument.startswith('--')
 
 
diff --git a/fire/fire_test.py b/fire/fire_test.py
index 267c6fa2..c850893f 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -539,7 +539,7 @@ def testSeparatorForChaining(self):
         fire.Fire(tc.ReturnsObj,
                   command=['get-obj', 'arg1', 'arg2', 'as-bool', 'True']),
         tc.BoolConverter)
-    # With a separator only the preceeding args are consumed by get_obj.
+    # With a separator only the preceding args are consumed by get_obj.
     self.assertEqual(
         fire.Fire(
             tc.ReturnsObj,
diff --git a/fire/helputils.py b/fire/helputils.py
index 162ad382..818eef45 100644
--- a/fire/helputils.py
+++ b/fire/helputils.py
@@ -76,6 +76,23 @@ def _DisplayValue(info, field, padding):
   return value
 
 
+def _GetFields(trace=None):
+  """Returns the field names to include in the help text for a component."""
+  del trace  # Unused.
+  return [
+      'type_name',
+      'string_form',
+      'file',
+      'line',
+      'docstring',
+      'init_docstring',
+      'class_docstring',
+      'call_docstring',
+      'length',
+      'usage',
+  ]
+
+
 def HelpString(component, trace=None, verbose=False):
   """Returns a help string for a supplied component.
 
@@ -91,26 +108,39 @@ def HelpString(component, trace=None, verbose=False):
   info = inspectutils.Info(component)
   info['usage'] = UsageString(component, trace, verbose)
 
-  fields = [
-      'type_name',
-      'string_form',
-      'file',
-      'line',
+  is_error_screen = False
+  if trace:
+    is_error_screen = trace.HasError()
 
-      'docstring',
-      'init_docstring',
-      'class_docstring',
-      'call_docstring',
-      'length',
+  if is_error_screen:
+    return _ErrorText(info, trace)
+  else:
+    return _HelpText(info, trace)
 
-      'usage',
-  ]
 
-  max_size = max(
-      len(_NormalizeField(field)) + 1
-      for field in fields
-      if field in info and info[field])
-  format_string = '{{field:{max_size}s}} {{value}}'.format(max_size=max_size)
+def _CommonHelpText(info, trace=None):
+  """Returns help text.
+
+  This was a copy of previous HelpString function and will be removed once the
+  correct text formatters are implemented.
+
+  Args:
+    info: The IR object containing metadata of an object.
+    trace: The Fire trace object containing all metadata of current execution.
+  Returns:
+    String suitable for display giving information about the component.
+  """
+  # TODO(joejoevictor): Improve this further.
+  fields = _GetFields(trace)
+
+  try:
+    max_size = max(
+        len(_NormalizeField(field)) + 1
+        for field in fields
+        if field in info and info[field])
+    format_string = '{{field:{max_size}s}} {{value}}'.format(max_size=max_size)
+  except ValueError:
+    return ''
 
   lines = []
   for field in fields:
@@ -126,6 +156,39 @@ def HelpString(component, trace=None, verbose=False):
   return '\n'.join(lines)
 
 
+def _ErrorText(info, trace=None):
+  """Returns help text for error screen.
+
+  Construct help text for error screen to inform the user about error occurred
+  and correct syntax for invoking the object.
+
+  Args:
+    info: The IR object containing metadata of an object.
+    trace: The Fire trace object containing all metadata of current execution.
+  Returns:
+    String suitable for display in error screen.
+  """
+  # TODO(joejoevictor): Implement real error text construction.
+  return _CommonHelpText(info, trace)
+
+
+def _HelpText(info, trace=None):
+  """Returns help text for extensive help screen.
+
+  Construct help text for help screen when user explicitly requesting help by
+  having -h, --help in the command sequence.
+
+  Args:
+    info: The IR object containing metadata of an object.
+    trace: The Fire trace object containing all metadata of current execution.
+  Returns:
+    String suitable for display in extensive help screen.
+  """
+
+  # TODO(joejoevictor): Implement real help text construction.
+  return _CommonHelpText(info, trace)
+
+
 def _UsageStringFromFullArgSpec(command, spec):
   """Get a usage string from the FullArgSpec for the given command.
 
@@ -180,7 +243,15 @@ def _UsageStringFromFullArgSpec(command, spec):
 
 def UsageString(component, trace=None, verbose=False):
   """Returns a string showing how to use the component as a Fire command."""
-  command = trace.GetCommand() + ' ' if trace else ''
+  if trace:
+    command = trace.GetCommand()
+  else:
+    command = None
+
+  if command:
+    command += ' '
+  else:
+    command = ''
 
   if inspect.isroutine(component) or inspect.isclass(component):
     spec = inspectutils.GetFullArgSpec(component)

From 5347858957e2f3ec9a390260e52c43a7fd293192 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 5 Oct 2018 16:08:56 -0700
Subject: [PATCH 037/324] Support capital letters for single-char flags

PiperOrigin-RevId: 215968361
Change-Id: I60cbabf5c74bdba78127c664dcf629436ec5d390
Reviewed-on: https://team-review.git.corp.google.com/c/330541
Reviewed-by: David Bieber <dbieber@google.com>
---
 fire/core.py            | 2 +-
 fire/fire_test.py       | 7 ++++++-
 fire/helputils.py       | 3 ++-
 fire/test_components.py | 6 ++++++
 4 files changed, 15 insertions(+), 3 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index a9d71055..c1b6708c 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -860,7 +860,7 @@ def _IsFlag(argument):
 
 def _IsSingleCharFlag(argument):
   """Determines if the argument is a single char flag (e.g. '-a')."""
-  return re.match('^-[a-z]$', argument)
+  return re.match('^-[a-zA-Z]$', argument)
 
 
 def _IsMultiCharFlag(argument):
diff --git a/fire/fire_test.py b/fire/fire_test.py
index c850893f..375ebf3c 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -385,7 +385,7 @@ def testBoolParsingLessExpectedCases(self):
         fire.Fire(tc.MixedDefaults, command=r'identity --alpha \"--test\"'),
         ('--test', '0'))
 
-  def testBoolShortcutParsing(self):
+  def testSingleCharFlagParsing(self):
     self.assertEqual(
         fire.Fire(tc.MixedDefaults,
                   command=['identity', '-a']), (True, '0'))
@@ -410,6 +410,11 @@ def testBoolShortcutParsing(self):
       # a naming conflict for the shortcut, triggering a FireError
       fire.Fire(tc.SimilarArgNames, command=['identity', '-b'])
 
+  def testSingleCharFlagParsingCapitalLetter(self):
+    self.assertEqual(
+        fire.Fire(tc.CapitalizedArgNames,
+                  command=['sum', '-D', '5', '-G', '10']), 15)
+
   def testBoolParsingWithNo(self):
     # In these examples --nothing always refers to the nothing argument:
     def fn1(thing, nothing):
diff --git a/fire/helputils.py b/fire/helputils.py
index 818eef45..f11254e0 100644
--- a/fire/helputils.py
+++ b/fire/helputils.py
@@ -130,7 +130,8 @@ def _CommonHelpText(info, trace=None):
   Returns:
     String suitable for display giving information about the component.
   """
-  # TODO(joejoevictor): Improve this further.
+  # TODO(joejoevictor): Currently this is just a copy of existing HelpString
+  # method. We will reimplement this further in later CLs.
   fields = _GetFields(trace)
 
   try:
diff --git a/fire/test_components.py b/fire/test_components.py
index ee687abd..f09bddbe 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -110,6 +110,12 @@ def identity(self, bool_one=False, bool_two=False):
     return bool_one, bool_two
 
 
+class CapitalizedArgNames(object):
+
+  def sum(self, Delta=1.0, Gamma=2.0):  # pylint: disable=invalid-name
+    return Delta + Gamma
+
+
 class Annotations(object):
 
   def double(self, count=0):

From 53a3498fd2d1a364b7af6c482772112ef6476694 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 5 Oct 2018 16:30:19 -0700
Subject: [PATCH 038/324] Support single-hyphen flags.

PiperOrigin-RevId: 215983601
Change-Id: I7d7d1323b786bc93f3f311a609e7b8f836011ac4
Reviewed-on: https://team-review.git.corp.google.com/c/330493
Reviewed-by: David Bieber <dbieber@google.com>
---
 fire/core.py      |  6 +++---
 fire/fire_test.py | 40 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 43 insertions(+), 3 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index c1b6708c..549bf701 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -798,7 +798,7 @@ def _ParseKeywordArgs(args, fn_spec):
 
       keyword = ''
       if _IsSingleCharFlag(argument):
-        keychar = argument[1]
+        keychar = argument.lstrip('-')
         potential_args = [arg for arg in fn_args if arg[0] == keychar]
         if len(potential_args) == 1:
           keyword = potential_args[0]
@@ -807,7 +807,7 @@ def _ParseKeywordArgs(args, fn_spec):
                           "refer to any of the following arguments: {}".format(
                               argument, potential_args))
       else:
-        keyword = argument[2:]
+        keyword = argument.lstrip('-')
 
       contains_equals = '=' in keyword
       is_bool_syntax = (not contains_equals and
@@ -865,7 +865,7 @@ def _IsSingleCharFlag(argument):
 
 def _IsMultiCharFlag(argument):
   """Determines if the argument is a multi char flag (e.g. '--alpha')."""
-  return argument.startswith('--')
+  return argument.startswith('--') or re.match('^-[a-zA-Z]', argument)
 
 
 def _ParseValue(value, index, arg, metadata):
diff --git a/fire/fire_test.py b/fire/fire_test.py
index 375ebf3c..398a9be8 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -92,12 +92,30 @@ def testFireNamedArgs(self):
         fire.Fire(tc.OldStyleWithDefaults, command=['triple', '--count', '5']),
         15)
 
+  def testFireNamedArgsSingleHyphen(self):
+    self.assertEqual(fire.Fire(tc.WithDefaults,
+                               command=['double', '-count', '5']), 10)
+    self.assertEqual(fire.Fire(tc.WithDefaults,
+                               command=['triple', '-count', '5']), 15)
+    self.assertEqual(
+        fire.Fire(tc.OldStyleWithDefaults, command=['double', '-count', '5']),
+        10)
+    self.assertEqual(
+        fire.Fire(tc.OldStyleWithDefaults, command=['triple', '-count', '5']),
+        15)
+
   def testFireNamedArgsWithEquals(self):
     self.assertEqual(fire.Fire(tc.WithDefaults,
                                command=['double', '--count=5']), 10)
     self.assertEqual(fire.Fire(tc.WithDefaults,
                                command=['triple', '--count=5']), 15)
 
+  def testFireNamedArgsWithEqualsSingleHyphen(self):
+    self.assertEqual(fire.Fire(tc.WithDefaults,
+                               command=['double', '-count=5']), 10)
+    self.assertEqual(fire.Fire(tc.WithDefaults,
+                               command=['triple', '-count=5']), 15)
+
   def testFireAllNamedArgs(self):
     self.assertEqual(fire.Fire(tc.MixedDefaults, command=['sum', '1', '2']), 5)
     self.assertEqual(fire.Fire(tc.MixedDefaults,
@@ -354,6 +372,23 @@ def testBoolParsingContinued(self):
         fire.Fire(tc.MixedDefaults, command=['identity', '10', '--beta']),
         (10, True))
 
+  def testBoolParsingSingleHyphen(self):
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '-alpha=False', '10']), (False, 10))
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '-alpha', '-beta', '10']), (True, 10))
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '-alpha', '-beta=10']), (True, 10))
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '-noalpha', '-beta']), (False, True))
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '-alpha', '-10', '-beta']), (-10, True))
+
   def testBoolParsingLessExpectedCases(self):
     # Note: Does not return (True, 10).
     self.assertEqual(
@@ -560,6 +595,11 @@ def testSeparatorForChaining(self):
                            '--separator', '$$']),
         True)
 
+  def testNegativeNumbers(self):
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['sum', '--alpha', '-3', '--beta', '-4']), -11)
+
   def testFloatForExpectedInt(self):
     self.assertEqual(
         fire.Fire(tc.MixedDefaults,

From fe1fda46d9ae2495920a0b7e78ecf7bb054fe727 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 8 Oct 2018 18:15:05 -0700
Subject: [PATCH 039/324] Single-character flag improvements:

Gives exact matches priority for shortcut flags (-a)
and supports equal signs with single-character args (-a=10 should be OK)

PiperOrigin-RevId: 216056777
Change-Id: I2fdf96842b59b2b90c15b384a13997b800c538c4
Reviewed-on: https://team-review.git.corp.google.com/c/332035
Reviewed-by: David Bieber <dbieber@google.com>
---
 fire/core.py      | 117 +++++++++++++++++++++++++++-------------------
 fire/fire_test.py |  21 ++++++++-
 2 files changed, 88 insertions(+), 50 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index 549bf701..02fe1d3f 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -129,12 +129,11 @@ def Fire(component=None, command=None, name=None):
   component_trace = _Fire(component, args, context, name)
 
   if component_trace.HasError():
-    for help_flag in ['-h', '--help']:
+    for help_flag in ('-h', '--help'):
       if help_flag in component_trace.elements[-1].args:
         command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
-        print(('WARNING: The proper way to show help is {cmd}.\n'
-               'Showing help anyway.\n').format(cmd=pipes.quote(command)),
-              file=sys.stderr)
+        print('INFO: Showing help with the command {cmd}.\n'.format(
+            cmd=pipes.quote(command)), file=sys.stderr)
     _PrintError(component_trace)
     raise FireExit(2, component_trace)
   elif component_trace.show_trace and component_trace.show_help:
@@ -209,9 +208,7 @@ def _IsHelpShortcut(component_trace, remaining_args):
   show_help = False
   if remaining_args:
     target = remaining_args[0]
-    if target == '-h':
-      show_help = True
-    elif target == '--help':
+    if target in ('-h', '--help'):
       # Check if --help would be consumed as a keyword argument, or is a member.
       component = component_trace.GetResult()
       if inspect.isclass(component) or inspect.isroutine(component):
@@ -487,7 +484,7 @@ def _Fire(component, args, context, name=None):
           component_trace.AddError(error, initial_args)
           return component_trace
 
-        # If we can't access the member, we try to treat component as a callable
+        # If we can't access the member, try to treat component as a callable.
         try:
           component, remaining_args = _CallAndUpdateTrace(component,
                                                           remaining_args,
@@ -589,9 +586,9 @@ def _CallAndUpdateTrace(component, args, component_trace, treatment='class',
     args: Args for calling the component
     component_trace: FireTrace object that contains action trace
     treatment: Type of treatment used. Indicating whether we treat the component
-               as a class, a routine, or a callable.
+        as a class, a routine, or a callable.
     target: Target in FireTrace element, default is None. If the value is None,
-            the component itself will be used as target.
+        the component itself will be used as target.
   Returns:
     component: The object that is the result of the callable call.
     remaining_args: The remaining args that haven't been consumed yet.
@@ -791,33 +788,53 @@ def _ParseKeywordArgs(args, fn_spec):
       skip_argument = False
       continue
 
-    arg_consumed = False
     if _IsFlag(argument):
-      # This is a named argument; get its value from this arg or the next.
-      got_argument = False
-
-      keyword = ''
-      if _IsSingleCharFlag(argument):
-        keychar = argument.lstrip('-')
-        potential_args = [arg for arg in fn_args if arg[0] == keychar]
-        if len(potential_args) == 1:
-          keyword = potential_args[0]
-        elif len(potential_args) > 1:
-          raise FireError("The argument '{}' is ambiguous as it could "
-                          "refer to any of the following arguments: {}".format(
-                              argument, potential_args))
+      # This is a named argument. We get its value from this arg or the next.
+
+      # Terminology:
+      # argument: A full token from the command line, e.g. '--alpha=10'
+      # stripped_argument: An argument without leading hyphens.
+      # key: The contents of the stripped argument up to the first equal sign.
+      # "shortcut flag": refers to an argument where the key is just the first
+      #   letter of a longer keyword.
+      # keyword: The Python function argument being set by this argument.
+      # value: The unparsed value for that Python function argument.
+      contains_equals = '=' in argument
+      stripped_argument = argument.lstrip('-')
+      if contains_equals:
+        key, value = stripped_argument.split('=', 1)
       else:
-        keyword = argument.lstrip('-')
+        key = stripped_argument
 
-      contains_equals = '=' in keyword
+      key = key.replace('-', '_')
       is_bool_syntax = (not contains_equals and
                         (index + 1 == len(args) or _IsFlag(args[index + 1])))
-      if contains_equals:
-        keyword, value = keyword.split('=', 1)
+
+      # Determine the keyword.
+      keyword = ''  # Indicates no valid keyword has been found yet.
+      if (key in fn_args
+          or (is_bool_syntax and key.startswith('no') and key[2:] in fn_args)
+          or fn_keywords):
+        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("The argument '{}' is ambiguous as it could "
+                          "refer to any of the following arguments: {}".format(
+                              argument, matching_fn_args))
+
+      # Determine the value.
+      if not keyword:
+        got_argument = False
+      elif contains_equals:
+        # Already got the value above.
         got_argument = True
       elif is_bool_syntax:
-        # Since there's no next arg or the next arg is a Flag, we consider
-        # this flag to be a boolean.
+        # There's no next arg or the next arg is a Flag, so we consider this
+        # flag to be a boolean.
         got_argument = True
         if keyword in fn_args:
           value = 'True'
@@ -827,40 +844,44 @@ def _ParseKeywordArgs(args, fn_spec):
         else:
           value = 'True'
       else:
-        if index + 1 < len(args):
-          value = args[index + 1]
-          got_argument = True
-
-      keyword = keyword.replace('-', '_')
+        # The assert should pass. Otherwise either contains_equals or
+        # is_bool_syntax would have been True.
+        assert index + 1 < len(args)
+        value = args[index + 1]
+        got_argument = True
 
       # In order for us to consume the argument as a keyword arg, we either:
       # Need to be explicitly expecting the keyword, or we need to be
       # accepting **kwargs.
+      skip_argument = not contains_equals and not is_bool_syntax
       if got_argument:
-        skip_argument = not contains_equals and not is_bool_syntax
-        arg_consumed = True
-        if keyword in fn_args or fn_keywords:
-          kwargs[keyword] = value
-        else:
-          remaining_kwargs.append(argument)
-          if skip_argument:
-            remaining_kwargs.append(args[index + 1])
-
-    if not arg_consumed:
-      # The argument was not consumed, so it is still a remaining argument.
+        kwargs[keyword] = value
+      else:
+        remaining_kwargs.append(argument)
+        if skip_argument:
+          remaining_kwargs.append(args[index + 1])
+    else:  # not _IsFlag(argument)
       remaining_args.append(argument)
 
   return kwargs, remaining_kwargs, remaining_args
 
 
 def _IsFlag(argument):
-  """Determines if the argument is a flag argument."""
+  """Determines if the argument is a flag argument.
+
+  If it starts with a hyphen and isn't a negative number, it's a flag.
+
+  Args:
+    argument: A command line argument that may or may not be a flag.
+  Returns:
+    A boolean indicating whether the argument is a flag.
+  """
   return _IsSingleCharFlag(argument) or _IsMultiCharFlag(argument)
 
 
 def _IsSingleCharFlag(argument):
   """Determines if the argument is a single char flag (e.g. '-a')."""
-  return re.match('^-[a-zA-Z]$', argument)
+  return re.match('^-[a-zA-Z]$', argument) or re.match('^-[a-zA-Z]=', argument)
 
 
 def _IsMultiCharFlag(argument):
diff --git a/fire/fire_test.py b/fire/fire_test.py
index 398a9be8..b19621c8 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -441,10 +441,27 @@ def testSingleCharFlagParsing(self):
                   command=['identity', '--alpha', 'True', '-b', '10']),
         (True, 10))
     with self.assertRaisesFireExit(2):
-      # This test attempts to use a boolean shortcut on a function with
-      # a naming conflict for the shortcut, triggering a FireError
+      # This test attempts to use an ambiguous shortcut flag on a function with
+      # a naming conflict for the shortcut, triggering a FireError.
       fire.Fire(tc.SimilarArgNames, command=['identity', '-b'])
 
+  def testSingleCharFlagParsingEqualSign(self):
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '-a=True']), (True, '0'))
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '-a=3', '--beta=10']), (3, 10))
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '-a=False', '-b=15']), (False, 15))
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '-a', '42', '-b=12']), (42, 12))
+    self.assertEqual(
+        fire.Fire(tc.MixedDefaults,
+                  command=['identity', '-a=42', '-b', '10']), (42, 10))
+
   def testSingleCharFlagParsingCapitalLetter(self):
     self.assertEqual(
         fire.Fire(tc.CapitalizedArgNames,

From 5f83beecdaa6f7525fa3236516bfba916e39ebd8 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 8 Oct 2018 18:30:27 -0700
Subject: [PATCH 040/324] Adds test for 1-letter arg names.

PiperOrigin-RevId: 216285688
Change-Id: I5ab05bc08432562e3c5e30f7bad43d7765ba2f70
Reviewed-on: https://team-review.git.corp.google.com/c/332036
Reviewed-by: David Bieber <dbieber@google.com>
---
 fire/fire_test.py       | 17 +++++++++++++++++
 fire/test_components.py |  3 +++
 2 files changed, 20 insertions(+)

diff --git a/fire/fire_test.py b/fire/fire_test.py
index b19621c8..734c123e 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -462,6 +462,23 @@ def testSingleCharFlagParsingEqualSign(self):
         fire.Fire(tc.MixedDefaults,
                   command=['identity', '-a=42', '-b', '10']), (42, 10))
 
+  def testSingleCharFlagParsingExactMatch(self):
+    self.assertEqual(
+        fire.Fire(tc.SimilarArgNames,
+                  command=['identity2', '-a']), (True, None))
+    self.assertEqual(
+        fire.Fire(tc.SimilarArgNames,
+                  command=['identity2', '-a=10']), (10, None))
+    self.assertEqual(
+        fire.Fire(tc.SimilarArgNames,
+                  command=['identity2', '--a']), (True, None))
+    self.assertEqual(
+        fire.Fire(tc.SimilarArgNames,
+                  command=['identity2', '-alpha']), (None, True))
+    self.assertEqual(
+        fire.Fire(tc.SimilarArgNames,
+                  command=['identity2', '-a', '-alpha']), (True, True))
+
   def testSingleCharFlagParsingCapitalLetter(self):
     self.assertEqual(
         fire.Fire(tc.CapitalizedArgNames,
diff --git a/fire/test_components.py b/fire/test_components.py
index f09bddbe..3530b151 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -109,6 +109,9 @@ class SimilarArgNames(object):
   def identity(self, bool_one=False, bool_two=False):
     return bool_one, bool_two
 
+  def identity2(self, a=None, alpha=None):
+    return a, alpha
+
 
 class CapitalizedArgNames(object):
 

From 687bf37aa71d87ba8dd7f859fb0f73f84cbb93bc Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Tue, 9 Oct 2018 10:26:05 -0700
Subject: [PATCH 041/324] Inline _CallCallable into _CallAndUpdateTrace.

PiperOrigin-RevId: 216367260
Change-Id: I5ab2e522be81655ba412bf534d3493a36bcd181f
Reviewed-on: https://team-review.git.corp.google.com/c/332673
Reviewed-by: David Bieber <dbieber@google.com>
---
 fire/core.py | 32 +++++++-------------------------
 1 file changed, 7 insertions(+), 25 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index 02fe1d3f..14eed638 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -575,11 +575,10 @@ def _GetMember(component, args):
 
 def _CallAndUpdateTrace(component, args, component_trace, treatment='class',
                         target=None):
-  """Call the component and update FireTrace.
+  """Call the component by consuming args from args, and update the FireTrace.
 
-  The component could a class, a routine, or a callable object. This function
-  will attempt to call the callable and add corresponding action trace to
-  component_trace if the invocation is successful. If not, raise FireError.
+  The component could be a class, a routine, or a callable object. This function
+  calls the component and adds the appropriate action to component_trace.
 
   Args:
     component: The component to call
@@ -596,8 +595,10 @@ def _CallAndUpdateTrace(component, args, component_trace, treatment='class',
   if not target:
     target = component
   filename, lineno = inspectutils.GetFileAndLine(component)
-  component, consumed_args, remaining_args, capacity = _CallCallable(
-      component.__call__ if treatment == 'callable' else component, args)
+  fn = component.__call__ if treatment == 'callable' else component
+  parse = _MakeParseFn(fn)
+  (varargs, kwargs), consumed_args, remaining_args, capacity = parse(args)
+  component = fn(*varargs, **kwargs)
 
   if treatment == 'class':
     action = trace.INSTANTIATED_CLASS
@@ -612,25 +613,6 @@ def _CallAndUpdateTrace(component, args, component_trace, treatment='class',
   return component, remaining_args
 
 
-def _CallCallable(fn, args):
-  """Calls the function fn by consuming args from args.
-
-  Args:
-    fn: The function to call or class to instantiate.
-    args: Args from which to consume for calling the function.
-  Returns:
-    component: The object that is the result of the function call.
-    consumed_args: The args that were consumed for the function call.
-    remaining_args: The remaining args that haven't been consumed yet.
-    capacity: Whether the call could have taken additional args.
-  """
-  parse = _MakeParseFn(fn)
-  (varargs, kwargs), consumed_args, remaining_args, capacity = parse(args)
-
-  result = fn(*varargs, **kwargs)
-  return result, consumed_args, remaining_args, capacity
-
-
 def _MakeParseFn(fn):
   """Creates a parse function for fn.
 

From 5867c55439ec7ac7b5f88791af28ff2c1c4edf92 Mon Sep 17 00:00:00 2001
From: James George <jamesgeorge998001@gmail.com>
Date: Sat, 1 Dec 2018 03:00:46 +0530
Subject: [PATCH 042/324] include license information in readme (#147)

Add license information
---
 README.md | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/README.md b/README.md
index 9c407ecc..10d6d4fd 100644
--- a/README.md
+++ b/README.md
@@ -88,7 +88,10 @@ Please see [The Python Fire Guide](docs/guide.md).
 
 _Note that flags are separated from the Fire command by an isolated `--` arg._
 
+## License
 
+  Licensed under the [Apache 2.0](https://github.com/google/python-fire/blob/master/LICENSE) License.
+  
 ## Disclaimer
 
 This is not an official Google product.

From 1d0465e8c1e7be1308b8540e41a8020854197312 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Tue, 4 Dec 2018 13:36:56 -0800
Subject: [PATCH 043/324] docstring parser and initial commit for new usage
 screens.

PiperOrigin-RevId: 224036127
Change-Id: Ia0cd2d79b743f31ea7b6fc00686338a79fcca79b
Reviewed-on: https://team-review.git.corp.google.com/c/370510
Reviewed-by: David Bieber <dbieber@google.com>
---
 .travis.yml                  |   1 +
 fire/completion.py           |   8 +-
 fire/core.py                 |   4 +-
 fire/docstrings.py           | 695 +++++++++++++++++++++++++++++++++++
 fire/docstrings_fuzz_test.py |  41 +++
 fire/docstrings_test.py      | 237 ++++++++++++
 fire/helputils.py            | 140 ++++++-
 fire/helputils_test.py       | 119 ++++++
 fire/test_components.py      |  12 +
 fire/testutils.py            |   6 +-
 fire/trace.py                |  22 ++
 fire/value_types.py          |  39 ++
 12 files changed, 1309 insertions(+), 15 deletions(-)
 create mode 100644 fire/docstrings.py
 create mode 100644 fire/docstrings_fuzz_test.py
 create mode 100644 fire/docstrings_test.py
 create mode 100644 fire/value_types.py

diff --git a/.travis.yml b/.travis.yml
index 7f0f5195..f877b2f3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,6 +8,7 @@ before_install:
   - pip install --upgrade setuptools pip
   - pip install --upgrade pylint pytest pytest-pylint pytest-runner
 install:
+  - pip install hypothesis
   - python setup.py develop
 script:
   - python -m pytest  # Run the tests without IPython.
diff --git a/fire/completion.py b/fire/completion.py
index 62af5a44..aeed5f89 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -18,8 +18,8 @@
 from __future__ import division
 from __future__ import print_function
 
-from collections import defaultdict
-from copy import copy
+import collections
+import copy
 import inspect
 
 from fire import inspectutils
@@ -47,7 +47,7 @@ def _BashScript(name, commands, default_options=None):
     completion in Bash.
   """
   default_options = default_options or set()
-  options_map = defaultdict(lambda: copy(default_options))
+  options_map = collections.defaultdict(lambda: copy.copy(default_options))
   for command in commands:
     start = (name + ' ' + ' '.join(command[:-1])).strip()
     completion = _FormatForCommand(command[-1])
@@ -114,7 +114,7 @@ def _FishScript(name, commands, default_options=None):
     completion in Fish.
   """
   default_options = default_options or set()
-  options_map = defaultdict(lambda: copy(default_options))
+  options_map = collections.defaultdict(lambda: copy.copy(default_options))
   for command in commands:
     start = (name + ' ' + ' '.join(command[:-1])).strip()
     completion = _FormatForCommand(command[-1])
diff --git a/fire/core.py b/fire/core.py
index 14eed638..cafa4ab8 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -69,6 +69,7 @@ def main(argv):
 from fire import interact
 from fire import parser
 from fire import trace
+from fire import value_types
 import six
 
 
@@ -242,8 +243,7 @@ def _PrintResult(component_trace, verbose=False):
     print(_DictAsString(result, verbose))
   elif isinstance(result, tuple):
     print(_OneLineResult(result))
-  elif isinstance(result,
-                  (bool, six.string_types, six.integer_types, float, complex)):
+  elif isinstance(result, value_types.VALUE_TYPES):
     print(result)
   elif result is not None:
     print(helputils.HelpString(result, component_trace, verbose))
diff --git a/fire/docstrings.py b/fire/docstrings.py
new file mode 100644
index 00000000..e250128a
--- /dev/null
+++ b/fire/docstrings.py
@@ -0,0 +1,695 @@
+# 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.
+
+"""Docstring parsing module for Python Fire.
+
+The following features of docstrings are not supported.
+TODO(dbieber): Support these features.
+- numpy docstrings may begin with the function signature.
+- whitespace may be important for proper structuring of a docstring
+- I've seen `argname` (with single backticks) as a style of documenting
+  arguments. The `argname` appears on one line, and the description on the next.
+- .. Sphinx directives such as .. note:: are not understood.
+- After a section ends, future contents may be included in the section. E.g.
+  :returns: This is what is returned.
+  Example: An example goes here.
+- @param is sometimes used.  E.g.
+  @param argname (type) Description
+  @return (type) Description
+- The true signature of a function is not used by the docstring parser. It could
+  be useful for determining whether something is a section header or an argument
+  for example.
+- This example confuses types as part of the docstrings.
+  Parameters
+  argname : argtype
+  Arg description
+- If there's no blank line after the summary, the description will be slurped
+  up into the summary.
+- "Examples" should be its own section type. aka "Usage".
+- "Notes" should be a section type.
+- Some people put parenthesis around their types in RST format, e.g.
+  :param (type) paramname:
+- :rtype: directive (return type)
+- Also ":rtype str" with no closing ":" has come up.
+- Return types are not supported.
+- "# Returns" as a section title style
+- ":raises ExceptionType: Description" ignores the ExceptionType currently.
+- "Defaults to X" occurs sometimes.
+- "True | False" indicates bool type.
+"""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+
+import collections
+import re
+
+import enum
+
+
+class DocstringInfo(
+    collections.namedtuple(
+        'DocstringInfo',
+        ('summary', 'description', 'args', 'returns', 'yields', 'raises'))):
+  pass
+DocstringInfo.__new__.__defaults__ = (None,) * len(DocstringInfo._fields)
+
+
+class ArgInfo(
+    collections.namedtuple(
+        'ArgInfo',
+        ('name', 'type', 'description'))):
+  pass
+ArgInfo.__new__.__defaults__ = (None,) * len(ArgInfo._fields)
+
+
+class Namespace(dict):
+  """A dict with attribute (dot-notation) access enabled."""
+
+  def __getattr__(self, key):
+    if key not in self:
+      self[key] = Namespace()
+    return self.get(key)
+
+  def __setattr__(self, key, value):
+    self[key] = value
+
+  def __delattr__(self, key):
+    if key in self:
+      del self[key]
+
+
+class Sections(enum.Enum):
+  ARGS = 0
+  RETURNS = 1
+  YIELDS = 2
+  RAISES = 3
+  TYPE = 4
+
+
+class Formats(enum.Enum):
+  GOOGLE = 0
+  NUMPY = 1
+  RST = 2
+
+
+SECTION_TITLES = {
+    Sections.ARGS: ('argument', 'arg', 'parameter', 'param'),
+    Sections.RETURNS: ('return',),
+    Sections.YIELDS: ('yield',),
+    Sections.RAISES: ('raise', 'except', 'exception', 'throw', 'error', 'warn'),
+    Sections.TYPE: ('type',),  # rst-only
+}
+
+
+def parse(docstring):
+  """Returns DocstringInfo about the given docstring.
+
+  This parser aims to parse Google, numpy, and rst formatted docstrings. These
+  are the three most common docstring styles at the time of this writing.
+
+  This parser aims to be permissive, working even when the docstring deviates
+  from the strict recommendations of these styles.
+
+  This parser does not aim to fully extract all structured information from a
+  docstring, since there are simply too many ways to structure information in a
+  docstring. Sometimes content will remain as unstructured text and simply gets
+  included in the description.
+
+  The Google docstring style guide is available at:
+  https://github.com/google/styleguide/blob/gh-pages/pyguide.md
+
+  The numpy docstring style guide is available at:
+  https://numpydoc.readthedocs.io/en/latest/format.html
+
+  Information about the rST docstring format is available at:
+  https://www.python.org/dev/peps/pep-0287/
+  The full set of directives such as param and type for rST docstrings are at:
+  http://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html
+
+  Note: This function does not claim to handle all docstrings well. A list of
+  limitations is available at the top of the file. It does aim to run without
+  crashing in O(n) time on all strings on length n. If you find a string that
+  causes this to crash or run unacceptably slowly, please consider submitting
+  a pull request.
+
+  Args:
+    docstring: The docstring to parse.
+  Returns:
+    A DocstringInfo containing information about the docstring.
+  """
+  if docstring is None:
+    return DocstringInfo()
+
+  lines = docstring.strip().split('\n')
+  lines_len = len(lines)
+  state = Namespace()  # TODO(dbieber): Switch to an explicit class.
+
+  # Variables in state include:
+  state.section.title = None
+  state.section.indentation = None
+  state.section.line1_indentation = None
+  state.section.format = None
+  state.summary.permitted = True
+  state.summary.lines = []
+  state.description.lines = []
+  state.args = []
+  state.current_arg = None
+  state.returns.lines = []
+  state.yields.lines = []
+  state.raises.lines = []
+
+  for index, line in enumerate(lines):
+    has_next = index + 1 < lines_len
+    next_line = lines[index + 1] if has_next else None
+    line_info = _create_line_info(line, next_line)
+    _consume_line(line_info, state)
+
+  summary = ' '.join(state.summary.lines) if state.summary.lines else None
+  description = _join_lines(state.description.lines)
+  returns = _join_lines(state.returns.lines)
+  yields = _join_lines(state.yields.lines)
+  raises = _join_lines(state.raises.lines)
+
+  args = [
+      ArgInfo(
+          name=arg.name,
+          type=_cast_to_known_type(_join_lines(arg.type.lines)),
+          description=_join_lines(arg.description.lines),
+      )
+      for arg in state.args
+  ]
+
+  return DocstringInfo(
+      summary=summary,
+      description=description,
+      args=args or None,
+      returns=returns,
+      raises=raises,
+      yields=yields,
+  )
+
+
+def _join_lines(lines):
+  """Joins lines with the appropriate connective whitespace.
+
+  This puts a single space between consecutive lines, unless there's a blank
+  line, in which case a full blank line is included.
+
+  Args:
+    lines: A list of lines to join.
+  Returns:
+    A string, the lines joined together.
+  """
+  # TODO(dbieber): Add parameters for variations in whitespace handling.
+  if not lines:
+    return None
+
+  started = False
+  group_texts = []  # Full text of each section.
+  group_lines = []  # Lines within the current section.
+  for line in lines:
+    stripped_line = line.strip()
+    if stripped_line:
+      started = True
+      group_lines.append(stripped_line)
+    else:
+      if started:
+        group_text = ' '.join(group_lines)
+        group_texts.append(group_text)
+        group_lines = []
+
+  if group_lines:  # Process the final group.
+    group_text = ' '.join(group_lines)
+    group_texts.append(group_text)
+
+  return '\n\n'.join(group_texts)
+
+
+def _get_or_create_arg_by_name(state, name):
+  """Gets or creates a new Arg.
+
+  These Arg objects (Namespaces) are turned into the ArgInfo namedtuples
+  returned by parse. Each Arg object is used to collect the name, type, and
+  description of a single argument to the docstring's function.
+
+  Args:
+    state: The state of the parser.
+    name: The name of the arg to create.
+  Returns:
+    The new Arg.
+  """
+  for arg in state.args:
+    if arg.name == name:
+      return arg
+  arg = Namespace()  # TODO(dbieber): Switch to an explicit class.
+  arg.name = name
+  arg.type.lines = []
+  arg.description.lines = []
+  state.args.append(arg)
+  return arg
+
+
+def _is_arg_name(name):
+  """Returns whether name is a valid arg name.
+
+  This is used to prevent multiple words (plaintext) from being misinterpretted
+  as an argument name. So if ":" appears in the middle of a line in a docstring,
+  we don't accidentally interpret the first half of that line as a single arg
+  name.
+
+  Args:
+    name: The name of the potential arg.
+  Returns:
+    True if name looks like an arg name, False otherwise.
+  """
+  name = name.strip()
+  return (name
+          and ' ' not in name
+          and ':' not in name)
+
+
+def _as_arg_name_and_type(text):
+  """Returns text as a name and type, if text looks like an arg name and type.
+
+  Example:
+    _as_arg_name_and_type("foo (int)") == "foo", "int"
+
+  Args:
+    text: The text, which may or may not be an arg name and type.
+  Returns:
+    The arg name and type, if text looks like an arg name and type.
+    None otherwise.
+  """
+  tokens = text.split()
+  if len(tokens) < 2:
+    return None
+  if _is_arg_name(tokens[0]):
+    type_token = ' '.join(tokens[1:])
+    type_token = type_token.lstrip('{([').rstrip('])}')
+    return tokens[0], type_token
+  else:
+    return None
+
+
+def _as_arg_names(names_str):
+  """Converts names_str to a list of arg names.
+
+  Example:
+    _as_arg_names("a, b, c") == ["a", "b", "c"]
+
+  Args:
+    names_str: A string with multiple space or comma separated arg names.
+  Returns:
+    A list of arg names, or None if names_str doesn't look like a list of arg
+    names.
+  """
+  names = re.split(',| ', names_str)
+  names = [name.strip() for name in names if name.strip()]
+  for name in names:
+    if not _is_arg_name(name):
+      return None
+  if not names:
+    return None
+  return names
+
+
+def _cast_to_known_type(name):
+  """Canonicalizes a string representing a type if possible.
+
+  # TODO(dbieber): Support additional canonicalization, such as string/str, and
+  # boolean/bool.
+
+  Example:
+    _cast_to_known_type("str.") == "str"
+
+  Args:
+    name: A string representing a type, or None.
+  Returns:
+    A canonicalized version of the type string.
+  """
+  if name is None:
+    return None
+  return name.rstrip('.')
+
+
+def _consume_google_args_line(line_info, state):
+  """Consume a single line from a Google args section."""
+  split_line = line_info.remaining.split(':', 1)
+  if len(split_line) > 1:
+    first, second = split_line  # first is either the "arg" or "arg (type)"
+    if _is_arg_name(first.strip()):
+      arg = _get_or_create_arg_by_name(state, first.strip())
+      arg.description.lines.append(second.strip())
+      state.current_arg = arg
+    else:
+      arg_name_and_type = _as_arg_name_and_type(first)
+      if arg_name_and_type:
+        arg_name, type_str = arg_name_and_type
+        arg = _get_or_create_arg_by_name(state, arg_name)
+        arg.type.lines.append(type_str)
+        arg.description.lines.append(second.strip())
+      else:
+        if state.current_arg:
+          state.current_arg.description.lines.append(split_line[0])
+  else:
+    if state.current_arg:
+      state.current_arg.description.lines.append(split_line[0])
+
+
+def _consume_line(line_info, state):
+  """Consumes one line of text, updating the state accordingly.
+
+  When _consume_line is called, part of the line may already have been processed
+  for header information.
+
+  Args:
+    line_info: Information about the current and next line of the docstring.
+    state: The state of the docstring parser.
+  """
+  _update_section_state(line_info, state)
+
+  if state.section.title is None:
+    if state.summary.permitted:
+      if line_info.remaining:
+        state.summary.lines.append(line_info.remaining)
+      elif state.summary.lines:
+        state.summary.permitted = False
+    else:
+      # We're past the end of the summary.
+      # Additions now contribute to the description.
+      state.description.lines.append(line_info.remaining)
+  else:
+    state.summary.permitted = False
+
+  if state.section.new and state.section.format == Formats.RST:
+    # The current line starts with an RST directive, e.g. ":param arg:".
+    directive = _get_directive(line_info)
+    directive_tokens = directive.split()
+    if state.section.title == Sections.ARGS:
+      name = directive_tokens[-1]
+      arg = _get_or_create_arg_by_name(state, name)
+      if len(directive_tokens) == 3:
+        # A param directive of the form ":param type arg:".
+        arg.type.lines.append(directive_tokens[1])
+      state.current_arg = arg
+    elif state.section.title == Sections.TYPE:
+      name = directive_tokens[-1]
+      arg = _get_or_create_arg_by_name(state, name)
+      state.current_arg = arg
+
+  if (state.section.format == Formats.NUMPY and
+      _line_is_hyphens(line_info.remaining)):
+    # Skip this all-hyphens line, which is part of the numpy section header.
+    return
+
+  if state.section.title == Sections.ARGS:
+    if state.section.format == Formats.GOOGLE:
+      _consume_google_args_line(line_info, state)
+    elif state.section.format == Formats.RST:
+      state.current_arg.description.lines.append(line_info.remaining.strip())
+    elif state.section.format == Formats.NUMPY:
+      line_stripped = line_info.remaining.strip()
+      if _is_arg_name(line_stripped):
+        # Token on it's own line can either be the last word of the description
+        # of the previous arg, or a new arg. TODO: Whitespace can distinguish.
+        arg = _get_or_create_arg_by_name(state, line_stripped)
+        state.current_arg = arg
+      elif ':' in line_stripped:
+        possible_args, type_data = line_stripped.split(':', 1)
+        arg_names = _as_arg_names(possible_args)  # re.split(' |,', s)
+        if arg_names:
+          for arg_name in arg_names:
+            arg = _get_or_create_arg_by_name(state, arg_name)
+            arg.type.lines.append(type_data)
+            state.current_arg = arg  # TODO(dbieber): Multiple current args.
+        else:  # Just an ordinary line.
+          if state.current_arg:
+            state.current_arg.description.lines.append(
+                line_info.remaining.strip())
+          else:
+            # TODO(dbieber): If not a blank line, add it to the description.
+            pass
+      else:  # Just an ordinary line.
+        if state.current_arg:
+          state.current_arg.description.lines.append(
+              line_info.remaining.strip())
+        else:
+          # TODO(dbieber): If not a blank line, add it to the description.
+          pass
+
+  elif state.section.title == Sections.RETURNS:
+    state.returns.lines.append(line_info.remaining.strip())
+  elif state.section.title == Sections.YIELDS:
+    state.yields.lines.append(line_info.remaining.strip())
+  elif state.section.title == Sections.RAISES:
+    state.raises.lines.append(line_info.remaining.strip())
+  elif state.section.title == Sections.TYPE:
+    if state.section.format == Formats.RST:
+      assert state.current_arg is not None
+      state.current_arg.type.lines.append(line_info.remaining.strip())
+    else:
+      pass
+
+
+def _create_line_info(line, next_line):
+  """Returns information about the current and next line of the docstring."""
+  line_info = Namespace()  # TODO(dbieber): Switch to an explicit class.
+  line_info.line = line
+  line_info.stripped = line.strip()
+  line_info.remaining = line_info.stripped
+  line_info.indentation = len(line) - len(line.lstrip())
+  line_info.next.line = next_line
+  line_info.next.stripped = next_line.strip() if next_line else None
+  line_info.next.indentation = (
+      len(next_line) - len(next_line.lstrip()) if next_line else None)
+  # Note: This counts all whitespace equally.
+  return line_info
+
+
+def _update_section_state(line_info, state):
+  """Uses line_info to determine the current section of the docstring.
+
+  Updates state and line_info.remaining.
+
+  Args:
+    line_info: Information about the current line.
+    state: The state of the parser.
+  """
+  section_updated = False
+
+  google_section_permitted = _google_section_permitted(line_info, state)
+  google_section = google_section_permitted and _google_section(line_info)
+  if google_section:
+    state.section.format = Formats.GOOGLE
+    state.section.title = google_section
+    line_info.remaining = _get_after_google_header(line_info)
+    section_updated = True
+
+  rst_section = _rst_section(line_info)
+  if rst_section:
+    state.section.format = Formats.RST
+    state.section.title = rst_section
+    line_info.remaining = _get_after_directive(line_info)
+    section_updated = True
+
+  numpy_section = _numpy_section(line_info)
+  if numpy_section:
+    state.section.format = Formats.NUMPY
+    state.section.title = numpy_section
+    line_info.remaining = ''
+    section_updated = True
+
+  if section_updated:
+    state.section.new = True
+    state.section.indentation = line_info.indentation
+    state.section.line1_indentation = line_info.next.indentation
+  else:
+    state.section.new = False
+
+
+def _google_section_permitted(line_info, state):
+  """Returns whether a new google section is permitted to start here.
+
+  Q: Why might a new Google section not be allowed?
+  A: If we're in the middle of a Google "Args" section, then lines that start
+  "param:" will usually be a new arg, rather than a new section.
+  We use whitespace to determine when the Args section has actually ended.
+
+  A Google section ends when either:
+  - A new google section begins at either
+    - indentation less than indentation of line 1 of the previous section
+    - or <= indentation of the previous section
+  - Or the docstring terminates.
+
+  Args:
+    line_info: Information about the current line.
+    state: The state of the parser.
+  Returns:
+    True or False, indicating whether a new Google section is permitted at the
+    current line.
+  """
+  if state.section.indentation is None:  # We're not in a section yet.
+    return True
+  return (line_info.indentation <= state.section.indentation
+          or line_info.indentation < state.section.line1_indentation)
+
+
+def _matches_section_title(title, section_title):
+  """Returns whether title is a match for a specific section_title.
+
+  Example:
+    _matches_section_title('Yields', 'yield') == True
+
+  Args:
+    title: The title to check for matching.
+    section_title: A specific known section title to check against.
+  """
+  title = title.lower()
+  section_title = section_title.lower()
+  return section_title in (title, title[:-1])  # Supports plurals / some typos.
+
+
+def _matches_section(title, section):
+  """Returns whether title is a match any known title for a specific section.
+
+  Example:
+    _matches_section_title('Yields', Sections.YIELDS) == True
+    _matches_section_title('param', Sections.Args) == True
+
+  Args:
+    title: The title to check for matching.
+    section: A specific section to check all possible titles for.
+  Returns:
+    True or False, indicating whether title is a match for the specified
+    section.
+  """
+  for section_title in SECTION_TITLES[section]:
+    if _matches_section_title(title, section_title):
+      return True
+  return False
+
+
+def _section_from_possible_title(possible_title):
+  """Returns a section matched by the possible title, or None if none match.
+
+  Args:
+    possible_title: A string that may be the title of a new section.
+  Returns:
+    A Section type if one matches, or None if no section type matches.
+  """
+  for section in SECTION_TITLES:
+    if _matches_section(possible_title, section):
+      return section
+  return None
+
+
+def _google_section(line_info):
+  """Checks whether the current line is the start of a new Google-style section.
+
+  This docstring is a Google-style docstring. Google-style sections look like
+  this:
+
+    Section Name:
+      section body goes here
+
+  Args:
+    line_info: Information about the current line.
+  Returns:
+    A Section type if one matches, or None if no section type matches.
+  """
+  colon_index = line_info.remaining.find(':')
+  possible_title = line_info.remaining[:colon_index]
+  return _section_from_possible_title(possible_title)
+
+
+def _get_after_google_header(line_info):
+  """Gets the remainder of the line, after a Google header."""
+  colon_index = line_info.remaining.find(':')
+  return line_info.remaining[colon_index + 1:]
+
+
+def _get_directive(line_info):
+  """Gets a directive from the start of the line.
+
+  If the line is ":param str foo: Description of foo", then
+  _get_directive(line_info) returns "param str foo".
+
+  Args:
+    line_info: Information about the current line.
+  Returns:
+    The contents of a directive, or None if the line doesn't start with a
+    directive.
+  """
+  if line_info.stripped.startswith(':'):
+    return line_info.stripped.split(':', 2)[1]
+  else:
+    return None
+
+
+def _get_after_directive(line_info):
+  """Gets the remainder of the line, after a directive."""
+  sections = line_info.stripped.split(':', 2)
+  if len(sections) > 2:
+    return sections[-1]
+  else:
+    return ''
+
+
+def _rst_section(line_info):
+  """Checks whether the current line is the start of a new RST-style section.
+
+  RST uses directives to specify information. An RST directive, which we refer
+  to as a section here, are surrounded with colons. For example, :param name:.
+
+  Args:
+    line_info: Information about the current line.
+  Returns:
+    A Section type if one matches, or None if no section type matches.
+  """
+  directive = _get_directive(line_info)
+  if directive:
+    possible_title = directive.split()[0]
+    return _section_from_possible_title(possible_title)
+  else:
+    return None
+
+
+def _line_is_hyphens(line):
+  """Returns whether the line is entirely hyphens (and not blank)."""
+  return line and not line.strip('-')
+
+
+def _numpy_section(line_info):
+  """Checks whether the current line is the start of a new numpy-style section.
+
+  Numpy style sections are followed by a full line of hyphens, for example:
+
+    Section Name
+    ------------
+    Section body goes here.
+
+  Args:
+    line_info: Information about the current line.
+  Returns:
+    A Section type if one matches, or None if no section type matches.
+  """
+  next_line_is_hyphens = _line_is_hyphens(line_info.next.stripped)
+  if next_line_is_hyphens:
+    possible_title = line_info.remaining
+    return _section_from_possible_title(possible_title)
+  else:
+    return None
diff --git a/fire/docstrings_fuzz_test.py b/fire/docstrings_fuzz_test.py
new file mode 100644
index 00000000..a7ede06a
--- /dev/null
+++ b/fire/docstrings_fuzz_test.py
@@ -0,0 +1,41 @@
+# 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.
+
+"""Fuzz tests for the docstring parser module."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+from fire import docstrings
+from fire import testutils
+
+from hypothesis import example
+from hypothesis import given
+from hypothesis import settings
+from hypothesis import strategies as st
+
+
+class DocstringsFuzzTest(testutils.BaseTestCase):
+
+  @settings(max_examples=10000,
+            timeout=120)
+  @given(st.text(min_size=1))
+  @example('This is a one-line docstring.')
+  def test_fuzz_parse(self, value):
+    docstrings.parse(value)
+
+
+if __name__ == '__main__':
+  testutils.main()
diff --git a/fire/docstrings_test.py b/fire/docstrings_test.py
new file mode 100644
index 00000000..96810c7e
--- /dev/null
+++ b/fire/docstrings_test.py
@@ -0,0 +1,237 @@
+# 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.
+
+"""Tests for fire docstrings module."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+from fire import docstrings
+from fire import testutils
+
+
+DocstringInfo = docstrings.DocstringInfo  # pylint: disable=invalid-name
+ArgInfo = docstrings.ArgInfo  # pylint: disable=invalid-name
+
+
+class DocstringsTest(testutils.BaseTestCase):
+
+  def test_one_line_simple(self):
+    docstring = """A simple one line docstring."""
+    docstring_info = docstrings.parse(docstring)
+    expected_docstring_info = DocstringInfo(
+        summary='A simple one line docstring.',
+    )
+    self.assertEqual(docstring_info, expected_docstring_info)
+
+  def test_one_line_simple_whitespace(self):
+    docstring = """
+      A simple one line docstring.
+    """
+    docstring_info = docstrings.parse(docstring)
+    expected_docstring_info = DocstringInfo(
+        summary='A simple one line docstring.',
+    )
+    self.assertEqual(docstring_info, expected_docstring_info)
+
+  def test_one_line_too_long(self):
+    # pylint: disable=line-too-long
+    docstring = """A one line docstring thats both a little too verbose and a little too long so it keeps going well beyond a reasonable length for a one-liner.
+    """
+    # pylint: enable=line-too-long
+    docstring_info = docstrings.parse(docstring)
+    expected_docstring_info = DocstringInfo(
+        summary='A one line docstring thats both a little too verbose and '
+        'a little too long so it keeps going well beyond a reasonable length '
+        'for a one-liner.',
+    )
+    self.assertEqual(docstring_info, expected_docstring_info)
+
+  def test_one_line_runs_over(self):
+    # pylint: disable=line-too-long
+    docstring = """A one line docstring thats both a little too verbose and a little too long
+    so it runs onto a second line.
+    """
+    # pylint: enable=line-too-long
+    docstring_info = docstrings.parse(docstring)
+    expected_docstring_info = DocstringInfo(
+        summary='A one line docstring thats both a little too verbose and '
+        'a little too long so it runs onto a second line.',
+    )
+    self.assertEqual(docstring_info, expected_docstring_info)
+
+  def test_one_line_runs_over_whitespace(self):
+    docstring = """
+      A one line docstring thats both a little too verbose and a little too long
+      so it runs onto a second line.
+    """
+    docstring_info = docstrings.parse(docstring)
+    expected_docstring_info = DocstringInfo(
+        summary='A one line docstring thats both a little too verbose and '
+        'a little too long so it runs onto a second line.',
+    )
+    self.assertEqual(docstring_info, expected_docstring_info)
+
+  def test_google_format_args_only(self):
+    docstring = """One line description.
+
+    Args:
+      arg1: arg1_description
+      arg2: arg2_description
+    """
+    docstring_info = docstrings.parse(docstring)
+    expected_docstring_info = DocstringInfo(
+        summary='One line description.',
+        args=[
+            ArgInfo(name='arg1', description='arg1_description'),
+            ArgInfo(name='arg2', description='arg2_description'),
+        ]
+    )
+    self.assertEqual(docstring_info, expected_docstring_info)
+
+  def test_google_format_arg_named_args(self):
+    docstring = """
+    Args:
+      args: arg_description
+    """
+    docstring_info = docstrings.parse(docstring)
+    expected_docstring_info = DocstringInfo(
+        args=[
+            ArgInfo(name='args', description='arg_description'),
+        ]
+    )
+    self.assertEqual(docstring_info, expected_docstring_info)
+
+  def test_google_format_typed_args_and_returns(self):
+    docstring = """Docstring summary.
+
+    This is a longer description of the docstring. It spans multiple lines, as
+    is allowed.
+
+    Args:
+        param1 (int): The first parameter.
+        param2 (str): The second parameter.
+
+    Returns:
+        bool: The return value. True for success, False otherwise.
+    """
+    docstring_info = docstrings.parse(docstring)
+    expected_docstring_info = DocstringInfo(
+        summary='Docstring summary.',
+        description='This is a longer description of the docstring. It spans '
+        'multiple lines, as is allowed.',
+        args=[
+            ArgInfo(name='param1', type='int',
+                    description='The first parameter.'),
+            ArgInfo(name='param2', type='str',
+                    description='The second parameter.'),
+        ],
+        returns='bool: The return value. True for success, False otherwise.'
+    )
+    self.assertEqual(docstring_info, expected_docstring_info)
+
+  def test_rst_format_typed_args_and_returns(self):
+    docstring = """Docstring summary.
+
+    This is a longer description of the docstring. It spans across multiple
+    lines.
+
+    :param arg1: Description of arg1.
+    :type arg1: str.
+    :param arg2: Description of arg2.
+    :type arg2: bool.
+    :returns:  int -- description of the return value.
+    :raises: AttributeError, KeyError
+    """
+    docstring_info = docstrings.parse(docstring)
+    expected_docstring_info = DocstringInfo(
+        summary='Docstring summary.',
+        description='This is a longer description of the docstring. It spans '
+        'across multiple lines.',
+        args=[
+            ArgInfo(name='arg1', type='str',
+                    description='Description of arg1.'),
+            ArgInfo(name='arg2', type='bool',
+                    description='Description of arg2.'),
+        ],
+        returns='int -- description of the return value.',
+        raises='AttributeError, KeyError',
+    )
+    self.assertEqual(docstring_info, expected_docstring_info)
+
+  def test_numpy_format_typed_args_and_returns(self):
+    docstring = """Docstring summary.
+
+    This is a longer description of the docstring. It spans across multiple
+    lines.
+
+    Parameters
+    ----------
+    param1 : int
+        The first parameter.
+    param2 : str
+        The second parameter.
+
+    Returns
+    -------
+    bool
+        True if successful, False otherwise.
+    """
+    docstring_info = docstrings.parse(docstring)
+    expected_docstring_info = DocstringInfo(
+        summary='Docstring summary.',
+        description='This is a longer description of the docstring. It spans '
+        'across multiple lines.',
+        args=[
+            ArgInfo(name='param1', type='int',
+                    description='The first parameter.'),
+            ArgInfo(name='param2', type='str',
+                    description='The second parameter.'),
+        ],
+        # TODO(dbieber): Support return type.
+        returns='bool True if successful, False otherwise.',
+    )
+    self.assertEqual(docstring_info, expected_docstring_info)
+
+  def test_multisection_docstring(self):
+    docstring = """Docstring summary.
+
+    This is the first section of a docstring description.
+
+    This is the second section of a docstring description. This docstring
+    description has just two sections.
+    """
+    docstring_info = docstrings.parse(docstring)
+    expected_docstring_info = DocstringInfo(
+        summary='Docstring summary.',
+        description='This is the first section of a docstring description.\n\n'
+        'This is the second section of a docstring description. This docstring '
+        'description has just two sections.',
+    )
+    self.assertEqual(docstring_info, expected_docstring_info)
+
+  def test_ill_formed_docstring(self):
+    docstring = """Docstring summary.
+
+    args: raises ::
+    :
+    pathological docstrings should not fail, and ideally should behave
+    reasonably.
+    """
+    docstrings.parse(docstring)
+
+
+if __name__ == '__main__':
+  testutils.main()
diff --git a/fire/helputils.py b/fire/helputils.py
index f11254e0..277acd6e 100644
--- a/fire/helputils.py
+++ b/fire/helputils.py
@@ -25,7 +25,9 @@
 import inspect
 
 from fire import completion
+from fire import docstrings
 from fire import inspectutils
+from fire import value_types
 
 
 def _NormalizeField(field):
@@ -106,14 +108,17 @@ def HelpString(component, trace=None, verbose=False):
     String suitable for display giving information about the component.
   """
   info = inspectutils.Info(component)
+  # TODO(dbieber): Stop using UsageString in favor of UsageText.
   info['usage'] = UsageString(component, trace, verbose)
+  info['docstring_info'] = docstrings.parse(info['docstring'])
 
   is_error_screen = False
   if trace:
     is_error_screen = trace.HasError()
 
   if is_error_screen:
-    return _ErrorText(info, trace)
+    # TODO(dbieber): Call UsageText instead of CommonHelpText once ready.
+    return _CommonHelpText(info, trace)
   else:
     return _HelpText(info, trace)
 
@@ -157,20 +162,141 @@ def _CommonHelpText(info, trace=None):
   return '\n'.join(lines)
 
 
-def _ErrorText(info, trace=None):
-  """Returns help text for error screen.
+def UsageText(component, trace=None, verbose=False):
+  if inspect.isroutine(component) or inspect.isclass(component):
+    return UsageTextForFunction(component, trace)
+  else:
+    return UsageTextForObject(component, trace, verbose)
+
+
+def UsageTextForFunction(component, trace=None):
+  """Returns usage text for function objects.
+
+  Args:
+    component: The component to determine the usage text for.
+    trace: The Fire trace object containing all metadata of current execution.
+
+  Returns:
+    String suitable for display in error screen.
+  """
+
+  output_template = """Usage: {current_command} {args_and_flags}
+{availability_lines}
+For detailed information on this command, run:
+{current_command}{hyphen_hyphen} --help
+"""
+
+  if trace:
+    command = trace.GetCommand()
+    is_help_an_arg = trace.NeedsSeparatingHyphenHyphen()
+  else:
+    command = None
+    is_help_an_arg = False
+
+  if not command:
+    command = ''
 
-  Construct help text for error screen to inform the user about error occurred
+  spec = inspectutils.GetFullArgSpec(component)
+  args = spec.args
+
+  if spec.defaults is None:
+    num_defaults = 0
+  else:
+    num_defaults = len(spec.defaults)
+  args_with_no_defaults = args[:len(args) - num_defaults]
+  args_with_defaults = args[len(args) - num_defaults:]
+  flags = args_with_defaults + spec.kwonlyargs
+
+  items = [arg.upper() for arg in args_with_no_defaults]
+  if flags:
+    items.append('<flags>')
+    availability_lines = (
+        '\nAvailable flags: '
+        + ' | '.join('--' + flag for flag in flags) + '\n')
+  else:
+    availability_lines = ''
+  args_and_flags = ' '.join(items)
+
+  hyphen_hyphen = ' --' if is_help_an_arg else ''
+
+  return output_template.format(
+      current_command=command,
+      args_and_flags=args_and_flags,
+      availability_lines=availability_lines,
+      hyphen_hyphen=hyphen_hyphen)
+
+
+def UsageTextForObject(component, trace=None, verbose=False):
+  """Returns help text for usage screen for objects.
+
+  Construct help text for usage screen to inform the user about error occurred
   and correct syntax for invoking the object.
 
   Args:
-    info: The IR object containing metadata of an object.
+    component: The component to determine the usage text for.
     trace: The Fire trace object containing all metadata of current execution.
+    verbose: Whether to include private members in the usage text.
   Returns:
     String suitable for display in error screen.
   """
-  # TODO(joejoevictor): Implement real error text construction.
-  return _CommonHelpText(info, trace)
+  output_template = """Usage: {current_command} <{possible_actions}>
+{availability_lines}
+
+For detailed information on this command, run:
+{current_command} --help
+"""
+  if trace:
+    command = trace.GetCommand()
+  else:
+    command = None
+
+  if not command:
+    command = ''
+
+  groups = []
+  commands = []
+  values = []
+
+  members = completion._Members(component, verbose)  # pylint: disable=protected-access
+  for member_name, member in members:
+    if value_types.IsGroup(member):
+      groups.append(member_name)
+    if value_types.IsCommand(member):
+      commands.append(member_name)
+    if value_types.IsValue(member):
+      values.append(member_name)
+
+  possible_actions = []
+  availability_lines = []
+  availability_lint_format = '{header:20s}{choices}'
+  if groups:
+    possible_actions.append('groups')
+    groups_string = ' | '.join(groups)
+    groups_text = availability_lint_format.format(
+        header='available groups:',
+        choices=groups_string)
+    availability_lines.append(groups_text)
+  if commands:
+    possible_actions.append('commands')
+    commands_string = ' | '.join(commands)
+    commands_text = availability_lint_format.format(
+        header='available commands:',
+        choices=commands_string)
+    availability_lines.append(commands_text)
+  if values:
+    possible_actions.append('values')
+    values_string = ' | '.join(values)
+    values_text = availability_lint_format.format(
+        header='available values:',
+        choices=values_string)
+    availability_lines.append(values_text)
+  possible_actions_string = '|'.join(possible_actions)
+  availability_lines_string = '\n'.join(availability_lines)
+
+  return output_template.format(
+      current_command=command,
+      possible_actions=possible_actions_string,
+      availability_lines=availability_lines_string)
 
 
 def _HelpText(info, trace=None):
diff --git a/fire/helputils_test.py b/fire/helputils_test.py
index 416ce7a7..6531e23f 100644
--- a/fire/helputils_test.py
+++ b/fire/helputils_test.py
@@ -19,10 +19,12 @@
 from __future__ import print_function
 
 import os
+import textwrap
 
 from fire import helputils
 from fire import test_components as tc
 from fire import testutils
+from fire import trace
 import six
 
 
@@ -128,5 +130,122 @@ def testHelpClassNoInit(self):
     self.assertIn('Line:        ', helpstring)
 
 
+class UsageTest(testutils.BaseTestCase):
+
+  def testUsageOutput(self):
+    component = tc.NoDefaults()
+    t = trace.FireTrace(component, name='NoDefaults')
+    usage_output = helputils.UsageText(component, trace=t, verbose=False)
+    expected_output = '''
+    Usage: NoDefaults <commands>
+    available commands: double | triple
+
+    For detailed information on this command, run:
+    NoDefaults --help
+    '''
+
+    self.assertEqual(
+        usage_output,
+        textwrap.dedent(expected_output).lstrip('\n'))
+
+  @testutils.skip('The functionality is not implemented yet')
+  def testUsageOutputVerbose(self):
+    component = tc.NoDefaults()
+    t = trace.FireTrace(component, name='NoDefaults')
+    usage_output = helputils.UsageText(component, trace=t, verbose=True)
+    expected_output = '''
+    Usage: NoDefaults <groups|commands|values>
+    available groups:   __delattr__ | __dict__ | __doc__ | __getattribute__ | __hash__ | __init__ | __repr__ | __setattr__ | __str__ | __weakref__
+    available commands: __class__ | __format__ | __new__ | __reduce__ | __reduce_ex__ | __sizeof__ | __subclasshook__ | double | triple
+    available values:   __module__
+
+    For detailed information on this command, run:
+    NoDefaults --help
+    '''
+    self.assertEqual(
+        usage_output,
+        textwrap.dedent(expected_output).lstrip('\n'))
+
+  def testUsageOutputMethod(self):
+    component = tc.NoDefaults().double
+    t = trace.FireTrace(component, name='NoDefaults')
+    t.AddAccessedProperty(component, 'double', ['double'], None, None)
+    usage_output = helputils.UsageText(component, trace=t, verbose=True)
+    expected_output = '''
+    Usage: NoDefaults double COUNT
+
+    For detailed information on this command, run:
+    NoDefaults double --help
+    '''
+    self.assertEqual(
+        usage_output,
+        textwrap.dedent(expected_output).lstrip('\n'))
+
+  def testUsageOutputFunctionWithHelp(self):
+    component = tc.function_with_help
+    t = trace.FireTrace(component, name='function_with_help')
+    usage_output = helputils.UsageText(component, trace=t, verbose=True)
+    expected_output = '''
+    Usage: function_with_help <flags>
+
+    Available flags: --help
+
+    For detailed information on this command, run:
+    function_with_help -- --help
+    '''
+    self.assertEqual(
+        usage_output,
+        textwrap.dedent(expected_output).lstrip('\n'))
+
+  def testUsageOutputFunctionWithDocstring(self):
+    component = tc.multiplier_with_docstring
+    t = trace.FireTrace(component, name='multiplier_with_docstring')
+    usage_output = helputils.UsageText(component, trace=t, verbose=True)
+    expected_output = '''
+    Usage: multiplier_with_docstring NUM <flags>
+
+    Available flags: --rate
+
+    For detailed information on this command, run:
+    multiplier_with_docstring --help
+    '''
+    self.assertEqual(
+        usage_output,
+        textwrap.dedent(expected_output).lstrip('\n'))
+
+  @testutils.skip('The functionality is not implemented yet')
+  def testUsageOutputCallable(self):
+    # This is both a group and a command!
+    component = tc.CallableWithKeywordArgument
+    t = trace.FireTrace(component, name='CallableWithKeywordArgument')
+    usage_output = helputils.UsageText(component, trace=t, verbose=True)
+    # TODO(zuhaohen): We need to handle the case for keyword args as well
+    # i.e. __call__ method of CallableWithKeywordArgument
+    expected_output = '''
+    Usage: CallableWithKeywordArgument <commands>
+
+    Available commands: print_msg
+
+    For detailed information on this command, run:
+    CallableWithKeywordArgument -- --help
+    '''
+    self.assertEqual(
+        usage_output,
+        textwrap.dedent(expected_output).lstrip('\n'))
+
+  def testUsageOutputConstructorWithParameter(self):
+    component = tc.InstanceVars
+    t = trace.FireTrace(component, name='InstanceVars')
+    usage_output = helputils.UsageText(component, trace=t, verbose=True)
+    expected_output = '''
+    Usage: InstanceVars ARG1 ARG2
+
+    For detailed information on this command, run:
+    InstanceVars --help
+    '''
+    self.assertEqual(
+        usage_output,
+        textwrap.dedent(expected_output).lstrip('\n'))
+
 if __name__ == '__main__':
   testutils.main()
diff --git a/fire/test_components.py b/fire/test_components.py
index 3530b151..d0a8daa5 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -32,6 +32,18 @@ def identity(arg1, arg2, arg3=10, arg4=20, *arg5, **arg6):  # pylint: disable=ke
 identity.__annotations__ = {'arg2': int, 'arg4': int}
 
 
+def multiplier_with_docstring(num, rate=2):
+  """Multiplies num by rate.
+
+  Args:
+    num (int): the num you want to multiply
+    rate (int): the rate for multiplication
+  Returns:
+    Multiplication of num by rate
+  """
+  return num * rate
+
+
 def function_with_help(help=True):  # pylint: disable=redefined-builtin
   return help
 
diff --git a/fire/testutils.py b/fire/testutils.py
index 08964e25..0541a9a5 100644
--- a/fire/testutils.py
+++ b/fire/testutils.py
@@ -95,5 +95,7 @@ def assertRaisesFireExit(self, code, regexp='.*'):
           raise
 
 
-def main():
-  unittest.main()
+# pylint: disable=invalid-name
+main = unittest.main
+skip = unittest.skip
+# pylint: enable=invalid-name
diff --git a/fire/trace.py b/fire/trace.py
index c2fc8bc5..af98676a 100644
--- a/fire/trace.py
+++ b/fire/trace.py
@@ -30,6 +30,7 @@
 from __future__ import print_function
 
 import pipes
+from fire import inspectutils
 
 INITIAL_COMPONENT = 'Initial component'
 INSTANTIATED_CLASS = 'Instantiated class'
@@ -215,6 +216,27 @@ def __str__(self):
         for index, element in enumerate(self.elements)
     )
 
+  def NeedsSeparatingHyphenHyphen(self, flag='help'):
+    """Returns whether a the trace need '--' before '--help'.
+
+    '--' is needed when the component takes keyword arguments, when the value of
+    flag matches one of the argument of the component, or the component takes in
+    keywork-only arguments(e.g. argument with default value).
+
+    Args:
+      flag: the flag available for the trace
+
+    Returns:
+      True for needed '--', False otherwise.
+
+    """
+    element = self.GetLastHealthyElement()
+    component = element.component
+    spec = inspectutils.GetFullArgSpec(component)
+    return (spec.varkw is not None
+            or flag in spec.args
+            or flag in spec.kwonlyargs)
+
 
 class FireTraceElement(object):
   """A FireTraceElement represents a single step taken by a Fire execution.
diff --git a/fire/value_types.py b/fire/value_types.py
new file mode 100644
index 00000000..77d05dc7
--- /dev/null
+++ b/fire/value_types.py
@@ -0,0 +1,39 @@
+# 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.
+
+"""Types of values."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import inspect
+
+import six
+
+
+VALUE_TYPES = (bool, six.string_types, six.integer_types, float, complex)
+
+
+def IsGroup(component):
+  # TODO(dbieber): Check if there are any subcomponents.
+  return not IsCommand(component) and not IsValue(component)
+
+
+def IsCommand(component):
+  return inspect.isroutine(component) or inspect.isclass(component)
+
+
+def IsValue(component):
+  return isinstance(component, VALUE_TYPES)

From 69f1ef0417ec1cb9547f40d40e45488e2dd33f91 Mon Sep 17 00:00:00 2001
From: "Bruno P. Kinoshita" <kinow@users.noreply.github.com>
Date: Fri, 14 Dec 2018 19:03:09 +1300
Subject: [PATCH 044/324] Fix minor typos (#154)

---
 fire/docstrings.py   | 2 +-
 fire/inspectutils.py | 2 +-
 fire/trace.py        | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/fire/docstrings.py b/fire/docstrings.py
index e250128a..5e049603 100644
--- a/fire/docstrings.py
+++ b/fire/docstrings.py
@@ -266,7 +266,7 @@ def _get_or_create_arg_by_name(state, name):
 def _is_arg_name(name):
   """Returns whether name is a valid arg name.
 
-  This is used to prevent multiple words (plaintext) from being misinterpretted
+  This is used to prevent multiple words (plaintext) from being misinterpreted
   as an argument name. So if ":" appears in the middle of a line in a docstring,
   we don't accidentally interpret the first half of that line as a single arg
   name.
diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index e7d6fc74..e9daec1f 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -63,7 +63,7 @@ class with an __init__ method.
     fn: The function or class of interest.
   Returns:
     A tuple with the following two items:
-      fn: The function to use for determing the arg spec of this function.
+      fn: The function to use for determining the arg spec of this function.
       skip_arg: Whether the first argument will be supplied automatically, and
         hence should be skipped when supplying args from a Fire command.
   """
diff --git a/fire/trace.py b/fire/trace.py
index af98676a..a544cc8b 100644
--- a/fire/trace.py
+++ b/fire/trace.py
@@ -221,7 +221,7 @@ def NeedsSeparatingHyphenHyphen(self, flag='help'):
 
     '--' is needed when the component takes keyword arguments, when the value of
     flag matches one of the argument of the component, or the component takes in
-    keywork-only arguments(e.g. argument with default value).
+    keyword-only arguments(e.g. argument with default value).
 
     Args:
       flag: the flag available for the trace

From 871ffabb0015daf9c4a8179d0aef38af01a829c9 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 14 Dec 2018 15:07:43 -0800
Subject: [PATCH 045/324] Docs and tests - License in README - _IncludeMember
 docstring - testUsageOutputVerbose

PiperOrigin-RevId: 225603056
Change-Id: Iceed29fa261026f3f41e6e29ddeb76147e2051fe
Reviewed-on: https://team-review.git.corp.google.com/c/378384
Reviewed-by: David Bieber <dbieber@google.com>
---
 README.md              |  5 +++--
 docs/index.md          |  4 ++++
 fire/completion.py     | 19 +++++++++++++++++++
 fire/helputils_test.py |  7 ++-----
 4 files changed, 28 insertions(+), 7 deletions(-)

diff --git a/README.md b/README.md
index 10d6d4fd..9aea0877 100644
--- a/README.md
+++ b/README.md
@@ -90,8 +90,9 @@ _Note that flags are separated from the Fire command by an isolated `--` arg._
 
 ## License
 
-  Licensed under the [Apache 2.0](https://github.com/google/python-fire/blob/master/LICENSE) License.
-  
+Licensed under the
+[Apache 2.0](https://github.com/google/python-fire/blob/master/LICENSE) License.
+
 ## Disclaimer
 
 This is not an official Google product.
diff --git a/docs/index.md b/docs/index.md
index 9b7d2e3a..f6503c0b 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -88,6 +88,10 @@ Please see [The Python Fire Guide](guide.md).
 
 _Note that flags are separated from the Fire command by an isolated `--` arg._
 
+## License
+
+Licensed under the
+[Apache 2.0](https://github.com/google/python-fire/blob/master/LICENSE) License.
 
 ## Disclaimer
 
diff --git a/fire/completion.py b/fire/completion.py
index aeed5f89..8b74776f 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -155,6 +155,25 @@ def _FishScript(name, commands, default_options=None):
 
 
 def _IncludeMember(name, verbose):
+  """Returns whether a member should be included in auto-completion or help.
+
+  Determines whether a member of an object with the specified name should be
+  included in auto-completion or help text(both usage and detailed help).
+
+  If the member starts with '__', it will always be excluded. If the member
+  starts with only one '_', it will be included for all non-string types. If
+  verbose is True, the members, including the private members, are always
+  included.
+
+  Args:
+    name: The name of the member.
+    verbose: Whether to include private members.
+  Returns
+    A boolean value indicating whether the member should be included.
+
+  """
+  if isinstance(name, six.string_types) and name[:2] == '__':
+    return False
   if verbose:
     return True
   if isinstance(name, six.string_types):
diff --git a/fire/helputils_test.py b/fire/helputils_test.py
index 6531e23f..6dfacd5e 100644
--- a/fire/helputils_test.py
+++ b/fire/helputils_test.py
@@ -148,16 +148,13 @@ def testUsageOutput(self):
         usage_output,
         textwrap.dedent(expected_output).lstrip('\n'))
 
-  @testutils.skip('The functionality is not implemented yet')
   def testUsageOutputVerbose(self):
     component = tc.NoDefaults()
     t = trace.FireTrace(component, name='NoDefaults')
     usage_output = helputils.UsageText(component, trace=t, verbose=True)
     expected_output = '''
-    Usage: NoDefaults <groups|commands|values>
-    available groups:   __delattr__ | __dict__ | __doc__ | __getattribute__ | __hash__ | __init__ | __repr__ | __setattr__ | __str__ | __weakref__
-    available commands: __class__ | __format__ | __new__ | __reduce__ | __reduce_ex__ | __sizeof__ | __subclasshook__ | double | triple
-    available values:   __module__
+    Usage: NoDefaults <commands>
+    available commands: double | triple
 
     For detailed information on this command, run:
     NoDefaults --help

From 57c777316d862e7d6425b54b813d264fbc27a2ce Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20Sch=C3=A4fer?=
 <33159547+floscha@users.noreply.github.com>
Date: Thu, 3 Jan 2019 20:22:59 +0100
Subject: [PATCH 046/324] Make namedtuple fields accessible by their name
 (#161)

* Make namedtuple fields accessible by their name (in addition to their indexes)
---
 fire/core.py            | 28 +++++++++++++++++++++++++---
 fire/core_test.py       | 12 ++++++++++++
 fire/inspectutils.py    | 22 ++++++++++++++++++++++
 fire/test_components.py |  9 +++++++++
 4 files changed, 68 insertions(+), 3 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index cafa4ab8..f8c0b6e2 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -419,7 +419,8 @@ def _Fire(component, args, context, name=None):
         # If the initial component is a class, keep an instance for use with -i.
         instance = component
 
-    elif isinstance(component, (list, tuple)) and remaining_args:
+    elif (isinstance(component, (list, tuple)) and remaining_args
+          and not inspectutils.IsNamedTuple(component)):
       # The component is a tuple or list; we'll try to access a member.
       arg = remaining_args[0]
       try:
@@ -437,10 +438,27 @@ def _Fire(component, args, context, name=None):
       component_trace.AddAccessedProperty(
           component, index, [arg], filename, lineno)
 
-    elif isinstance(component, dict) and remaining_args:
+    elif ((isinstance(component, dict) or inspectutils.IsNamedTuple(component))
+          and remaining_args):
       # The component is a dict; we'll try to access a member.
       target = remaining_args[0]
-      if target in component:
+
+      # Allow indexing for namedtuples.
+      try:
+        index = int(target)
+        is_target_int = True
+      except ValueError:
+        is_target_int = False
+
+      if inspectutils.IsNamedTuple(component) and is_target_int:
+        try:
+          component = component[index]
+        except (ValueError, IndexError):
+          error = FireError(
+              'Unable to index into component with argument:', target)
+          component_trace.AddError(error, initial_args)
+          return component_trace
+      elif target in component:
         component = component[target]
       elif target.replace('-', '_') in component:
         component = component[target.replace('-', '_')]
@@ -449,6 +467,10 @@ def _Fire(component, args, context, name=None):
         # another type.
         # TODO(dbieber): Consider alternatives for accessing non-string keys.
         found_target = False
+        # If the component is a namedtuple, we need to convert it to dict to
+        # be able to use the .items() method.
+        if inspectutils.IsNamedTuple(component):
+          component = component._asdict()
         for key, value in component.items():
           if target == str(key):
             component = value
diff --git a/fire/core_test.py b/fire/core_test.py
index adfdd514..f9f12099 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -140,6 +140,18 @@ def testPrintOrderedDict(self):
     with self.assertOutputMatches(stdout='{}'):
       core.Fire(tc.OrderedDictionary, command=['empty'])
 
+  def testPrintNamedTupleField(self):
+    with self.assertOutputMatches(stdout='11', stderr=None):
+      core.Fire(tc.NamedTuple, command=['point', 'x'])
+
+  def testPrintNamedTupleIndex(self):
+    with self.assertOutputMatches(stdout='22', stderr=None):
+      core.Fire(tc.NamedTuple, command=['point', '1'])
+
+  def testPrintNamedTupleNegativeIndex(self):
+    with self.assertOutputMatches(stdout='11', stderr=None):
+      core.Fire(tc.NamedTuple, command=['point', '-2'])
+
   def testCallable(self):
     with self.assertOutputMatches(stdout=r'foo:\s+foo\s+', stderr=None):
       core.Fire(tc.CallableWithKeywordArgument(), command=['--foo=foo'])
diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index e9daec1f..45f7ce6c 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -202,3 +202,25 @@ def _InfoBackup(component):
     pass
 
   return info
+
+
+def IsNamedTuple(component):
+  """Return true if the component is a namedtuple.
+
+  Unfortunately, Python offers no native way to check for a namedtuple type.
+  Instead, we need to use a simple hack which should suffice for our case.
+  namedtuples are internally implemented as tuples, therefore we need to:
+    1. Check if the component is an instance of tuple.
+    2. Check if the component has a _fields attribute which regular tuples do
+       not have.
+
+  Args:
+    component: The component to analyze.
+  Returns:
+    True if the component is a namedtuple or False otherwise.
+  """
+  if not isinstance(component, tuple):
+    return False
+
+  has_fields = bool(getattr(component, '_fields', None))
+  return has_fields
diff --git a/fire/test_components.py b/fire/test_components.py
index d0a8daa5..4968d5cf 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -277,6 +277,15 @@ def non_empty(self):
     return ordered_dict
 
 
+class NamedTuple(object):
+
+  def point(self):
+    """Point example straight from Python docs."""
+    # pylint: disable=invalid-name
+    Point = collections.namedtuple('Point', ['x', 'y'])
+    return Point(11, y=22)
+
+
 class CallableWithKeywordArgument(object):
   """Test class for supporting callable."""
 

From e58c858dbf239b1e7afcf4300d784ccf8ddc4c93 Mon Sep 17 00:00:00 2001
From: Python Fire Team <no-reply@google.com>
Date: Fri, 18 Jan 2019 17:26:07 -0500
Subject: [PATCH 047/324] Implemented HelpTextForObject

-Generates detailed help screen for objects.
-Added basic test case to test help text generation.

PiperOrigin-RevId: 229994423
---
 fire/helputils.py       | 129 ++++++++++++++++++++++++++++++++++++++++
 fire/helputils_test.py  |  37 ++++++++++++
 fire/test_components.py |  23 +++++++
 3 files changed, 189 insertions(+)

diff --git a/fire/helputils.py b/fire/helputils.py
index 277acd6e..4674db4e 100644
--- a/fire/helputils.py
+++ b/fire/helputils.py
@@ -162,6 +162,135 @@ def _CommonHelpText(info, trace=None):
   return '\n'.join(lines)
 
 
+def HelpText(component, info, trace=None, verbose=False):
+  if inspect.isroutine(component) or inspect.isclass(component):
+    return HelpTextForFunction(component, info, trace)
+  else:
+    return HelpTextForObject(component, info, trace, verbose)
+
+
+def HelpTextForFunction(component, info, trace=None, verbose=False):
+  del component, info, trace, verbose
+
+
+def HelpTextForObject(component, info, trace=None, verbose=False):
+  """Generates help text for python objects.
+
+  Args:
+    component: Current component to generate help text for.
+    info: Info containing metadata of component.
+    trace: FireTrace object that leads to current component.
+    verbose: Whether to display help text in verbose mode.
+
+  Returns:
+    Formatted help text for display.
+  """
+
+  output_template = """NAME
+    {current_command} - {command_summary}
+
+SYNOPSIS
+    {synopsis}
+
+DESCRIPTION
+    {command_description}
+{detail_section}
+"""
+
+  if trace:
+    current_command = trace.GetCommand()
+  else:
+    current_command = None
+
+  if not current_command:
+    current_command = ''
+
+  docstring_info = info['docstring_info']
+  command_summary = docstring_info.summary if docstring_info.summary else ''
+  if docstring_info.description:
+    command_description = docstring_info.description
+  else:
+    command_description = ''
+
+  groups = []
+  commands = []
+  values = []
+  members = completion._Members(component, verbose)  # pylint: disable=protected-access
+  for member_name, member in members:
+    if value_types.IsGroup(member):
+      groups.append((member_name, member))
+    if value_types.IsCommand(member):
+      commands.append((member_name, member))
+    if value_types.IsValue(member):
+      values.append((member_name, member))
+
+  possible_actions = []
+  # TODO(joejoevictor): Add global flags to here. Also, if it's a callable, there
+  # will be additional flags.
+  possible_flags = ''
+  detail_section_string = ''
+  item_template = """
+        {name}
+            {command_summary}
+"""
+
+  if groups:
+    # TODO(joejoevictor): Add missing GROUPS section handling
+    possible_actions.append('GROUP')
+  if commands:
+    possible_actions.append('COMMAND')
+    commands_str_template = """
+COMMANDS
+    COMMAND is one of the followings:
+{items}
+"""
+    command_item_strings = []
+    for command_name, command in commands:
+      command_docstring_info = docstrings.parse(
+          inspectutils.Info(command)['docstring'])
+      command_item_strings.append(
+          item_template.format(
+              name=command_name,
+              command_summary=command_docstring_info.summary))
+    detail_section_string += commands_str_template.format(
+        items=('\n'.join(command_item_strings)).rstrip('\n'))
+
+  if values:
+    possible_actions.append('VALUES')
+    values_str_template = """
+VALUES
+    VALUE is one of the followings:
+{items}
+"""
+    value_item_strings = []
+    for value_name, value in values:
+      del value
+      init_docstring_info = docstrings.parse(
+          inspectutils.Info(component.__class__.__init__)['docstring'])
+      for arg_info in init_docstring_info.args:
+        if arg_info.name == value_name:
+          value_item_strings.append(
+              item_template.format(
+                  name=value_name, command_summary=arg_info.description))
+    detail_section_string += values_str_template.format(
+        items=('\n'.join(value_item_strings)).rstrip('\n'))
+
+  possible_actions_string = ' ' + (' | '.join(possible_actions))
+
+  synopsis_template = '{current_command}{possible_actions}{possible_flags}'
+  synopsis_string = synopsis_template.format(
+      current_command=current_command,
+      possible_actions=possible_actions_string,
+      possible_flags=possible_flags)
+
+  return output_template.format(
+      current_command=current_command,
+      command_summary=command_summary,
+      synopsis=synopsis_string,
+      command_description=command_description,
+      detail_section=detail_section_string)
+
+
 def UsageText(component, trace=None, verbose=False):
   if inspect.isroutine(component) or inspect.isclass(component):
     return UsageTextForFunction(component, trace)
diff --git a/fire/helputils_test.py b/fire/helputils_test.py
index 6dfacd5e..56cabbb7 100644
--- a/fire/helputils_test.py
+++ b/fire/helputils_test.py
@@ -21,7 +21,9 @@
 import os
 import textwrap
 
+from fire import docstrings
 from fire import helputils
+from fire import inspectutils
 from fire import test_components as tc
 from fire import testutils
 from fire import trace
@@ -130,6 +132,40 @@ def testHelpClassNoInit(self):
     self.assertIn('Line:        ', helpstring)
 
 
+class HelpScreenTest(testutils.BaseTestCase):
+
+  def testHelpScreen(self):
+    component = tc.ClassWithDocstring()
+    t = trace.FireTrace(component, name='ClassWithDocstring')
+    info = inspectutils.Info(component)
+    info['docstring_info'] = docstrings.parse(info['docstring'])
+    help_output = helputils.HelpText(component, info, t)
+    expected_output = """
+NAME
+    ClassWithDocstring - Test class for testing help text output.
+
+SYNOPSIS
+    ClassWithDocstring COMMAND | VALUES
+
+DESCRIPTION
+    This is some detail description of this test class.
+
+COMMANDS
+    COMMAND is one of the followings:
+
+        print_msg
+            Prints a message.
+
+VALUES
+    VALUE is one of the followings:
+
+        message
+            The default message to print.
+
+"""
+    self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
+
+
 class UsageTest(testutils.BaseTestCase):
 
   def testUsageOutput(self):
@@ -244,5 +280,6 @@ def testUsageOutputConstructorWithParameter(self):
         usage_output,
         textwrap.dedent(expected_output).lstrip('\n'))
 
+
 if __name__ == '__main__':
   testutils.main()
diff --git a/fire/test_components.py b/fire/test_components.py
index 4968d5cf..2ba1155c 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -295,3 +295,26 @@ def __call__(self, **kwargs):
 
   def print_msg(self, msg):
     print(msg)
+
+
+class ClassWithDocstring(object):
+  """Test class for testing help text output.
+
+  This is some detail description of this test class.
+  """
+
+  def __init__(self, message='Hello!'):
+    """Constructor of the test class.
+
+    Constructs a new ClassWithDocstring object.
+
+    Args:
+      message: The default message to print.
+    """
+    self.message = message
+
+  def print_msg(self, msg=None):
+    """Prints a message."""
+    if msg is None:
+      msg = self.message
+    print(msg)

From 105930e3d984c9d5d05fe8406e18eb57bdea32e9 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 23 Jan 2019 12:38:00 -0500
Subject: [PATCH 048/324] Remove deprecated hypothesis setting.

PiperOrigin-RevId: 230545402
Change-Id: Ib1ead3e70bfec9a7fe1aa803200c307fd5d84375
---
 fire/docstrings_fuzz_test.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/fire/docstrings_fuzz_test.py b/fire/docstrings_fuzz_test.py
index a7ede06a..ea068d08 100644
--- a/fire/docstrings_fuzz_test.py
+++ b/fire/docstrings_fuzz_test.py
@@ -29,8 +29,7 @@
 
 class DocstringsFuzzTest(testutils.BaseTestCase):
 
-  @settings(max_examples=10000,
-            timeout=120)
+  @settings(max_examples=10000)
   @given(st.text(min_size=1))
   @example('This is a one-line docstring.')
   def test_fuzz_parse(self, value):

From a81d1d2f13d12494d55a00c287dbacc783d4a5ae Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 30 Jan 2019 14:52:39 -0500
Subject: [PATCH 049/324] Add docstring about interactive mode to interact.py.

PiperOrigin-RevId: 231638941
Change-Id: If574662dceaf7b219b9124cb9c33ee76ec104f97
---
 fire/interact.py | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/fire/interact.py b/fire/interact.py
index d7d458c8..9f0a01e6 100644
--- a/fire/interact.py
+++ b/fire/interact.py
@@ -12,7 +12,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""This module enables interactive mode in Python Fire."""
+"""This module enables interactive mode in Python Fire.
+
+It uses IPython as an optional dependency. When IPython is installed, the
+interactive flag will use IPython's REPL. When IPython is not installed, the
+interactive flag will start a Python REPL with the builtin `code` module's
+InteractiveConsole class.
+"""
 
 from __future__ import absolute_import
 from __future__ import division

From 855f317c31df736f28810bb4a94bc62492f1ed80 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 30 Jan 2019 15:26:23 -0500
Subject: [PATCH 050/324] Shorten line too long in helputils

---
 fire/helputils.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/fire/helputils.py b/fire/helputils.py
index 4674db4e..6bd572c8 100644
--- a/fire/helputils.py
+++ b/fire/helputils.py
@@ -225,8 +225,8 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
       values.append((member_name, member))
 
   possible_actions = []
-  # TODO(joejoevictor): Add global flags to here. Also, if it's a callable, there
-  # will be additional flags.
+  # TODO(joejoevictor): Add global flags to here. Also, if it's a callable,
+  # there will be additional flags.
   possible_flags = ''
   detail_section_string = ''
   item_template = """

From b9aafbdf34ba8ed0ef87089546c30e87f24e4c20 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 30 Jan 2019 12:42:47 -0800
Subject: [PATCH 051/324] Add docstring about helputils.

PiperOrigin-RevId: 231648600
Change-Id: Iac178bf1b1ea8099f51f8288041517c23fd445d5
---
 fire/helputils.py | 19 +++++++++++++++----
 1 file changed, 15 insertions(+), 4 deletions(-)

diff --git a/fire/helputils.py b/fire/helputils.py
index 6bd572c8..83301bc1 100644
--- a/fire/helputils.py
+++ b/fire/helputils.py
@@ -16,6 +16,17 @@
 
 Can produce help strings suitable for display in Fire CLIs for any type of
 Python object, module, class, or function.
+
+There are two types of informative strings: Usage and Help screens.
+
+Usage screens are shown when the user accesses a group or accesses a command
+without calling it. A Usage screen shows information about how to use that group
+or command. Usage screens are typically short and show the minimal information
+necessary for the user to determine how to proceed.
+
+Help screens are shown when the user requests help with the help flag (--help).
+Help screens are shown in a less-style console view, and contain detailed help
+information.
 """
 
 from __future__ import absolute_import
@@ -135,8 +146,8 @@ def _CommonHelpText(info, trace=None):
   Returns:
     String suitable for display giving information about the component.
   """
-  # TODO(joejoevictor): Currently this is just a copy of existing HelpString
-  # method. We will reimplement this further in later CLs.
+  # TODO(joejoevictor): Currently this is just a copy of existing
+  # HelpString method. We will reimplement this further in later CLs.
   fields = _GetFields(trace)
 
   try:
@@ -225,8 +236,8 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
       values.append((member_name, member))
 
   possible_actions = []
-  # TODO(joejoevictor): Add global flags to here. Also, if it's a callable,
-  # there will be additional flags.
+  # TODO(joejoevictor): Add global flags to here. Also, if it's a callable, there
+  # will be additional flags.
   possible_flags = ''
   detail_section_string = ''
   item_template = """

From c99a0349980bca39700ce683b796453377bb805c Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 30 Jan 2019 15:04:34 -0800
Subject: [PATCH 052/324] Add deadline to hypothesis docstring test.

PiperOrigin-RevId: 231674992
Change-Id: If476a6d13976881539e61807efded5855e5321d6
---
 fire/docstrings_fuzz_test.py | 2 +-
 fire/helputils.py            | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/fire/docstrings_fuzz_test.py b/fire/docstrings_fuzz_test.py
index ea068d08..0b26b141 100644
--- a/fire/docstrings_fuzz_test.py
+++ b/fire/docstrings_fuzz_test.py
@@ -29,7 +29,7 @@
 
 class DocstringsFuzzTest(testutils.BaseTestCase):
 
-  @settings(max_examples=10000)
+  @settings(max_examples=10000, deadline=1000)
   @given(st.text(min_size=1))
   @example('This is a one-line docstring.')
   def test_fuzz_parse(self, value):
diff --git a/fire/helputils.py b/fire/helputils.py
index 83301bc1..af1ca8cf 100644
--- a/fire/helputils.py
+++ b/fire/helputils.py
@@ -236,8 +236,8 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
       values.append((member_name, member))
 
   possible_actions = []
-  # TODO(joejoevictor): Add global flags to here. Also, if it's a callable, there
-  # will be additional flags.
+  # TODO(joejoevictor): Add global flags to here. Also, if it's a callable,
+  # there will be additional flags.
   possible_flags = ''
   detail_section_string = ''
   item_template = """

From c13d87beee605565a6aece6b7464baa9fc552518 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 15 Feb 2019 12:56:47 -0800
Subject: [PATCH 053/324] Release console package as part of fire.

PiperOrigin-RevId: 234196877
Change-Id: I17ff52e9330e2f009e8f86323662cf2abeda4389
---
 fire/console/README.md          |   3 +
 fire/console/console_attr.py    | 784 ++++++++++++++++++++++++++++++++
 fire/console/console_attr_os.py | 260 +++++++++++
 fire/console/console_pager.py   | 299 ++++++++++++
 fire/console/encoding.py        | 207 +++++++++
 5 files changed, 1553 insertions(+)
 create mode 100644 fire/console/README.md
 create mode 100644 fire/console/console_attr.py
 create mode 100644 fire/console/console_attr_os.py
 create mode 100644 fire/console/console_pager.py
 create mode 100644 fire/console/encoding.py

diff --git a/fire/console/README.md b/fire/console/README.md
new file mode 100644
index 00000000..b23401f7
--- /dev/null
+++ b/fire/console/README.md
@@ -0,0 +1,3 @@
+This is the console package from googlecloudsdk, as used by Python Fire.
+Python Fire does not accept pull requests modifying the console package; rather,
+changes to console should go through the upstream project googlecloudsdk.
diff --git a/fire/console/console_attr.py b/fire/console/console_attr.py
new file mode 100644
index 00000000..94334f45
--- /dev/null
+++ b/fire/console/console_attr.py
@@ -0,0 +1,784 @@
+# -*- coding: utf-8 -*- #
+
+# Copyright 2015 Google Inc. All Rights Reserved.
+#
+# 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.
+
+r"""A module for console attributes, special characters and functions.
+
+The target architectures {linux, macos, windows} support inline encoding for
+all attributes except color. Windows requires win32 calls to manipulate the
+console color state.
+
+Usage:
+
+  # Get the console attribute state.
+  out = log.out
+  con = console_attr.GetConsoleAttr(out=out)
+
+  # Get the ISO 8879:1986//ENTITIES Box and Line Drawing characters.
+  box = con.GetBoxLineCharacters()
+  # Print an X inside a box.
+  out.write(box.dr)
+  out.write(box.h)
+  out.write(box.dl)
+  out.write('\n')
+  out.write(box.v)
+  out.write('X')
+  out.write(box.v)
+  out.write('\n')
+  out.write(box.ur)
+  out.write(box.h)
+  out.write(box.ul)
+  out.write('\n')
+
+  # Print the bullet characters.
+  for c in con.GetBullets():
+    out.write(c)
+  out.write('\n')
+
+  # Print FAIL in red.
+  out.write('Epic ')
+  con.Colorize('FAIL', 'red')
+  out.write(', my first.')
+
+  # Print italic and bold text.
+  bold = con.GetFontCode(bold=True)
+  italic = con.GetFontCode(italic=True)
+  normal = con.GetFontCode()
+  out.write('This is {bold}bold{normal}, this is {italic}italic{normal},'
+            ' and this is normal.\n'.format(bold=bold, italic=italic,
+                                            normal=normal))
+
+  # Read one character from stdin with echo disabled.
+  c = con.GetRawKey()
+  if c is None:
+    print 'EOF\n'
+
+  # Return the display width of a string that may contain FontCode() chars.
+  display_width = con.DisplayWidth(string)
+
+  # Reset the memoized state.
+  con = console_attr.ResetConsoleAttr()
+
+  # Print the console width and height in characters.
+  width, height = con.GetTermSize()
+  print 'width={width}, height={height}'.format(width=width, height=height)
+
+  # Colorize table data cells.
+  fail = console_attr.Colorizer('FAIL', 'red')
+  pass = console_attr.Colorizer('PASS', 'green')
+  cells = ['label', fail, 'more text', pass, 'end']
+  for cell in cells;
+    if isinstance(cell, console_attr.Colorizer):
+      cell.Render()
+    else:
+      out.write(cell)
+"""
+
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import unicode_literals
+
+import os
+import sys
+import unicodedata
+
+from fire.console import console_attr_os
+from fire.console import encoding as encoding_util
+
+import six
+
+
+class BoxLineCharacters(object):
+  """Box/line drawing characters.
+
+  The element names are from ISO 8879:1986//ENTITIES Box and Line Drawing//EN:
+    http://www.w3.org/2003/entities/iso8879doc/isobox.html
+  """
+
+
+class BoxLineCharactersUnicode(BoxLineCharacters):
+  """unicode Box/line drawing characters (cp437 compatible unicode)."""
+  dl = '┐'
+  dr = '┌'
+  h = '─'
+  hd = '┬'
+  hu = '┴'
+  ul = '┘'
+  ur = '└'
+  v = '│'
+  vh = '┼'
+  vl = '┤'
+  vr = '├'
+  d_dl = '╗'
+  d_dr = '╔'
+  d_h = '═'
+  d_hd = '╦'
+  d_hu = '╩'
+  d_ul = '╝'
+  d_ur = '╚'
+  d_v = '║'
+  d_vh = '╬'
+  d_vl = '╣'
+  d_vr = '╠'
+
+
+class BoxLineCharactersAscii(BoxLineCharacters):
+  """ASCII Box/line drawing characters."""
+  dl = '+'
+  dr = '+'
+  h = '-'
+  hd = '+'
+  hu = '+'
+  ul = '+'
+  ur = '+'
+  v = '|'
+  vh = '+'
+  vl = '+'
+  vr = '+'
+  d_dl = '#'
+  d_dr = '#'
+  d_h = '='
+  d_hd = '#'
+  d_hu = '#'
+  d_ul = '#'
+  d_ur = '#'
+  d_v = '#'
+  d_vh = '#'
+  d_vl = '#'
+  d_vr = '#'
+
+
+class ProgressTrackerSymbols(object):
+  """Characters used by progress trackers."""
+
+
+class ProgressTrackerSymbolsUnicode(ProgressTrackerSymbols):
+  """Characters used by progress trackers."""
+
+  @property
+  def spin_marks(self):
+    return ['⠏', '⠛', '⠹', '⠼', '⠶', '⠧']
+
+  success = '✓'
+  failed = 'X'
+  interrupted = '-'
+  not_started = '.'
+  prefix_length = 2
+
+
+class ProgressTrackerSymbolsAscii(ProgressTrackerSymbols):
+  """Characters used by progress trackers."""
+
+  @property
+  def spin_marks(self):
+    return ['|', '/', '-', '\\',]
+
+  success = 'OK'
+  failed = 'X'
+  interrupted = '-'
+  not_started = '.'
+  prefix_length = 3
+
+
+class ConsoleAttr(object):
+  """Console attribute and special drawing characters and functions accessor.
+
+  Use GetConsoleAttr() to get a global ConsoleAttr object shared by all callers.
+  Use ConsoleAttr() for abstracting multiple consoles.
+
+  If _out is not associated with a console, or if the console properties cannot
+  be determined, the default behavior is ASCII art with no attributes.
+
+  Attributes:
+    _ANSI_COLOR: The ANSI color control sequence dict.
+    _ANSI_COLOR_RESET: The ANSI color reset control sequence string.
+    _csi: The ANSI Control Sequence indicator string, '' if not supported.
+    _encoding: The character encoding.
+        ascii: ASCII art. This is the default.
+        utf8: UTF-8 unicode.
+        win: Windows code page 437.
+    _font_bold: The ANSI bold font embellishment code string.
+    _font_italic: The ANSI italic font embellishment code string.
+    _get_raw_key: A function that reads one keypress from stdin with no echo.
+    _out: The console output file stream.
+    _term: TERM environment variable value.
+    _term_size: The terminal (x, y) dimensions in characters.
+  """
+
+  _CONSOLE_ATTR_STATE = None
+
+  _ANSI_COLOR = {
+      'red': '31;1m',
+      'yellow': '33;1m',
+      'green': '32m',
+      'blue': '34;1m'
+      }
+  _ANSI_COLOR_RESET = '39;0m'
+
+  _BULLETS_UNICODE = ('▪', '◆', '▸', '▫', '◇', '▹')
+  _BULLETS_WINDOWS = ('■', '≡', '∞', 'Φ', '·')  # cp437 compatible unicode
+  _BULLETS_ASCII = ('o', '*', '+', '-')
+
+  def __init__(self, encoding=None):
+    """Constructor.
+
+    Args:
+      encoding: Encoding override.
+        ascii -- ASCII art. This is the default.
+        utf8 -- UTF-8 unicode.
+        win -- Windows code page 437.
+    """
+    # Normalize the encoding name.
+    if not encoding:
+      encoding = self._GetConsoleEncoding()
+    elif encoding == 'win':
+      encoding = 'cp437'
+    self._encoding = encoding or 'ascii'
+    self._term = os.getenv('TERM', '').lower()
+
+    # ANSI "standard" attributes.
+    if self.SupportsAnsi():
+      # Select Graphic Rendition paramaters from
+      # http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
+      # Italic '3' would be nice here but its not widely supported.
+      self._csi = '\x1b['
+      self._font_bold = '1'
+      self._font_italic = '4'
+    else:
+      self._csi = None
+      self._font_bold = ''
+      self._font_italic = ''
+
+    # Encoded character attributes.
+    if self._encoding == 'utf8':
+      self._box_line_characters = BoxLineCharactersUnicode()
+      self._bullets = self._BULLETS_UNICODE
+      self._progress_tracker_symbols = ProgressTrackerSymbolsUnicode()
+    elif self._encoding == 'cp437':
+      self._box_line_characters = BoxLineCharactersUnicode()
+      self._bullets = self._BULLETS_WINDOWS
+      # Windows does not suport the unicode characters used for the spinner.
+      self._progress_tracker_symbols = ProgressTrackerSymbolsAscii()
+    else:
+      self._box_line_characters = BoxLineCharactersAscii()
+      self._bullets = self._BULLETS_ASCII
+      self._progress_tracker_symbols = ProgressTrackerSymbolsAscii()
+
+    # OS specific attributes.
+    self._get_raw_key = [console_attr_os.GetRawKeyFunction()]
+    self._term_size = console_attr_os.GetTermSize()
+
+    self._display_width_cache = {}
+
+  def _GetConsoleEncoding(self):
+    """Gets the encoding as declared by the stdout stream.
+
+    Returns:
+      str, The encoding name or None if it could not be determined.
+    """
+    console_encoding = getattr(sys.stdout, 'encoding', None)
+    if not console_encoding:
+      return None
+    console_encoding = console_encoding.lower()
+    if 'utf-8' in console_encoding:
+      return 'utf8'
+    elif 'cp437' in console_encoding:
+      return 'cp437'
+    return None
+
+  def Colorize(self, string, color, justify=None):
+    """Generates a colorized string, optionally justified.
+
+    Args:
+      string: The string to write.
+      color: The color name -- must be in _ANSI_COLOR.
+      justify: The justification function, no justification if None. For
+        example, justify=lambda s: s.center(10)
+
+    Returns:
+      str, The colorized string that can be printed to the console.
+    """
+    if justify:
+      string = justify(string)
+    if self._csi and color in self._ANSI_COLOR:
+      return '{csi}{color_code}{string}{csi}{reset_code}'.format(
+          csi=self._csi,
+          color_code=self._ANSI_COLOR[color],
+          reset_code=self._ANSI_COLOR_RESET,
+          string=string)
+    # TODO: Add elif self._encoding == 'cp437': code here.
+    return string
+
+  def ConvertOutputToUnicode(self, buf):
+    """Converts a console output string buf to unicode.
+
+    Mainly used for testing. Allows test comparisons in unicode while ensuring
+    that unicode => encoding => unicode works.
+
+    Args:
+      buf: The console output string to convert.
+
+    Returns:
+      The console output string buf converted to unicode.
+    """
+    if isinstance(buf, six.text_type):
+      buf = buf.encode(self._encoding)
+    return six.text_type(buf, self._encoding, 'replace')
+
+  def GetBoxLineCharacters(self):
+    """Returns the box/line drawing characters object.
+
+    The element names are from ISO 8879:1986//ENTITIES Box and Line Drawing//EN:
+      http://www.w3.org/2003/entities/iso8879doc/isobox.html
+
+    Returns:
+      A BoxLineCharacters object for the console output device.
+    """
+    return self._box_line_characters
+
+  def GetBullets(self):
+    """Returns the bullet characters list.
+
+    Use the list elements in order for best appearance in nested bullet lists,
+    wrapping back to the first element for deep nesting. The list size depends
+    on the console implementation.
+
+    Returns:
+      A tuple of bullet characters.
+    """
+    return self._bullets
+
+  def GetProgressTrackerSymbols(self):
+    """Returns the progress tracker characters object.
+
+    Returns:
+      A ProgressTrackerSymbols object for the console output device.
+    """
+    return self._progress_tracker_symbols
+
+  def GetControlSequenceIndicator(self):
+    """Returns the control sequence indicator string.
+
+    Returns:
+      The conrol sequence indicator string or None if control sequences are not
+      supported.
+    """
+    return self._csi
+
+  def GetControlSequenceLen(self, buf):
+    """Returns the control sequence length at the beginning of buf.
+
+    Used in display width computations. Control sequences have display width 0.
+
+    Args:
+      buf: The string to check for a control sequence.
+
+    Returns:
+      The conrol sequence length at the beginning of buf or 0 if buf does not
+      start with a control sequence.
+    """
+    if not self._csi or not buf.startswith(self._csi):
+      return 0
+    n = 0
+    for c in buf:
+      n += 1
+      if c.isalpha():
+        break
+    return n
+
+  def GetEncoding(self):
+    """Returns the current encoding."""
+    return self._encoding
+
+  def GetFontCode(self, bold=False, italic=False):
+    """Returns a font code string for 0 or more embellishments.
+
+    GetFontCode() with no args returns the default font code string.
+
+    Args:
+      bold: True for bold embellishment.
+      italic: True for italic embellishment.
+
+    Returns:
+      The font code string for the requested embellishments. Write this string
+        to the console output to control the font settings.
+    """
+    if not self._csi:
+      return ''
+    codes = []
+    if bold:
+      codes.append(self._font_bold)
+    if italic:
+      codes.append(self._font_italic)
+    return '{csi}{codes}m'.format(csi=self._csi, codes=';'.join(codes))
+
+  def GetRawKey(self):
+    """Reads one key press from stdin with no echo.
+
+    Returns:
+      The key name, None for EOF, <KEY-*> for function keys, otherwise a
+      character.
+    """
+    return self._get_raw_key[0]()
+
+  def GetTermSize(self):
+    """Returns the terminal (x, y) dimensions in characters.
+
+    Returns:
+      (x, y): A tuple of the terminal x and y dimensions.
+    """
+    return self._term_size
+
+  def DisplayWidth(self, buf):
+    """Returns the display width of buf, handling unicode and ANSI controls.
+
+    Args:
+      buf: The string to count from.
+
+    Returns:
+      The display width of buf, handling unicode and ANSI controls.
+    """
+    if not isinstance(buf, six.string_types):
+      # Handle non-string objects like Colorizer().
+      return len(buf)
+
+    cached = self._display_width_cache.get(buf, None)
+    if cached is not None:
+      return cached
+
+    width = 0
+    max_width = 0
+    i = 0
+    while i < len(buf):
+      if self._csi and buf[i:].startswith(self._csi):
+        i += self.GetControlSequenceLen(buf[i:])
+      elif buf[i] == '\n':
+        # A newline incidates the start of a new line.
+        # Newline characters have 0 width.
+        max_width = max(width, max_width)
+        width = 0
+        i += 1
+      else:
+        width += GetCharacterDisplayWidth(buf[i])
+        i += 1
+    max_width = max(width, max_width)
+
+    self._display_width_cache[buf] = max_width
+    return max_width
+
+  def SplitIntoNormalAndControl(self, buf):
+    """Returns a list of (normal_string, control_sequence) tuples from buf.
+
+    Args:
+      buf: The input string containing one or more control sequences
+        interspersed with normal strings.
+
+    Returns:
+      A list of (normal_string, control_sequence) tuples.
+    """
+    if not self._csi or not buf:
+      return [(buf, '')]
+    seq = []
+    i = 0
+    while i < len(buf):
+      c = buf.find(self._csi, i)
+      if c < 0:
+        seq.append((buf[i:], ''))
+        break
+      normal = buf[i:c]
+      i = c + self.GetControlSequenceLen(buf[c:])
+      seq.append((normal, buf[c:i]))
+    return seq
+
+  def SplitLine(self, line, width):
+    """Splits line into width length chunks.
+
+    Args:
+      line: The line to split.
+      width: The width of each chunk except the last which could be smaller than
+        width.
+
+    Returns:
+      A list of chunks, all but the last with display width == width.
+    """
+    lines = []
+    chunk = ''
+    w = 0
+    keep = False
+    for normal, control in self.SplitIntoNormalAndControl(line):
+      keep = True
+      while True:
+        n = width - w
+        w += len(normal)
+        if w <= width:
+          break
+        lines.append(chunk + normal[:n])
+        chunk = ''
+        keep = False
+        w = 0
+        normal = normal[n:]
+      chunk += normal + control
+    if chunk or keep:
+      lines.append(chunk)
+    return lines
+
+  def SupportsAnsi(self):
+    return (self._encoding != 'ascii' and
+            ('screen' in self._term or 'xterm' in self._term))
+
+
+class Colorizer(object):
+  """Resource string colorizer.
+
+  Attributes:
+    _con: ConsoleAttr object.
+    _color: Color name.
+    _string: The string to colorize.
+    _justify: The justification function, no justification if None. For example,
+      justify=lambda s: s.center(10)
+  """
+
+  def __init__(self, string, color, justify=None):
+    """Constructor.
+
+    Args:
+      string: The string to colorize.
+      color: Color name used to index ConsoleAttr._ANSI_COLOR.
+      justify: The justification function, no justification if None. For
+        example, justify=lambda s: s.center(10)
+    """
+    self._con = GetConsoleAttr()
+    self._color = color
+    self._string = string
+    self._justify = justify
+
+  def __eq__(self, other):
+    return self._string == six.text_type(other)
+
+  def __ne__(self, other):
+    return not self == other
+
+  def __gt__(self, other):
+    return self._string > six.text_type(other)
+
+  def __lt__(self, other):
+    return self._string < six.text_type(other)
+
+  def __ge__(self, other):
+    return not self < other
+
+  def __le__(self, other):
+    return not self > other
+
+  def __len__(self):
+    return self._con.DisplayWidth(self._string)
+
+  def __str__(self):
+    return self._string
+
+  def Render(self, stream, justify=None):
+    """Renders the string as self._color on the console.
+
+    Args:
+      stream: The stream to render the string to. The stream given here *must*
+        have the same encoding as sys.stdout for this to work properly.
+      justify: The justification function, self._justify if None.
+    """
+    stream.write(
+        self._con.Colorize(self._string, self._color, justify or self._justify))
+
+
+def GetConsoleAttr(encoding=None, reset=False):
+  """Gets the console attribute state.
+
+  If this is the first call or reset is True or encoding is not None and does
+  not match the current encoding or out is not None and does not match the
+  current out then the state is (re)initialized. Otherwise the current state
+  is returned.
+
+  This call associates the out file stream with the console. All console related
+  output should go to the same stream.
+
+  Args:
+    encoding: Encoding override.
+      ascii -- ASCII. This is the default.
+      utf8 -- UTF-8 unicode.
+      win -- Windows code page 437.
+    reset: Force re-initialization if True.
+
+  Returns:
+    The global ConsoleAttr state object.
+  """
+  attr = ConsoleAttr._CONSOLE_ATTR_STATE  # pylint: disable=protected-access
+  if not reset:
+    if not attr:
+      reset = True
+    elif encoding and encoding != attr.GetEncoding():
+      reset = True
+  if reset:
+    attr = ConsoleAttr(encoding=encoding)
+    ConsoleAttr._CONSOLE_ATTR_STATE = attr  # pylint: disable=protected-access
+  return attr
+
+
+def ResetConsoleAttr(encoding=None):
+  """Resets the console attribute state to the console default.
+
+  Args:
+    encoding: Reset to this encoding instead of the default.
+      ascii -- ASCII. This is the default.
+      utf8 -- UTF-8 unicode.
+      win -- Windows code page 437.
+
+  Returns:
+    The global ConsoleAttr state object.
+  """
+  return GetConsoleAttr(encoding=encoding, reset=True)
+
+
+def GetCharacterDisplayWidth(char):
+  """Returns the monospaced terminal display width of char.
+
+  Assumptions:
+    - monospaced display
+    - ambiguous or unknown chars default to width 1
+    - ASCII control char width is 1 => don't use this for control chars
+
+  Args:
+    char: The character to determine the display width of.
+
+  Returns:
+    The monospaced terminal display width of char: either 0, 1, or 2.
+  """
+  if not isinstance(char, six.text_type):
+    # Non-unicode chars have width 1. Don't use this function on control chars.
+    return 1
+
+  # Normalize to avoid special cases.
+  char = unicodedata.normalize('NFC', char)
+
+  if unicodedata.combining(char) != 0:
+    # Modifies the previous character and does not move the cursor.
+    return 0
+  elif unicodedata.category(char) == 'Cf':
+    # Unprintable formatting char.
+    return 0
+  elif unicodedata.east_asian_width(char) in 'FW':
+    # Fullwidth or Wide chars take 2 character positions.
+    return 2
+  else:
+    # Don't use this function on control chars.
+    return 1
+
+
+def SafeText(data, encoding=None, escape=True):
+  br"""Converts the data to a text string compatible with the given encoding.
+
+  This works the same way as Decode() below except it guarantees that any
+  characters in the resulting text string can be re-encoded using the given
+  encoding (or GetConsoleAttr().GetEncoding() if None is given). This means
+  that the string will be safe to print to sys.stdout (for example) without
+  getting codec exceptions if the user's terminal doesn't support the encoding
+  used by the source of the text.
+
+  Args:
+    data: Any bytes, string, or object that has str() or unicode() methods.
+    encoding: The encoding name to ensure compatibility with. Defaults to
+      GetConsoleAttr().GetEncoding().
+    escape: Replace unencodable characters with a \uXXXX or \xXX equivalent if
+      True. Otherwise replace unencodable characters with an appropriate unknown
+      character, '?' for ASCII, and the unicode unknown replacement character
+      \uFFFE for unicode.
+
+  Returns:
+    A text string representation of the data, but modified to remove any
+    characters that would result in an encoding exception with the target
+    encoding. In the worst case, with escape=False, it will contain only ?
+    characters.
+  """
+  if data is None:
+    return 'None'
+  encoding = encoding or GetConsoleAttr().GetEncoding()
+  string = encoding_util.Decode(data, encoding=encoding)
+
+  try:
+    # No change needed if the string encodes to the output encoding.
+    string.encode(encoding)
+    return string
+  except UnicodeError:
+    # The string does not encode to the output encoding. Encode it with error
+    # handling then convert it back into a text string (which will be
+    # guaranteed to only contain characters that can be encoded later.
+    return (string
+            .encode(encoding, 'backslashreplace' if escape else 'replace')
+            .decode(encoding))
+
+
+def EncodeToBytes(data):
+  r"""Encode data to bytes.
+
+  The primary use case is for base64/mime style 7-bit ascii encoding where the
+  encoder input must be bytes. "safe" means that the conversion always returns
+  bytes and will not raise codec exceptions.
+
+  If data is text then an 8-bit ascii encoding is attempted, then the console
+  encoding, and finally utf-8.
+
+  Args:
+    data: Any bytes, string, or object that has str() or unicode() methods.
+
+  Returns:
+    A bytes string representation of the data.
+  """
+  if data is None:
+    return b''
+  if isinstance(data, bytes):
+    # Already bytes - our work is done.
+    return data
+
+  # Coerce to text that will be converted to bytes.
+  s = six.text_type(data)
+
+  try:
+    # Assume the text can be directly converted to bytes (8-bit ascii).
+    return s.encode('iso-8859-1')
+  except UnicodeEncodeError:
+    pass
+
+  try:
+    # Try the output encoding.
+    return s.encode(GetConsoleAttr().GetEncoding())
+  except UnicodeEncodeError:
+    pass
+
+  # Punt to utf-8.
+  return s.encode('utf-8')
+
+
+def Decode(data, encoding=None):
+  """Converts the given string, bytes, or object to a text string.
+
+  Args:
+    data: Any bytes, string, or object that has str() or unicode() methods.
+    encoding: A suggesting encoding used to decode. If this encoding doesn't
+      work, other defaults are tried. Defaults to
+      GetConsoleAttr().GetEncoding().
+
+  Returns:
+    A text string representation of the data.
+  """
+  encoding = encoding or GetConsoleAttr().GetEncoding()
+  return encoding_util.Decode(data, encoding=encoding)
diff --git a/fire/console/console_attr_os.py b/fire/console/console_attr_os.py
new file mode 100644
index 00000000..52decc2c
--- /dev/null
+++ b/fire/console/console_attr_os.py
@@ -0,0 +1,260 @@
+# -*- coding: utf-8 -*- #
+# Copyright 2015 Google Inc. All Rights Reserved.
+#
+# 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.
+
+"""OS specific console_attr helper functions."""
+# This file contains platform specific code which is not currently handled
+# by pytype.
+# pytype: skip-file
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import unicode_literals
+
+import os
+import sys
+
+from fire.console import encoding
+
+
+def GetTermSize():
+  """Gets the terminal x and y dimensions in characters.
+
+  _GetTermSize*() helper functions taken from:
+    http://stackoverflow.com/questions/263890/
+
+  Returns:
+    (columns, lines): A tuple containing the terminal x and y dimensions.
+  """
+  xy = None
+  # Believe the first helper that doesn't bail.
+  for get_terminal_size in (_GetTermSizePosix,
+                            _GetTermSizeWindows,
+                            _GetTermSizeEnvironment,
+                            _GetTermSizeTput):
+    try:
+      xy = get_terminal_size()
+      if xy:
+        break
+    except:  # pylint: disable=bare-except
+      pass
+  return xy or (80, 24)
+
+
+def _GetTermSizePosix():
+  """Returns the Posix terminal x and y dimensions."""
+  # pylint: disable=g-import-not-at-top
+  import fcntl
+  # pylint: disable=g-import-not-at-top
+  import struct
+  # pylint: disable=g-import-not-at-top
+  import termios
+
+  def _GetXY(fd):
+    """Returns the terminal (x,y) size for fd.
+
+    Args:
+      fd: The terminal file descriptor.
+
+    Returns:
+      The terminal (x,y) size for fd or None on error.
+    """
+    try:
+      # This magic incantation converts a struct from ioctl(2) containing two
+      # binary shorts to a (rows, columns) int tuple.
+      rc = struct.unpack(b'hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, 'junk'))
+      return (rc[1], rc[0]) if rc else None
+    except:  # pylint: disable=bare-except
+      return None
+
+  xy = _GetXY(0) or _GetXY(1) or _GetXY(2)
+  if not xy:
+    fd = None
+    try:
+      fd = os.open(os.ctermid(), os.O_RDONLY)
+      xy = _GetXY(fd)
+    except:  # pylint: disable=bare-except
+      xy = None
+    finally:
+      if fd is not None:
+        os.close(fd)
+  return xy
+
+
+def _GetTermSizeWindows():
+  """Returns the Windows terminal x and y dimensions."""
+  # pylint:disable=g-import-not-at-top
+  import struct
+  # pylint: disable=g-import-not-at-top
+  from ctypes import create_string_buffer
+  # pylint:disable=g-import-not-at-top
+  from ctypes import windll
+
+  # stdin handle is -10
+  # stdout handle is -11
+  # stderr handle is -12
+
+  h = windll.kernel32.GetStdHandle(-12)
+  csbi = create_string_buffer(22)
+  if not windll.kernel32.GetConsoleScreenBufferInfo(h, csbi):
+    return None
+  (unused_bufx, unused_bufy, unused_curx, unused_cury, unused_wattr,
+   left, top, right, bottom,
+   unused_maxx, unused_maxy) = struct.unpack(b'hhhhHhhhhhh', csbi.raw)
+  x = right - left + 1
+  y = bottom - top + 1
+  return (x, y)
+
+
+def _GetTermSizeEnvironment():
+  """Returns the terminal x and y dimensions from the environment."""
+  return (int(os.environ['COLUMNS']), int(os.environ['LINES']))
+
+
+def _GetTermSizeTput():
+  """Returns the terminal x and y dimemsions from tput(1)."""
+  import subprocess  # pylint: disable=g-import-not-at-top
+  output = encoding.Decode(subprocess.check_output(['tput', 'cols'],
+                                                   stderr=subprocess.STDOUT))
+  cols = int(output)
+  output = encoding.Decode(subprocess.check_output(['tput', 'lines'],
+                                                   stderr=subprocess.STDOUT))
+  rows = int(output)
+  return (cols, rows)
+
+
+_ANSI_CSI = '\x1b'  # ANSI control sequence indicator (ESC)
+_CONTROL_D = '\x04'  # unix EOF (^D)
+_CONTROL_Z = '\x1a'  # Windows EOF (^Z)
+_WINDOWS_CSI_1 = '\x00'  # Windows control sequence indicator #1
+_WINDOWS_CSI_2 = '\xe0'  # Windows control sequence indicator #2
+
+
+def GetRawKeyFunction():
+  """Returns a function that reads one keypress from stdin with no echo.
+
+  Returns:
+    A function that reads one keypress from stdin with no echo or a function
+    that always returns None if stdin does not support it.
+  """
+  # Believe the first helper that doesn't bail.
+  for get_raw_key_function in (_GetRawKeyFunctionPosix,
+                               _GetRawKeyFunctionWindows):
+    try:
+      return get_raw_key_function()
+    except:  # pylint: disable=bare-except
+      pass
+  return lambda: None
+
+
+def _GetRawKeyFunctionPosix():
+  """_GetRawKeyFunction helper using Posix APIs."""
+  # pylint: disable=g-import-not-at-top
+  import tty
+  # pylint: disable=g-import-not-at-top
+  import termios
+
+  def _GetRawKeyPosix():
+    """Reads and returns one keypress from stdin, no echo, using Posix APIs.
+
+    Returns:
+      The key name, None for EOF, <*> for function keys, otherwise a
+      character.
+    """
+    ansi_to_key = {
+        'A': '<UP-ARROW>',
+        'B': '<DOWN-ARROW>',
+        'D': '<LEFT-ARROW>',
+        'C': '<RIGHT-ARROW>',
+        '5': '<PAGE-UP>',
+        '6': '<PAGE-DOWN>',
+        'H': '<HOME>',
+        'F': '<END>',
+        'M': '<DOWN-ARROW>',
+        'S': '<PAGE-UP>',
+        'T': '<PAGE-DOWN>',
+    }
+
+    # Flush pending output. sys.stdin.read() would do this, but it's explicitly
+    # bypassed in _GetKeyChar().
+    sys.stdout.flush()
+
+    fd = sys.stdin.fileno()
+
+    def _GetKeyChar():
+      return encoding.Decode(os.read(fd, 1))
+
+    old_settings = termios.tcgetattr(fd)
+    try:
+      tty.setraw(fd)
+      c = _GetKeyChar()
+      if c == _ANSI_CSI:
+        c = _GetKeyChar()
+        while True:
+          if c == _ANSI_CSI:
+            return c
+          if c.isalpha():
+            break
+          prev_c = c
+          c = _GetKeyChar()
+          if c == '~':
+            c = prev_c
+            break
+        return ansi_to_key.get(c, '')
+    except:  # pylint:disable=bare-except
+      c = None
+    finally:
+      termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
+    return None if c in (_CONTROL_D, _CONTROL_Z) else c
+
+  return _GetRawKeyPosix
+
+
+def _GetRawKeyFunctionWindows():
+  """_GetRawKeyFunction helper using Windows APIs."""
+  # pylint: disable=g-import-not-at-top
+  import msvcrt
+
+  def _GetRawKeyWindows():
+    """Reads and returns one keypress from stdin, no echo, using Windows APIs.
+
+    Returns:
+      The key name, None for EOF, <*> for function keys, otherwise a
+      character.
+    """
+    windows_to_key = {
+        'H': '<UP-ARROW>',
+        'P': '<DOWN-ARROW>',
+        'K': '<LEFT-ARROW>',
+        'M': '<RIGHT-ARROW>',
+        'I': '<PAGE-UP>',
+        'Q': '<PAGE-DOWN>',
+        'G': '<HOME>',
+        'O': '<END>',
+    }
+
+    # Flush pending output. sys.stdin.read() would do this it's explicitly
+    # bypassed in _GetKeyChar().
+    sys.stdout.flush()
+
+    def _GetKeyChar():
+      return encoding.Decode(msvcrt.getch())
+
+    c = _GetKeyChar()
+    # Special function key is a two character sequence; return the second char.
+    if c in (_WINDOWS_CSI_1, _WINDOWS_CSI_2):
+      return windows_to_key.get(_GetKeyChar(), '')
+    return None if c in (_CONTROL_D, _CONTROL_Z) else c
+
+  return _GetRawKeyWindows
diff --git a/fire/console/console_pager.py b/fire/console/console_pager.py
new file mode 100644
index 00000000..c15a8ee5
--- /dev/null
+++ b/fire/console/console_pager.py
@@ -0,0 +1,299 @@
+# -*- coding: utf-8 -*- #
+# Copyright 2015 Google Inc. All Rights Reserved.
+#
+# 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.
+
+"""Simple console pager."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import unicode_literals
+
+import re
+import sys
+
+from fire.console import console_attr
+
+
+class Pager(object):
+  """A simple console text pager.
+
+  This pager requires the entire contents to be available. The contents are
+  written one page of lines at a time. The prompt is written after each page of
+  lines. A one character response is expected. See HELP_TEXT below for more
+  info.
+
+  The contents are written as is. For example, ANSI control codes will be in
+  effect. This is different from pagers like more(1) which is ANSI control code
+  agnostic and miscalculates line lengths, and less(1) which displays control
+  character names by default.
+
+  Attributes:
+    _attr: The current ConsoleAttr handle.
+    _clear: A string that clears the prompt when written to _out.
+    _contents: The entire contents of the text lines to page.
+    _height: The terminal height in characters.
+    _out: The output stream, log.out (effectively) if None.
+    _prompt: The page break prompt.
+    _search_direction: The search direction command, n:forward, N:reverse.
+    _search_pattern: The current forward/reverse search compiled RE.
+    _width: The termonal width in characters.
+  """
+
+  HELP_TEXT = """
+  Simple pager commands:
+
+    b, ^B, <PAGE-UP>, <LEFT-ARROW>
+      Back one page.
+    f, ^F, <SPACE>, <PAGE-DOWN>, <RIGHT-ARROW>
+      Forward one page. Does not quit if there are no more lines.
+    g, <HOME>
+      Back to the first page.
+    <number>g
+      Go to <number> lines from the top.
+    G, <END>
+      Forward to the last page.
+    <number>G
+      Go to <number> lines from the bottom.
+    h
+      Print pager command help.
+    j, +, <DOWN-ARROW>
+      Forward one line.
+    k, -, <UP-ARROW>
+      Back one line.
+    /pattern
+      Forward search for pattern.
+    ?pattern
+      Backward search for pattern.
+    n
+      Repeat current search.
+    N
+      Repeat current search in the opposite direction.
+    q, Q, ^C, ^D, ^Z
+      Quit return to the caller.
+    any other character
+      Prompt again.
+
+  Hit any key to continue:"""
+
+  PREV_POS_NXT_REPRINT = -1, -1
+
+  def __init__(self, contents, out=None, prompt=None):
+    """Constructor.
+
+    Args:
+      contents: The entire contents of the text lines to page.
+      out: The output stream, log.out (effectively) if None.
+      prompt: The page break prompt, a defalt prompt is used if None..
+    """
+    self._contents = contents
+    self._out = out or sys.stdout
+    self._search_pattern = None
+    self._search_direction = None
+
+    # prev_pos, prev_next values to force reprint
+    self.prev_pos, self.prev_nxt = self.PREV_POS_NXT_REPRINT
+    # Initialize the console attributes.
+    self._attr = console_attr.GetConsoleAttr()
+    self._width, self._height = self._attr.GetTermSize()
+
+    # Initialize the prompt and the prompt clear string.
+    if not prompt:
+      prompt = '{bold}--({{percent}}%)--{normal}'.format(
+          bold=self._attr.GetFontCode(bold=True),
+          normal=self._attr.GetFontCode())
+    self._clear = '\r{0}\r'.format(' ' * (self._attr.DisplayWidth(prompt) - 6))
+    self._prompt = prompt
+
+    # Initialize a list of lines with long lines split into separate display
+    # lines.
+    self._lines = []
+    for line in contents.splitlines():
+      self._lines += self._attr.SplitLine(line, self._width)
+
+  def _Write(self, s):
+    """Mockable helper that writes s to self._out."""
+    self._out.write(s)
+
+  def _GetSearchCommand(self, c):
+    """Consumes a search command and returns the equivalent pager command.
+
+    The search pattern is an RE that is pre-compiled and cached for subsequent
+    /<newline>, ?<newline>, n, or N commands.
+
+    Args:
+      c: The search command char.
+
+    Returns:
+      The pager command char.
+    """
+    self._Write(c)
+    buf = ''
+    while True:
+      p = self._attr.GetRawKey()
+      if p in (None, '\n', '\r') or len(p) != 1:
+        break
+      self._Write(p)
+      buf += p
+    self._Write('\r' + ' ' * len(buf) + '\r')
+    if buf:
+      try:
+        self._search_pattern = re.compile(buf)
+      except re.error:
+        # Silently ignore pattern errors.
+        self._search_pattern = None
+        return ''
+    self._search_direction = 'n' if c == '/' else 'N'
+    return 'n'
+
+  def _Help(self):
+    """Print command help and wait for any character to continue."""
+    clear = self._height - (len(self.HELP_TEXT) -
+                            len(self.HELP_TEXT.replace('\n', '')))
+    if clear > 0:
+      self._Write('\n' * clear)
+    self._Write(self.HELP_TEXT)
+    self._attr.GetRawKey()
+    self._Write('\n')
+
+  def Run(self):
+    """Run the pager."""
+    # No paging if the contents are small enough.
+    if len(self._lines) <= self._height:
+      self._Write(self._contents)
+      return
+
+    # We will not always reset previous values.
+    reset_prev_values = True
+    # Save room for the prompt at the bottom of the page.
+    self._height -= 1
+
+    # Loop over all the pages.
+    pos = 0
+    while pos < len(self._lines):
+      # Write a page of lines.
+      nxt = pos + self._height
+      if nxt > len(self._lines):
+        nxt = len(self._lines)
+        pos = nxt - self._height
+      # Checks if the starting position is in between the current printed lines
+      # so we don't need to reprint all the lines.
+      if self.prev_pos < pos < self.prev_nxt:
+        # we start where the previous page ended.
+        self._Write('\n'.join(self._lines[self.prev_nxt:nxt]) + '\n')
+      elif pos != self.prev_pos and nxt != self.prev_nxt:
+        self._Write('\n'.join(self._lines[pos:nxt]) + '\n')
+
+      # Handle the prompt response.
+      percent = self._prompt.format(percent=100 * nxt // len(self._lines))
+      digits = ''
+      while True:
+        # We want to reset prev values if we just exited out of the while loop
+        if reset_prev_values:
+          self.prev_pos, self.prev_nxt = pos, nxt
+          reset_prev_values = False
+        self._Write(percent)
+        c = self._attr.GetRawKey()
+        self._Write(self._clear)
+
+        # Parse the command.
+        if c in (None,    # EOF.
+                 'q',     # Quit.
+                 'Q',     # Quit.
+                 '\x03',  # ^C  (unix & windows terminal interrupt)
+                 '\x1b',  # ESC.
+                ):
+          # Quit.
+          return
+        elif c in ('/', '?'):
+          c = self._GetSearchCommand(c)
+        elif c.isdigit():
+          # Collect digits for operation count.
+          digits += c
+          continue
+
+        # Set the optional command count.
+        if digits:
+          count = int(digits)
+          digits = ''
+        else:
+          count = 0
+
+        # Finally commit to command c.
+        if c in ('<PAGE-UP>', '<LEFT-ARROW>', 'b', '\x02'):
+          # Previous page.
+          nxt = pos - self._height
+          if nxt < 0:
+            nxt = 0
+        elif c in ('<PAGE-DOWN>', '<RIGHT-ARROW>', 'f', '\x06', ' '):
+          # Next page.
+          if nxt >= len(self._lines):
+            continue
+          nxt = pos + self._height
+          if nxt >= len(self._lines):
+            nxt = pos
+        elif c in ('<HOME>', 'g'):
+          # First page.
+          nxt = count - 1
+          if nxt > len(self._lines) - self._height:
+            nxt = len(self._lines) - self._height
+          if nxt < 0:
+            nxt = 0
+        elif c in ('<END>', 'G'):
+          # Last page.
+          nxt = len(self._lines) - count
+          if nxt > len(self._lines) - self._height:
+            nxt = len(self._lines) - self._height
+          if nxt < 0:
+            nxt = 0
+        elif c == 'h':
+          self._Help()
+          # Special case when we want to reprint the previous display.
+          self.prev_pos, self.prev_nxt = self.PREV_POS_NXT_REPRINT
+          nxt = pos
+          break
+        elif c in ('<DOWN-ARROW>', 'j', '+', '\n', '\r'):
+          # Next line.
+          if nxt >= len(self._lines):
+            continue
+          nxt = pos + 1
+          if nxt >= len(self._lines):
+            nxt = pos
+        elif c in ('<UP-ARROW>', 'k', '-'):
+          # Previous line.
+          nxt = pos - 1
+          if nxt < 0:
+            nxt = 0
+        elif c in ('n', 'N'):
+          # Next pattern match search.
+          if not self._search_pattern:
+            continue
+          nxt = pos
+          i = pos
+          direction = 1 if c == self._search_direction else -1
+          while True:
+            i += direction
+            if i < 0 or i >= len(self._lines):
+              break
+            if self._search_pattern.search(self._lines[i]):
+              nxt = i
+              break
+        else:
+          # Silently ignore everything else.
+          continue
+        if nxt != pos:
+          # We will exit the while loop because position changed so we can reset
+          # prev values.
+          reset_prev_values = True
+          break
+      pos = nxt
diff --git a/fire/console/encoding.py b/fire/console/encoding.py
new file mode 100644
index 00000000..e8b1e571
--- /dev/null
+++ b/fire/console/encoding.py
@@ -0,0 +1,207 @@
+# -*- coding: utf-8 -*- #
+
+# Copyright 2015 Google Inc. All Rights Reserved.
+#
+# 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.
+
+"""A module for dealing with unknown string and environment encodings."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import unicode_literals
+
+import sys
+
+import six
+
+
+def Encode(string, encoding=None):
+  """Encode the text string to a byte string.
+
+  Args:
+    string: str, The text string to encode.
+    encoding: The suggested encoding if known.
+
+  Returns:
+    str, The binary string.
+  """
+  if string is None:
+    return None
+  if not six.PY2:
+    # In Python 3, the environment sets and gets accept and return text strings
+    # only, and it handles the encoding itself so this is not necessary.
+    return string
+  if isinstance(string, six.binary_type):
+    # Already an encoded byte string, we are done
+    return string
+
+  encoding = encoding or _GetEncoding()
+  return string.encode(encoding)
+
+
+def Decode(data, encoding=None):
+  """Returns string with non-ascii characters decoded to UNICODE.
+
+  UTF-8, the suggested encoding, and the usual suspects will be attempted in
+  order.
+
+  Args:
+    data: A string or object that has str() and unicode() methods that may
+      contain an encoding incompatible with the standard output encoding.
+    encoding: The suggested encoding if known.
+
+  Returns:
+    A text string representing the decoded byte string.
+  """
+  if data is None:
+    return None
+
+  # First we are going to get the data object to be a text string.
+  # Don't use six.string_types here because on Python 3 bytes is not considered
+  # a string type and we want to include that.
+  if isinstance(data, six.text_type) or isinstance(data, six.binary_type):
+    string = data
+  else:
+    # Some non-string type of object.
+    try:
+      string = six.text_type(data)
+    except (TypeError, UnicodeError):
+      # The string cannot be converted to unicode -- default to str() which will
+      # catch objects with special __str__ methods.
+      string = str(data)
+
+  if isinstance(string, six.text_type):
+    # Our work is done here.
+    return string
+
+  try:
+    # Just return the string if its pure ASCII.
+    return string.decode('ascii')
+  except UnicodeError:
+    # The string is not ASCII encoded.
+    pass
+
+  # Try the suggested encoding if specified.
+  if encoding:
+    try:
+      return string.decode(encoding)
+    except UnicodeError:
+      # Bad suggestion.
+      pass
+
+  # Try UTF-8 because the other encodings could be extended ASCII. It would
+  # be exceptional if a valid extended ascii encoding with extended chars
+  # were also a valid UITF-8 encoding.
+  try:
+    return string.decode('utf8')
+  except UnicodeError:
+    # Not a UTF-8 encoding.
+    pass
+
+  # Try the filesystem encoding.
+  try:
+    return string.decode(sys.getfilesystemencoding())
+  except UnicodeError:
+    # string is not encoded for filesystem paths.
+    pass
+
+  # Try the system default encoding.
+  try:
+    return string.decode(sys.getdefaultencoding())
+  except UnicodeError:
+    # string is not encoded using the default encoding.
+    pass
+
+  # We don't know the string encoding.
+  # This works around a Python str.encode() "feature" that throws
+  # an ASCII *decode* exception on str strings that contain 8th bit set
+  # bytes. For example, this sequence throws an exception:
+  #   string = '\xdc'  # iso-8859-1 'Ü'
+  #   string = string.encode('ascii', 'backslashreplace')
+  # even though 'backslashreplace' is documented to handle encoding
+  # errors. We work around the problem by first decoding the str string
+  # from an 8-bit encoding to unicode, selecting any 8-bit encoding that
+  # uses all 256 bytes (such as ISO-8559-1):
+  #   string = string.decode('iso-8859-1')
+  # Using this produces a sequence that works:
+  #   string = '\xdc'
+  #   string = string.decode('iso-8859-1')
+  #   string = string.encode('ascii', 'backslashreplace')
+  return string.decode('iso-8859-1')
+
+
+def GetEncodedValue(env, name, default=None):
+  """Returns the decoded value of the env var name.
+
+  Args:
+    env: {str: str}, The env dict.
+    name: str, The env var name.
+    default: The value to return if name is not in env.
+
+  Returns:
+    The decoded value of the env var name.
+  """
+  name = Encode(name)
+  value = env.get(name)
+  if value is None:
+    return default
+  # In Python 3, the environment sets and gets accept and return text strings
+  # only, and it handles the encoding itself so this is not necessary.
+  return Decode(value)
+
+
+def SetEncodedValue(env, name, value, encoding=None):
+  """Sets the value of name in env to an encoded value.
+
+  Args:
+    env: {str: str}, The env dict.
+    name: str, The env var name.
+    value: str or unicode, The value for name. If None then name is removed from
+      env.
+    encoding: str, The encoding to use or None to try to infer it.
+  """
+  # Python 2 *and* 3 unicode support falls apart at filesystem/argv/environment
+  # boundaries. The encoding used for filesystem paths and environment variable
+  # names/values is under user control on most systems. With one of those values
+  # in hand there is no way to tell exactly how the value was encoded. We get
+  # some reasonable hints from sys.getfilesystemencoding() or
+  # sys.getdefaultencoding() and use them to encode values that the receiving
+  # process will have a chance at decoding. Leaving the values as unicode
+  # strings will cause os module Unicode exceptions. What good is a language
+  # unicode model when the module support could care less?
+  name = Encode(name, encoding=encoding)
+  if value is None:
+    env.pop(name, None)
+    return
+  env[name] = Encode(value, encoding=encoding)
+
+
+def EncodeEnv(env, encoding=None):
+  """Encodes all the key value pairs in env in preparation for subprocess.
+
+  Args:
+    env: {str: str}, The environment you are going to pass to subprocess.
+    encoding: str, The encoding to use or None to use the default.
+
+  Returns:
+    {bytes: bytes}, The environment to pass to subprocess.
+  """
+  encoding = encoding or _GetEncoding()
+  return {
+      Encode(k, encoding=encoding): Encode(v, encoding=encoding)
+      for k, v in six.iteritems(env)}
+
+
+def _GetEncoding():
+  """Gets the default encoding to use."""
+  return sys.getfilesystemencoding() or sys.getdefaultencoding()

From c11a3fa79321f3388ebb78492b9300910b9c95f6 Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Thu, 21 Feb 2019 09:28:48 -0800
Subject: [PATCH 054/324] HelpTextForFunction: detailed help text for functions

Implemented HelpTextForFunction to generate detail help text for function component.

PiperOrigin-RevId: 235009112
Change-Id: Ia627ff59c70c48c27d3d1afdc46de1ac209459cd
---
 fire/helputils.py       | 119 +++++++++++++++++++++++++++++++++++++---
 fire/helputils_test.py  |  25 +++++++++
 fire/test_components.py |  29 ++++++++++
 3 files changed, 164 insertions(+), 9 deletions(-)

diff --git a/fire/helputils.py b/fire/helputils.py
index af1ca8cf..e0f6f44b 100644
--- a/fire/helputils.py
+++ b/fire/helputils.py
@@ -173,6 +173,25 @@ def _CommonHelpText(info, trace=None):
   return '\n'.join(lines)
 
 
+def GetSummaryAndDescription(docstring_info):
+  """Retrieves summary and description for help text generation."""
+
+  # To handle both empty string and None
+  summary = docstring_info.summary if docstring_info.summary else None
+  description = docstring_info.description if docstring_info.description else None
+  return summary, description
+
+
+def GetCurrentCommand(trace=None):
+  """Returns current command for the purpose of generating help text."""
+  if trace:
+    current_command = trace.GetCommand()
+  else:
+    current_command = ''
+
+  return current_command
+
+
 def HelpText(component, info, trace=None, verbose=False):
   if inspect.isroutine(component) or inspect.isclass(component):
     return HelpTextForFunction(component, info, trace)
@@ -181,7 +200,96 @@ def HelpText(component, info, trace=None, verbose=False):
 
 
 def HelpTextForFunction(component, info, trace=None, verbose=False):
-  del component, info, trace, verbose
+  """Returns detail help text for a function component.
+
+  Args:
+    component: Current component to generate help text for.
+    info: Info containing metadata of component.
+    trace: FireTrace object that leads to current component.
+    verbose: Whether to display help text in verbose mode.
+
+  Returns:
+    Formatted help text for display.
+  """
+  # TODO(joejoevictor): Implement verbose related output
+  del verbose
+
+  current_command = GetCurrentCommand(trace)
+  summary, description = GetSummaryAndDescription(info['docstring_info'])
+  spec = inspectutils.GetFullArgSpec(component)
+  args = spec.args
+
+  if spec.defaults is None:
+    num_defaults = 0
+  else:
+    num_defaults = len(spec.defaults)
+  args_with_no_defaults = args[:len(args) - num_defaults]
+
+  # TODO(joejoevictor): Generate flag section using these
+  # args_with_defaults = args[len(args) - num_defaults:]
+  # flags = args_with_defaults + spec.kwonlyargs
+
+  output_template = """NAME
+    {name_section}
+
+SYNOPSIS
+    {synopsis_section}
+
+DESCRIPTION
+    {description_section}
+{args_and_flags_section}
+NOTES
+    You could also use flags syntax for POSITIONAL ARGUMENTS
+"""
+
+  # Name section
+  name_section_template = '{current_command}{command_summary}'
+  command_summary_str = ' - ' + summary if summary else ''
+  name_section = name_section_template.format(
+      current_command=current_command, command_summary=command_summary_str)
+
+  items = [arg.upper() for arg in args_with_no_defaults]
+  args_and_flags = ' '.join(items)
+
+  # Synopsis section
+  synopsis_section_template = '{current_command} {args_and_flags}'
+  positional_arguments = '|'.join(args)
+  if positional_arguments:
+    positional_arguments = ' ' + positional_arguments
+  synopsis_section = synopsis_section_template.format(
+      current_command=current_command, args_and_flags=args_and_flags)
+
+  # Description section
+  description_section = description if description else summary
+
+  args_and_flags_section = ''
+
+  # Positional arguments and flags section
+
+  pos_arg_template = """
+POSITIONAL ARGUMENTS
+{items}
+"""
+  pos_arg_items = []
+  for arg in args_with_no_defaults:
+    item_template = '    {arg_name}\n        {arg_description}\n'
+    arg_description = None
+    for arg_in_docstring in info['docstring_info'].args:
+      if arg_in_docstring.name == arg:
+        arg_description = arg_in_docstring.description
+
+    item = item_template.format(
+        arg_name=arg.upper(), arg_description=arg_description)
+    pos_arg_items.append(item)
+  if pos_arg_items:
+    args_and_flags_section += pos_arg_template.format(
+        items='\n'.join(pos_arg_items).rstrip('\n'))
+
+  return output_template.format(
+      name_section=name_section,
+      synopsis_section=synopsis_section,
+      description_section=description_section,
+      args_and_flags_section=args_and_flags_section)
 
 
 def HelpTextForObject(component, info, trace=None, verbose=False):
@@ -208,13 +316,7 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
 {detail_section}
 """
 
-  if trace:
-    current_command = trace.GetCommand()
-  else:
-    current_command = None
-
-  if not current_command:
-    current_command = ''
+  current_command = GetCurrentCommand(trace)
 
   docstring_info = info['docstring_info']
   command_summary = docstring_info.summary if docstring_info.summary else ''
@@ -338,7 +440,6 @@ def UsageTextForFunction(component, trace=None):
 
   spec = inspectutils.GetFullArgSpec(component)
   args = spec.args
-
   if spec.defaults is None:
     num_defaults = 0
   else:
diff --git a/fire/helputils_test.py b/fire/helputils_test.py
index 56cabbb7..445823de 100644
--- a/fire/helputils_test.py
+++ b/fire/helputils_test.py
@@ -165,6 +165,31 @@ def testHelpScreen(self):
 """
     self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
 
+  def testHelpScreen_withLineBreak(self):
+    component = tc.ClassWithMultilineDocstring.example_generator
+    t = trace.FireTrace(component, name='example_generator')
+    info = inspectutils.Info(component)
+    info['docstring_info'] = docstrings.parse(info['docstring'])
+    help_output = helputils.HelpText(component, info, t)
+    expected_output = """
+    NAME
+        example_generator - Generators have a ``Yields`` section instead of a ``Returns`` section.
+
+    SYNOPSIS
+        example_generator N
+
+    DESCRIPTION
+        Generators have a ``Yields`` section instead of a ``Returns`` section.
+
+    POSITIONAL ARGUMENTS
+        N
+            The upper limit of the range to generate, from 0 to `n` - 1.
+
+    NOTES
+        You could also use flags syntax for POSITIONAL ARGUMENTS
+    """
+    self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
+
 
 class UsageTest(testutils.BaseTestCase):
 
diff --git a/fire/test_components.py b/fire/test_components.py
index 2ba1155c..03d8c15b 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -318,3 +318,32 @@ def print_msg(self, msg=None):
     if msg is None:
       msg = self.message
     print(msg)
+
+
+class ClassWithMultilineDocstring(object):
+  """Test class for testing help text output with multiline docstring.
+
+  This is a test class that has a long docstring description that spans across
+  multiple lines for testing line breaking in help text.
+  """
+
+  @staticmethod
+  def example_generator(n):
+    """Generators have a ``Yields`` section instead of a ``Returns`` section.
+
+    Args:
+        n (int): The upper limit of the range to generate, from 0 to `n` - 1.
+
+    Yields:
+        int: The next number in the range of 0 to `n` - 1.
+
+    Examples:
+        Examples should be written in doctest format, and should illustrate how
+        to use the function.
+
+        >>> print([i for i in example_generator(4)])
+        [0, 1, 2, 3]
+
+    """
+    for i in range(n):
+      yield i

From 42ff6cfa53e2bf5e9cce34f915e5c2443579578e Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 21 Feb 2019 09:35:11 -0800
Subject: [PATCH 055/324] Use the console package in Python Fire for help text

PiperOrigin-RevId: 235010314
Change-Id: I7095c36bfb28d4c4a059654656334322185f3d3b
---
 fire/core.py      | 59 ++++++++++++++++++++++++++++++-----------------
 fire/core_test.py |  6 ++---
 2 files changed, 41 insertions(+), 24 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index f8c0b6e2..05a01e11 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -70,6 +70,7 @@ def main(argv):
 from fire import parser
 from fire import trace
 from fire import value_types
+from fire.console import console_pager
 import six
 
 
@@ -130,37 +131,45 @@ def Fire(component=None, command=None, name=None):
   component_trace = _Fire(component, args, context, name)
 
   if component_trace.HasError():
-    for help_flag in ('-h', '--help'):
-      if help_flag in component_trace.elements[-1].args:
-        command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
-        print('INFO: Showing help with the command {cmd}.\n'.format(
-            cmd=pipes.quote(command)), file=sys.stderr)
-    _PrintError(component_trace)
+    _DisplayError(component_trace)
     raise FireExit(2, component_trace)
   elif component_trace.show_trace and component_trace.show_help:
-    print('Fire trace:\n{trace}\n'.format(trace=component_trace),
-          file=sys.stderr)
+    output = ['Fire trace:\n{trace}\n'.format(trace=component_trace)]
     result = component_trace.GetResult()
-    print(
-        helputils.HelpString(result, component_trace, component_trace.verbose),
-        file=sys.stderr)
+    help_string = helputils.HelpString(
+        result, component_trace, component_trace.verbose)
+    output.append(help_string)
+    Display(output)
     raise FireExit(0, component_trace)
   elif component_trace.show_trace:
-    print('Fire trace:\n{trace}'.format(trace=component_trace),
-          file=sys.stderr)
+    output = ['Fire trace:\n{trace}'.format(trace=component_trace)]
+    Display(output)
     raise FireExit(0, component_trace)
   elif component_trace.show_help:
     result = component_trace.GetResult()
-    print(
-        helputils.HelpString(result, component_trace, component_trace.verbose),
-        file=sys.stderr)
+    help_string = helputils.HelpString(
+        result, component_trace, component_trace.verbose)
+    output = [help_string]
+    Display(output)
     raise FireExit(0, component_trace)
   else:
+    # The command succeeded normally; print the result.
     _PrintResult(component_trace, verbose=component_trace.verbose)
     result = component_trace.GetResult()
     return result
 
 
+def Display(lines):
+  text = '\n'.join(lines) + '\n'
+  pager = console_pager.Pager(text, out=sys.stderr)
+  try:
+    pager.Run()
+  except:  # pylint: disable=bare-except
+    # pager.Run() fails with termios.error(25, 'Inappropriate ioctl for device')
+    # for outputs that don't fit on a single screen in our test environment.
+    pass
+
+
 def CompletionScript(name, component, shell):
   """Returns the text of the completion script for a Fire CLI."""
   return completion.Script(name, component, shell=shell)
@@ -249,13 +258,21 @@ def _PrintResult(component_trace, verbose=False):
     print(helputils.HelpString(result, component_trace, verbose))
 
 
-def _PrintError(component_trace):
+def _DisplayError(component_trace):
   """Prints the Fire trace and the error to stdout."""
-  print('Fire trace:\n{trace}\n'.format(trace=component_trace), file=sys.stderr)
+  output = []
+  for help_flag in ('-h', '--help'):
+    if help_flag in component_trace.elements[-1].args:
+      command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
+      message = 'INFO: Showing help with the command {cmd}.\n'.format(
+          cmd=pipes.quote(command))
+      output.append(message)
+  output.append('Fire trace:\n{trace}\n'.format(trace=component_trace))
   result = component_trace.GetResult()
-  print(
-      helputils.HelpString(result, component_trace, component_trace.verbose),
-      file=sys.stderr)
+  help_string = helputils.HelpString(result, component_trace,
+                                     component_trace.verbose)
+  output.append(help_string)
+  Display(output)
 
 
 def _DictAsString(result, verbose=False):
diff --git a/fire/core_test.py b/fire/core_test.py
index f9f12099..2a906002 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -82,11 +82,11 @@ def testHelpWithClass(self):
       core.Fire(tc.InstanceVars, command=['-h'])
 
   def testHelpWithMember(self):
-    with self.assertRaisesFireExit(0, 'Usage:.*upper'):
+    with self.assertRaisesFireExit(0, 'Usage:.*capitalize'):
       core.Fire(tc.TypedProperties, command=['gamma', '--', '--help'])
-    with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*upper'):
+    with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*capitalize'):
       core.Fire(tc.TypedProperties, command=['gamma', '--help'])
-    with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*upper'):
+    with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*capitalize'):
       core.Fire(tc.TypedProperties, command=['gamma', '-h'])
     with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*delta'):
       core.Fire(tc.TypedProperties, command=['delta', '--help'])

From f47c938b8e417eb02c26bc3134870f6f636cd44a Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 21 Feb 2019 09:38:47 -0800
Subject: [PATCH 056/324] quick lint fixes for helputils

PiperOrigin-RevId: 235010954
Change-Id: I1db851071dfe7d0f6c8e6715dd0248ed3ad97d93
---
 fire/helputils.py      | 3 ++-
 fire/helputils_test.py | 2 +-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/fire/helputils.py b/fire/helputils.py
index e0f6f44b..08386c34 100644
--- a/fire/helputils.py
+++ b/fire/helputils.py
@@ -178,7 +178,8 @@ def GetSummaryAndDescription(docstring_info):
 
   # To handle both empty string and None
   summary = docstring_info.summary if docstring_info.summary else None
-  description = docstring_info.description if docstring_info.description else None
+  description = (
+      docstring_info.description if docstring_info.description else None)
   return summary, description
 
 
diff --git a/fire/helputils_test.py b/fire/helputils_test.py
index 445823de..05571228 100644
--- a/fire/helputils_test.py
+++ b/fire/helputils_test.py
@@ -165,7 +165,7 @@ def testHelpScreen(self):
 """
     self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
 
-  def testHelpScreen_withLineBreak(self):
+  def testHelpScreenWithLineBreak(self):
     component = tc.ClassWithMultilineDocstring.example_generator
     t = trace.FireTrace(component, name='example_generator')
     info = inspectutils.Info(component)

From e35e650f72abb23cdb7104ff75611b78142844e4 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 21 Feb 2019 09:52:19 -0800
Subject: [PATCH 057/324] Add __init__.py to console package.

PiperOrigin-RevId: 235013192
Change-Id: I47e7da8685b58dae44527da971c9dbd88fcfd92b
---
 fire/console/__init__.py | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 fire/console/__init__.py

diff --git a/fire/console/__init__.py b/fire/console/__init__.py
new file mode 100644
index 00000000..e69de29b

From 00608beef215708ec1bbb0b68e8692f2b8cf20a0 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 21 Feb 2019 10:16:18 -0800
Subject: [PATCH 058/324] don't run pylint against the console package.

PiperOrigin-RevId: 235017763
Change-Id: I54b3d1a46525fea184795ff7fbe56b6e1edc78dd
---
 .travis.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.travis.yml b/.travis.yml
index f877b2f3..7ec70752 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -14,4 +14,4 @@ script:
   - python -m pytest  # Run the tests without IPython.
   - pip install ipython
   - python -m pytest  # Now run the tests with IPython.
-  - if [[ $TRAVIS_PYTHON_VERSION != 3.6 ]]; then pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py; fi
+  - if [[ $TRAVIS_PYTHON_VERSION != 3.6 ]]; then pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console; fi

From 5f2fa985187b837fe55dab596eece91f1931c929 Mon Sep 17 00:00:00 2001
From: Mehmood Deshmukh <meshde.md@gmail.com>
Date: Fri, 22 Feb 2019 03:33:16 +0530
Subject: [PATCH 059/324] Completion Enhancements (#144)

Completion Enhancements:
* Makes option flags available in completion even if prev word is another option
* Ensure module flags appear before a func or after its args
---
 fire/completion.py      | 272 ++++++++++++++++++++++++++++++++--------
 fire/completion_test.py |   5 +-
 2 files changed, 227 insertions(+), 50 deletions(-)

diff --git a/fire/completion.py b/fire/completion.py
index 8b74776f..e766a89f 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -46,13 +46,11 @@ def _BashScript(name, commands, default_options=None):
     A string which is the Bash script. Source the bash script to enable tab
     completion in Bash.
   """
+
   default_options = default_options or set()
-  options_map = collections.defaultdict(lambda: copy.copy(default_options))
-  for command in commands:
-    start = (name + ' ' + ' '.join(command[:-1])).strip()
-    completion = _FormatForCommand(command[-1])
-    options_map[start].add(completion)
-    options_map[start.replace('_', '-')].add(completion)
+  global_options, options_map, subcommands_map = _GetMaps(
+      name, commands, default_options
+  )
 
   bash_completion_template = """# bash completion support for {name}
 # DO NOT EDIT.
@@ -60,41 +58,128 @@ def _BashScript(name, commands, default_options=None):
 
 _complete-{identifier}()
 {{
-  local start cur opts
+  local cur prev opts lastcommand
   COMPREPLY=()
-  start="${{COMP_WORDS[@]:0:COMP_CWORD}}"
+  prev="${{COMP_WORDS[COMP_CWORD-1]}}"
   cur="${{COMP_WORDS[COMP_CWORD]}}"
+  lastcommand=$(get_lastcommand)
 
   opts="{default_options}"
+  GLOBAL_OPTIONS="{global_options}"
 
-{start_checks}
+{checks}
 
   COMPREPLY=( $(compgen -W "${{opts}}" -- ${{cur}}) )
   return 0
 }}
 
+get_lastcommand()
+{{
+  local lastcommand i
+
+  lastcommand=
+  for ((i=0; i < ${{#COMP_WORDS[@]}}; ++i)); do
+    if [[ ${{COMP_WORDS[i]}} != -* ]] && [[ -n ${{COMP_WORDS[i]}} ]] && [[
+      ${{COMP_WORDS[i]}} != $cur ]]; then
+      lastcommand=${{COMP_WORDS[i]}}
+    fi
+  done
+
+  echo $lastcommand
+}}
+
+filter_options()
+{{
+  local opts
+  opts=""
+  for opt in "$@"
+  do
+    if ! option_already_entered $opt; then
+      opts="$opts $opt"
+    fi
+  done
+
+  echo $opts
+}}
+
+option_already_entered()
+{{
+  local opt
+  for opt in ${{COMP_WORDS[@]:0:COMP_CWORD}}
+  do
+    if [ $1 == $opt ]; then
+      return 0
+    fi
+  done
+  return 1
+}}
+
+is_prev_global()
+{{
+  local opt
+  for opt in $GLOBAL_OPTIONS
+  do
+    if [ $opt == $prev ]; then
+      return 0
+    fi
+  done
+  return 1
+}}
+
 complete -F _complete-{identifier} {command}
 """
-  start_check_template = """
-  if [[ "$start" == "{start}" ]] ; then
-    opts="{completions}"
-  fi"""
-
-  start_checks = '\n'.join(
-      start_check_template.format(
-          start=start,
-          completions=' '.join(sorted(options_map[start]))
+
+  check_wrapper = """
+  case "${{lastcommand}}" in
+  {lastcommand_checks}
+  esac"""
+
+  lastcommand_check_template = """
+    {command})
+      {opts_assignment}
+      opts=$(filter_options $opts)
+    ;;"""
+
+  opts_assignment_subcommand_template = """
+      if is_prev_global; then
+        opts="${{GLOBAL_OPTIONS}}"
+      else
+        opts="{options} ${{GLOBAL_OPTIONS}}"
+      fi"""
+
+  opts_assignment_main_command_template = """
+      opts="{options} ${{GLOBAL_OPTIONS}}" """
+
+  def _GetOptsAssignmentTemplate(command):
+    if command == name:
+      return opts_assignment_main_command_template
+    else:
+      return opts_assignment_subcommand_template
+
+  lastcommand_checks = '\n'.join(
+      lastcommand_check_template.format(
+          command=command,
+          opts_assignment=_GetOptsAssignmentTemplate(command).format(
+              options=' '.join(sorted(
+                  options_map[command].union(subcommands_map[command])
+              )),
+          ),
       )
-      for start in options_map
+      for command in set(subcommands_map.keys()).union(set(options_map.keys()))
+  )
+
+  checks = check_wrapper.format(
+      lastcommand_checks=lastcommand_checks,
   )
 
   return (
       bash_completion_template.format(
           name=name,
           command=name,
-          start_checks=start_checks,
+          checks=checks,
           default_options=' '.join(default_options),
-          identifier=name.replace('/', '').replace('.', '').replace(',', '')
+          identifier=name.replace('/', '').replace('.', '').replace(',', ''),
+          global_options=' '.join(global_options),
       )
   )
 
@@ -114,44 +199,85 @@ def _FishScript(name, commands, default_options=None):
     completion in Fish.
   """
   default_options = default_options or set()
-  options_map = collections.defaultdict(lambda: copy.copy(default_options))
-  for command in commands:
-    start = (name + ' ' + ' '.join(command[:-1])).strip()
-    completion = _FormatForCommand(command[-1])
-    options_map[start].add(completion)
-    options_map[start.replace('_', '-')].add(completion)
+  global_options, options_map, subcommands_map = _GetMaps(
+      name, commands, default_options
+  )
+
   fish_source = """function __fish_using_command
     set cmd (commandline -opc)
-    if [ (count $cmd) -eq (count $argv) ]
-        for i in (seq (count $argv))
-            if [ $cmd[$i] != $argv[$i] ]
+    for i in (seq (count $cmd) 1)
+        switch $cmd[$i]
+        case "-*"
+        case "*"
+            if [ $cmd[$i] = $argv[1] ]
+                return 0
+            else
                 return 1
             end
         end
-        return 0
     end
     return 1
 end
+
+function __option_entered_check
+    set cmd (commandline -opc)
+    for i in (seq (count $cmd))
+        switch $cmd[$i]
+        case "-*"
+            if [ $cmd[$i] = $argv[1] ]
+                return 1
+            end
+        end
+    end
+    return 0
+end
+
+function __is_prev_global
+    set cmd (commandline -opc)
+    set global_options {global_options}
+    set prev (count $cmd)
+
+    for opt in $global_options
+        if [ "--$opt" = $cmd[$prev] ]
+            echo $prev
+            return 0
+        end
+    end
+    return 1
+end
+
 """
-  subcommand_template = ("complete -c {name} -n '__fish_using_command {start}' "
-                         "-f -a {subcommand}\n")
+
+  subcommand_template = ("complete -c {name} -n '__fish_using_command "
+                         "{command}' -f -a {subcommand}\n")
   flag_template = ("complete -c {name} -n "
-                   "'__fish_using_command {start}' -l {option}\n")
-  for start in options_map:
-    for option in sorted(options_map[start]):
-      if option.startswith('--'):
-        fish_source += flag_template.format(
-            name=name,
-            start=start,
-            option=option[2:]
-        )
-      else:
-        fish_source += subcommand_template.format(
-            name=name,
-            start=start,
-            subcommand=option
-        )
-  return fish_source
+                   "'__fish_using_command {command};{prev_global_check} and "
+                   "__option_entered_check --{option}' -l {option}\n")
+
+  prev_global_check = " and __is_prev_global;"
+  for command in set(subcommands_map.keys()).union(set(options_map.keys())):
+    for subcommand in subcommands_map[command]:
+      fish_source += subcommand_template.format(
+          name=name,
+          command=command,
+          subcommand=subcommand,
+      )
+
+    for option in options_map[command].union(global_options):
+      check_needed = command != name
+      fish_source += flag_template.format(
+          name=name,
+          command=command,
+          prev_global_check=prev_global_check if check_needed else "",
+          option=option.lstrip("--"),
+      )
+
+  return fish_source.format(
+      global_options=' '.join(
+          '"{option}"'.format(option=option)
+          for option in global_options
+      )
+  )
 
 
 def _IncludeMember(name, verbose):
@@ -302,3 +428,51 @@ def _Commands(component, depth=3):
 
     for command in _Commands(member, depth - 1):
       yield (member_name,) + command
+
+
+def _IsOption(arg):
+  return arg.startswith('-')
+
+def _GetMaps(name, commands, default_options):
+  """Returns sets of subcommands and options for each command.
+
+  Args:
+    name: The first token in the commands, also the name of the command.
+    commands: A list of all possible commands that tab completion can complete
+        to. Each command is a list or tuple of the string tokens that make up
+        that command.
+    default_options: A dict of options that can be used with any command. Use
+        this if there are flags that can always be appended to a command.
+  Returns:
+    global_options: A set of all options of the first token of the command.
+    subcommands_map: A dict storing set of subcommands for each
+        command/subcommand.
+    options_map: A dict storing set of options for each subcommand.
+  """
+
+  global_options = copy.copy(default_options)
+  options_map = collections.defaultdict(lambda: copy.copy(default_options))
+  subcommands_map = collections.defaultdict(set)
+
+  for command in commands:
+    if len(command) == 1:
+
+      if _IsOption(command[0]):
+        global_options.add(command[0])
+      else:
+        subcommands_map[name].add(command[0])
+
+    elif command:
+
+      subcommand = command[-2]
+      arg = _FormatForCommand(command[-1])
+
+      if _IsOption(arg):
+        args_map = options_map
+      else:
+        args_map = subcommands_map
+
+      args_map[subcommand].add(arg)
+      args_map[subcommand.replace('_', '-')].add(arg)
+
+  return global_options, options_map, subcommands_map
diff --git a/fire/completion_test.py b/fire/completion_test.py
index 47d80b4a..eaf80a70 100644
--- a/fire/completion_test.py
+++ b/fire/completion_test.py
@@ -36,7 +36,10 @@ def testCompletionBashScript(self):
     script = completion._BashScript(name='command', commands=commands)  # pylint: disable=protected-access
     self.assertIn('command', script)
     self.assertIn('halt', script)
-    self.assertIn('"$start" == "command"', script)
+
+    assert_template = "{command})"
+    for last_command in ['command', 'halt']:
+      self.assertIn(assert_template.format(command=last_command), script)
 
   def testCompletionFishScript(self):
     # A sanity check test to make sure the fish completion script satisfies

From 8afe885e6d0728aa201ea2da3c2cf33d5490872c Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 21 Feb 2019 14:57:03 -0500
Subject: [PATCH 060/324] Adds copybara workflow for Python Fire for creating a
 CL from a PR.

Usage:
copybara third_party/py/fire/copy.bara.sky github_pr_to_piper 144
PiperOrigin-RevId: 235040108
Change-Id: I2af5727890cdce30116b3e783a7a05b16b91db86
---
 fire/completion.py      | 272 ++++++++--------------------------------
 fire/completion_test.py |   5 +-
 2 files changed, 50 insertions(+), 227 deletions(-)

diff --git a/fire/completion.py b/fire/completion.py
index e766a89f..8b74776f 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -46,11 +46,13 @@ def _BashScript(name, commands, default_options=None):
     A string which is the Bash script. Source the bash script to enable tab
     completion in Bash.
   """
-
   default_options = default_options or set()
-  global_options, options_map, subcommands_map = _GetMaps(
-      name, commands, default_options
-  )
+  options_map = collections.defaultdict(lambda: copy.copy(default_options))
+  for command in commands:
+    start = (name + ' ' + ' '.join(command[:-1])).strip()
+    completion = _FormatForCommand(command[-1])
+    options_map[start].add(completion)
+    options_map[start.replace('_', '-')].add(completion)
 
   bash_completion_template = """# bash completion support for {name}
 # DO NOT EDIT.
@@ -58,128 +60,41 @@ def _BashScript(name, commands, default_options=None):
 
 _complete-{identifier}()
 {{
-  local cur prev opts lastcommand
+  local start cur opts
   COMPREPLY=()
-  prev="${{COMP_WORDS[COMP_CWORD-1]}}"
+  start="${{COMP_WORDS[@]:0:COMP_CWORD}}"
   cur="${{COMP_WORDS[COMP_CWORD]}}"
-  lastcommand=$(get_lastcommand)
 
   opts="{default_options}"
-  GLOBAL_OPTIONS="{global_options}"
 
-{checks}
+{start_checks}
 
   COMPREPLY=( $(compgen -W "${{opts}}" -- ${{cur}}) )
   return 0
 }}
 
-get_lastcommand()
-{{
-  local lastcommand i
-
-  lastcommand=
-  for ((i=0; i < ${{#COMP_WORDS[@]}}; ++i)); do
-    if [[ ${{COMP_WORDS[i]}} != -* ]] && [[ -n ${{COMP_WORDS[i]}} ]] && [[
-      ${{COMP_WORDS[i]}} != $cur ]]; then
-      lastcommand=${{COMP_WORDS[i]}}
-    fi
-  done
-
-  echo $lastcommand
-}}
-
-filter_options()
-{{
-  local opts
-  opts=""
-  for opt in "$@"
-  do
-    if ! option_already_entered $opt; then
-      opts="$opts $opt"
-    fi
-  done
-
-  echo $opts
-}}
-
-option_already_entered()
-{{
-  local opt
-  for opt in ${{COMP_WORDS[@]:0:COMP_CWORD}}
-  do
-    if [ $1 == $opt ]; then
-      return 0
-    fi
-  done
-  return 1
-}}
-
-is_prev_global()
-{{
-  local opt
-  for opt in $GLOBAL_OPTIONS
-  do
-    if [ $opt == $prev ]; then
-      return 0
-    fi
-  done
-  return 1
-}}
-
 complete -F _complete-{identifier} {command}
 """
-
-  check_wrapper = """
-  case "${{lastcommand}}" in
-  {lastcommand_checks}
-  esac"""
-
-  lastcommand_check_template = """
-    {command})
-      {opts_assignment}
-      opts=$(filter_options $opts)
-    ;;"""
-
-  opts_assignment_subcommand_template = """
-      if is_prev_global; then
-        opts="${{GLOBAL_OPTIONS}}"
-      else
-        opts="{options} ${{GLOBAL_OPTIONS}}"
-      fi"""
-
-  opts_assignment_main_command_template = """
-      opts="{options} ${{GLOBAL_OPTIONS}}" """
-
-  def _GetOptsAssignmentTemplate(command):
-    if command == name:
-      return opts_assignment_main_command_template
-    else:
-      return opts_assignment_subcommand_template
-
-  lastcommand_checks = '\n'.join(
-      lastcommand_check_template.format(
-          command=command,
-          opts_assignment=_GetOptsAssignmentTemplate(command).format(
-              options=' '.join(sorted(
-                  options_map[command].union(subcommands_map[command])
-              )),
-          ),
+  start_check_template = """
+  if [[ "$start" == "{start}" ]] ; then
+    opts="{completions}"
+  fi"""
+
+  start_checks = '\n'.join(
+      start_check_template.format(
+          start=start,
+          completions=' '.join(sorted(options_map[start]))
       )
-      for command in set(subcommands_map.keys()).union(set(options_map.keys()))
-  )
-
-  checks = check_wrapper.format(
-      lastcommand_checks=lastcommand_checks,
+      for start in options_map
   )
 
   return (
       bash_completion_template.format(
           name=name,
           command=name,
-          checks=checks,
+          start_checks=start_checks,
           default_options=' '.join(default_options),
-          identifier=name.replace('/', '').replace('.', '').replace(',', ''),
-          global_options=' '.join(global_options),
+          identifier=name.replace('/', '').replace('.', '').replace(',', '')
       )
   )
 
@@ -199,85 +114,44 @@ def _FishScript(name, commands, default_options=None):
     completion in Fish.
   """
   default_options = default_options or set()
-  global_options, options_map, subcommands_map = _GetMaps(
-      name, commands, default_options
-  )
-
+  options_map = collections.defaultdict(lambda: copy.copy(default_options))
+  for command in commands:
+    start = (name + ' ' + ' '.join(command[:-1])).strip()
+    completion = _FormatForCommand(command[-1])
+    options_map[start].add(completion)
+    options_map[start.replace('_', '-')].add(completion)
   fish_source = """function __fish_using_command
     set cmd (commandline -opc)
-    for i in (seq (count $cmd) 1)
-        switch $cmd[$i]
-        case "-*"
-        case "*"
-            if [ $cmd[$i] = $argv[1] ]
-                return 0
-            else
+    if [ (count $cmd) -eq (count $argv) ]
+        for i in (seq (count $argv))
+            if [ $cmd[$i] != $argv[$i] ]
                 return 1
             end
         end
+        return 0
     end
     return 1
 end
-
-function __option_entered_check
-    set cmd (commandline -opc)
-    for i in (seq (count $cmd))
-        switch $cmd[$i]
-        case "-*"
-            if [ $cmd[$i] = $argv[1] ]
-                return 1
-            end
-        end
-    end
-    return 0
-end
-
-function __is_prev_global
-    set cmd (commandline -opc)
-    set global_options {global_options}
-    set prev (count $cmd)
-
-    for opt in $global_options
-        if [ "--$opt" = $cmd[$prev] ]
-            echo $prev
-            return 0
-        end
-    end
-    return 1
-end
-
 """
-
-  subcommand_template = ("complete -c {name} -n '__fish_using_command "
-                         "{command}' -f -a {subcommand}\n")
+  subcommand_template = ("complete -c {name} -n '__fish_using_command {start}' "
+                         "-f -a {subcommand}\n")
   flag_template = ("complete -c {name} -n "
-                   "'__fish_using_command {command};{prev_global_check} and "
-                   "__option_entered_check --{option}' -l {option}\n")
-
-  prev_global_check = " and __is_prev_global;"
-  for command in set(subcommands_map.keys()).union(set(options_map.keys())):
-    for subcommand in subcommands_map[command]:
-      fish_source += subcommand_template.format(
-          name=name,
-          command=command,
-          subcommand=subcommand,
-      )
-
-    for option in options_map[command].union(global_options):
-      check_needed = command != name
-      fish_source += flag_template.format(
-          name=name,
-          command=command,
-          prev_global_check=prev_global_check if check_needed else "",
-          option=option.lstrip("--"),
-      )
-
-  return fish_source.format(
-      global_options=' '.join(
-          '"{option}"'.format(option=option)
-          for option in global_options
-      )
-  )
+                   "'__fish_using_command {start}' -l {option}\n")
+  for start in options_map:
+    for option in sorted(options_map[start]):
+      if option.startswith('--'):
+        fish_source += flag_template.format(
+            name=name,
+            start=start,
+            option=option[2:]
+        )
+      else:
+        fish_source += subcommand_template.format(
+            name=name,
+            start=start,
+            subcommand=option
+        )
+  return fish_source
 
 
 def _IncludeMember(name, verbose):
@@ -428,51 +302,3 @@ def _Commands(component, depth=3):
 
     for command in _Commands(member, depth - 1):
       yield (member_name,) + command
-
-
-def _IsOption(arg):
-  return arg.startswith('-')
-
-def _GetMaps(name, commands, default_options):
-  """Returns sets of subcommands and options for each command.
-
-  Args:
-    name: The first token in the commands, also the name of the command.
-    commands: A list of all possible commands that tab completion can complete
-        to. Each command is a list or tuple of the string tokens that make up
-        that command.
-    default_options: A dict of options that can be used with any command. Use
-        this if there are flags that can always be appended to a command.
-  Returns:
-    global_options: A set of all options of the first token of the command.
-    subcommands_map: A dict storing set of subcommands for each
-        command/subcommand.
-    options_map: A dict storing set of options for each subcommand.
-  """
-
-  global_options = copy.copy(default_options)
-  options_map = collections.defaultdict(lambda: copy.copy(default_options))
-  subcommands_map = collections.defaultdict(set)
-
-  for command in commands:
-    if len(command) == 1:
-
-      if _IsOption(command[0]):
-        global_options.add(command[0])
-      else:
-        subcommands_map[name].add(command[0])
-
-    elif command:
-
-      subcommand = command[-2]
-      arg = _FormatForCommand(command[-1])
-
-      if _IsOption(arg):
-        args_map = options_map
-      else:
-        args_map = subcommands_map
-
-      args_map[subcommand].add(arg)
-      args_map[subcommand.replace('_', '-')].add(arg)
-
-  return global_options, options_map, subcommands_map
diff --git a/fire/completion_test.py b/fire/completion_test.py
index eaf80a70..47d80b4a 100644
--- a/fire/completion_test.py
+++ b/fire/completion_test.py
@@ -36,10 +36,7 @@ def testCompletionBashScript(self):
     script = completion._BashScript(name='command', commands=commands)  # pylint: disable=protected-access
     self.assertIn('command', script)
     self.assertIn('halt', script)
-
-    assert_template = "{command})"
-    for last_command in ['command', 'halt']:
-      self.assertIn(assert_template.format(command=last_command), script)
+    self.assertIn('"$start" == "command"', script)
 
   def testCompletionFishScript(self):
     # A sanity check test to make sure the fish completion script satisfies

From cb311f16a86c29bcde6de3e2e04d129774b431ce Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 21 Feb 2019 17:00:56 -0500
Subject: [PATCH 061/324] Reduce number of examples for docstrings fuzzing by
 10x.

PiperOrigin-RevId: 235063811
Change-Id: I525b64a43fd78e96730efd19c572d56859ab2c97
---
 fire/docstrings_fuzz_test.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fire/docstrings_fuzz_test.py b/fire/docstrings_fuzz_test.py
index 0b26b141..7609f4f8 100644
--- a/fire/docstrings_fuzz_test.py
+++ b/fire/docstrings_fuzz_test.py
@@ -29,7 +29,7 @@
 
 class DocstringsFuzzTest(testutils.BaseTestCase):
 
-  @settings(max_examples=10000, deadline=1000)
+  @settings(max_examples=1000, deadline=1000)
   @given(st.text(min_size=1))
   @example('This is a one-line docstring.')
   def test_fuzz_parse(self, value):

From 7651785f962389017ab492c3a0698f96d7033881 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 21 Feb 2019 17:56:00 -0500
Subject: [PATCH 062/324] Completion enhancements cleanup (ought to be nearly a
 no-op). Completion enhancements author: meshde

PiperOrigin-RevId: 235075422
Change-Id: I56ad1357b7ba9a021110a760e7095a6fb36e8ab8
---
 fire/completion.py      | 273 ++++++++++++++++++++++++++++++++--------
 fire/completion_test.py |   5 +-
 2 files changed, 227 insertions(+), 51 deletions(-)

diff --git a/fire/completion.py b/fire/completion.py
index 8b74776f..cda7c936 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -47,12 +47,9 @@ def _BashScript(name, commands, default_options=None):
     completion in Bash.
   """
   default_options = default_options or set()
-  options_map = collections.defaultdict(lambda: copy.copy(default_options))
-  for command in commands:
-    start = (name + ' ' + ' '.join(command[:-1])).strip()
-    completion = _FormatForCommand(command[-1])
-    options_map[start].add(completion)
-    options_map[start.replace('_', '-')].add(completion)
+  global_options, options_map, subcommands_map = _GetMaps(
+      name, commands, default_options
+  )
 
   bash_completion_template = """# bash completion support for {name}
 # DO NOT EDIT.
@@ -60,41 +57,130 @@ def _BashScript(name, commands, default_options=None):
 
 _complete-{identifier}()
 {{
-  local start cur opts
+  local cur prev opts lastcommand
   COMPREPLY=()
-  start="${{COMP_WORDS[@]:0:COMP_CWORD}}"
+  prev="${{COMP_WORDS[COMP_CWORD-1]}}"
   cur="${{COMP_WORDS[COMP_CWORD]}}"
+  lastcommand=$(get_lastcommand)
 
   opts="{default_options}"
+  GLOBAL_OPTIONS="{global_options}"
 
-{start_checks}
+{checks}
 
   COMPREPLY=( $(compgen -W "${{opts}}" -- ${{cur}}) )
   return 0
 }}
 
+get_lastcommand()
+{{
+  local lastcommand i
+
+  lastcommand=
+  for ((i=0; i < ${{#COMP_WORDS[@]}}; ++i)); do
+    if [[ ${{COMP_WORDS[i]}} != -* ]] && [[ -n ${{COMP_WORDS[i]}} ]] && [[
+      ${{COMP_WORDS[i]}} != $cur ]]; then
+      lastcommand=${{COMP_WORDS[i]}}
+    fi
+  done
+
+  echo $lastcommand
+}}
+
+filter_options()
+{{
+  local opts
+  opts=""
+  for opt in "$@"
+  do
+    if ! option_already_entered $opt; then
+      opts="$opts $opt"
+    fi
+  done
+
+  echo $opts
+}}
+
+option_already_entered()
+{{
+  local opt
+  for opt in ${{COMP_WORDS[@]:0:COMP_CWORD}}
+  do
+    if [ $1 == $opt ]; then
+      return 0
+    fi
+  done
+  return 1
+}}
+
+is_prev_global()
+{{
+  local opt
+  for opt in $GLOBAL_OPTIONS
+  do
+    if [ $opt == $prev ]; then
+      return 0
+    fi
+  done
+  return 1
+}}
+
 complete -F _complete-{identifier} {command}
 """
-  start_check_template = """
-  if [[ "$start" == "{start}" ]] ; then
-    opts="{completions}"
-  fi"""
-
-  start_checks = '\n'.join(
-      start_check_template.format(
-          start=start,
-          completions=' '.join(sorted(options_map[start]))
-      )
-      for start in options_map
+
+  check_wrapper = """
+  case "${{lastcommand}}" in
+  {lastcommand_checks}
+  esac"""
+
+  lastcommand_check_template = """
+    {command})
+      {opts_assignment}
+      opts=$(filter_options $opts)
+    ;;"""
+
+  opts_assignment_subcommand_template = """
+      if is_prev_global; then
+        opts="${{GLOBAL_OPTIONS}}"
+      else
+        opts="{options} ${{GLOBAL_OPTIONS}}"
+      fi"""
+
+  opts_assignment_main_command_template = """
+      opts="{options} ${{GLOBAL_OPTIONS}}" """
+
+  def _GetOptsAssignmentTemplate(command):
+    if command == name:
+      return opts_assignment_main_command_template
+    else:
+      return opts_assignment_subcommand_template
+
+  lines = []
+  for command in set(subcommands_map.keys()).union(set(options_map.keys())):
+    opts_assignment = _GetOptsAssignmentTemplate(command).format(
+        options=' '.join(
+            sorted(options_map[command].union(subcommands_map[command]))
+        ),
+    )
+    lines.append(
+        lastcommand_check_template.format(
+            command=command,
+            opts_assignment=opts_assignment)
+    )
+  lastcommand_checks = '\n'.join(lines)
+
+  checks = check_wrapper.format(
+      lastcommand_checks=lastcommand_checks,
   )
 
   return (
       bash_completion_template.format(
           name=name,
           command=name,
-          start_checks=start_checks,
+          checks=checks,
           default_options=' '.join(default_options),
-          identifier=name.replace('/', '').replace('.', '').replace(',', '')
+          identifier=name.replace('/', '').replace('.', '').replace(',', ''),
+          global_options=' '.join(global_options),
       )
   )
 
@@ -114,44 +200,85 @@ def _FishScript(name, commands, default_options=None):
     completion in Fish.
   """
   default_options = default_options or set()
-  options_map = collections.defaultdict(lambda: copy.copy(default_options))
-  for command in commands:
-    start = (name + ' ' + ' '.join(command[:-1])).strip()
-    completion = _FormatForCommand(command[-1])
-    options_map[start].add(completion)
-    options_map[start.replace('_', '-')].add(completion)
+  global_options, options_map, subcommands_map = _GetMaps(
+      name, commands, default_options
+  )
+
   fish_source = """function __fish_using_command
     set cmd (commandline -opc)
-    if [ (count $cmd) -eq (count $argv) ]
-        for i in (seq (count $argv))
-            if [ $cmd[$i] != $argv[$i] ]
+    for i in (seq (count $cmd) 1)
+        switch $cmd[$i]
+        case "-*"
+        case "*"
+            if [ $cmd[$i] = $argv[1] ]
+                return 0
+            else
+                return 1
+            end
+        end
+    end
+    return 1
+end
+
+function __option_entered_check
+    set cmd (commandline -opc)
+    for i in (seq (count $cmd))
+        switch $cmd[$i]
+        case "-*"
+            if [ $cmd[$i] = $argv[1] ]
                 return 1
             end
         end
-        return 0
+    end
+    return 0
+end
+
+function __is_prev_global
+    set cmd (commandline -opc)
+    set global_options {global_options}
+    set prev (count $cmd)
+
+    for opt in $global_options
+        if [ "--$opt" = $cmd[$prev] ]
+            echo $prev
+            return 0
+        end
     end
     return 1
 end
+
 """
-  subcommand_template = ("complete -c {name} -n '__fish_using_command {start}' "
-                         "-f -a {subcommand}\n")
+
+  subcommand_template = ("complete -c {name} -n '__fish_using_command "
+                         "{command}' -f -a {subcommand}\n")
   flag_template = ("complete -c {name} -n "
-                   "'__fish_using_command {start}' -l {option}\n")
-  for start in options_map:
-    for option in sorted(options_map[start]):
-      if option.startswith('--'):
-        fish_source += flag_template.format(
-            name=name,
-            start=start,
-            option=option[2:]
-        )
-      else:
-        fish_source += subcommand_template.format(
-            name=name,
-            start=start,
-            subcommand=option
-        )
-  return fish_source
+                   "'__fish_using_command {command};{prev_global_check} and "
+                   "__option_entered_check --{option}' -l {option}\n")
+
+  prev_global_check = ' and __is_prev_global;'
+  for command in set(subcommands_map.keys()).union(set(options_map.keys())):
+    for subcommand in subcommands_map[command]:
+      fish_source += subcommand_template.format(
+          name=name,
+          command=command,
+          subcommand=subcommand,
+      )
+
+    for option in options_map[command].union(global_options):
+      check_needed = command != name
+      fish_source += flag_template.format(
+          name=name,
+          command=command,
+          prev_global_check=prev_global_check if check_needed else '',
+          option=option.lstrip('--'),
+      )
+
+  return fish_source.format(
+      global_options=' '.join(
+          '"{option}"'.format(option=option)
+          for option in global_options
+      )
+  )
 
 
 def _IncludeMember(name, verbose):
@@ -302,3 +429,49 @@ def _Commands(component, depth=3):
 
     for command in _Commands(member, depth - 1):
       yield (member_name,) + command
+
+
+def _IsOption(arg):
+  return arg.startswith('-')
+
+
+def _GetMaps(name, commands, default_options):
+  """Returns sets of subcommands and options for each command.
+
+  Args:
+    name: The first token in the commands, also the name of the command.
+    commands: A list of all possible commands that tab completion can complete
+        to. Each command is a list or tuple of the string tokens that make up
+        that command.
+    default_options: A dict of options that can be used with any command. Use
+        this if there are flags that can always be appended to a command.
+  Returns:
+    global_options: A set of all options of the first token of the command.
+    subcommands_map: A dict storing set of subcommands for each
+        command/subcommand.
+    options_map: A dict storing set of options for each subcommand.
+  """
+  global_options = copy.copy(default_options)
+  options_map = collections.defaultdict(lambda: copy.copy(default_options))
+  subcommands_map = collections.defaultdict(set)
+
+  for command in commands:
+    if len(command) == 1:
+      if _IsOption(command[0]):
+        global_options.add(command[0])
+      else:
+        subcommands_map[name].add(command[0])
+
+    elif command:
+      subcommand = command[-2]
+      arg = _FormatForCommand(command[-1])
+
+      if _IsOption(arg):
+        args_map = options_map
+      else:
+        args_map = subcommands_map
+
+      args_map[subcommand].add(arg)
+      args_map[subcommand.replace('_', '-')].add(arg)
+
+  return global_options, options_map, subcommands_map
diff --git a/fire/completion_test.py b/fire/completion_test.py
index 47d80b4a..582e5bbc 100644
--- a/fire/completion_test.py
+++ b/fire/completion_test.py
@@ -36,7 +36,10 @@ def testCompletionBashScript(self):
     script = completion._BashScript(name='command', commands=commands)  # pylint: disable=protected-access
     self.assertIn('command', script)
     self.assertIn('halt', script)
-    self.assertIn('"$start" == "command"', script)
+
+    assert_template = '{command})'
+    for last_command in ['command', 'halt']:
+      self.assertIn(assert_template.format(command=last_command), script)
 
   def testCompletionFishScript(self):
     # A sanity check test to make sure the fish completion script satisfies

From 7538c3dd82ac5a49c696ce6fae1204fc2f8b3de1 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 22 Feb 2019 14:39:58 -0500
Subject: [PATCH 063/324] Decrease max_examples by 10x

PiperOrigin-RevId: 235229117
Change-Id: I6a50ff559a738cced52779e52515ca09d819f3b4
---
 fire/completion.py      | 34 +++++++++++++++++-----------------
 fire/completion_test.py |  2 +-
 2 files changed, 18 insertions(+), 18 deletions(-)

diff --git a/fire/completion.py b/fire/completion.py
index cda7c936..4f28129a 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -155,19 +155,17 @@ def _GetOptsAssignmentTemplate(command):
     else:
       return opts_assignment_subcommand_template
 
-  lines = []
-  for command in set(subcommands_map.keys()).union(set(options_map.keys())):
-    opts_assignment = _GetOptsAssignmentTemplate(command).format(
-        options=' '.join(
-            sorted(options_map[command].union(subcommands_map[command]))
-        ),
-    )
-    lines.append(
-        lastcommand_check_template.format(
-            command=command,
-            opts_assignment=opts_assignment)
-    )
-  lastcommand_checks = '\n'.join(lines)
+  lastcommand_checks = '\n'.join(
+      lastcommand_check_template.format(
+          command=command,
+          opts_assignment=_GetOptsAssignmentTemplate(command).format(
+              options=' '.join(sorted(
+                  options_map[command].union(subcommands_map[command])
+              )),
+          ),
+      )
+      for command in set(subcommands_map.keys()).union(set(options_map.keys()))
+  )
 
   checks = check_wrapper.format(
       lastcommand_checks=lastcommand_checks,
@@ -255,7 +253,7 @@ def _FishScript(name, commands, default_options=None):
                    "'__fish_using_command {command};{prev_global_check} and "
                    "__option_entered_check --{option}' -l {option}\n")
 
-  prev_global_check = ' and __is_prev_global;'
+  prev_global_check = " and __is_prev_global;"
   for command in set(subcommands_map.keys()).union(set(options_map.keys())):
     for subcommand in subcommands_map[command]:
       fish_source += subcommand_template.format(
@@ -269,8 +267,8 @@ def _FishScript(name, commands, default_options=None):
       fish_source += flag_template.format(
           name=name,
           command=command,
-          prev_global_check=prev_global_check if check_needed else '',
-          option=option.lstrip('--'),
+          prev_global_check=prev_global_check if check_needed else "",
+          option=option.lstrip("--"),
       )
 
   return fish_source.format(
@@ -434,7 +432,6 @@ def _Commands(component, depth=3):
 def _IsOption(arg):
   return arg.startswith('-')
 
-
 def _GetMaps(name, commands, default_options):
   """Returns sets of subcommands and options for each command.
 
@@ -451,18 +448,21 @@ def _GetMaps(name, commands, default_options):
         command/subcommand.
     options_map: A dict storing set of options for each subcommand.
   """
+
   global_options = copy.copy(default_options)
   options_map = collections.defaultdict(lambda: copy.copy(default_options))
   subcommands_map = collections.defaultdict(set)
 
   for command in commands:
     if len(command) == 1:
+
       if _IsOption(command[0]):
         global_options.add(command[0])
       else:
         subcommands_map[name].add(command[0])
 
     elif command:
+
       subcommand = command[-2]
       arg = _FormatForCommand(command[-1])
 
diff --git a/fire/completion_test.py b/fire/completion_test.py
index 582e5bbc..eaf80a70 100644
--- a/fire/completion_test.py
+++ b/fire/completion_test.py
@@ -37,7 +37,7 @@ def testCompletionBashScript(self):
     self.assertIn('command', script)
     self.assertIn('halt', script)
 
-    assert_template = '{command})'
+    assert_template = "{command})"
     for last_command in ['command', 'halt']:
       self.assertIn(assert_template.format(command=last_command), script)
 

From 9104daf7aef084106bb19106441339f4095b59ce Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 22 Feb 2019 13:47:56 -0800
Subject: [PATCH 064/324] clean up of completion enhancement

PiperOrigin-RevId: 235251580
Change-Id: Ia8b4c9cfb2688f74be1a2ee7e5a088ae42246409
---
 fire/completion.py      | 34 +++++++++++++++++-----------------
 fire/completion_test.py |  2 +-
 2 files changed, 18 insertions(+), 18 deletions(-)

diff --git a/fire/completion.py b/fire/completion.py
index 4f28129a..cda7c936 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -155,17 +155,19 @@ def _GetOptsAssignmentTemplate(command):
     else:
       return opts_assignment_subcommand_template
 
-  lastcommand_checks = '\n'.join(
-      lastcommand_check_template.format(
-          command=command,
-          opts_assignment=_GetOptsAssignmentTemplate(command).format(
-              options=' '.join(sorted(
-                  options_map[command].union(subcommands_map[command])
-              )),
-          ),
-      )
-      for command in set(subcommands_map.keys()).union(set(options_map.keys()))
-  )
+  lines = []
+  for command in set(subcommands_map.keys()).union(set(options_map.keys())):
+    opts_assignment = _GetOptsAssignmentTemplate(command).format(
+        options=' '.join(
+            sorted(options_map[command].union(subcommands_map[command]))
+        ),
+    )
+    lines.append(
+        lastcommand_check_template.format(
+            command=command,
+            opts_assignment=opts_assignment)
+    )
+  lastcommand_checks = '\n'.join(lines)
 
   checks = check_wrapper.format(
       lastcommand_checks=lastcommand_checks,
@@ -253,7 +255,7 @@ def _FishScript(name, commands, default_options=None):
                    "'__fish_using_command {command};{prev_global_check} and "
                    "__option_entered_check --{option}' -l {option}\n")
 
-  prev_global_check = " and __is_prev_global;"
+  prev_global_check = ' and __is_prev_global;'
   for command in set(subcommands_map.keys()).union(set(options_map.keys())):
     for subcommand in subcommands_map[command]:
       fish_source += subcommand_template.format(
@@ -267,8 +269,8 @@ def _FishScript(name, commands, default_options=None):
       fish_source += flag_template.format(
           name=name,
           command=command,
-          prev_global_check=prev_global_check if check_needed else "",
-          option=option.lstrip("--"),
+          prev_global_check=prev_global_check if check_needed else '',
+          option=option.lstrip('--'),
       )
 
   return fish_source.format(
@@ -432,6 +434,7 @@ def _Commands(component, depth=3):
 def _IsOption(arg):
   return arg.startswith('-')
 
+
 def _GetMaps(name, commands, default_options):
   """Returns sets of subcommands and options for each command.
 
@@ -448,21 +451,18 @@ def _GetMaps(name, commands, default_options):
         command/subcommand.
     options_map: A dict storing set of options for each subcommand.
   """
-
   global_options = copy.copy(default_options)
   options_map = collections.defaultdict(lambda: copy.copy(default_options))
   subcommands_map = collections.defaultdict(set)
 
   for command in commands:
     if len(command) == 1:
-
       if _IsOption(command[0]):
         global_options.add(command[0])
       else:
         subcommands_map[name].add(command[0])
 
     elif command:
-
       subcommand = command[-2]
       arg = _FormatForCommand(command[-1])
 
diff --git a/fire/completion_test.py b/fire/completion_test.py
index eaf80a70..582e5bbc 100644
--- a/fire/completion_test.py
+++ b/fire/completion_test.py
@@ -37,7 +37,7 @@ def testCompletionBashScript(self):
     self.assertIn('command', script)
     self.assertIn('halt', script)
 
-    assert_template = "{command})"
+    assert_template = '{command})'
     for last_command in ['command', 'halt']:
       self.assertIn(assert_template.format(command=last_command), script)
 

From 6dc65316950c4daa9b68b9e9ad498ec86444dd75 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 22 Feb 2019 13:49:52 -0800
Subject: [PATCH 065/324] disable g-bad-exception-name for FireExit Exception.

PiperOrigin-RevId: 235251946
Change-Id: Id6ff561b840e415bdf564b1ef01e8434e13ddf33
---
 fire/core.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fire/core.py b/fire/core.py
index 05a01e11..0fd5cfac 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -183,7 +183,7 @@ class FireError(Exception):
   """
 
 
-class FireExit(SystemExit):
+class FireExit(SystemExit):  # pylint: disable=g-bad-exception-name
   """An exception raised by Fire to the client in the case of a FireError.
 
   The trace of the Fire program is available on the `trace` property.

From 668007ae41391f5964870b4597e41493a936a11e Mon Sep 17 00:00:00 2001
From: Andrew Au <cshung@gmail.com>
Date: Tue, 26 Feb 2019 08:55:59 -0800
Subject: [PATCH 066/324] initial support for Python 3.7 and additional
 builtins

PiperOrigin-RevId: 235730005
Change-Id: Idcbbd79d074aeb0db0133bc6d72f7d61df555f81
---
 fire/helputils_test.py    | 1 -
 fire/inspectutils.py      | 9 +++++----
 fire/inspectutils_test.py | 2 --
 3 files changed, 5 insertions(+), 7 deletions(-)

diff --git a/fire/helputils_test.py b/fire/helputils_test.py
index 05571228..c3d5055d 100644
--- a/fire/helputils_test.py
+++ b/fire/helputils_test.py
@@ -88,7 +88,6 @@ def testHelpStringBuiltin(self):
     helpstring = helputils.HelpString('test'.upper)
     self.assertIn('Type:        builtin_function_or_method', helpstring)
     self.assertIn('String form: <built-in method upper of', helpstring)
-    self.assertIn('Usage:       [VARS ...] [--KWARGS ...]', helpstring)
 
   def testHelpStringIntType(self):
     helpstring = helputils.HelpString(int)
diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index 45f7ce6c..4e854c2d 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -73,11 +73,12 @@ class with an __init__ method.
     skip_arg = True
     if six.PY2 and hasattr(fn, '__init__'):
       fn = fn.__init__
-  else:
+  elif inspect.ismethod(fn):
     # If the function is a bound method, we skip the `self` argument.
-    is_method = inspect.ismethod(fn)
-    skip_arg = is_method and fn.__self__ is not None
-
+    skip_arg = fn.__self__ is not None
+  elif inspect.isbuiltin(fn):
+    # If the function is a bound builtin, we skip the `self` argument.
+    skip_arg = fn.__self__ is not None
   return fn, skip_arg
 
 
diff --git a/fire/inspectutils_test.py b/fire/inspectutils_test.py
index 8c6dd9a3..ba428653 100644
--- a/fire/inspectutils_test.py
+++ b/fire/inspectutils_test.py
@@ -56,8 +56,6 @@ def testGetFullArgSpecFromBuiltin(self):
     spec = inspectutils.GetFullArgSpec('test'.upper)
     self.assertEqual(spec.args, [])
     self.assertEqual(spec.defaults, ())
-    self.assertEqual(spec.varargs, 'vars')
-    self.assertEqual(spec.varkw, 'kwargs')
     self.assertEqual(spec.kwonlyargs, [])
     self.assertEqual(spec.kwonlydefaults, {})
     self.assertEqual(spec.annotations, {})

From 2bfb82ef23bd470c445bf87ca286e67ee5604f5c Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Tue, 26 Feb 2019 09:47:17 -0800
Subject: [PATCH 067/324] Support Python 3.7 in travis tests for Python Fire

PiperOrigin-RevId: 235738586
Change-Id: I85b79f073b2db8b95e98d38bb3b3e7bcce4b8ab6
---
 .travis.yml | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/.travis.yml b/.travis.yml
index 7ec70752..86d63b87 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,6 +4,13 @@ python:
   - "3.4"
   - "3.5"
   - "3.6"
+# Workaround for testing Python 3.7:
+# https://github.com/travis-ci/travis-ci/issues/9815
+matrix:
+  include:
+    - python: 3.7
+      dist: xenial
+      sudo: yes
 before_install:
   - pip install --upgrade setuptools pip
   - pip install --upgrade pylint pytest pytest-pylint pytest-runner

From 3578817f1631dcc08fad650d0535a950f47b8165 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Tue, 26 Feb 2019 09:52:22 -0800
Subject: [PATCH 068/324] Enable pylint for all Python versions

PiperOrigin-RevId: 235739402
Change-Id: Ia95401b3983546c6b392789b12ed5e7754cf5c8f
---
 .travis.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.travis.yml b/.travis.yml
index 86d63b87..61910d58 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -21,4 +21,4 @@ script:
   - python -m pytest  # Run the tests without IPython.
   - pip install ipython
   - python -m pytest  # Now run the tests with IPython.
-  - if [[ $TRAVIS_PYTHON_VERSION != 3.6 ]]; then pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console; fi
+  - pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console

From 2363b5f2b770d853017585f62633670fd52c8d3e Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Tue, 26 Feb 2019 13:35:02 -0800
Subject: [PATCH 069/324] Mark Python 3.7 support in setup.py

PiperOrigin-RevId: 235782693
Change-Id: I55f0bfd76f6d1750223ca7378581985d48fae14d
---
 setup.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/setup.py b/setup.py
index bd802702..cf7d2b50 100644
--- a/setup.py
+++ b/setup.py
@@ -68,6 +68,7 @@
         'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.7',
 
         'Operating System :: OS Independent',
         'Operating System :: POSIX',

From 1c1f6dd1107034b9693c9a6e5ed417331e69bc31 Mon Sep 17 00:00:00 2001
From: Rebecca Chen <rechen@google.com>
Date: Mon, 4 Mar 2019 11:38:27 -0800
Subject: [PATCH 070/324] Add type-checking to Python Fire via pytype.

PiperOrigin-RevId: 236693773
Change-Id: I8eb393b69283445b952780c288704e4b8e262628
---
 .gitignore         |  3 +++
 .travis.yml        | 11 ++++++++++-
 fire/core.py       |  2 +-
 fire/docstrings.py |  4 ++--
 fire/trace.py      |  2 ++
 setup.cfg          |  4 ++++
 6 files changed, 22 insertions(+), 4 deletions(-)

diff --git a/.gitignore b/.gitignore
index a8c927df..a2166684 100644
--- a/.gitignore
+++ b/.gitignore
@@ -98,3 +98,6 @@ ENV/
 
 # PyCharm IDE
 .idea/
+
+# Type-checking
+.pytype/
diff --git a/.travis.yml b/.travis.yml
index 61910d58..16ab4e4b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -15,10 +15,19 @@ before_install:
   - pip install --upgrade setuptools pip
   - pip install --upgrade pylint pytest pytest-pylint pytest-runner
 install:
-  - pip install hypothesis
+  - pip install hypothesis python-Levenshtein
   - python setup.py develop
 script:
   - python -m pytest  # Run the tests without IPython.
   - pip install ipython
   - python -m pytest  # Now run the tests with IPython.
   - pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console
+  - pip install pytype
+  # Run type-checking, excluding files that define or use py3 features in py2.
+  - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then
+      pytype -x
+        fire/fire_test.py
+        fire/inspectutils_test.py
+        fire/test_components_py3.py;
+    else
+      pytype; fi
diff --git a/fire/core.py b/fire/core.py
index 0fd5cfac..ced56e8c 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -487,7 +487,7 @@ def _Fire(component, args, context, name=None):
         # If the component is a namedtuple, we need to convert it to dict to
         # be able to use the .items() method.
         if inspectutils.IsNamedTuple(component):
-          component = component._asdict()
+          component = component._asdict()  # pytype: disable=attribute-error
         for key, value in component.items():
           if target == str(key):
             component = value
diff --git a/fire/docstrings.py b/fire/docstrings.py
index 5e049603..4f0ffd93 100644
--- a/fire/docstrings.py
+++ b/fire/docstrings.py
@@ -82,7 +82,7 @@ class Namespace(dict):
   def __getattr__(self, key):
     if key not in self:
       self[key] = Namespace()
-    return self.get(key)
+    return self[key]
 
   def __setattr__(self, key, value):
     self[key] = value
@@ -398,7 +398,7 @@ def _consume_line(line_info, state):
   if state.section.new and state.section.format == Formats.RST:
     # The current line starts with an RST directive, e.g. ":param arg:".
     directive = _get_directive(line_info)
-    directive_tokens = directive.split()
+    directive_tokens = directive.split()  # pytype: disable=attribute-error
     if state.section.title == Sections.ARGS:
       name = directive_tokens[-1]
       arg = _get_or_create_arg_by_name(state, name)
diff --git a/fire/trace.py b/fire/trace.py
index a544cc8b..fc50ba82 100644
--- a/fire/trace.py
+++ b/fire/trace.py
@@ -65,7 +65,9 @@ def __init__(self, initial_component, name=None, separator='-', verbose=False,
 
   def GetResult(self):
     """Returns the component from the last element of the trace."""
+    # pytype: disable=attribute-error
     return self.GetLastHealthyElement().component
+    # pytype: enable=attribute-error
 
   def GetLastHealthyElement(self):
     """Returns the last element of the trace that is not an error.
diff --git a/setup.cfg b/setup.cfg
index 592eb0f9..058f329c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -9,3 +9,7 @@ test = pytest
 
 [tool:pytest]
 addopts = --ignore=fire/test_components_py3.py --ignore=fire/parser_fuzz_test.py
+
+[pytype]
+inputs = .
+output = .pytype

From 36fcb1995ba7dd0bf42d59e799ab8ad7cbfb91a3 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 4 Mar 2019 13:32:59 -0800
Subject: [PATCH 071/324] Resolve no-else-raise linter errors.

PiperOrigin-RevId: 236714504
Change-Id: Ifaeee426ee05a5e5deb3039639da6bd6ab8de7d8
---
 fire/core.py | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index ced56e8c..e6138a6c 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -133,7 +133,7 @@ def Fire(component=None, command=None, name=None):
   if component_trace.HasError():
     _DisplayError(component_trace)
     raise FireExit(2, component_trace)
-  elif component_trace.show_trace and component_trace.show_help:
+  if component_trace.show_trace and component_trace.show_help:
     output = ['Fire trace:\n{trace}\n'.format(trace=component_trace)]
     result = component_trace.GetResult()
     help_string = helputils.HelpString(
@@ -141,22 +141,22 @@ def Fire(component=None, command=None, name=None):
     output.append(help_string)
     Display(output)
     raise FireExit(0, component_trace)
-  elif component_trace.show_trace:
+  if component_trace.show_trace:
     output = ['Fire trace:\n{trace}'.format(trace=component_trace)]
     Display(output)
     raise FireExit(0, component_trace)
-  elif component_trace.show_help:
+  if component_trace.show_help:
     result = component_trace.GetResult()
     help_string = helputils.HelpString(
         result, component_trace, component_trace.verbose)
     output = [help_string]
     Display(output)
     raise FireExit(0, component_trace)
-  else:
-    # The command succeeded normally; print the result.
-    _PrintResult(component_trace, verbose=component_trace.verbose)
-    result = component_trace.GetResult()
-    return result
+
+  # The command succeeded normally; print the result.
+  _PrintResult(component_trace, verbose=component_trace.verbose)
+  result = component_trace.GetResult()
+  return result
 
 
 def Display(lines):

From 665c4976934208e4630703e58956b5185f2c4636 Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Tue, 19 Mar 2019 12:41:03 -0700
Subject: [PATCH 072/324] Generating flags information for function objects

PiperOrigin-RevId: 239247774
Change-Id: I2dc83a9006f94239cfc5b267c434304e4317f39a
---
 fire/helputils.py       | 59 ++++++++++++++++++++++++++++++++---------
 fire/helputils_test.py  | 27 ++++++++++++++++++-
 fire/test_components.py |  9 +++++++
 3 files changed, 82 insertions(+), 13 deletions(-)

diff --git a/fire/helputils.py b/fire/helputils.py
index 08386c34..6471585d 100644
--- a/fire/helputils.py
+++ b/fire/helputils.py
@@ -106,6 +106,20 @@ def _GetFields(trace=None):
   ]
 
 
+def GetArgsAngFlags(component):
+  """Returns all types of arguments and flags of a component."""
+  spec = inspectutils.GetFullArgSpec(component)
+  args = spec.args
+  if spec.defaults is None:
+    num_defaults = 0
+  else:
+    num_defaults = len(spec.defaults)
+  args_with_no_defaults = args[:len(args) - num_defaults]
+  args_with_defaults = args[len(args) - num_defaults:]
+  flags = args_with_defaults + spec.kwonlyargs
+  return args_with_no_defaults, args_with_defaults, flags
+
+
 def HelpString(component, trace=None, verbose=False):
   """Returns a help string for a supplied component.
 
@@ -220,15 +234,8 @@ def HelpTextForFunction(component, info, trace=None, verbose=False):
   spec = inspectutils.GetFullArgSpec(component)
   args = spec.args
 
-  if spec.defaults is None:
-    num_defaults = 0
-  else:
-    num_defaults = len(spec.defaults)
-  args_with_no_defaults = args[:len(args) - num_defaults]
-
-  # TODO(joejoevictor): Generate flag section using these
-  # args_with_defaults = args[len(args) - num_defaults:]
-  # flags = args_with_defaults + spec.kwonlyargs
+  args_with_no_defaults, args_with_defaults, flags = GetArgsAngFlags(component)
+  del args_with_defaults
 
   output_template = """NAME
     {name_section}
@@ -249,8 +256,18 @@ def HelpTextForFunction(component, info, trace=None, verbose=False):
   name_section = name_section_template.format(
       current_command=current_command, command_summary=command_summary_str)
 
-  items = [arg.upper() for arg in args_with_no_defaults]
-  args_and_flags = ' '.join(items)
+  args_and_flags = ''
+  if args_with_no_defaults:
+    items = [arg.upper() for arg in args_with_no_defaults]
+    args_and_flags = ' '.join(items)
+
+  synopsis_flag_template = '[--{flag_name}={flag_name_upper}]'
+  if flags:
+    items = [
+        synopsis_flag_template.format(
+            flag_name=flag, flag_name_upper=flag.upper()) for flag in flags
+    ]
+    args_and_flags = args_and_flags + ' '.join(items)
 
   # Synopsis section
   synopsis_section_template = '{current_command} {args_and_flags}'
@@ -266,7 +283,6 @@ def HelpTextForFunction(component, info, trace=None, verbose=False):
   args_and_flags_section = ''
 
   # Positional arguments and flags section
-
   pos_arg_template = """
 POSITIONAL ARGUMENTS
 {items}
@@ -286,6 +302,25 @@ def HelpTextForFunction(component, info, trace=None, verbose=False):
     args_and_flags_section += pos_arg_template.format(
         items='\n'.join(pos_arg_items).rstrip('\n'))
 
+  flags_template = """
+FLAGS
+{items}
+"""
+  flag_items = []
+  for flag in flags:
+    item_template = '    --{flag_name}\n        {flag_description}\n'
+    flag_description = None
+    for arg_in_docstring in info['docstring_info'].args:
+      if arg_in_docstring.name == flag:
+        flag_description = arg_in_docstring.description
+
+    item = item_template.format(
+        flag_name=flag, flag_description=flag_description)
+    flag_items.append(item)
+  if flag_items:
+    args_and_flags_section += flags_template.format(
+        items='\n'.join(flag_items).rstrip('\n'))
+
   return output_template.format(
       name_section=name_section,
       synopsis_section=synopsis_section,
diff --git a/fire/helputils_test.py b/fire/helputils_test.py
index c3d5055d..90cd8d6f 100644
--- a/fire/helputils_test.py
+++ b/fire/helputils_test.py
@@ -164,7 +164,7 @@ def testHelpScreen(self):
 """
     self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
 
-  def testHelpScreenWithLineBreak(self):
+  def testHelpScreenForFunction_docstringWithLineBreak(self):
     component = tc.ClassWithMultilineDocstring.example_generator
     t = trace.FireTrace(component, name='example_generator')
     info = inspectutils.Info(component)
@@ -189,6 +189,31 @@ def testHelpScreenWithLineBreak(self):
     """
     self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
 
+  def testHelpScreenForFunction_functionWithDefaultArgs(self):
+    component = tc.WithDefaults().double
+    t = trace.FireTrace(component, name='double')
+    info = inspectutils.Info(component)
+    info['docstring_info'] = docstrings.parse(info['docstring'])
+    help_output = helputils.HelpText(component, info, t)
+    expected_output = """
+    NAME
+        double - Returns the input multiplied by 2.
+
+    SYNOPSIS
+        double [--count=COUNT]
+
+    DESCRIPTION
+        Returns the input multiplied by 2.
+
+    FLAGS
+        --count
+            Input number that you want to double.
+
+    NOTES
+        You could also use flags syntax for POSITIONAL ARGUMENTS
+    """
+    self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
+
 
 class UsageTest(testutils.BaseTestCase):
 
diff --git a/fire/test_components.py b/fire/test_components.py
index 03d8c15b..ad3eecd6 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -87,8 +87,17 @@ def triple(self, count):
 
 
 class WithDefaults(object):
+  """Class with functions that have default arguments."""
 
   def double(self, count=0):
+    """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 triple(self, count=0):

From 2f80522f6d27be3eef36781400e26ad2768f5a37 Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Tue, 19 Mar 2019 13:27:12 -0700
Subject: [PATCH 073/324] Fixed naming of test cases broke travis build

PiperOrigin-RevId: 239256432
Change-Id: Idb349ed14613fbb3d65082eac6e0d7dc1f30555c
---
 fire/helputils_test.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/fire/helputils_test.py b/fire/helputils_test.py
index 90cd8d6f..5be88ce0 100644
--- a/fire/helputils_test.py
+++ b/fire/helputils_test.py
@@ -164,7 +164,7 @@ def testHelpScreen(self):
 """
     self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
 
-  def testHelpScreenForFunction_docstringWithLineBreak(self):
+  def testHelpScreenForFunctionDocstringWithLineBreak(self):
     component = tc.ClassWithMultilineDocstring.example_generator
     t = trace.FireTrace(component, name='example_generator')
     info = inspectutils.Info(component)
@@ -189,7 +189,7 @@ def testHelpScreenForFunction_docstringWithLineBreak(self):
     """
     self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
 
-  def testHelpScreenForFunction_functionWithDefaultArgs(self):
+  def testHelpScreenForFunctionFunctionWithDefaultArgs(self):
     component = tc.WithDefaults().double
     t = trace.FireTrace(component, name='double')
     info = inspectutils.Info(component)

From c507c093fa6622ab5efee21709ffbf25974e4cf7 Mon Sep 17 00:00:00 2001
From: Rebecca Chen <rechen@google.com>
Date: Thu, 21 Mar 2019 13:40:33 -0700
Subject: [PATCH 074/324] Fix or ignore type errors generated by the next
 release of pytype.

PiperOrigin-RevId: 239662678
Change-Id: Iab3c34c8c5b59e0de2b0ff887699428fe28c68d5
---
 fire/parser.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fire/parser.py b/fire/parser.py
index 1e524d05..404e18e7 100644
--- a/fire/parser.py
+++ b/fire/parser.py
@@ -94,7 +94,7 @@ def _LiteralEval(value):
     SyntaxError: If the value string has a syntax error.
   """
   root = ast.parse(value, mode='eval')
-  if isinstance(root.body, ast.BinOp):
+  if isinstance(root.body, ast.BinOp):  # pytype: disable=attribute-error
     raise ValueError(value)
 
   for node in ast.walk(root):

From 669cf281a5c7bf615c7d51478fbec30833f99fbc Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Tue, 26 Mar 2019 14:36:02 -0700
Subject: [PATCH 075/324] Split helputils into helputils (old, still used) and
 helptext (new module, scheduled to replace helputils)

PiperOrigin-RevId: 240429364
Change-Id: Ifddbb6a4aff20407df9a84931898239d0b26649c
---
 fire/helptext.py       | 473 +++++++++++++++++++++++++++++++++++++++++
 fire/helptext_test.py  | 231 ++++++++++++++++++++
 fire/helputils.py      | 416 +-----------------------------------
 fire/helputils_test.py | 203 ------------------
 4 files changed, 706 insertions(+), 617 deletions(-)
 create mode 100644 fire/helptext.py
 create mode 100644 fire/helptext_test.py

diff --git a/fire/helptext.py b/fire/helptext.py
new file mode 100644
index 00000000..bed7b9d2
--- /dev/null
+++ b/fire/helptext.py
@@ -0,0 +1,473 @@
+# 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.
+
+"""helptext is the new, work in progress, help text module for Fire.
+
+This is a fork of, and is intended to replace, helputils.
+
+Utility for producing help strings for use in Fire CLIs.
+
+Can produce help strings suitable for display in Fire CLIs for any type of
+Python object, module, class, or function.
+
+There are two types of informative strings: Usage and Help screens.
+
+Usage screens are shown when the user accesses a group or accesses a command
+without calling it. A Usage screen shows information about how to use that group
+or command. Usage screens are typically short and show the minimal information
+necessary for the user to determine how to proceed.
+
+Help screens are shown when the user requests help with the help flag (--help).
+Help screens are shown in a less-style console view, and contain detailed help
+information.
+"""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import inspect
+
+from fire import completion
+from fire import docstrings
+from fire import inspectutils
+from fire import value_types
+
+
+def Text(component, trace=None, verbose=False):
+  """Returns the text to show for a supplied component.
+
+  The component can be any Python class, object, function, module, etc.
+
+  Args:
+    component: The component to determine the help string for.
+    trace: The Fire trace leading to this component.
+    verbose: Whether to include private members in the help string.
+  Returns:
+    String suitable for display giving information about the component.
+  """
+  info = inspectutils.Info(component)
+  info['docstring_info'] = docstrings.parse(info['docstring'])
+
+  is_error_screen = False
+  if trace:
+    is_error_screen = trace.HasError()
+
+  if is_error_screen:
+    return UsageText(info, trace, verbose=verbose)
+  else:
+    return HelpText(info, trace, verbose=verbose)
+
+
+def GetArgsAngFlags(component):
+  """Returns all types of arguments and flags of a component."""
+  spec = inspectutils.GetFullArgSpec(component)
+  args = spec.args
+  if spec.defaults is None:
+    num_defaults = 0
+  else:
+    num_defaults = len(spec.defaults)
+  args_with_no_defaults = args[:len(args) - num_defaults]
+  args_with_defaults = args[len(args) - num_defaults:]
+  flags = args_with_defaults + spec.kwonlyargs
+  return args_with_no_defaults, args_with_defaults, flags
+
+
+def GetSummaryAndDescription(docstring_info):
+  """Retrieves summary and description for help text generation."""
+
+  # To handle both empty string and None
+  summary = docstring_info.summary if docstring_info.summary else None
+  description = (
+      docstring_info.description if docstring_info.description else None)
+  return summary, description
+
+
+def GetCurrentCommand(trace=None):
+  """Returns current command for the purpose of generating help text."""
+  if trace:
+    current_command = trace.GetCommand()
+  else:
+    current_command = ''
+
+  return current_command
+
+
+def HelpText(component, info, trace=None, verbose=False):
+  if inspect.isroutine(component) or inspect.isclass(component):
+    return HelpTextForFunction(component, info, trace)
+  else:
+    return HelpTextForObject(component, info, trace, verbose)
+
+
+def HelpTextForFunction(component, info, trace=None, verbose=False):
+  """Returns detail help text for a function component.
+
+  Args:
+    component: Current component to generate help text for.
+    info: Info containing metadata of component.
+    trace: FireTrace object that leads to current component.
+    verbose: Whether to display help text in verbose mode.
+
+  Returns:
+    Formatted help text for display.
+  """
+  # TODO(joejoevictor): Implement verbose related output
+  del verbose
+
+  current_command = GetCurrentCommand(trace)
+  summary, description = GetSummaryAndDescription(info['docstring_info'])
+  spec = inspectutils.GetFullArgSpec(component)
+  args = spec.args
+
+  args_with_no_defaults, args_with_defaults, flags = GetArgsAngFlags(component)
+  del args_with_defaults
+
+  output_template = """NAME
+    {name_section}
+
+SYNOPSIS
+    {synopsis_section}
+
+DESCRIPTION
+    {description_section}
+{args_and_flags_section}
+NOTES
+    You could also use flags syntax for POSITIONAL ARGUMENTS
+"""
+
+  # Name section
+  name_section_template = '{current_command}{command_summary}'
+  command_summary_str = ' - ' + summary if summary else ''
+  name_section = name_section_template.format(
+      current_command=current_command, command_summary=command_summary_str)
+
+  args_and_flags = ''
+  if args_with_no_defaults:
+    items = [arg.upper() for arg in args_with_no_defaults]
+    args_and_flags = ' '.join(items)
+
+  synopsis_flag_template = '[--{flag_name}={flag_name_upper}]'
+  if flags:
+    items = [
+        synopsis_flag_template.format(
+            flag_name=flag, flag_name_upper=flag.upper()) for flag in flags
+    ]
+    args_and_flags = args_and_flags + ' '.join(items)
+
+  # Synopsis section
+  synopsis_section_template = '{current_command} {args_and_flags}'
+  positional_arguments = '|'.join(args)
+  if positional_arguments:
+    positional_arguments = ' ' + positional_arguments
+  synopsis_section = synopsis_section_template.format(
+      current_command=current_command, args_and_flags=args_and_flags)
+
+  # Description section
+  description_section = description if description else summary
+
+  args_and_flags_section = ''
+
+  # Positional arguments and flags section
+  pos_arg_template = """
+POSITIONAL ARGUMENTS
+{items}
+"""
+  pos_arg_items = []
+  for arg in args_with_no_defaults:
+    item_template = '    {arg_name}\n        {arg_description}\n'
+    arg_description = None
+    for arg_in_docstring in info['docstring_info'].args:
+      if arg_in_docstring.name == arg:
+        arg_description = arg_in_docstring.description
+
+    item = item_template.format(
+        arg_name=arg.upper(), arg_description=arg_description)
+    pos_arg_items.append(item)
+  if pos_arg_items:
+    args_and_flags_section += pos_arg_template.format(
+        items='\n'.join(pos_arg_items).rstrip('\n'))
+
+  flags_template = """
+FLAGS
+{items}
+"""
+  flag_items = []
+  for flag in flags:
+    item_template = '    --{flag_name}\n        {flag_description}\n'
+    flag_description = None
+    for arg_in_docstring in info['docstring_info'].args:
+      if arg_in_docstring.name == flag:
+        flag_description = arg_in_docstring.description
+
+    item = item_template.format(
+        flag_name=flag, flag_description=flag_description)
+    flag_items.append(item)
+  if flag_items:
+    args_and_flags_section += flags_template.format(
+        items='\n'.join(flag_items).rstrip('\n'))
+
+  return output_template.format(
+      name_section=name_section,
+      synopsis_section=synopsis_section,
+      description_section=description_section,
+      args_and_flags_section=args_and_flags_section)
+
+
+def HelpTextForObject(component, info, trace=None, verbose=False):
+  """Generates help text for python objects.
+
+  Args:
+    component: Current component to generate help text for.
+    info: Info containing metadata of component.
+    trace: FireTrace object that leads to current component.
+    verbose: Whether to display help text in verbose mode.
+
+  Returns:
+    Formatted help text for display.
+  """
+
+  output_template = """NAME
+    {current_command} - {command_summary}
+
+SYNOPSIS
+    {synopsis}
+
+DESCRIPTION
+    {command_description}
+{detail_section}
+"""
+
+  current_command = GetCurrentCommand(trace)
+
+  docstring_info = info['docstring_info']
+  command_summary = docstring_info.summary if docstring_info.summary else ''
+  if docstring_info.description:
+    command_description = docstring_info.description
+  else:
+    command_description = ''
+
+  groups = []
+  commands = []
+  values = []
+  members = completion._Members(component, verbose)  # pylint: disable=protected-access
+  for member_name, member in members:
+    if value_types.IsGroup(member):
+      groups.append((member_name, member))
+    if value_types.IsCommand(member):
+      commands.append((member_name, member))
+    if value_types.IsValue(member):
+      values.append((member_name, member))
+
+  possible_actions = []
+  # TODO(joejoevictor): Add global flags to here. Also, if it's a callable,
+  # there will be additional flags.
+  possible_flags = ''
+  detail_section_string = ''
+  item_template = """
+        {name}
+            {command_summary}
+"""
+
+  if groups:
+    # TODO(joejoevictor): Add missing GROUPS section handling
+    possible_actions.append('GROUP')
+  if commands:
+    possible_actions.append('COMMAND')
+    commands_str_template = """
+COMMANDS
+    COMMAND is one of the followings:
+{items}
+"""
+    command_item_strings = []
+    for command_name, command in commands:
+      command_docstring_info = docstrings.parse(
+          inspectutils.Info(command)['docstring'])
+      command_item_strings.append(
+          item_template.format(
+              name=command_name,
+              command_summary=command_docstring_info.summary))
+    detail_section_string += commands_str_template.format(
+        items=('\n'.join(command_item_strings)).rstrip('\n'))
+
+  if values:
+    possible_actions.append('VALUES')
+    values_str_template = """
+VALUES
+    VALUE is one of the followings:
+{items}
+"""
+    value_item_strings = []
+    for value_name, value in values:
+      del value
+      init_docstring_info = docstrings.parse(
+          inspectutils.Info(component.__class__.__init__)['docstring'])
+      for arg_info in init_docstring_info.args:
+        if arg_info.name == value_name:
+          value_item_strings.append(
+              item_template.format(
+                  name=value_name, command_summary=arg_info.description))
+    detail_section_string += values_str_template.format(
+        items=('\n'.join(value_item_strings)).rstrip('\n'))
+
+  possible_actions_string = ' ' + (' | '.join(possible_actions))
+
+  synopsis_template = '{current_command}{possible_actions}{possible_flags}'
+  synopsis_string = synopsis_template.format(
+      current_command=current_command,
+      possible_actions=possible_actions_string,
+      possible_flags=possible_flags)
+
+  return output_template.format(
+      current_command=current_command,
+      command_summary=command_summary,
+      synopsis=synopsis_string,
+      command_description=command_description,
+      detail_section=detail_section_string)
+
+
+def UsageText(component, trace=None, verbose=False):
+  if inspect.isroutine(component) or inspect.isclass(component):
+    return UsageTextForFunction(component, trace)
+  else:
+    return UsageTextForObject(component, trace, verbose)
+
+
+def UsageTextForFunction(component, trace=None):
+  """Returns usage text for function objects.
+
+  Args:
+    component: The component to determine the usage text for.
+    trace: The Fire trace object containing all metadata of current execution.
+
+  Returns:
+    String suitable for display in error screen.
+  """
+
+  output_template = """Usage: {current_command} {args_and_flags}
+{availability_lines}
+For detailed information on this command, run:
+{current_command}{hyphen_hyphen} --help
+"""
+
+  if trace:
+    command = trace.GetCommand()
+    is_help_an_arg = trace.NeedsSeparatingHyphenHyphen()
+  else:
+    command = None
+    is_help_an_arg = False
+
+  if not command:
+    command = ''
+
+  spec = inspectutils.GetFullArgSpec(component)
+  args = spec.args
+  if spec.defaults is None:
+    num_defaults = 0
+  else:
+    num_defaults = len(spec.defaults)
+  args_with_no_defaults = args[:len(args) - num_defaults]
+  args_with_defaults = args[len(args) - num_defaults:]
+  flags = args_with_defaults + spec.kwonlyargs
+
+  items = [arg.upper() for arg in args_with_no_defaults]
+  if flags:
+    items.append('<flags>')
+    availability_lines = (
+        '\nAvailable flags: '
+        + ' | '.join('--' + flag for flag in flags) + '\n')
+  else:
+    availability_lines = ''
+  args_and_flags = ' '.join(items)
+
+  hyphen_hyphen = ' --' if is_help_an_arg else ''
+
+  return output_template.format(
+      current_command=command,
+      args_and_flags=args_and_flags,
+      availability_lines=availability_lines,
+      hyphen_hyphen=hyphen_hyphen)
+
+
+def UsageTextForObject(component, trace=None, verbose=False):
+  """Returns help text for usage screen for objects.
+
+  Construct help text for usage screen to inform the user about error occurred
+  and correct syntax for invoking the object.
+
+  Args:
+    component: The component to determine the usage text for.
+    trace: The Fire trace object containing all metadata of current execution.
+    verbose: Whether to include private members in the usage text.
+  Returns:
+    String suitable for display in error screen.
+  """
+  output_template = """Usage: {current_command} <{possible_actions}>
+{availability_lines}
+
+For detailed information on this command, run:
+{current_command} --help
+"""
+  if trace:
+    command = trace.GetCommand()
+  else:
+    command = None
+
+  if not command:
+    command = ''
+
+  groups = []
+  commands = []
+  values = []
+
+  members = completion._Members(component, verbose)  # pylint: disable=protected-access
+  for member_name, member in members:
+    if value_types.IsGroup(member):
+      groups.append(member_name)
+    if value_types.IsCommand(member):
+      commands.append(member_name)
+    if value_types.IsValue(member):
+      values.append(member_name)
+
+  possible_actions = []
+  availability_lines = []
+  availability_lint_format = '{header:20s}{choices}'
+  if groups:
+    possible_actions.append('groups')
+    groups_string = ' | '.join(groups)
+    groups_text = availability_lint_format.format(
+        header='available groups:',
+        choices=groups_string)
+    availability_lines.append(groups_text)
+  if commands:
+    possible_actions.append('commands')
+    commands_string = ' | '.join(commands)
+    commands_text = availability_lint_format.format(
+        header='available commands:',
+        choices=commands_string)
+    availability_lines.append(commands_text)
+  if values:
+    possible_actions.append('values')
+    values_string = ' | '.join(values)
+    values_text = availability_lint_format.format(
+        header='available values:',
+        choices=values_string)
+    availability_lines.append(values_text)
+  possible_actions_string = '|'.join(possible_actions)
+  availability_lines_string = '\n'.join(availability_lines)
+
+  return output_template.format(
+      current_command=command,
+      possible_actions=possible_actions_string,
+      availability_lines=availability_lines_string)
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
new file mode 100644
index 00000000..e87ebeff
--- /dev/null
+++ b/fire/helptext_test.py
@@ -0,0 +1,231 @@
+# 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.
+
+"""Tests for the helptext module."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import textwrap
+
+from fire import docstrings
+from fire import helptext
+from fire import inspectutils
+from fire import test_components as tc
+from fire import testutils
+from fire import trace
+
+
+class HelpScreenTest(testutils.BaseTestCase):
+
+  def testHelpScreen(self):
+    component = tc.ClassWithDocstring()
+    t = trace.FireTrace(component, name='ClassWithDocstring')
+    info = inspectutils.Info(component)
+    info['docstring_info'] = docstrings.parse(info['docstring'])
+    help_output = helptext.HelpText(component, info, t)
+    expected_output = """
+NAME
+    ClassWithDocstring - Test class for testing help text output.
+
+SYNOPSIS
+    ClassWithDocstring COMMAND | VALUES
+
+DESCRIPTION
+    This is some detail description of this test class.
+
+COMMANDS
+    COMMAND is one of the followings:
+
+        print_msg
+            Prints a message.
+
+VALUES
+    VALUE is one of the followings:
+
+        message
+            The default message to print.
+
+"""
+    self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
+
+  def testHelpScreenForFunctionDocstringWithLineBreak(self):
+    component = tc.ClassWithMultilineDocstring.example_generator
+    t = trace.FireTrace(component, name='example_generator')
+    info = inspectutils.Info(component)
+    info['docstring_info'] = docstrings.parse(info['docstring'])
+    help_output = helptext.HelpText(component, info, t)
+    expected_output = """
+    NAME
+        example_generator - Generators have a ``Yields`` section instead of a ``Returns`` section.
+
+    SYNOPSIS
+        example_generator N
+
+    DESCRIPTION
+        Generators have a ``Yields`` section instead of a ``Returns`` section.
+
+    POSITIONAL ARGUMENTS
+        N
+            The upper limit of the range to generate, from 0 to `n` - 1.
+
+    NOTES
+        You could also use flags syntax for POSITIONAL ARGUMENTS
+    """
+    self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
+
+  def testHelpScreenForFunctionFunctionWithDefaultArgs(self):
+    component = tc.WithDefaults().double
+    t = trace.FireTrace(component, name='double')
+    info = inspectutils.Info(component)
+    info['docstring_info'] = docstrings.parse(info['docstring'])
+    help_output = helptext.HelpText(component, info, t)
+    expected_output = """
+    NAME
+        double - Returns the input multiplied by 2.
+
+    SYNOPSIS
+        double [--count=COUNT]
+
+    DESCRIPTION
+        Returns the input multiplied by 2.
+
+    FLAGS
+        --count
+            Input number that you want to double.
+
+    NOTES
+        You could also use flags syntax for POSITIONAL ARGUMENTS
+    """
+    self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
+
+
+class UsageTest(testutils.BaseTestCase):
+
+  def testUsageOutput(self):
+    component = tc.NoDefaults()
+    t = trace.FireTrace(component, name='NoDefaults')
+    usage_output = helptext.UsageText(component, trace=t, verbose=False)
+    expected_output = '''
+    Usage: NoDefaults <commands>
+    available commands: double | triple
+
+    For detailed information on this command, run:
+    NoDefaults --help
+    '''
+
+    self.assertEqual(
+        usage_output,
+        textwrap.dedent(expected_output).lstrip('\n'))
+
+  def testUsageOutputVerbose(self):
+    component = tc.NoDefaults()
+    t = trace.FireTrace(component, name='NoDefaults')
+    usage_output = helptext.UsageText(component, trace=t, verbose=True)
+    expected_output = '''
+    Usage: NoDefaults <commands>
+    available commands: double | triple
+
+    For detailed information on this command, run:
+    NoDefaults --help
+    '''
+    self.assertEqual(
+        usage_output,
+        textwrap.dedent(expected_output).lstrip('\n'))
+
+  def testUsageOutputMethod(self):
+    component = tc.NoDefaults().double
+    t = trace.FireTrace(component, name='NoDefaults')
+    t.AddAccessedProperty(component, 'double', ['double'], None, None)
+    usage_output = helptext.UsageText(component, trace=t, verbose=True)
+    expected_output = '''
+    Usage: NoDefaults double COUNT
+
+    For detailed information on this command, run:
+    NoDefaults double --help
+    '''
+    self.assertEqual(
+        usage_output,
+        textwrap.dedent(expected_output).lstrip('\n'))
+
+  def testUsageOutputFunctionWithHelp(self):
+    component = tc.function_with_help
+    t = trace.FireTrace(component, name='function_with_help')
+    usage_output = helptext.UsageText(component, trace=t, verbose=True)
+    expected_output = '''
+    Usage: function_with_help <flags>
+
+    Available flags: --help
+
+    For detailed information on this command, run:
+    function_with_help -- --help
+    '''
+    self.assertEqual(
+        usage_output,
+        textwrap.dedent(expected_output).lstrip('\n'))
+
+  def testUsageOutputFunctionWithDocstring(self):
+    component = tc.multiplier_with_docstring
+    t = trace.FireTrace(component, name='multiplier_with_docstring')
+    usage_output = helptext.UsageText(component, trace=t, verbose=True)
+    expected_output = '''
+    Usage: multiplier_with_docstring NUM <flags>
+
+    Available flags: --rate
+
+    For detailed information on this command, run:
+    multiplier_with_docstring --help
+    '''
+    self.assertEqual(
+        usage_output,
+        textwrap.dedent(expected_output).lstrip('\n'))
+
+  @testutils.skip('The functionality is not implemented yet')
+  def testUsageOutputCallable(self):
+    # This is both a group and a command!
+    component = tc.CallableWithKeywordArgument
+    t = trace.FireTrace(component, name='CallableWithKeywordArgument')
+    usage_output = helptext.UsageText(component, trace=t, verbose=True)
+    # TODO(zuhaohen): We need to handle the case for keyword args as well
+    # i.e. __call__ method of CallableWithKeywordArgument
+    expected_output = '''
+    Usage: CallableWithKeywordArgument <commands>
+
+    Available commands: print_msg
+
+    For detailed information on this command, run:
+    CallableWithKeywordArgument -- --help
+    '''
+    self.assertEqual(
+        usage_output,
+        textwrap.dedent(expected_output).lstrip('\n'))
+
+  def testUsageOutputConstructorWithParameter(self):
+    component = tc.InstanceVars
+    t = trace.FireTrace(component, name='InstanceVars')
+    usage_output = helptext.UsageText(component, trace=t, verbose=True)
+    expected_output = '''
+    Usage: InstanceVars ARG1 ARG2
+
+    For detailed information on this command, run:
+    InstanceVars --help
+    '''
+    self.assertEqual(
+        usage_output,
+        textwrap.dedent(expected_output).lstrip('\n'))
+
+
+if __name__ == '__main__':
+  testutils.main()
diff --git a/fire/helputils.py b/fire/helputils.py
index 6471585d..2d906a11 100644
--- a/fire/helputils.py
+++ b/fire/helputils.py
@@ -38,7 +38,6 @@
 from fire import completion
 from fire import docstrings
 from fire import inspectutils
-from fire import value_types
 
 
 def _NormalizeField(field):
@@ -106,20 +105,6 @@ def _GetFields(trace=None):
   ]
 
 
-def GetArgsAngFlags(component):
-  """Returns all types of arguments and flags of a component."""
-  spec = inspectutils.GetFullArgSpec(component)
-  args = spec.args
-  if spec.defaults is None:
-    num_defaults = 0
-  else:
-    num_defaults = len(spec.defaults)
-  args_with_no_defaults = args[:len(args) - num_defaults]
-  args_with_defaults = args[len(args) - num_defaults:]
-  flags = args_with_defaults + spec.kwonlyargs
-  return args_with_no_defaults, args_with_defaults, flags
-
-
 def HelpString(component, trace=None, verbose=False):
   """Returns a help string for a supplied component.
 
@@ -133,22 +118,13 @@ def HelpString(component, trace=None, verbose=False):
     String suitable for display giving information about the component.
   """
   info = inspectutils.Info(component)
-  # TODO(dbieber): Stop using UsageString in favor of UsageText.
   info['usage'] = UsageString(component, trace, verbose)
   info['docstring_info'] = docstrings.parse(info['docstring'])
 
-  is_error_screen = False
-  if trace:
-    is_error_screen = trace.HasError()
-
-  if is_error_screen:
-    # TODO(dbieber): Call UsageText instead of CommonHelpText once ready.
-    return _CommonHelpText(info, trace)
-  else:
-    return _HelpText(info, trace)
+  return _HelpText(info, trace)
 
 
-def _CommonHelpText(info, trace=None):
+def _HelpText(info, trace=None):
   """Returns help text.
 
   This was a copy of previous HelpString function and will be removed once the
@@ -160,8 +136,6 @@ def _CommonHelpText(info, trace=None):
   Returns:
     String suitable for display giving information about the component.
   """
-  # TODO(joejoevictor): Currently this is just a copy of existing
-  # HelpString method. We will reimplement this further in later CLs.
   fields = _GetFields(trace)
 
   try:
@@ -207,392 +181,6 @@ def GetCurrentCommand(trace=None):
   return current_command
 
 
-def HelpText(component, info, trace=None, verbose=False):
-  if inspect.isroutine(component) or inspect.isclass(component):
-    return HelpTextForFunction(component, info, trace)
-  else:
-    return HelpTextForObject(component, info, trace, verbose)
-
-
-def HelpTextForFunction(component, info, trace=None, verbose=False):
-  """Returns detail help text for a function component.
-
-  Args:
-    component: Current component to generate help text for.
-    info: Info containing metadata of component.
-    trace: FireTrace object that leads to current component.
-    verbose: Whether to display help text in verbose mode.
-
-  Returns:
-    Formatted help text for display.
-  """
-  # TODO(joejoevictor): Implement verbose related output
-  del verbose
-
-  current_command = GetCurrentCommand(trace)
-  summary, description = GetSummaryAndDescription(info['docstring_info'])
-  spec = inspectutils.GetFullArgSpec(component)
-  args = spec.args
-
-  args_with_no_defaults, args_with_defaults, flags = GetArgsAngFlags(component)
-  del args_with_defaults
-
-  output_template = """NAME
-    {name_section}
-
-SYNOPSIS
-    {synopsis_section}
-
-DESCRIPTION
-    {description_section}
-{args_and_flags_section}
-NOTES
-    You could also use flags syntax for POSITIONAL ARGUMENTS
-"""
-
-  # Name section
-  name_section_template = '{current_command}{command_summary}'
-  command_summary_str = ' - ' + summary if summary else ''
-  name_section = name_section_template.format(
-      current_command=current_command, command_summary=command_summary_str)
-
-  args_and_flags = ''
-  if args_with_no_defaults:
-    items = [arg.upper() for arg in args_with_no_defaults]
-    args_and_flags = ' '.join(items)
-
-  synopsis_flag_template = '[--{flag_name}={flag_name_upper}]'
-  if flags:
-    items = [
-        synopsis_flag_template.format(
-            flag_name=flag, flag_name_upper=flag.upper()) for flag in flags
-    ]
-    args_and_flags = args_and_flags + ' '.join(items)
-
-  # Synopsis section
-  synopsis_section_template = '{current_command} {args_and_flags}'
-  positional_arguments = '|'.join(args)
-  if positional_arguments:
-    positional_arguments = ' ' + positional_arguments
-  synopsis_section = synopsis_section_template.format(
-      current_command=current_command, args_and_flags=args_and_flags)
-
-  # Description section
-  description_section = description if description else summary
-
-  args_and_flags_section = ''
-
-  # Positional arguments and flags section
-  pos_arg_template = """
-POSITIONAL ARGUMENTS
-{items}
-"""
-  pos_arg_items = []
-  for arg in args_with_no_defaults:
-    item_template = '    {arg_name}\n        {arg_description}\n'
-    arg_description = None
-    for arg_in_docstring in info['docstring_info'].args:
-      if arg_in_docstring.name == arg:
-        arg_description = arg_in_docstring.description
-
-    item = item_template.format(
-        arg_name=arg.upper(), arg_description=arg_description)
-    pos_arg_items.append(item)
-  if pos_arg_items:
-    args_and_flags_section += pos_arg_template.format(
-        items='\n'.join(pos_arg_items).rstrip('\n'))
-
-  flags_template = """
-FLAGS
-{items}
-"""
-  flag_items = []
-  for flag in flags:
-    item_template = '    --{flag_name}\n        {flag_description}\n'
-    flag_description = None
-    for arg_in_docstring in info['docstring_info'].args:
-      if arg_in_docstring.name == flag:
-        flag_description = arg_in_docstring.description
-
-    item = item_template.format(
-        flag_name=flag, flag_description=flag_description)
-    flag_items.append(item)
-  if flag_items:
-    args_and_flags_section += flags_template.format(
-        items='\n'.join(flag_items).rstrip('\n'))
-
-  return output_template.format(
-      name_section=name_section,
-      synopsis_section=synopsis_section,
-      description_section=description_section,
-      args_and_flags_section=args_and_flags_section)
-
-
-def HelpTextForObject(component, info, trace=None, verbose=False):
-  """Generates help text for python objects.
-
-  Args:
-    component: Current component to generate help text for.
-    info: Info containing metadata of component.
-    trace: FireTrace object that leads to current component.
-    verbose: Whether to display help text in verbose mode.
-
-  Returns:
-    Formatted help text for display.
-  """
-
-  output_template = """NAME
-    {current_command} - {command_summary}
-
-SYNOPSIS
-    {synopsis}
-
-DESCRIPTION
-    {command_description}
-{detail_section}
-"""
-
-  current_command = GetCurrentCommand(trace)
-
-  docstring_info = info['docstring_info']
-  command_summary = docstring_info.summary if docstring_info.summary else ''
-  if docstring_info.description:
-    command_description = docstring_info.description
-  else:
-    command_description = ''
-
-  groups = []
-  commands = []
-  values = []
-  members = completion._Members(component, verbose)  # pylint: disable=protected-access
-  for member_name, member in members:
-    if value_types.IsGroup(member):
-      groups.append((member_name, member))
-    if value_types.IsCommand(member):
-      commands.append((member_name, member))
-    if value_types.IsValue(member):
-      values.append((member_name, member))
-
-  possible_actions = []
-  # TODO(joejoevictor): Add global flags to here. Also, if it's a callable,
-  # there will be additional flags.
-  possible_flags = ''
-  detail_section_string = ''
-  item_template = """
-        {name}
-            {command_summary}
-"""
-
-  if groups:
-    # TODO(joejoevictor): Add missing GROUPS section handling
-    possible_actions.append('GROUP')
-  if commands:
-    possible_actions.append('COMMAND')
-    commands_str_template = """
-COMMANDS
-    COMMAND is one of the followings:
-{items}
-"""
-    command_item_strings = []
-    for command_name, command in commands:
-      command_docstring_info = docstrings.parse(
-          inspectutils.Info(command)['docstring'])
-      command_item_strings.append(
-          item_template.format(
-              name=command_name,
-              command_summary=command_docstring_info.summary))
-    detail_section_string += commands_str_template.format(
-        items=('\n'.join(command_item_strings)).rstrip('\n'))
-
-  if values:
-    possible_actions.append('VALUES')
-    values_str_template = """
-VALUES
-    VALUE is one of the followings:
-{items}
-"""
-    value_item_strings = []
-    for value_name, value in values:
-      del value
-      init_docstring_info = docstrings.parse(
-          inspectutils.Info(component.__class__.__init__)['docstring'])
-      for arg_info in init_docstring_info.args:
-        if arg_info.name == value_name:
-          value_item_strings.append(
-              item_template.format(
-                  name=value_name, command_summary=arg_info.description))
-    detail_section_string += values_str_template.format(
-        items=('\n'.join(value_item_strings)).rstrip('\n'))
-
-  possible_actions_string = ' ' + (' | '.join(possible_actions))
-
-  synopsis_template = '{current_command}{possible_actions}{possible_flags}'
-  synopsis_string = synopsis_template.format(
-      current_command=current_command,
-      possible_actions=possible_actions_string,
-      possible_flags=possible_flags)
-
-  return output_template.format(
-      current_command=current_command,
-      command_summary=command_summary,
-      synopsis=synopsis_string,
-      command_description=command_description,
-      detail_section=detail_section_string)
-
-
-def UsageText(component, trace=None, verbose=False):
-  if inspect.isroutine(component) or inspect.isclass(component):
-    return UsageTextForFunction(component, trace)
-  else:
-    return UsageTextForObject(component, trace, verbose)
-
-
-def UsageTextForFunction(component, trace=None):
-  """Returns usage text for function objects.
-
-  Args:
-    component: The component to determine the usage text for.
-    trace: The Fire trace object containing all metadata of current execution.
-
-  Returns:
-    String suitable for display in error screen.
-  """
-
-  output_template = """Usage: {current_command} {args_and_flags}
-{availability_lines}
-For detailed information on this command, run:
-{current_command}{hyphen_hyphen} --help
-"""
-
-  if trace:
-    command = trace.GetCommand()
-    is_help_an_arg = trace.NeedsSeparatingHyphenHyphen()
-  else:
-    command = None
-    is_help_an_arg = False
-
-  if not command:
-    command = ''
-
-  spec = inspectutils.GetFullArgSpec(component)
-  args = spec.args
-  if spec.defaults is None:
-    num_defaults = 0
-  else:
-    num_defaults = len(spec.defaults)
-  args_with_no_defaults = args[:len(args) - num_defaults]
-  args_with_defaults = args[len(args) - num_defaults:]
-  flags = args_with_defaults + spec.kwonlyargs
-
-  items = [arg.upper() for arg in args_with_no_defaults]
-  if flags:
-    items.append('<flags>')
-    availability_lines = (
-        '\nAvailable flags: '
-        + ' | '.join('--' + flag for flag in flags) + '\n')
-  else:
-    availability_lines = ''
-  args_and_flags = ' '.join(items)
-
-  hyphen_hyphen = ' --' if is_help_an_arg else ''
-
-  return output_template.format(
-      current_command=command,
-      args_and_flags=args_and_flags,
-      availability_lines=availability_lines,
-      hyphen_hyphen=hyphen_hyphen)
-
-
-def UsageTextForObject(component, trace=None, verbose=False):
-  """Returns help text for usage screen for objects.
-
-  Construct help text for usage screen to inform the user about error occurred
-  and correct syntax for invoking the object.
-
-  Args:
-    component: The component to determine the usage text for.
-    trace: The Fire trace object containing all metadata of current execution.
-    verbose: Whether to include private members in the usage text.
-  Returns:
-    String suitable for display in error screen.
-  """
-  output_template = """Usage: {current_command} <{possible_actions}>
-{availability_lines}
-
-For detailed information on this command, run:
-{current_command} --help
-"""
-  if trace:
-    command = trace.GetCommand()
-  else:
-    command = None
-
-  if not command:
-    command = ''
-
-  groups = []
-  commands = []
-  values = []
-
-  members = completion._Members(component, verbose)  # pylint: disable=protected-access
-  for member_name, member in members:
-    if value_types.IsGroup(member):
-      groups.append(member_name)
-    if value_types.IsCommand(member):
-      commands.append(member_name)
-    if value_types.IsValue(member):
-      values.append(member_name)
-
-  possible_actions = []
-  availability_lines = []
-  availability_lint_format = '{header:20s}{choices}'
-  if groups:
-    possible_actions.append('groups')
-    groups_string = ' | '.join(groups)
-    groups_text = availability_lint_format.format(
-        header='available groups:',
-        choices=groups_string)
-    availability_lines.append(groups_text)
-  if commands:
-    possible_actions.append('commands')
-    commands_string = ' | '.join(commands)
-    commands_text = availability_lint_format.format(
-        header='available commands:',
-        choices=commands_string)
-    availability_lines.append(commands_text)
-  if values:
-    possible_actions.append('values')
-    values_string = ' | '.join(values)
-    values_text = availability_lint_format.format(
-        header='available values:',
-        choices=values_string)
-    availability_lines.append(values_text)
-  possible_actions_string = '|'.join(possible_actions)
-  availability_lines_string = '\n'.join(availability_lines)
-
-  return output_template.format(
-      current_command=command,
-      possible_actions=possible_actions_string,
-      availability_lines=availability_lines_string)
-
-
-def _HelpText(info, trace=None):
-  """Returns help text for extensive help screen.
-
-  Construct help text for help screen when user explicitly requesting help by
-  having -h, --help in the command sequence.
-
-  Args:
-    info: The IR object containing metadata of an object.
-    trace: The Fire trace object containing all metadata of current execution.
-  Returns:
-    String suitable for display in extensive help screen.
-  """
-
-  # TODO(joejoevictor): Implement real help text construction.
-  return _CommonHelpText(info, trace)
-
-
 def _UsageStringFromFullArgSpec(command, spec):
   """Get a usage string from the FullArgSpec for the given command.
 
diff --git a/fire/helputils_test.py b/fire/helputils_test.py
index 5be88ce0..08f7752c 100644
--- a/fire/helputils_test.py
+++ b/fire/helputils_test.py
@@ -19,14 +19,10 @@
 from __future__ import print_function
 
 import os
-import textwrap
 
-from fire import docstrings
 from fire import helputils
-from fire import inspectutils
 from fire import test_components as tc
 from fire import testutils
-from fire import trace
 import six
 
 
@@ -131,204 +127,5 @@ def testHelpClassNoInit(self):
     self.assertIn('Line:        ', helpstring)
 
 
-class HelpScreenTest(testutils.BaseTestCase):
-
-  def testHelpScreen(self):
-    component = tc.ClassWithDocstring()
-    t = trace.FireTrace(component, name='ClassWithDocstring')
-    info = inspectutils.Info(component)
-    info['docstring_info'] = docstrings.parse(info['docstring'])
-    help_output = helputils.HelpText(component, info, t)
-    expected_output = """
-NAME
-    ClassWithDocstring - Test class for testing help text output.
-
-SYNOPSIS
-    ClassWithDocstring COMMAND | VALUES
-
-DESCRIPTION
-    This is some detail description of this test class.
-
-COMMANDS
-    COMMAND is one of the followings:
-
-        print_msg
-            Prints a message.
-
-VALUES
-    VALUE is one of the followings:
-
-        message
-            The default message to print.
-
-"""
-    self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
-
-  def testHelpScreenForFunctionDocstringWithLineBreak(self):
-    component = tc.ClassWithMultilineDocstring.example_generator
-    t = trace.FireTrace(component, name='example_generator')
-    info = inspectutils.Info(component)
-    info['docstring_info'] = docstrings.parse(info['docstring'])
-    help_output = helputils.HelpText(component, info, t)
-    expected_output = """
-    NAME
-        example_generator - Generators have a ``Yields`` section instead of a ``Returns`` section.
-
-    SYNOPSIS
-        example_generator N
-
-    DESCRIPTION
-        Generators have a ``Yields`` section instead of a ``Returns`` section.
-
-    POSITIONAL ARGUMENTS
-        N
-            The upper limit of the range to generate, from 0 to `n` - 1.
-
-    NOTES
-        You could also use flags syntax for POSITIONAL ARGUMENTS
-    """
-    self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
-
-  def testHelpScreenForFunctionFunctionWithDefaultArgs(self):
-    component = tc.WithDefaults().double
-    t = trace.FireTrace(component, name='double')
-    info = inspectutils.Info(component)
-    info['docstring_info'] = docstrings.parse(info['docstring'])
-    help_output = helputils.HelpText(component, info, t)
-    expected_output = """
-    NAME
-        double - Returns the input multiplied by 2.
-
-    SYNOPSIS
-        double [--count=COUNT]
-
-    DESCRIPTION
-        Returns the input multiplied by 2.
-
-    FLAGS
-        --count
-            Input number that you want to double.
-
-    NOTES
-        You could also use flags syntax for POSITIONAL ARGUMENTS
-    """
-    self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
-
-
-class UsageTest(testutils.BaseTestCase):
-
-  def testUsageOutput(self):
-    component = tc.NoDefaults()
-    t = trace.FireTrace(component, name='NoDefaults')
-    usage_output = helputils.UsageText(component, trace=t, verbose=False)
-    expected_output = '''
-    Usage: NoDefaults <commands>
-    available commands: double | triple
-
-    For detailed information on this command, run:
-    NoDefaults --help
-    '''
-
-    self.assertEqual(
-        usage_output,
-        textwrap.dedent(expected_output).lstrip('\n'))
-
-  def testUsageOutputVerbose(self):
-    component = tc.NoDefaults()
-    t = trace.FireTrace(component, name='NoDefaults')
-    usage_output = helputils.UsageText(component, trace=t, verbose=True)
-    expected_output = '''
-    Usage: NoDefaults <commands>
-    available commands: double | triple
-
-    For detailed information on this command, run:
-    NoDefaults --help
-    '''
-    self.assertEqual(
-        usage_output,
-        textwrap.dedent(expected_output).lstrip('\n'))
-
-  def testUsageOutputMethod(self):
-    component = tc.NoDefaults().double
-    t = trace.FireTrace(component, name='NoDefaults')
-    t.AddAccessedProperty(component, 'double', ['double'], None, None)
-    usage_output = helputils.UsageText(component, trace=t, verbose=True)
-    expected_output = '''
-    Usage: NoDefaults double COUNT
-
-    For detailed information on this command, run:
-    NoDefaults double --help
-    '''
-    self.assertEqual(
-        usage_output,
-        textwrap.dedent(expected_output).lstrip('\n'))
-
-  def testUsageOutputFunctionWithHelp(self):
-    component = tc.function_with_help
-    t = trace.FireTrace(component, name='function_with_help')
-    usage_output = helputils.UsageText(component, trace=t, verbose=True)
-    expected_output = '''
-    Usage: function_with_help <flags>
-
-    Available flags: --help
-
-    For detailed information on this command, run:
-    function_with_help -- --help
-    '''
-    self.assertEqual(
-        usage_output,
-        textwrap.dedent(expected_output).lstrip('\n'))
-
-  def testUsageOutputFunctionWithDocstring(self):
-    component = tc.multiplier_with_docstring
-    t = trace.FireTrace(component, name='multiplier_with_docstring')
-    usage_output = helputils.UsageText(component, trace=t, verbose=True)
-    expected_output = '''
-    Usage: multiplier_with_docstring NUM <flags>
-
-    Available flags: --rate
-
-    For detailed information on this command, run:
-    multiplier_with_docstring --help
-    '''
-    self.assertEqual(
-        usage_output,
-        textwrap.dedent(expected_output).lstrip('\n'))
-
-  @testutils.skip('The functionality is not implemented yet')
-  def testUsageOutputCallable(self):
-    # This is both a group and a command!
-    component = tc.CallableWithKeywordArgument
-    t = trace.FireTrace(component, name='CallableWithKeywordArgument')
-    usage_output = helputils.UsageText(component, trace=t, verbose=True)
-    # TODO(zuhaohen): We need to handle the case for keyword args as well
-    # i.e. __call__ method of CallableWithKeywordArgument
-    expected_output = '''
-    Usage: CallableWithKeywordArgument <commands>
-
-    Available commands: print_msg
-
-    For detailed information on this command, run:
-    CallableWithKeywordArgument -- --help
-    '''
-    self.assertEqual(
-        usage_output,
-        textwrap.dedent(expected_output).lstrip('\n'))
-
-  def testUsageOutputConstructorWithParameter(self):
-    component = tc.InstanceVars
-    t = trace.FireTrace(component, name='InstanceVars')
-    usage_output = helputils.UsageText(component, trace=t, verbose=True)
-    expected_output = '''
-    Usage: InstanceVars ARG1 ARG2
-
-    For detailed information on this command, run:
-    InstanceVars --help
-    '''
-    self.assertEqual(
-        usage_output,
-        textwrap.dedent(expected_output).lstrip('\n'))
-
-
 if __name__ == '__main__':
   testutils.main()

From d0af0270b1e91d392f344baca3452e7aa117872a Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 27 Mar 2019 10:44:34 -0700
Subject: [PATCH 076/324] Start of clean up and formatting for help text for
 Python Fire

PiperOrigin-RevId: 240594699
Change-Id: Ic443d71c52f2a6bcdf32fec1a7ebdc7e8746153e
---
 fire/helptext.py      | 232 ++++++++++++++++++++++++------------------
 fire/helptext_test.py |  50 +++++----
 2 files changed, 164 insertions(+), 118 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index bed7b9d2..f9f16124 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -43,9 +43,10 @@
 from fire import docstrings
 from fire import inspectutils
 from fire import value_types
+import termcolor
 
 
-def Text(component, trace=None, verbose=False):
+def HelpString(component, trace=None, verbose=False):
   """Returns the text to show for a supplied component.
 
   The component can be any Python class, object, function, module, etc.
@@ -65,9 +66,9 @@ def Text(component, trace=None, verbose=False):
     is_error_screen = trace.HasError()
 
   if is_error_screen:
-    return UsageText(info, trace, verbose=verbose)
+    return UsageText(component, info, trace, verbose=verbose)
   else:
-    return HelpText(info, trace, verbose=verbose)
+    return HelpText(component, info, trace, verbose=verbose)
 
 
 def GetArgsAngFlags(component):
@@ -134,19 +135,6 @@ def HelpTextForFunction(component, info, trace=None, verbose=False):
   args_with_no_defaults, args_with_defaults, flags = GetArgsAngFlags(component)
   del args_with_defaults
 
-  output_template = """NAME
-    {name_section}
-
-SYNOPSIS
-    {synopsis_section}
-
-DESCRIPTION
-    {description_section}
-{args_and_flags_section}
-NOTES
-    You could also use flags syntax for POSITIONAL ARGUMENTS
-"""
-
   # Name section
   name_section_template = '{current_command}{command_summary}'
   command_summary_str = ' - ' + summary if summary else ''
@@ -177,52 +165,113 @@ def HelpTextForFunction(component, info, trace=None, verbose=False):
   # Description section
   description_section = description if description else summary
 
-  args_and_flags_section = ''
-
   # Positional arguments and flags section
-  pos_arg_template = """
-POSITIONAL ARGUMENTS
-{items}
-"""
+  docstring_info = info['docstring_info']
+  args_and_flags_sections = []
+  notes_sections = []
+
   pos_arg_items = []
-  for arg in args_with_no_defaults:
-    item_template = '    {arg_name}\n        {arg_description}\n'
-    arg_description = None
-    for arg_in_docstring in info['docstring_info'].args:
+  pos_arg_items = [
+      _CreatePositionalArgItem(arg, docstring_info)
+      for arg in args_with_no_defaults
+  ]
+  if pos_arg_items:
+    positional_arguments_section = ('POSITIONAL ARGUMENTS',
+                                    '\n'.join(pos_arg_items).rstrip('\n'))
+    args_and_flags_sections.append(positional_arguments_section)
+    notes_sections.append(
+        ('NOTES', 'You could also use flags syntax for POSITIONAL ARGUMENTS')
+    )
+
+  flag_items = [
+      _CreateFlagItem(flag, docstring_info)
+      for flag in flags
+  ]
+
+  if flag_items:
+    flags_section = ('FLAGS', '\n'.join(flag_items))
+    args_and_flags_sections.append(flags_section)
+
+  output_sections = [
+      ('NAME', name_section),
+      ('SYNOPSIS', synopsis_section),
+      ('DESCRIPTION', description_section),
+  ] + args_and_flags_sections + notes_sections
+
+  return '\n\n'.join(
+      _CreateOutputSection(name, content)
+      for name, content in output_sections
+  )
+
+
+def _CreateOutputSection(name, content):
+  return """{name}
+{content}""".format(name=Bold(name), content=Indent(content, 4))
+
+
+def _CreatePositionalArgItem(arg, docstring_info):
+  """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.
+  Returns:
+    A string to be used in constructing the help screen for the function.
+  """
+  description = None
+  if docstring_info.args:
+    for arg_in_docstring in docstring_info.args:
       if arg_in_docstring.name == arg:
-        arg_description = arg_in_docstring.description
+        description = arg_in_docstring.description
 
-    item = item_template.format(
-        arg_name=arg.upper(), arg_description=arg_description)
-    pos_arg_items.append(item)
-  if pos_arg_items:
-    args_and_flags_section += pos_arg_template.format(
-        items='\n'.join(pos_arg_items).rstrip('\n'))
+  if description:
+    return """{arg}
+    {description}""".format(
+        arg=arg.upper(),
+        description=description)
+  else:
+    return arg.upper()
 
-  flags_template = """
-FLAGS
-{items}
-"""
-  flag_items = []
-  for flag in flags:
-    item_template = '    --{flag_name}\n        {flag_description}\n'
-    flag_description = None
-    for arg_in_docstring in info['docstring_info'].args:
+
+def _CreateFlagItem(flag, docstring_info):
+  """Returns a string describing a flag using information from the docstring.
+
+  Args:
+    flag: The name of the flag.
+    docstring_info: A docstrings.DocstringInfo namedtuple with information about
+      the containing function's docstring.
+  Returns:
+    A string to be used in constructing the help screen for the function.
+  """
+  description = None
+  if docstring_info.args:
+    for arg_in_docstring in docstring_info.args:
       if arg_in_docstring.name == flag:
-        flag_description = arg_in_docstring.description
+        description = arg_in_docstring.description
+        break
 
-    item = item_template.format(
-        flag_name=flag, flag_description=flag_description)
-    flag_items.append(item)
-  if flag_items:
-    args_and_flags_section += flags_template.format(
-        items='\n'.join(flag_items).rstrip('\n'))
+  if description:
+    return """--{flag}
+{description}""".format(flag=flag,
+                        description=Indent(description, 2))
+  else:
+    return '--{flag}'.format(flag=flag)
 
-  return output_template.format(
-      name_section=name_section,
-      synopsis_section=synopsis_section,
-      description_section=description_section,
-      args_and_flags_section=args_and_flags_section)
+
+def _CreateItem(name, description):
+  return """{name}
+{description}""".format(name=name,
+                        description=Indent(description, 2))
+
+
+def Indent(text, spaces=2):
+  lines = text.split('\n')
+  return '\n'.join(' ' * spaces + line for line in lines)
+
+
+def Bold(text):
+  return termcolor.colored(text, attrs=['bold'])
 
 
 def HelpTextForObject(component, info, trace=None, verbose=False):
@@ -237,18 +286,6 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
   Returns:
     Formatted help text for display.
   """
-
-  output_template = """NAME
-    {current_command} - {command_summary}
-
-SYNOPSIS
-    {synopsis}
-
-DESCRIPTION
-    {command_description}
-{detail_section}
-"""
-
   current_command = GetCurrentCommand(trace)
 
   docstring_info = info['docstring_info']
@@ -270,44 +307,28 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
     if value_types.IsValue(member):
       values.append((member_name, member))
 
+  usage_details_sections = []
   possible_actions = []
   # TODO(joejoevictor): Add global flags to here. Also, if it's a callable,
   # there will be additional flags.
   possible_flags = ''
-  detail_section_string = ''
-  item_template = """
-        {name}
-            {command_summary}
-"""
 
   if groups:
     # TODO(joejoevictor): Add missing GROUPS section handling
     possible_actions.append('GROUP')
   if commands:
     possible_actions.append('COMMAND')
-    commands_str_template = """
-COMMANDS
-    COMMAND is one of the followings:
-{items}
-"""
     command_item_strings = []
     for command_name, command in commands:
       command_docstring_info = docstrings.parse(
           inspectutils.Info(command)['docstring'])
       command_item_strings.append(
-          item_template.format(
-              name=command_name,
-              command_summary=command_docstring_info.summary))
-    detail_section_string += commands_str_template.format(
-        items=('\n'.join(command_item_strings)).rstrip('\n'))
+          _CreateItem(command_name, command_docstring_info.summary))
+    usage_details_sections.append(
+        ('COMMANDS', _NewChoicesSection('COMMAND', command_item_strings)))
 
   if values:
     possible_actions.append('VALUES')
-    values_str_template = """
-VALUES
-    VALUE is one of the followings:
-{items}
-"""
     value_item_strings = []
     for value_name, value in values:
       del value
@@ -315,11 +336,10 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
           inspectutils.Info(component.__class__.__init__)['docstring'])
       for arg_info in init_docstring_info.args:
         if arg_info.name == value_name:
-          value_item_strings.append(
-              item_template.format(
-                  name=value_name, command_summary=arg_info.description))
-    detail_section_string += values_str_template.format(
-        items=('\n'.join(value_item_strings)).rstrip('\n'))
+          value_item_strings.append(_CreateItem(value_name,
+                                                arg_info.description))
+    usage_details_sections.append(
+        ('VALUES', _NewChoicesSection('VALUE', value_item_strings)))
 
   possible_actions_string = ' ' + (' | '.join(possible_actions))
 
@@ -329,15 +349,33 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
       possible_actions=possible_actions_string,
       possible_flags=possible_flags)
 
-  return output_template.format(
+  description_sections = []
+  if command_description:
+    description_sections.append(('DESCRIPTION', command_description))
+
+  name_line = '{current_command} - {command_summary}'.format(
       current_command=current_command,
-      command_summary=command_summary,
-      synopsis=synopsis_string,
-      command_description=command_description,
-      detail_section=detail_section_string)
+      command_summary=command_summary)
+  output_sections = [
+      ('NAME', name_line),
+      ('SYNOPSIS', synopsis_string),
+  ] + description_sections + usage_details_sections
+
+  return '\n\n'.join(
+      _CreateOutputSection(name, content)
+      for name, content in output_sections
+  )
+
+
+def _NewChoicesSection(name, choices):
+  return """{name} is one of the followings:
+{items}""".format(
+    name=Bold(name),
+    items=Indent('\n'.join(choices), 2))
 
 
-def UsageText(component, trace=None, verbose=False):
+def UsageText(component, info, trace=None, verbose=False):
+  del info  # Unused.
   if inspect.isroutine(component) or inspect.isclass(component):
     return UsageTextForFunction(component, trace)
   else:
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index e87ebeff..b585a4fc 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -18,6 +18,7 @@
 from __future__ import division
 from __future__ import print_function
 
+import os
 import textwrap
 
 from fire import docstrings
@@ -30,6 +31,9 @@
 
 class HelpScreenTest(testutils.BaseTestCase):
 
+  def setUp(self):
+    os.environ['ANSI_COLORS_DISABLED'] = '1'
+
   def testHelpScreen(self):
     component = tc.ClassWithDocstring()
     t = trace.FireTrace(component, name='ClassWithDocstring')
@@ -48,18 +52,16 @@ def testHelpScreen(self):
 
 COMMANDS
     COMMAND is one of the followings:
-
-        print_msg
-            Prints a message.
+      print_msg
+        Prints a message.
 
 VALUES
     VALUE is one of the followings:
-
-        message
-            The default message to print.
-
+      message
+        The default message to print.
 """
-    self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
+    self.assertEqual(textwrap.dedent(expected_output).strip(),
+                     help_output.strip())
 
   def testHelpScreenForFunctionDocstringWithLineBreak(self):
     component = tc.ClassWithMultilineDocstring.example_generator
@@ -84,7 +86,8 @@ def testHelpScreenForFunctionDocstringWithLineBreak(self):
     NOTES
         You could also use flags syntax for POSITIONAL ARGUMENTS
     """
-    self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
+    self.assertEqual(textwrap.dedent(expected_output).strip(),
+                     help_output.strip())
 
   def testHelpScreenForFunctionFunctionWithDefaultArgs(self):
     component = tc.WithDefaults().double
@@ -104,12 +107,10 @@ def testHelpScreenForFunctionFunctionWithDefaultArgs(self):
 
     FLAGS
         --count
-            Input number that you want to double.
-
-    NOTES
-        You could also use flags syntax for POSITIONAL ARGUMENTS
+          Input number that you want to double.
     """
-    self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
+    self.assertEqual(textwrap.dedent(expected_output).strip(),
+                     help_output.strip())
 
 
 class UsageTest(testutils.BaseTestCase):
@@ -117,7 +118,8 @@ class UsageTest(testutils.BaseTestCase):
   def testUsageOutput(self):
     component = tc.NoDefaults()
     t = trace.FireTrace(component, name='NoDefaults')
-    usage_output = helptext.UsageText(component, trace=t, verbose=False)
+    info = inspectutils.Info(component)
+    usage_output = helptext.UsageText(component, info, trace=t, verbose=False)
     expected_output = '''
     Usage: NoDefaults <commands>
     available commands: double | triple
@@ -133,7 +135,8 @@ def testUsageOutput(self):
   def testUsageOutputVerbose(self):
     component = tc.NoDefaults()
     t = trace.FireTrace(component, name='NoDefaults')
-    usage_output = helptext.UsageText(component, trace=t, verbose=True)
+    info = inspectutils.Info(component)
+    usage_output = helptext.UsageText(component, info, trace=t, verbose=True)
     expected_output = '''
     Usage: NoDefaults <commands>
     available commands: double | triple
@@ -149,7 +152,8 @@ def testUsageOutputMethod(self):
     component = tc.NoDefaults().double
     t = trace.FireTrace(component, name='NoDefaults')
     t.AddAccessedProperty(component, 'double', ['double'], None, None)
-    usage_output = helptext.UsageText(component, trace=t, verbose=True)
+    info = inspectutils.Info(component)
+    usage_output = helptext.UsageText(component, info, trace=t, verbose=True)
     expected_output = '''
     Usage: NoDefaults double COUNT
 
@@ -163,7 +167,8 @@ def testUsageOutputMethod(self):
   def testUsageOutputFunctionWithHelp(self):
     component = tc.function_with_help
     t = trace.FireTrace(component, name='function_with_help')
-    usage_output = helptext.UsageText(component, trace=t, verbose=True)
+    info = inspectutils.Info(component)
+    usage_output = helptext.UsageText(component, info, trace=t, verbose=True)
     expected_output = '''
     Usage: function_with_help <flags>
 
@@ -179,7 +184,8 @@ def testUsageOutputFunctionWithHelp(self):
   def testUsageOutputFunctionWithDocstring(self):
     component = tc.multiplier_with_docstring
     t = trace.FireTrace(component, name='multiplier_with_docstring')
-    usage_output = helptext.UsageText(component, trace=t, verbose=True)
+    info = inspectutils.Info(component)
+    usage_output = helptext.UsageText(component, info, trace=t, verbose=True)
     expected_output = '''
     Usage: multiplier_with_docstring NUM <flags>
 
@@ -197,7 +203,8 @@ def testUsageOutputCallable(self):
     # This is both a group and a command!
     component = tc.CallableWithKeywordArgument
     t = trace.FireTrace(component, name='CallableWithKeywordArgument')
-    usage_output = helptext.UsageText(component, trace=t, verbose=True)
+    info = inspectutils.Info(component)
+    usage_output = helptext.UsageText(component, info, trace=t, verbose=True)
     # TODO(zuhaohen): We need to handle the case for keyword args as well
     # i.e. __call__ method of CallableWithKeywordArgument
     expected_output = '''
@@ -215,7 +222,8 @@ def testUsageOutputCallable(self):
   def testUsageOutputConstructorWithParameter(self):
     component = tc.InstanceVars
     t = trace.FireTrace(component, name='InstanceVars')
-    usage_output = helptext.UsageText(component, trace=t, verbose=True)
+    info = inspectutils.Info(component)
+    usage_output = helptext.UsageText(component, info, trace=t, verbose=True)
     expected_output = '''
     Usage: InstanceVars ARG1 ARG2
 

From d15fa284f78cbcbda679e73b1f399d5ce833116f Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 27 Mar 2019 13:18:37 -0700
Subject: [PATCH 077/324] Add termcolor to travis.yml. Not needed as a dep in
 setup.py yet.

PiperOrigin-RevId: 240627218
Change-Id: I63b37ab89389380bf6a324a70de68a4f03065643
---
 .travis.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.travis.yml b/.travis.yml
index 16ab4e4b..44edde6e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -15,6 +15,7 @@ before_install:
   - pip install --upgrade setuptools pip
   - pip install --upgrade pylint pytest pytest-pylint pytest-runner
 install:
+  - pip install termcolor
   - pip install hypothesis python-Levenshtein
   - python setup.py develop
 script:

From 1b185bb181eb55a6f9b52ac20c84c4c30403da45 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Tue, 2 Apr 2019 17:25:47 -0700
Subject: [PATCH 078/324] Use singular "VALUE" in synopsis

PiperOrigin-RevId: 241637123
Change-Id: I46e92d97e690ad331b87b3b20a2c896f42ccd262
---
 fire/helptext.py      | 2 +-
 fire/helptext_test.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index f9f16124..f69e1ba9 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -328,7 +328,7 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
         ('COMMANDS', _NewChoicesSection('COMMAND', command_item_strings)))
 
   if values:
-    possible_actions.append('VALUES')
+    possible_actions.append('VALUE')
     value_item_strings = []
     for value_name, value in values:
       del value
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index b585a4fc..d7def910 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -45,7 +45,7 @@ def testHelpScreen(self):
     ClassWithDocstring - Test class for testing help text output.
 
 SYNOPSIS
-    ClassWithDocstring COMMAND | VALUES
+    ClassWithDocstring COMMAND | VALUE
 
 DESCRIPTION
     This is some detail description of this test class.

From 2b152ce28724912975293cd6096d957dee1d9712 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Tue, 9 Apr 2019 12:33:04 -0700
Subject: [PATCH 079/324] Use _CreateItem for flags and positional args and
 adds underlines.

PiperOrigin-RevId: 242718248
Change-Id: I1ecba5510c7e497b9cff3f39d3854cf3df429302
---
 fire/helptext.py      | 42 +++++++++++++++++++++++-------------------
 fire/helptext_test.py | 14 ++++++++------
 2 files changed, 31 insertions(+), 25 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index f69e1ba9..db374314 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -225,13 +225,11 @@ def _CreatePositionalArgItem(arg, docstring_info):
       if arg_in_docstring.name == arg:
         description = arg_in_docstring.description
 
+  arg = arg.upper()
   if description:
-    return """{arg}
-    {description}""".format(
-        arg=arg.upper(),
-        description=description)
+    return _CreateItem(arg, description, indent=4)
   else:
-    return arg.upper()
+    return arg
 
 
 def _CreateFlagItem(flag, docstring_info):
@@ -251,29 +249,34 @@ def _CreateFlagItem(flag, docstring_info):
         description = arg_in_docstring.description
         break
 
+  flag = '--{flag}'.format(flag=flag)
   if description:
-    return """--{flag}
-{description}""".format(flag=flag,
-                        description=Indent(description, 2))
+    return _CreateItem(flag, description, indent=2)
   else:
-    return '--{flag}'.format(flag=flag)
+    return flag
 
 
-def _CreateItem(name, description):
+def _CreateItem(name, description, indent=2):
   return """{name}
 {description}""".format(name=name,
-                        description=Indent(description, 2))
+                        description=Indent(description, indent))
 
 
 def Indent(text, spaces=2):
   lines = text.split('\n')
-  return '\n'.join(' ' * spaces + line for line in lines)
+  return '\n'.join(
+      ' ' * spaces + line if line else line
+      for line in lines)
 
 
 def Bold(text):
   return termcolor.colored(text, attrs=['bold'])
 
 
+def Underline(text):
+  return termcolor.colored(text, attrs=['underline'])
+
+
 def HelpTextForObject(component, info, trace=None, verbose=False):
   """Generates help text for python objects.
 
@@ -341,9 +344,10 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
     usage_details_sections.append(
         ('VALUES', _NewChoicesSection('VALUE', value_item_strings)))
 
-  possible_actions_string = ' ' + (' | '.join(possible_actions))
+  possible_actions_string = ' | '.join(
+      Underline(action) for action in possible_actions)
 
-  synopsis_template = '{current_command}{possible_actions}{possible_flags}'
+  synopsis_template = '{current_command} {possible_actions}{possible_flags}'
   synopsis_string = synopsis_template.format(
       current_command=current_command,
       possible_actions=possible_actions_string,
@@ -368,10 +372,10 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
 
 
 def _NewChoicesSection(name, choices):
-  return """{name} is one of the followings:
-{items}""".format(
-    name=Bold(name),
-    items=Indent('\n'.join(choices), 2))
+  return _CreateItem(
+      '{name} is one of the followings:'.format(name=Bold(Underline(name))),
+      '\n' + '\n\n'.join(choices),
+      indent=1)
 
 
 def UsageText(component, info, trace=None, verbose=False):
@@ -454,7 +458,7 @@ def UsageTextForObject(component, trace=None, verbose=False):
   output_template = """Usage: {current_command} <{possible_actions}>
 {availability_lines}
 
-For detailed information on this command, run:
+For detailed information on this command and its flags, run:
 {current_command} --help
 """
   if trace:
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index d7def910..bdae8e4e 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -52,13 +52,15 @@ def testHelpScreen(self):
 
 COMMANDS
     COMMAND is one of the followings:
-      print_msg
-        Prints a message.
+
+     print_msg
+       Prints a message.
 
 VALUES
     VALUE is one of the followings:
-      message
-        The default message to print.
+
+     message
+       The default message to print.
 """
     self.assertEqual(textwrap.dedent(expected_output).strip(),
                      help_output.strip())
@@ -124,7 +126,7 @@ def testUsageOutput(self):
     Usage: NoDefaults <commands>
     available commands: double | triple
 
-    For detailed information on this command, run:
+    For detailed information on this command and its flags, run:
     NoDefaults --help
     '''
 
@@ -141,7 +143,7 @@ def testUsageOutputVerbose(self):
     Usage: NoDefaults <commands>
     available commands: double | triple
 
-    For detailed information on this command, run:
+    For detailed information on this command and its flags, run:
     NoDefaults --help
     '''
     self.assertEqual(

From 9eaca743b07f08ec5edfdd6f40615bb0a5c4c3a3 Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Tue, 9 Apr 2019 15:25:06 -0700
Subject: [PATCH 080/324] Migrate the test case 'testHelpStringClass' over to
 new help screen.

PiperOrigin-RevId: 242751497
Change-Id: I5a8a23c0f5e904d1ed22264329f9e7facb782239
---
 fire/helptext.py      | 39 +++++++++++++++++++++++++++++++--------
 fire/helptext_test.py | 24 +++++++++++++++++++++++-
 2 files changed, 54 insertions(+), 9 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index db374314..29e6b982 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -112,6 +112,30 @@ def HelpText(component, info, trace=None, verbose=False):
     return HelpTextForObject(component, info, trace, verbose)
 
 
+def GetDescriptionSectionText(summary, description):
+  """Returns description section text based on the input docstring info.
+
+  Returns the string that should be used as description section based on the
+  input. The logic is the following: If there's description available, use it.
+  Otherwise, use summary if available. If neither description or summary is
+  available, returns None.
+
+  Args:
+    summary: summary found in object summary
+    description: description found in object docstring
+
+  Returns:
+    String for the description section in help screen.
+  """
+  if not (description or summary):
+    return None
+
+  if description:
+    return description
+  else:
+    return summary
+
+
 def HelpTextForFunction(component, info, trace=None, verbose=False):
   """Returns detail help text for a function component.
 
@@ -163,7 +187,10 @@ def HelpTextForFunction(component, info, trace=None, verbose=False):
       current_command=current_command, args_and_flags=args_and_flags)
 
   # Description section
-  description_section = description if description else summary
+  command_description = GetDescriptionSectionText(summary, description)
+  description_sections = []
+  if command_description:
+    description_sections.append(('DESCRIPTION', command_description))
 
   # Positional arguments and flags section
   docstring_info = info['docstring_info']
@@ -195,8 +222,7 @@ def HelpTextForFunction(component, info, trace=None, verbose=False):
   output_sections = [
       ('NAME', name_section),
       ('SYNOPSIS', synopsis_section),
-      ('DESCRIPTION', description_section),
-  ] + args_and_flags_sections + notes_sections
+  ] + description_sections + args_and_flags_sections + notes_sections
 
   return '\n\n'.join(
       _CreateOutputSection(name, content)
@@ -293,11 +319,8 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
 
   docstring_info = info['docstring_info']
   command_summary = docstring_info.summary if docstring_info.summary else ''
-  if docstring_info.description:
-    command_description = docstring_info.description
-  else:
-    command_description = ''
-
+  command_description = GetDescriptionSectionText(docstring_info.summary,
+                                                  docstring_info.description)
   groups = []
   commands = []
   values = []
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index bdae8e4e..1adb2f41 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -29,7 +29,29 @@
 from fire import trace
 
 
-class HelpScreenTest(testutils.BaseTestCase):
+class HelpTest(testutils.BaseTestCase):
+
+  def checkTextInSection(self, text, actual_output):
+    self.assertIn(textwrap.dedent(text).lstrip('\n'), actual_output)
+
+  def checkTextNotInSection(self, text, actual_output):
+    self.assertNotIn(textwrap.dedent(text).lstrip('\n'), actual_output)
+
+  def testHelpTextNoDefaults(self):
+    component = tc.NoDefaults
+    # TODO(joejoevictor): We should have inspectutils.Info to generate
+    # info['docstring_info'] as well.
+    info = inspectutils.Info(component)
+    info['docstring_info'] = docstrings.parse(info['docstring'])
+    help_screen = helptext.HelpText(
+        component=component,
+        info=info,
+        trace=trace.FireTrace(component, name='NoDefaults'))
+
+    self.checkTextInSection('NAME\n    NoDefaults', help_screen)
+    self.checkTextInSection('SYNOPSIS\n    NoDefaults', help_screen)
+    self.checkTextNotInSection('DESCRIPTION', help_screen)
+    self.checkTextNotInSection('NOTES', help_screen)
 
   def setUp(self):
     os.environ['ANSI_COLORS_DISABLED'] = '1'

From f006733e982743925069aa9ca98f500702d94e3d Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Tue, 9 Apr 2019 16:16:01 -0700
Subject: [PATCH 081/324] Fixed the helper function names in test class

PiperOrigin-RevId: 242760890
Change-Id: Iac52c7a9d29a3a5bbec03b703b9d5f413fd56ce4
---
 fire/helptext_test.py | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 1adb2f41..b2c0381b 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -31,10 +31,10 @@
 
 class HelpTest(testutils.BaseTestCase):
 
-  def checkTextInSection(self, text, actual_output):
+  def assertTextInSection(self, text, actual_output):
     self.assertIn(textwrap.dedent(text).lstrip('\n'), actual_output)
 
-  def checkTextNotInSection(self, text, actual_output):
+  def assertTextNotInSection(self, text, actual_output):
     self.assertNotIn(textwrap.dedent(text).lstrip('\n'), actual_output)
 
   def testHelpTextNoDefaults(self):
@@ -48,10 +48,10 @@ def testHelpTextNoDefaults(self):
         info=info,
         trace=trace.FireTrace(component, name='NoDefaults'))
 
-    self.checkTextInSection('NAME\n    NoDefaults', help_screen)
-    self.checkTextInSection('SYNOPSIS\n    NoDefaults', help_screen)
-    self.checkTextNotInSection('DESCRIPTION', help_screen)
-    self.checkTextNotInSection('NOTES', help_screen)
+    self.assertTextInSection('NAME\n    NoDefaults', help_screen)
+    self.assertTextInSection('SYNOPSIS\n    NoDefaults', help_screen)
+    self.assertTextNotInSection('DESCRIPTION', help_screen)
+    self.assertTextNotInSection('NOTES', help_screen)
 
   def setUp(self):
     os.environ['ANSI_COLORS_DISABLED'] = '1'

From 20f8ba8fecffd894aeb7c90990d4c18a94392878 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 10 Apr 2019 09:21:22 -0700
Subject: [PATCH 082/324] Move helptext formatting functions to their own
 module and add tests

PiperOrigin-RevId: 242881960
Change-Id: Id1b214711e70f3d990ff01700c80de89518e6b28
---
 fire/formatting.py      | 37 +++++++++++++++++++++++++++++++++
 fire/formatting_test.py | 45 +++++++++++++++++++++++++++++++++++++++++
 fire/helptext.py        | 27 +++++++------------------
 3 files changed, 89 insertions(+), 20 deletions(-)
 create mode 100644 fire/formatting.py
 create mode 100644 fire/formatting_test.py

diff --git a/fire/formatting.py b/fire/formatting.py
new file mode 100644
index 00000000..7d4a54d8
--- /dev/null
+++ b/fire/formatting.py
@@ -0,0 +1,37 @@
+# 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.
+
+"""Formatting utilities for use in creating help text."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import termcolor
+
+
+def Indent(text, spaces=2):
+  lines = text.split('\n')
+  return '\n'.join(
+      ' ' * spaces + line if line else line
+      for line in lines)
+
+
+def Bold(text):
+  return termcolor.colored(text, attrs=['bold'])
+
+
+def Underline(text):
+  return termcolor.colored(text, attrs=['underline'])
+
diff --git a/fire/formatting_test.py b/fire/formatting_test.py
new file mode 100644
index 00000000..805d5455
--- /dev/null
+++ b/fire/formatting_test.py
@@ -0,0 +1,45 @@
+# 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.
+
+"""Tests for formatting.py."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+from fire import formatting
+from fire import testutils
+
+
+class FormattingTest(testutils.BaseTestCase):
+
+  def test_bold(self):
+    text = formatting.Bold('hello')
+    self.assertEqual('\x1b[1mhello\x1b[0m', text)
+
+  def test_underline(self):
+    text = formatting.Underline('hello')
+    self.assertEqual('\x1b[4mhello\x1b[0m', text)
+
+  def test_indent(self):
+    text = formatting.Indent('hello', spaces=2)
+    self.assertEqual('  hello', text)
+
+  def test_indent_multiple_lines(self):
+    text = formatting.Indent('hello\nworld', spaces=2)
+    self.assertEqual('  hello\n  world', text)
+
+
+if __name__ == '__main__':
+  testutils.main()
diff --git a/fire/helptext.py b/fire/helptext.py
index 29e6b982..6e715636 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -41,9 +41,9 @@
 
 from fire import completion
 from fire import docstrings
+from fire import formatting
 from fire import inspectutils
 from fire import value_types
-import termcolor
 
 
 def HelpString(component, trace=None, verbose=False):
@@ -232,7 +232,8 @@ def HelpTextForFunction(component, info, trace=None, verbose=False):
 
 def _CreateOutputSection(name, content):
   return """{name}
-{content}""".format(name=Bold(name), content=Indent(content, 4))
+{content}""".format(name=formatting.Bold(name),
+                    content=formatting.Indent(content, 4))
 
 
 def _CreatePositionalArgItem(arg, docstring_info):
@@ -285,22 +286,7 @@ def _CreateFlagItem(flag, docstring_info):
 def _CreateItem(name, description, indent=2):
   return """{name}
 {description}""".format(name=name,
-                        description=Indent(description, indent))
-
-
-def Indent(text, spaces=2):
-  lines = text.split('\n')
-  return '\n'.join(
-      ' ' * spaces + line if line else line
-      for line in lines)
-
-
-def Bold(text):
-  return termcolor.colored(text, attrs=['bold'])
-
-
-def Underline(text):
-  return termcolor.colored(text, attrs=['underline'])
+                        description=formatting.Indent(description, indent))
 
 
 def HelpTextForObject(component, info, trace=None, verbose=False):
@@ -368,7 +354,7 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
         ('VALUES', _NewChoicesSection('VALUE', value_item_strings)))
 
   possible_actions_string = ' | '.join(
-      Underline(action) for action in possible_actions)
+      formatting.Underline(action) for action in possible_actions)
 
   synopsis_template = '{current_command} {possible_actions}{possible_flags}'
   synopsis_string = synopsis_template.format(
@@ -396,7 +382,8 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
 
 def _NewChoicesSection(name, choices):
   return _CreateItem(
-      '{name} is one of the followings:'.format(name=Bold(Underline(name))),
+      '{name} is one of the followings:'.format(
+          name=formatting.Bold(formatting.Underline(name))),
       '\n' + '\n\n'.join(choices),
       indent=1)
 

From 82f18d538071271aa4f9b819a0dca8c02098188e Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Wed, 10 Apr 2019 13:24:27 -0700
Subject: [PATCH 083/324] Handles the case where IPython's inspector returns
 '<no docstring>' when there's docstring on the component.

PiperOrigin-RevId: 242928882
Change-Id: I91da5f7d731ae2d906eb97868af713cbf647df25
---
 fire/inspectutils.py      | 4 ++++
 fire/inspectutils_test.py | 4 ++++
 2 files changed, 8 insertions(+)

diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index 4e854c2d..2d33e35b 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -162,6 +162,10 @@ def Info(component):
     from IPython.core import oinspect  # pylint: disable=g-import-not-at-top
     inspector = oinspect.Inspector()
     info = inspector.info(component)
+
+    # IPython's oinspect.Inspector.info may return '<no docstring>'
+    if info['docstring'] == '<no docstring>':
+      info['docstring'] = None
   except ImportError:
     info = _InfoBackup(component)
 
diff --git a/fire/inspectutils_test.py b/fire/inspectutils_test.py
index ba428653..0ebd4059 100644
--- a/fire/inspectutils_test.py
+++ b/fire/inspectutils_test.py
@@ -112,6 +112,10 @@ def testInfoClassNoInit(self):
     self.assertIn(os.path.join('fire', 'test_components.py'), info.get('file'))
     self.assertGreater(info.get('line'), 0)
 
+  def testInfoNoDocstring(self):
+    info = inspectutils.Info(tc.NoDefaults)
+    self.assertEqual(info['docstring'], None, 'Docstring should be None')
+
 
 if __name__ == '__main__':
   testutils.main()

From 1bfc1d09279d5712cf01672bec0ae941ffccb160 Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Thu, 11 Apr 2019 05:21:00 -0700
Subject: [PATCH 084/324] Fixed the formatting issue breaking travis build

PiperOrigin-RevId: 243049555
Change-Id: I547c0d2a4158b388d1f67f3165d52f609cb6be15
---
 fire/formatting.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/fire/formatting.py b/fire/formatting.py
index 7d4a54d8..3f818377 100644
--- a/fire/formatting.py
+++ b/fire/formatting.py
@@ -34,4 +34,3 @@ def Bold(text):
 
 def Underline(text):
   return termcolor.colored(text, attrs=['underline'])
-

From 643438126ed8d8a50d4f7e7e024b4d3c984b9084 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Tue, 16 Apr 2019 09:36:43 -0700
Subject: [PATCH 085/324] More Python 3 support.

PiperOrigin-RevId: 243823028
Change-Id: I6a5c2614365814af72282bc52a40bc9111395869
---
 fire/core.py  | 5 ++---
 fire/trace.py | 6 +++---
 2 files changed, 5 insertions(+), 6 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index e6138a6c..369863b9 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -56,7 +56,6 @@ def main(argv):
 import inspect
 import json
 import os
-import pipes
 import re
 import shlex
 import sys
@@ -233,7 +232,7 @@ def _IsHelpShortcut(component_trace, remaining_args):
     component_trace.show_help = True
     command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
     print('INFO: Showing help with the command {cmd}.\n'.format(
-        cmd=pipes.quote(command)), file=sys.stderr)
+        cmd=six.moves.shlex_quote(command)), file=sys.stderr)
   return show_help
 
 
@@ -265,7 +264,7 @@ def _DisplayError(component_trace):
     if help_flag in component_trace.elements[-1].args:
       command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
       message = 'INFO: Showing help with the command {cmd}.\n'.format(
-          cmd=pipes.quote(command))
+          cmd=six.moves.shlex_quote(command))
       output.append(message)
   output.append('Fire trace:\n{trace}\n'.format(trace=component_trace))
   result = component_trace.GetResult()
diff --git a/fire/trace.py b/fire/trace.py
index fc50ba82..9b16cc6e 100644
--- a/fire/trace.py
+++ b/fire/trace.py
@@ -29,8 +29,8 @@
 from __future__ import division
 from __future__ import print_function
 
-import pipes
 from fire import inspectutils
+import six
 
 INITIAL_COMPONENT = 'Initial component'
 INSTANTIATED_CLASS = 'Instantiated class'
@@ -165,8 +165,8 @@ def display(arg1, arg2='!'):
   def _Quote(self, arg):
     if arg.startswith('--') and '=' in arg:
       prefix, value = arg.split('=', 1)
-      return pipes.quote(prefix) + '=' + pipes.quote(value)
-    return pipes.quote(arg)
+      return six.moves.shlex_quote(prefix) + '=' + six.moves.shlex_quote(value)
+    return six.moves.shlex_quote(arg)
 
   def GetCommand(self):
     """Returns the command representing the trace up to this point.

From 925ae49420cb80d37127090fa75124b55ba447f4 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Tue, 16 Apr 2019 16:43:36 -0700
Subject: [PATCH 086/324] Switch back to pipes.quote

PiperOrigin-RevId: 243904197
Change-Id: Ie15e9db18da83b5602e6ccac171990166ac1b9e9
---
 fire/core.py  | 5 +++--
 fire/trace.py | 7 ++++---
 2 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index 369863b9..e6138a6c 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -56,6 +56,7 @@ def main(argv):
 import inspect
 import json
 import os
+import pipes
 import re
 import shlex
 import sys
@@ -232,7 +233,7 @@ def _IsHelpShortcut(component_trace, remaining_args):
     component_trace.show_help = True
     command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
     print('INFO: Showing help with the command {cmd}.\n'.format(
-        cmd=six.moves.shlex_quote(command)), file=sys.stderr)
+        cmd=pipes.quote(command)), file=sys.stderr)
   return show_help
 
 
@@ -264,7 +265,7 @@ def _DisplayError(component_trace):
     if help_flag in component_trace.elements[-1].args:
       command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
       message = 'INFO: Showing help with the command {cmd}.\n'.format(
-          cmd=six.moves.shlex_quote(command))
+          cmd=pipes.quote(command))
       output.append(message)
   output.append('Fire trace:\n{trace}\n'.format(trace=component_trace))
   result = component_trace.GetResult()
diff --git a/fire/trace.py b/fire/trace.py
index 9b16cc6e..f898f88d 100644
--- a/fire/trace.py
+++ b/fire/trace.py
@@ -29,8 +29,9 @@
 from __future__ import division
 from __future__ import print_function
 
+import pipes
+
 from fire import inspectutils
-import six
 
 INITIAL_COMPONENT = 'Initial component'
 INSTANTIATED_CLASS = 'Instantiated class'
@@ -165,8 +166,8 @@ def display(arg1, arg2='!'):
   def _Quote(self, arg):
     if arg.startswith('--') and '=' in arg:
       prefix, value = arg.split('=', 1)
-      return six.moves.shlex_quote(prefix) + '=' + six.moves.shlex_quote(value)
-    return six.moves.shlex_quote(arg)
+      return pipes.quote(prefix) + '=' + pipes.quote(value)
+    return pipes.quote(arg)
 
   def GetCommand(self):
     """Returns the command representing the trace up to this point.

From 91b59abe18daecffa7fbc6484b578e6c429de400 Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Thu, 18 Apr 2019 10:05:14 -0700
Subject: [PATCH 087/324] Ported HelpUtilsTest.testHelpStringObject to
 HelpTest.testHelpTextNoDefaultsObject

PiperOrigin-RevId: 244206507
Change-Id: I7448ab5a2aff08bafbd8bd8976dadcef19584e28
---
 fire/helptext.py      | 29 +++++++++++++++++------------
 fire/helptext_test.py | 31 ++++++++++++++++++++-----------
 fire/inspectutils.py  |  4 ++++
 3 files changed, 41 insertions(+), 23 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index 6e715636..e2e1d334 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -40,7 +40,6 @@
 import inspect
 
 from fire import completion
-from fire import docstrings
 from fire import formatting
 from fire import inspectutils
 from fire import value_types
@@ -59,7 +58,6 @@ def HelpString(component, trace=None, verbose=False):
     String suitable for display giving information about the component.
   """
   info = inspectutils.Info(component)
-  info['docstring_info'] = docstrings.parse(info['docstring'])
 
   is_error_screen = False
   if trace:
@@ -332,10 +330,15 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
     possible_actions.append('COMMAND')
     command_item_strings = []
     for command_name, command in commands:
-      command_docstring_info = docstrings.parse(
-          inspectutils.Info(command)['docstring'])
-      command_item_strings.append(
-          _CreateItem(command_name, command_docstring_info.summary))
+      command_info = inspectutils.Info(command)
+      command_item = command_name
+      if 'docstring_info' in command_info:
+        command_docstring_info = command_info['docstring_info']
+        if command_docstring_info and command_docstring_info.summary:
+          command_item = _CreateItem(command_name,
+                                     command_docstring_info.summary)
+
+      command_item_strings.append(command_item)
     usage_details_sections.append(
         ('COMMANDS', _NewChoicesSection('COMMAND', command_item_strings)))
 
@@ -344,12 +347,14 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
     value_item_strings = []
     for value_name, value in values:
       del value
-      init_docstring_info = docstrings.parse(
-          inspectutils.Info(component.__class__.__init__)['docstring'])
-      for arg_info in init_docstring_info.args:
-        if arg_info.name == value_name:
-          value_item_strings.append(_CreateItem(value_name,
-                                                arg_info.description))
+      init_info = inspectutils.Info(component.__class__.__init__)
+      value_item = value_name
+      if 'docstring_info' in init_info:
+        init_docstring_info = init_info['docstring_info']
+        for arg_info in init_docstring_info.args:
+          if arg_info.name == value_name:
+            value_item = _CreateItem(value_name, arg_info.description)
+      value_item_strings.append(value_item)
     usage_details_sections.append(
         ('VALUES', _NewChoicesSection('VALUE', value_item_strings)))
 
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index b2c0381b..e0c69d39 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -31,12 +31,6 @@
 
 class HelpTest(testutils.BaseTestCase):
 
-  def assertTextInSection(self, text, actual_output):
-    self.assertIn(textwrap.dedent(text).lstrip('\n'), actual_output)
-
-  def assertTextNotInSection(self, text, actual_output):
-    self.assertNotIn(textwrap.dedent(text).lstrip('\n'), actual_output)
-
   def testHelpTextNoDefaults(self):
     component = tc.NoDefaults
     # TODO(joejoevictor): We should have inspectutils.Info to generate
@@ -47,11 +41,27 @@ def testHelpTextNoDefaults(self):
         component=component,
         info=info,
         trace=trace.FireTrace(component, name='NoDefaults'))
+    self.assertIn('NAME\n    NoDefaults', help_screen)
+    self.assertIn('SYNOPSIS\n    NoDefaults', help_screen)
+    self.assertNotIn('DESCRIPTION', help_screen)
+    self.assertNotIn('NOTES', help_screen)
 
-    self.assertTextInSection('NAME\n    NoDefaults', help_screen)
-    self.assertTextInSection('SYNOPSIS\n    NoDefaults', help_screen)
-    self.assertTextNotInSection('DESCRIPTION', help_screen)
-    self.assertTextNotInSection('NOTES', help_screen)
+  def testHelpTextNoDefaultsObject(self):
+    component = tc.NoDefaults()
+    info = inspectutils.Info(component)
+    info['docstring_info'] = docstrings.parse(info['docstring'])
+    help_screen = helptext.HelpText(
+        component=component,
+        info=info,
+        trace=trace.FireTrace(component, name='NoDefaults'))
+    self.assertIn('NAME\n    NoDefaults', help_screen)
+    self.assertIn('SYNOPSIS\n    NoDefaults COMMAND', help_screen)
+    self.assertNotIn('DESCRIPTION', help_screen)
+    self.assertIn('COMMANDS\n    COMMAND is one of the followings:',
+                  help_screen)
+    self.assertIn('double', help_screen)
+    self.assertIn('triple', help_screen)
+    self.assertNotIn('NOTES', help_screen)
 
   def setUp(self):
     os.environ['ANSI_COLORS_DISABLED'] = '1'
@@ -60,7 +70,6 @@ def testHelpScreen(self):
     component = tc.ClassWithDocstring()
     t = trace.FireTrace(component, name='ClassWithDocstring')
     info = inspectutils.Info(component)
-    info['docstring_info'] = docstrings.parse(info['docstring'])
     help_output = helptext.HelpText(component, info, t)
     expected_output = """
 NAME
diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index 2d33e35b..645b7b18 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -19,6 +19,7 @@
 from __future__ import print_function
 
 import inspect
+from fire import docstrings
 
 import six
 
@@ -175,6 +176,9 @@ def Info(component):
   except (TypeError, IOError):
     info['line'] = None
 
+  if 'docstring' in info:
+    info['docstring_info'] = docstrings.parse(info['docstring'])
+
   return info
 
 

From 1a14611be3a80b1accc3c32ddbaac5747e945ffc Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Thu, 18 Apr 2019 10:34:17 -0700
Subject: [PATCH 088/324] Ported HelpUtilsTest.testHelpStringFunction over to
 HelpTest.testHelpTextFunction

PiperOrigin-RevId: 244212926
Change-Id: I6a9697c7d7187c5d28cf521e53ef3e5a0872be48
---
 fire/helptext_test.py | 20 ++++++++++++++++++--
 1 file changed, 18 insertions(+), 2 deletions(-)

diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index e0c69d39..9fdee7e0 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -31,6 +31,9 @@
 
 class HelpTest(testutils.BaseTestCase):
 
+  def setUp(self):
+    os.environ['ANSI_COLORS_DISABLED'] = '1'
+
   def testHelpTextNoDefaults(self):
     component = tc.NoDefaults
     # TODO(joejoevictor): We should have inspectutils.Info to generate
@@ -63,8 +66,21 @@ def testHelpTextNoDefaultsObject(self):
     self.assertIn('triple', help_screen)
     self.assertNotIn('NOTES', help_screen)
 
-  def setUp(self):
-    os.environ['ANSI_COLORS_DISABLED'] = '1'
+  def testHelpTextFunction(self):
+    component = tc.NoDefaults().double
+    info = inspectutils.Info(component)
+    info['docstring_info'] = docstrings.parse(info['docstring'])
+    help_screen = helptext.HelpText(
+        component=component,
+        info=info,
+        trace=trace.FireTrace(component, name='double'))
+    self.assertIn('NAME\n    double', help_screen)
+    self.assertIn('SYNOPSIS\n    double COUNT', help_screen)
+    self.assertNotIn('DESCRIPTION', help_screen)
+    self.assertIn('POSITIONAL ARGUMENTS\n    COUNT', help_screen)
+    self.assertIn(
+        'NOTES\n    You could also use flags syntax for POSITIONAL ARGUMENTS',
+        help_screen)
 
   def testHelpScreen(self):
     component = tc.ClassWithDocstring()

From 4a0ab95536438a21b72ccc83dfeeb7707158590d Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Fri, 19 Apr 2019 10:12:04 -0700
Subject: [PATCH 089/324] Ported
 HelpUtilsTest.testHelpStringFunctionWithDefaults over to
 HelpTest.testHelpTextFunctionWithDefaults

PiperOrigin-RevId: 244377224
Change-Id: If532e8d9195501671467a10674391b16ffc6651d
---
 fire/helptext_test.py | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 9fdee7e0..507cd497 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -82,6 +82,20 @@ def testHelpTextFunction(self):
         'NOTES\n    You could also use flags syntax for POSITIONAL ARGUMENTS',
         help_screen)
 
+  def testHelpTextFunctionWithDefaults(self):
+    component = tc.WithDefaults().triple
+    info = inspectutils.Info(component)
+    info['docstring_info'] = docstrings.parse(info['docstring'])
+    help_screen = helptext.HelpText(
+        component=component,
+        info=info,
+        trace=trace.FireTrace(component, name='triple'))
+    self.assertIn('NAME\n    triple', help_screen)
+    self.assertIn('SYNOPSIS\n    triple [--count=COUNT]', help_screen)
+    self.assertNotIn('DESCRIPTION', help_screen)
+    self.assertIn('FLAGS\n    --count', help_screen)
+    self.assertNotIn('NOTES', help_screen)
+
   def testHelpScreen(self):
     component = tc.ClassWithDocstring()
     t = trace.FireTrace(component, name='ClassWithDocstring')

From 1e2053bdd86f9e5fe0b4311b6fd9ff6a8c85167b Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Fri, 19 Apr 2019 10:32:07 -0700
Subject: [PATCH 090/324] Removed docstrings.parse() calls in the tests.

PiperOrigin-RevId: 244380832
Change-Id: I2b71df317b70d95820faa54cbc03d4b28027ec38
---
 fire/helptext_test.py | 9 ---------
 1 file changed, 9 deletions(-)

diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 507cd497..89eade95 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -21,7 +21,6 @@
 import os
 import textwrap
 
-from fire import docstrings
 from fire import helptext
 from fire import inspectutils
 from fire import test_components as tc
@@ -36,10 +35,7 @@ def setUp(self):
 
   def testHelpTextNoDefaults(self):
     component = tc.NoDefaults
-    # TODO(joejoevictor): We should have inspectutils.Info to generate
-    # info['docstring_info'] as well.
     info = inspectutils.Info(component)
-    info['docstring_info'] = docstrings.parse(info['docstring'])
     help_screen = helptext.HelpText(
         component=component,
         info=info,
@@ -52,7 +48,6 @@ def testHelpTextNoDefaults(self):
   def testHelpTextNoDefaultsObject(self):
     component = tc.NoDefaults()
     info = inspectutils.Info(component)
-    info['docstring_info'] = docstrings.parse(info['docstring'])
     help_screen = helptext.HelpText(
         component=component,
         info=info,
@@ -69,7 +64,6 @@ def testHelpTextNoDefaultsObject(self):
   def testHelpTextFunction(self):
     component = tc.NoDefaults().double
     info = inspectutils.Info(component)
-    info['docstring_info'] = docstrings.parse(info['docstring'])
     help_screen = helptext.HelpText(
         component=component,
         info=info,
@@ -85,7 +79,6 @@ def testHelpTextFunction(self):
   def testHelpTextFunctionWithDefaults(self):
     component = tc.WithDefaults().triple
     info = inspectutils.Info(component)
-    info['docstring_info'] = docstrings.parse(info['docstring'])
     help_screen = helptext.HelpText(
         component=component,
         info=info,
@@ -130,7 +123,6 @@ def testHelpScreenForFunctionDocstringWithLineBreak(self):
     component = tc.ClassWithMultilineDocstring.example_generator
     t = trace.FireTrace(component, name='example_generator')
     info = inspectutils.Info(component)
-    info['docstring_info'] = docstrings.parse(info['docstring'])
     help_output = helptext.HelpText(component, info, t)
     expected_output = """
     NAME
@@ -156,7 +148,6 @@ def testHelpScreenForFunctionFunctionWithDefaultArgs(self):
     component = tc.WithDefaults().double
     t = trace.FireTrace(component, name='double')
     info = inspectutils.Info(component)
-    info['docstring_info'] = docstrings.parse(info['docstring'])
     help_output = helptext.HelpText(component, info, t)
     expected_output = """
     NAME

From 5972332f2e4b63da2a5461d260e9426f052e2630 Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Fri, 19 Apr 2019 20:05:24 -0700
Subject: [PATCH 091/324] Migrate more tests from helputils_test.py to
 helptext_test.py

PiperOrigin-RevId: 244461697
Change-Id: I96d77f4cce436b017279ef203f6c4530d1e2f93b
---
 fire/helptext_test.py | 25 +++++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 89eade95..190dad1b 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -89,6 +89,31 @@ def testHelpTextFunctionWithDefaults(self):
     self.assertIn('FLAGS\n    --count', help_screen)
     self.assertNotIn('NOTES', help_screen)
 
+  def testHelpTextFunctionWithBuiltin(self):
+    component = 'test'.upper
+    info = inspectutils.Info(component)
+    help_screen = helptext.HelpText(
+        component=component,
+        info=info,
+        trace=trace.FireTrace(component, 'upper'))
+    self.assertIn('NAME\n    upper', help_screen)
+    self.assertIn('SYNOPSIS\n    upper', help_screen)
+    # We don't check description content here since the content is python
+    # version dependent.
+    self.assertIn('DESCRIPTION\n', help_screen)
+    self.assertNotIn('NOTES', help_screen)
+
+  def testHelpTextFunctionIntType(self):
+    component = int
+    info = inspectutils.Info(component)
+    help_screen = helptext.HelpText(
+        component=component, info=info, trace=trace.FireTrace(component, 'int'))
+    self.assertIn('NAME\n    int', help_screen)
+    self.assertIn('SYNOPSIS\n    int', help_screen)
+    # We don't check description content here since the content is python
+    # version dependent.
+    self.assertIn('DESCRIPTION\n', help_screen)
+
   def testHelpScreen(self):
     component = tc.ClassWithDocstring()
     t = trace.FireTrace(component, name='ClassWithDocstring')

From 3c1248181dcd02d0c4871e84bb2997f060f4fdc1 Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Fri, 26 Apr 2019 14:34:10 -0700
Subject: [PATCH 092/324] Migrated the remaining testcases in helputils_test.py
 to

PiperOrigin-RevId: 245489137
Change-Id: I027546109bc87e5b08bd9424159691b4200a437d
---
 fire/helptext.py      |  7 ++---
 fire/helptext_test.py | 59 +++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 63 insertions(+), 3 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index e2e1d334..12ed0390 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -351,9 +351,10 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
       value_item = value_name
       if 'docstring_info' in init_info:
         init_docstring_info = init_info['docstring_info']
-        for arg_info in init_docstring_info.args:
-          if arg_info.name == value_name:
-            value_item = _CreateItem(value_name, arg_info.description)
+        if init_docstring_info.args:
+          for arg_info in init_docstring_info.args:
+            if arg_info.name == value_name:
+              value_item = _CreateItem(value_name, arg_info.description)
       value_item_strings.append(value_item)
     usage_details_sections.append(
         ('VALUES', _NewChoicesSection('VALUE', value_item_strings)))
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 190dad1b..dafd6569 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -114,6 +114,65 @@ def testHelpTextFunctionIntType(self):
     # version dependent.
     self.assertIn('DESCRIPTION\n', help_screen)
 
+  def testHelpTextEmptyList(self):
+    component = []
+    info = inspectutils.Info(component)
+    help_screen = helptext.HelpText(
+        component=component,
+        info=info,
+        trace=trace.FireTrace(component, 'list'))
+    self.assertIn('NAME\n    list', help_screen)
+    self.assertIn('SYNOPSIS\n    list COMMAND', help_screen)
+    # We don't check description content here since the content could be python
+    # version dependent.
+    self.assertIn('DESCRIPTION\n', help_screen)
+    # We don't check the listed commands either since the list API could
+    # potentially change between Python versions.
+    self.assertIn('COMMANDS\n    COMMAND is one of the followings:\n',
+                  help_screen)
+
+  def testHelpTextShortList(self):
+    component = [10]
+    info = inspectutils.Info(component)
+    help_screen = helptext.HelpText(
+        component=component,
+        info=info,
+        trace=trace.FireTrace(component, 'list'))
+    self.assertIn('NAME\n    list', help_screen)
+    self.assertIn('SYNOPSIS\n    list COMMAND', help_screen)
+    # We don't check description content here since the content could be python
+    # version dependent.
+    self.assertIn('DESCRIPTION\n', help_screen)
+
+    # We don't check the listed commands comprehensively since the list API
+    # could potentially change between Python versions. Check a few
+    # functions(command) that we're confident likely remain available.
+    self.assertIn('COMMANDS\n    COMMAND is one of the followings:\n',
+                  help_screen)
+    self.assertIn('     append\n', help_screen)
+
+  def testHelpTextInt(self):
+    component = 7
+    info = inspectutils.Info(component)
+    help_screen = helptext.HelpText(
+        component=component, info=info, trace=trace.FireTrace(component, '7'))
+    self.assertIn('NAME\n    7', help_screen)
+    self.assertIn('SYNOPSIS\n    7 COMMAND | VALUE', help_screen)
+    self.assertIn('DESCRIPTION\n', help_screen)
+    self.assertIn('COMMANDS\n    COMMAND is one of the followings:\n',
+                  help_screen)
+    self.assertIn('VALUES\n    VALUE is one of the followings:\n', help_screen)
+
+  def testHelpTextNoInit(self):
+    component = tc.OldStyleEmpty
+    info = inspectutils.Info(component)
+    help_screen = helptext.HelpText(
+        component=component,
+        info=info,
+        trace=trace.FireTrace(component, 'OldStyleEmpty'))
+    self.assertIn('NAME\n    OldStyleEmpty', help_screen)
+    self.assertIn('SYNOPSIS\n    OldStyleEmpty', help_screen)
+
   def testHelpScreen(self):
     component = tc.ClassWithDocstring()
     t = trace.FireTrace(component, name='ClassWithDocstring')

From 25305e41287eff9d1a14efd5f445fe2ba04236ce Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 17 May 2019 21:50:36 -0700
Subject: [PATCH 093/324] Use singular command|value|group rather than plural
 commands|values|groups in usage text

PiperOrigin-RevId: 248834729
Change-Id: Id0f34792c3213a7a5c5e028b0e1261a8412ffa0d
---
 fire/helptext.py      | 6 +++---
 fire/helptext_test.py | 6 +++---
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index 12ed0390..6648fef0 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -502,21 +502,21 @@ def UsageTextForObject(component, trace=None, verbose=False):
   availability_lines = []
   availability_lint_format = '{header:20s}{choices}'
   if groups:
-    possible_actions.append('groups')
+    possible_actions.append('group')
     groups_string = ' | '.join(groups)
     groups_text = availability_lint_format.format(
         header='available groups:',
         choices=groups_string)
     availability_lines.append(groups_text)
   if commands:
-    possible_actions.append('commands')
+    possible_actions.append('command')
     commands_string = ' | '.join(commands)
     commands_text = availability_lint_format.format(
         header='available commands:',
         choices=commands_string)
     availability_lines.append(commands_text)
   if values:
-    possible_actions.append('values')
+    possible_actions.append('value')
     values_string = ' | '.join(values)
     values_text = availability_lint_format.format(
         header='available values:',
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index dafd6569..c2943f5a 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -259,7 +259,7 @@ def testUsageOutput(self):
     info = inspectutils.Info(component)
     usage_output = helptext.UsageText(component, info, trace=t, verbose=False)
     expected_output = '''
-    Usage: NoDefaults <commands>
+    Usage: NoDefaults <command>
     available commands: double | triple
 
     For detailed information on this command and its flags, run:
@@ -276,7 +276,7 @@ def testUsageOutputVerbose(self):
     info = inspectutils.Info(component)
     usage_output = helptext.UsageText(component, info, trace=t, verbose=True)
     expected_output = '''
-    Usage: NoDefaults <commands>
+    Usage: NoDefaults <command>
     available commands: double | triple
 
     For detailed information on this command and its flags, run:
@@ -346,7 +346,7 @@ def testUsageOutputCallable(self):
     # TODO(zuhaohen): We need to handle the case for keyword args as well
     # i.e. __call__ method of CallableWithKeywordArgument
     expected_output = '''
-    Usage: CallableWithKeywordArgument <commands>
+    Usage: CallableWithKeywordArgument <command>
 
     Available commands: print_msg
 

From e6233b241a8340c246d989438aff8fe0ec1632b4 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 22 May 2019 15:27:45 -0700
Subject: [PATCH 094/324] Spacing in usage text (both indentation and line
 wrapping)

PiperOrigin-RevId: 249532602
Change-Id: Ib7cff1d07b4ca69c508e25237d6b688a0c77fcd7
---
 fire/formatting.py      | 23 +++++++++++++++++++++++
 fire/formatting_test.py | 11 +++++++++++
 fire/helptext.py        | 25 +++++++++++++++----------
 fire/helptext_test.py   |  6 +++---
 4 files changed, 52 insertions(+), 13 deletions(-)

diff --git a/fire/formatting.py b/fire/formatting.py
index 3f818377..e204f0cd 100644
--- a/fire/formatting.py
+++ b/fire/formatting.py
@@ -34,3 +34,26 @@ def Bold(text):
 
 def Underline(text):
   return termcolor.colored(text, attrs=['underline'])
+
+
+def WrappedJoin(items, separator=' | ', width=80):
+  """Joins the items by the separator, wrapping lines at the given width."""
+  lines = []
+  current_line = ''
+  for index, item in enumerate(items):
+    is_final_item = index == len(items) - 1
+    if is_final_item:
+      if len(current_line) + len(item) <= width:
+        current_line += item
+      else:
+        lines.append(current_line.rstrip())
+        current_line = item
+    else:
+      if len(current_line) + len(item) + len(separator) <= width:
+        current_line += item + separator
+      else:
+        lines.append(current_line.rstrip())
+        current_line = item + separator
+
+  lines.append(current_line)
+  return lines
diff --git a/fire/formatting_test.py b/fire/formatting_test.py
index 805d5455..c19db054 100644
--- a/fire/formatting_test.py
+++ b/fire/formatting_test.py
@@ -40,6 +40,17 @@ def test_indent_multiple_lines(self):
     text = formatting.Indent('hello\nworld', spaces=2)
     self.assertEqual('  hello\n  world', text)
 
+  def test_wrap_one_item(self):
+    lines = formatting.WrappedJoin(['rice'])
+    self.assertEqual(['rice'], lines)
+
+  def test_wrap_multiple_items(self):
+    lines = formatting.WrappedJoin(['rice', 'beans', 'chicken', 'cheese'],
+                                   width=15)
+    self.assertEqual(['rice | beans |',
+                      'chicken |',
+                      'cheese'], lines)
+
 
 if __name__ == '__main__':
   testutils.main()
diff --git a/fire/helptext.py b/fire/helptext.py
index 6648fef0..c9dc3ff0 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -458,6 +458,15 @@ def UsageTextForFunction(component, trace=None):
       hyphen_hyphen=hyphen_hyphen)
 
 
+def _CreateAvailabilityLine(header, items,
+                            header_indent=2, items_indent=25, line_length=80):
+  items_width = line_length - items_indent
+  items_text = '\n'.join(formatting.WrappedJoin(items, width=items_width))
+  indented_items_text = formatting.Indent(items_text, spaces=items_indent)
+  indented_header = formatting.Indent(header, spaces=header_indent)
+  return indented_header + indented_items_text[len(indented_header):]
+
+
 def UsageTextForObject(component, trace=None, verbose=False):
   """Returns help text for usage screen for objects.
 
@@ -500,27 +509,23 @@ def UsageTextForObject(component, trace=None, verbose=False):
 
   possible_actions = []
   availability_lines = []
-  availability_lint_format = '{header:20s}{choices}'
   if groups:
     possible_actions.append('group')
-    groups_string = ' | '.join(groups)
-    groups_text = availability_lint_format.format(
+    groups_text = _CreateAvailabilityLine(
         header='available groups:',
-        choices=groups_string)
+        items=groups)
     availability_lines.append(groups_text)
   if commands:
     possible_actions.append('command')
-    commands_string = ' | '.join(commands)
-    commands_text = availability_lint_format.format(
+    commands_text = _CreateAvailabilityLine(
         header='available commands:',
-        choices=commands_string)
+        items=commands)
     availability_lines.append(commands_text)
   if values:
     possible_actions.append('value')
-    values_string = ' | '.join(values)
-    values_text = availability_lint_format.format(
+    values_text = _CreateAvailabilityLine(
         header='available values:',
-        choices=values_string)
+        items=values)
     availability_lines.append(values_text)
   possible_actions_string = '|'.join(possible_actions)
   availability_lines_string = '\n'.join(availability_lines)
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index c2943f5a..8bc1357a 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -260,7 +260,7 @@ def testUsageOutput(self):
     usage_output = helptext.UsageText(component, info, trace=t, verbose=False)
     expected_output = '''
     Usage: NoDefaults <command>
-    available commands: double | triple
+      available commands:    double | triple
 
     For detailed information on this command and its flags, run:
     NoDefaults --help
@@ -277,7 +277,7 @@ def testUsageOutputVerbose(self):
     usage_output = helptext.UsageText(component, info, trace=t, verbose=True)
     expected_output = '''
     Usage: NoDefaults <command>
-    available commands: double | triple
+      available commands:    double | triple
 
     For detailed information on this command and its flags, run:
     NoDefaults --help
@@ -348,7 +348,7 @@ def testUsageOutputCallable(self):
     expected_output = '''
     Usage: CallableWithKeywordArgument <command>
 
-    Available commands: print_msg
+      Available commands:    print_msg
 
     For detailed information on this command, run:
     CallableWithKeywordArgument -- --help

From b949850b279b5bb09e3589b2ae6a3612f3e60f7a Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 24 May 2019 08:15:15 -0700
Subject: [PATCH 095/324] Give usage details sections their own functions

PiperOrigin-RevId: 249835441
Change-Id: Id6d8d8912df7bc0ce0490f1a6b71e9726172c786
---
 fire/helptext.py | 66 +++++++++++++++++++++++++++---------------------
 1 file changed, 37 insertions(+), 29 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index c9dc3ff0..4353e102 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -324,40 +324,16 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
   possible_flags = ''
 
   if groups:
-    # TODO(joejoevictor): Add missing GROUPS section handling
     possible_actions.append('GROUP')
+    # TODO(joejoevictor): Add missing GROUPS section handling
   if commands:
     possible_actions.append('COMMAND')
-    command_item_strings = []
-    for command_name, command in commands:
-      command_info = inspectutils.Info(command)
-      command_item = command_name
-      if 'docstring_info' in command_info:
-        command_docstring_info = command_info['docstring_info']
-        if command_docstring_info and command_docstring_info.summary:
-          command_item = _CreateItem(command_name,
-                                     command_docstring_info.summary)
-
-      command_item_strings.append(command_item)
-    usage_details_sections.append(
-        ('COMMANDS', _NewChoicesSection('COMMAND', command_item_strings)))
-
+    usage_details_section = CommandUsageDetailsSection(commands)
+    usage_details_sections.append(usage_details_section)
   if values:
     possible_actions.append('VALUE')
-    value_item_strings = []
-    for value_name, value in values:
-      del value
-      init_info = inspectutils.Info(component.__class__.__init__)
-      value_item = value_name
-      if 'docstring_info' in init_info:
-        init_docstring_info = init_info['docstring_info']
-        if init_docstring_info.args:
-          for arg_info in init_docstring_info.args:
-            if arg_info.name == value_name:
-              value_item = _CreateItem(value_name, arg_info.description)
-      value_item_strings.append(value_item)
-    usage_details_sections.append(
-        ('VALUES', _NewChoicesSection('VALUE', value_item_strings)))
+    usage_details_section = ValuesUsageDetailsSection(component, values)
+    usage_details_sections.append(usage_details_section)
 
   possible_actions_string = ' | '.join(
       formatting.Underline(action) for action in possible_actions)
@@ -386,6 +362,38 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
   )
 
 
+def ValuesUsageDetailsSection(component, values):
+  """Creates a section tuple for the values section of the usage details."""
+  value_item_strings = []
+  for value_name, value in values:
+    del value
+    init_info = inspectutils.Info(component.__class__.__init__)
+    value_item = value_name
+    if 'docstring_info' in init_info:
+      init_docstring_info = init_info['docstring_info']
+      if init_docstring_info.args:
+        for arg_info in init_docstring_info.args:
+          if arg_info.name == value_name:
+            value_item = _CreateItem(value_name, arg_info.description)
+    value_item_strings.append(value_item)
+  return ('VALUES', _NewChoicesSection('VALUE', value_item_strings))
+
+
+def CommandUsageDetailsSection(commands):
+  """Creates a section tuple for the commands section of the usage details."""
+  command_item_strings = []
+  for command_name, command in commands:
+    command_info = inspectutils.Info(command)
+    command_item = command_name
+    if 'docstring_info' in command_info:
+      command_docstring_info = command_info['docstring_info']
+      if command_docstring_info and command_docstring_info.summary:
+        command_item = _CreateItem(command_name,
+                                   command_docstring_info.summary)
+    command_item_strings.append(command_item)
+  return ('COMMANDS', _NewChoicesSection('COMMAND', command_item_strings))
+
+
 def _NewChoicesSection(name, choices):
   return _CreateItem(
       '{name} is one of the followings:'.format(

From 966b1a844bc1d18996170481f2da2aea5f9caa58 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 24 May 2019 08:36:13 -0700
Subject: [PATCH 096/324] Adds fire.console package to setup.py

PiperOrigin-RevId: 249838212
Change-Id: I62bc22bced9bf356e6420d228fa10fde0d822bf6
---
 setup.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.py b/setup.py
index cf7d2b50..0ba2f294 100644
--- a/setup.py
+++ b/setup.py
@@ -78,7 +78,7 @@
 
     keywords='command line interface cli python fire interactive bash tool',
 
-    packages=['fire'],
+    packages=['fire', 'fire.console'],
 
     install_requires=DEPENDENCIES,
     tests_require=TEST_DEPENDENCIES,

From 7f54c6dd184c815280ea6645386cdc17a08a0ad8 Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Fri, 24 May 2019 08:36:44 -0700
Subject: [PATCH 097/324] Include bold and underline in help screens

PiperOrigin-RevId: 249838286
Change-Id: Idebf220861d2025edacf433fdaec36a335280afd
---
 fire/formatting.py    |  4 ++++
 fire/helptext.py      | 16 ++++++++--------
 fire/helptext_test.py | 14 ++++++++++++++
 3 files changed, 26 insertions(+), 8 deletions(-)

diff --git a/fire/formatting.py b/fire/formatting.py
index e204f0cd..6619b246 100644
--- a/fire/formatting.py
+++ b/fire/formatting.py
@@ -36,6 +36,10 @@ def Underline(text):
   return termcolor.colored(text, attrs=['underline'])
 
 
+def BoldUnderline(text):
+  return Bold(Underline(text))
+
+
 def WrappedJoin(items, separator=' | ', width=80):
   """Joins the items by the separator, wrapping lines at the given width."""
   lines = []
diff --git a/fire/helptext.py b/fire/helptext.py
index 4353e102..b256aafe 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -165,14 +165,15 @@ def HelpTextForFunction(component, info, trace=None, verbose=False):
 
   args_and_flags = ''
   if args_with_no_defaults:
-    items = [arg.upper() for arg in args_with_no_defaults]
+    items = [formatting.Underline(arg.upper()) for arg in args_with_no_defaults]
     args_and_flags = ' '.join(items)
 
   synopsis_flag_template = '[--{flag_name}={flag_name_upper}]'
   if flags:
     items = [
         synopsis_flag_template.format(
-            flag_name=flag, flag_name_upper=flag.upper()) for flag in flags
+            flag_name=formatting.Underline(flag), flag_name_upper=flag.upper())
+        for flag in flags
     ]
     args_and_flags = args_and_flags + ' '.join(items)
 
@@ -252,9 +253,9 @@ def _CreatePositionalArgItem(arg, docstring_info):
 
   arg = arg.upper()
   if description:
-    return _CreateItem(arg, description, indent=4)
+    return _CreateItem(formatting.BoldUnderline(arg), description, indent=4)
   else:
-    return arg
+    return formatting.BoldUnderline(arg)
 
 
 def _CreateFlagItem(flag, docstring_info):
@@ -274,11 +275,10 @@ def _CreateFlagItem(flag, docstring_info):
         description = arg_in_docstring.description
         break
 
-  flag = '--{flag}'.format(flag=flag)
+  flag = '--{flag}'.format(flag=formatting.Underline(flag))
   if description:
     return _CreateItem(flag, description, indent=2)
-  else:
-    return flag
+  return flag
 
 
 def _CreateItem(name, description, indent=2):
@@ -324,8 +324,8 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
   possible_flags = ''
 
   if groups:
-    possible_actions.append('GROUP')
     # TODO(joejoevictor): Add missing GROUPS section handling
+    possible_actions.append('GROUP')
   if commands:
     possible_actions.append('COMMAND')
     usage_details_section = CommandUsageDetailsSection(commands)
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 8bc1357a..818f08f4 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -21,6 +21,7 @@
 import os
 import textwrap
 
+from fire import formatting
 from fire import helptext
 from fire import inspectutils
 from fire import test_components as tc
@@ -250,6 +251,19 @@ def testHelpScreenForFunctionFunctionWithDefaultArgs(self):
     self.assertEqual(textwrap.dedent(expected_output).strip(),
                      help_output.strip())
 
+  def testHelpTextUnderlineFlag(self):
+    component = tc.WithDefaults().triple
+    info = inspectutils.Info(component)
+    t = trace.FireTrace(component, name='triple')
+    help_screen = helptext.HelpText(component, info, t)
+    self.assertIn(formatting.Bold('NAME') + '\n    triple', help_screen)
+    self.assertIn(
+        formatting.Bold('SYNOPSIS') + '\n    triple [--count=COUNT]',
+        help_screen)
+    self.assertIn(
+        formatting.Bold('FLAGS') + '\n    --' + formatting.Underline('count'),
+        help_screen)
+
 
 class UsageTest(testutils.BaseTestCase):
 

From 93431c7556f34212d300d8cbe4dd3ba67b413796 Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Fri, 24 May 2019 10:08:53 -0700
Subject: [PATCH 098/324] Bold command name in COMMANDS section in help screen.

PiperOrigin-RevId: 249852534
Change-Id: I6bdf7209a440a2f3f3296326a24d1fc0c293678a
---
 fire/helptext.py      |  6 +++---
 fire/helptext_test.py | 13 +++++++++++++
 2 files changed, 16 insertions(+), 3 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index b256aafe..7db9c767 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -384,12 +384,12 @@ def CommandUsageDetailsSection(commands):
   command_item_strings = []
   for command_name, command in commands:
     command_info = inspectutils.Info(command)
-    command_item = command_name
+    command_item = formatting.Bold(command_name)
     if 'docstring_info' in command_info:
       command_docstring_info = command_info['docstring_info']
       if command_docstring_info and command_docstring_info.summary:
-        command_item = _CreateItem(command_name,
-                                   command_docstring_info.summary)
+        command_item = _CreateItem(
+            formatting.Bold(command_name), command_docstring_info.summary)
     command_item_strings.append(command_item)
   return ('COMMANDS', _NewChoicesSection('COMMAND', command_item_strings))
 
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 818f08f4..e74a148d 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -264,6 +264,19 @@ def testHelpTextUnderlineFlag(self):
         formatting.Bold('FLAGS') + '\n    --' + formatting.Underline('count'),
         help_screen)
 
+  def testHelpTextBoldCommandName(self):
+    component = tc.ClassWithDocstring()
+    info = inspectutils.Info(component)
+    t = trace.FireTrace(component, name='ClassWithDocstring')
+    help_screen = helptext.HelpText(component, info, t)
+    self.assertIn(
+        formatting.Bold('NAME') + '\n    ClassWithDocstring', help_screen)
+    self.assertIn(formatting.Bold('COMMANDS') + '\n', help_screen)
+    self.assertIn(
+        formatting.BoldUnderline('COMMAND') + ' is one of the followings:\n',
+        help_screen)
+    self.assertIn(formatting.Bold('print_msg') + '\n', help_screen)
+
 
 class UsageTest(testutils.BaseTestCase):
 

From 59dd4e028cef7046201ef9a19445a4e6c3073c11 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 7 Jun 2019 10:29:44 -0700
Subject: [PATCH 099/324] Adds support for groups to helptext and removes
 showing trace by default in favor of just showing a single line error by
 default.

PiperOrigin-RevId: 252074360
Change-Id: I0e99eb6b07f47815dce6e9098998b139e9e61f86
---
 fire/core.py          |  4 ++--
 fire/helptext.py      | 50 ++++++++++++++++++++++++++++---------------
 fire/helptext_test.py | 16 +++++++-------
 fire/trace.py         |  9 +++++---
 4 files changed, 49 insertions(+), 30 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index e6138a6c..7b2fa319 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -240,7 +240,7 @@ def _IsHelpShortcut(component_trace, remaining_args):
 def _PrintResult(component_trace, verbose=False):
   """Prints the result of the Fire call to stdout in a human readable way."""
   # TODO(dbieber): Design human readable deserializable serialization method
-  # and move serialization to it's own module.
+  # and move serialization to its own module.
   result = component_trace.GetResult()
 
   if isinstance(result, (list, set, types.GeneratorType)):
@@ -267,7 +267,7 @@ def _DisplayError(component_trace):
       message = 'INFO: Showing help with the command {cmd}.\n'.format(
           cmd=pipes.quote(command))
       output.append(message)
-  output.append('Fire trace:\n{trace}\n'.format(trace=component_trace))
+  output.append('ERROR: ' + component_trace.elements[-1].ErrorAsStr())
   result = component_trace.GetResult()
   help_string = helputils.HelpString(result, component_trace,
                                      component_trace.verbose)
diff --git a/fire/helptext.py b/fire/helptext.py
index 7db9c767..70e12577 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -324,8 +324,9 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
   possible_flags = ''
 
   if groups:
-    # TODO(joejoevictor): Add missing GROUPS section handling
     possible_actions.append('GROUP')
+    usage_details_section = GroupUsageDetailsSection(groups)
+    usage_details_sections.append(usage_details_section)
   if commands:
     possible_actions.append('COMMAND')
     usage_details_section = CommandUsageDetailsSection(commands)
@@ -362,6 +363,36 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
   )
 
 
+def GroupUsageDetailsSection(groups):
+  """Creates a section tuple for the groups section of the usage details."""
+  group_item_strings = []
+  for group_name, group in groups:
+    group_info = inspectutils.Info(group)
+    group_item = group_name
+    if 'docstring_info' in group_info:
+      group_docstring_info = group_info['docstring_info']
+      if group_docstring_info and group_docstring_info.summary:
+        group_item = _CreateItem(group_name,
+                                 group_docstring_info.summary)
+    group_item_strings.append(group_item)
+  return ('GROUPS', _NewChoicesSection('GROUP', group_item_strings))
+
+
+def CommandUsageDetailsSection(commands):
+  """Creates a section tuple for the commands section of the usage details."""
+  command_item_strings = []
+  for command_name, command in commands:
+    command_info = inspectutils.Info(command)
+    command_item = command_name
+    if 'docstring_info' in command_info:
+      command_docstring_info = command_info['docstring_info']
+      if command_docstring_info and command_docstring_info.summary:
+        command_item = _CreateItem(command_name,
+                                   command_docstring_info.summary)
+    command_item_strings.append(command_item)
+  return ('COMMANDS', _NewChoicesSection('COMMAND', command_item_strings))
+
+
 def ValuesUsageDetailsSection(component, values):
   """Creates a section tuple for the values section of the usage details."""
   value_item_strings = []
@@ -379,24 +410,9 @@ def ValuesUsageDetailsSection(component, values):
   return ('VALUES', _NewChoicesSection('VALUE', value_item_strings))
 
 
-def CommandUsageDetailsSection(commands):
-  """Creates a section tuple for the commands section of the usage details."""
-  command_item_strings = []
-  for command_name, command in commands:
-    command_info = inspectutils.Info(command)
-    command_item = formatting.Bold(command_name)
-    if 'docstring_info' in command_info:
-      command_docstring_info = command_info['docstring_info']
-      if command_docstring_info and command_docstring_info.summary:
-        command_item = _CreateItem(
-            formatting.Bold(command_name), command_docstring_info.summary)
-    command_item_strings.append(command_item)
-  return ('COMMANDS', _NewChoicesSection('COMMAND', command_item_strings))
-
-
 def _NewChoicesSection(name, choices):
   return _CreateItem(
-      '{name} is one of the followings:'.format(
+      '{name} is one of the following:'.format(
           name=formatting.Bold(formatting.Underline(name))),
       '\n' + '\n\n'.join(choices),
       indent=1)
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index e74a148d..8aaec3b1 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -56,7 +56,7 @@ def testHelpTextNoDefaultsObject(self):
     self.assertIn('NAME\n    NoDefaults', help_screen)
     self.assertIn('SYNOPSIS\n    NoDefaults COMMAND', help_screen)
     self.assertNotIn('DESCRIPTION', help_screen)
-    self.assertIn('COMMANDS\n    COMMAND is one of the followings:',
+    self.assertIn('COMMANDS\n    COMMAND is one of the following:',
                   help_screen)
     self.assertIn('double', help_screen)
     self.assertIn('triple', help_screen)
@@ -129,7 +129,7 @@ def testHelpTextEmptyList(self):
     self.assertIn('DESCRIPTION\n', help_screen)
     # We don't check the listed commands either since the list API could
     # potentially change between Python versions.
-    self.assertIn('COMMANDS\n    COMMAND is one of the followings:\n',
+    self.assertIn('COMMANDS\n    COMMAND is one of the following:\n',
                   help_screen)
 
   def testHelpTextShortList(self):
@@ -148,7 +148,7 @@ def testHelpTextShortList(self):
     # We don't check the listed commands comprehensively since the list API
     # could potentially change between Python versions. Check a few
     # functions(command) that we're confident likely remain available.
-    self.assertIn('COMMANDS\n    COMMAND is one of the followings:\n',
+    self.assertIn('COMMANDS\n    COMMAND is one of the following:\n',
                   help_screen)
     self.assertIn('     append\n', help_screen)
 
@@ -160,9 +160,9 @@ def testHelpTextInt(self):
     self.assertIn('NAME\n    7', help_screen)
     self.assertIn('SYNOPSIS\n    7 COMMAND | VALUE', help_screen)
     self.assertIn('DESCRIPTION\n', help_screen)
-    self.assertIn('COMMANDS\n    COMMAND is one of the followings:\n',
+    self.assertIn('COMMANDS\n    COMMAND is one of the following:\n',
                   help_screen)
-    self.assertIn('VALUES\n    VALUE is one of the followings:\n', help_screen)
+    self.assertIn('VALUES\n    VALUE is one of the following:\n', help_screen)
 
   def testHelpTextNoInit(self):
     component = tc.OldStyleEmpty
@@ -190,13 +190,13 @@ def testHelpScreen(self):
     This is some detail description of this test class.
 
 COMMANDS
-    COMMAND is one of the followings:
+    COMMAND is one of the following:
 
      print_msg
        Prints a message.
 
 VALUES
-    VALUE is one of the followings:
+    VALUE is one of the following:
 
      message
        The default message to print.
@@ -273,7 +273,7 @@ def testHelpTextBoldCommandName(self):
         formatting.Bold('NAME') + '\n    ClassWithDocstring', help_screen)
     self.assertIn(formatting.Bold('COMMANDS') + '\n', help_screen)
     self.assertIn(
-        formatting.BoldUnderline('COMMAND') + ' is one of the followings:\n',
+        formatting.BoldUnderline('COMMAND') + ' is one of the following:\n',
         help_screen)
     self.assertIn(formatting.Bold('print_msg') + '\n', help_screen)
 
diff --git a/fire/trace.py b/fire/trace.py
index f898f88d..32e93c6a 100644
--- a/fire/trace.py
+++ b/fire/trace.py
@@ -291,8 +291,13 @@ def HasSeparator(self):
   def AddSeparator(self):
     self._separator = True
 
+  def ErrorAsStr(self):
+    return ' '.join(str(arg) for arg in self._error.args)
+
   def __str__(self):
-    if not self.HasError():
+    if self.HasError():
+      return self.ErrorAsStr()
+    else:
       # Format is: {action} "{target}" ({filename}:{lineno})
       string = self._action
       if self._target is not None:
@@ -304,5 +309,3 @@ def __str__(self):
 
         string += ' ({path})'.format(path=path)
       return string
-    else:
-      return str(self._error)

From 111295ee29beab3fa0fd74c03d714d9223f1f065 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 7 Jun 2019 11:07:14 -0700
Subject: [PATCH 100/324] Simplify __str__ function for FireTrace.

PiperOrigin-RevId: 252082227
Change-Id: I7547acfcc1c834c1540ad3382316665e22afb01f
---
 fire/trace.py | 15 ++++++++-------
 1 file changed, 8 insertions(+), 7 deletions(-)

diff --git a/fire/trace.py b/fire/trace.py
index 32e93c6a..12cc4392 100644
--- a/fire/trace.py
+++ b/fire/trace.py
@@ -211,13 +211,14 @@ def NeedsSeparator(self):
     return element.HasCapacity() and not element.HasSeparator()
 
   def __str__(self):
-    return '\n'.join(
-        '{index}. {trace_string}'.format(
-            index=index + 1,
-            trace_string=element,
-        )
-        for index, element in enumerate(self.elements)
-    )
+    lines = []
+    for index, element in enumerate(self.elements):
+      line = '{index}. {trace_string}'.format(
+          index=index + 1,
+          trace_string=element,
+      )
+      lines.append(line)
+    return '\n'.join(lines)
 
   def NeedsSeparatingHyphenHyphen(self, flag='help'):
     """Returns whether a the trace need '--' before '--help'.

From 585d4724412796601a27475c56e35660636bb5e2 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 7 Jun 2019 11:13:19 -0700
Subject: [PATCH 101/324] Include __version__ in __init__.py

PiperOrigin-RevId: 252083411
Change-Id: I92bfcc76494d5fe806db3eda67d9686fc6e058c1
---
 fire/__init__.py | 1 +
 setup.py         | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/fire/__init__.py b/fire/__init__.py
index ab0874f8..e15450db 100644
--- a/fire/__init__.py
+++ b/fire/__init__.py
@@ -21,3 +21,4 @@
 from fire.core import Fire
 
 __all__ = ['Fire']
+__version__ = '0.1.4'
diff --git a/setup.py b/setup.py
index 0ba2f294..264e7336 100644
--- a/setup.py
+++ b/setup.py
@@ -39,7 +39,7 @@
     'python-Levenshtein',
 ]
 
-VERSION = '0.1.3'
+VERSION = '0.1.4'
 URL = 'https://github.com/google/python-fire'
 
 setup(

From 9e65820e6b84257da1a4fe9e247d59d86de070dc Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 7 Jun 2019 15:05:57 -0700
Subject: [PATCH 102/324] indent the line after "For detailed information on
 this command and its flags, run:" and removes "and its flags" from the final
 note.

PiperOrigin-RevId: 252127187
Change-Id: I5f59fc6301297b5088c4ecf501d526dca4071d02
---
 fire/helptext.py      | 12 ++++++------
 fire/helptext_test.py | 18 +++++++++---------
 2 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index 70e12577..b6eee43b 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -440,15 +440,15 @@ def UsageTextForFunction(component, trace=None):
   output_template = """Usage: {current_command} {args_and_flags}
 {availability_lines}
 For detailed information on this command, run:
-{current_command}{hyphen_hyphen} --help
+  {current_command}{hyphen_hyphen} --help
 """
 
   if trace:
     command = trace.GetCommand()
-    is_help_an_arg = trace.NeedsSeparatingHyphenHyphen()
+    needs_separating_hyphen_hyphen = trace.NeedsSeparatingHyphenHyphen()
   else:
     command = None
-    is_help_an_arg = False
+    needs_separating_hyphen_hyphen = False
 
   if not command:
     command = ''
@@ -473,7 +473,7 @@ def UsageTextForFunction(component, trace=None):
     availability_lines = ''
   args_and_flags = ' '.join(items)
 
-  hyphen_hyphen = ' --' if is_help_an_arg else ''
+  hyphen_hyphen = ' --' if needs_separating_hyphen_hyphen else ''
 
   return output_template.format(
       current_command=command,
@@ -507,8 +507,8 @@ def UsageTextForObject(component, trace=None, verbose=False):
   output_template = """Usage: {current_command} <{possible_actions}>
 {availability_lines}
 
-For detailed information on this command and its flags, run:
-{current_command} --help
+For detailed information on this command, run:
+  {current_command} --help
 """
   if trace:
     command = trace.GetCommand()
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 8aaec3b1..c8b08ed5 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -289,8 +289,8 @@ def testUsageOutput(self):
     Usage: NoDefaults <command>
       available commands:    double | triple
 
-    For detailed information on this command and its flags, run:
-    NoDefaults --help
+    For detailed information on this command, run:
+      NoDefaults --help
     '''
 
     self.assertEqual(
@@ -306,8 +306,8 @@ def testUsageOutputVerbose(self):
     Usage: NoDefaults <command>
       available commands:    double | triple
 
-    For detailed information on this command and its flags, run:
-    NoDefaults --help
+    For detailed information on this command, run:
+      NoDefaults --help
     '''
     self.assertEqual(
         usage_output,
@@ -323,7 +323,7 @@ def testUsageOutputMethod(self):
     Usage: NoDefaults double COUNT
 
     For detailed information on this command, run:
-    NoDefaults double --help
+      NoDefaults double --help
     '''
     self.assertEqual(
         usage_output,
@@ -340,7 +340,7 @@ def testUsageOutputFunctionWithHelp(self):
     Available flags: --help
 
     For detailed information on this command, run:
-    function_with_help -- --help
+      function_with_help -- --help
     '''
     self.assertEqual(
         usage_output,
@@ -357,7 +357,7 @@ def testUsageOutputFunctionWithDocstring(self):
     Available flags: --rate
 
     For detailed information on this command, run:
-    multiplier_with_docstring --help
+      multiplier_with_docstring --help
     '''
     self.assertEqual(
         usage_output,
@@ -378,7 +378,7 @@ def testUsageOutputCallable(self):
       Available commands:    print_msg
 
     For detailed information on this command, run:
-    CallableWithKeywordArgument -- --help
+      CallableWithKeywordArgument -- --help
     '''
     self.assertEqual(
         usage_output,
@@ -393,7 +393,7 @@ def testUsageOutputConstructorWithParameter(self):
     Usage: InstanceVars ARG1 ARG2
 
     For detailed information on this command, run:
-    InstanceVars --help
+      InstanceVars --help
     '''
     self.assertEqual(
         usage_output,

From f64f887d8249b07823b9914508e244ab7d70e51c Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Wed, 12 Jun 2019 16:21:26 -0700
Subject: [PATCH 103/324] Add a test case for object's group detail display in
 help screen.

PiperOrigin-RevId: 252922833
Change-Id: Iaf6bdcb0391895f6ddf396c4ab5c2b9deef25cbe
---
 fire/helptext_test.py | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index c8b08ed5..ef9947fb 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -277,6 +277,22 @@ def testHelpTextBoldCommandName(self):
         help_screen)
     self.assertIn(formatting.Bold('print_msg') + '\n', help_screen)
 
+  def testHelpTextObjectWithGroupAndValues(self):
+    component = tc.TypedProperties()
+    t = trace.FireTrace(component, name='TypedProperties')
+    info = inspectutils.Info(component)
+    help_screen = helptext.HelpText(
+        component=component, info=info, trace=t, verbose=True)
+    print(help_screen)
+    self.assertIn('GROUPS', help_screen)
+    self.assertIn('GROUP is one of the following:', help_screen)
+    self.assertIn(
+        'charlie\n       Class with functions that have default arguments.',
+        help_screen)
+    self.assertIn('VALUES', help_screen)
+    self.assertIn('VALUE is one of the following:', help_screen)
+    self.assertIn('alpha', help_screen)
+
 
 class UsageTest(testutils.BaseTestCase):
 

From 5e46d2d7220335635a9740f262ef026a7cf27067 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 12 Jun 2019 17:02:15 -0700
Subject: [PATCH 104/324] Don't show empty <> when there are no groups,
 commands, or values. Remove extra new line between usage and note.

PiperOrigin-RevId: 252930014
Change-Id: Id7c9cdcd6996d426998b04c7f0ac813e1f3b5c86
---
 fire/helptext.py      | 15 ++++++++++-----
 fire/helptext_test.py | 30 ++++++++++++++++++++++++++++++
 2 files changed, 40 insertions(+), 5 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index b6eee43b..134d7bd6 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -488,7 +488,7 @@ def _CreateAvailabilityLine(header, items,
   items_text = '\n'.join(formatting.WrappedJoin(items, width=items_width))
   indented_items_text = formatting.Indent(items_text, spaces=items_indent)
   indented_header = formatting.Indent(header, spaces=header_indent)
-  return indented_header + indented_items_text[len(indented_header):]
+  return indented_header + indented_items_text[len(indented_header):] + '\n'
 
 
 def UsageTextForObject(component, trace=None, verbose=False):
@@ -504,9 +504,8 @@ def UsageTextForObject(component, trace=None, verbose=False):
   Returns:
     String suitable for display in error screen.
   """
-  output_template = """Usage: {current_command} <{possible_actions}>
+  output_template = """Usage: {current_command}{possible_actions}
 {availability_lines}
-
 For detailed information on this command, run:
   {current_command} --help
 """
@@ -551,8 +550,14 @@ def UsageTextForObject(component, trace=None, verbose=False):
         header='available values:',
         items=values)
     availability_lines.append(values_text)
-  possible_actions_string = '|'.join(possible_actions)
-  availability_lines_string = '\n'.join(availability_lines)
+
+  if possible_actions:
+    possible_actions_string = ' <{actions}>'.format(
+        actions='|'.join(possible_actions))
+  else:
+    possible_actions_string = ''
+
+  availability_lines_string = ''.join(availability_lines)
 
   return output_template.format(
       current_command=command,
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index ef9947fb..343b5f59 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -415,6 +415,36 @@ def testUsageOutputConstructorWithParameter(self):
         usage_output,
         textwrap.dedent(expected_output).lstrip('\n'))
 
+  def testUsageOutputEmptyDict(self):
+    component = {}
+    t = trace.FireTrace(component, name='EmptyDict')
+    info = inspectutils.Info(component)
+    usage_output = helptext.UsageText(component, info, trace=t, verbose=True)
+    expected_output = '''
+    Usage: EmptyDict
+
+    For detailed information on this command, run:
+      EmptyDict --help
+    '''
+    self.assertEqual(
+        usage_output,
+        textwrap.dedent(expected_output).lstrip('\n'))
+
+  def testUsageOutputNone(self):
+    component = None
+    t = trace.FireTrace(component, name='None')
+    info = inspectutils.Info(component)
+    usage_output = helptext.UsageText(component, info, trace=t, verbose=True)
+    expected_output = '''
+    Usage: None
+
+    For detailed information on this command, run:
+      None --help
+    '''
+    self.assertEqual(
+        usage_output,
+        textwrap.dedent(expected_output).lstrip('\n'))
+
 
 if __name__ == '__main__':
   testutils.main()

From 7b307b6ff79e6413b6c83d1b1d35060035f0d8f6 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 12 Jun 2019 17:08:14 -0700
Subject: [PATCH 105/324] Cut over to helptext, remove helputils.

PiperOrigin-RevId: 252931159
Change-Id: I1b80159a500e6ec50e9511b9a2af65177ae5945d
---
 fire/core.py           |  12 +-
 fire/core_test.py      |  22 ++--
 fire/fire_test.py      |   8 +-
 fire/helputils.py      | 263 -----------------------------------------
 fire/helputils_test.py | 131 --------------------
 setup.py               |   1 +
 6 files changed, 22 insertions(+), 415 deletions(-)
 delete mode 100644 fire/helputils.py
 delete mode 100644 fire/helputils_test.py

diff --git a/fire/core.py b/fire/core.py
index 7b2fa319..17871911 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -64,7 +64,7 @@ def main(argv):
 
 from fire import completion
 from fire import decorators
-from fire import helputils
+from fire import helptext
 from fire import inspectutils
 from fire import interact
 from fire import parser
@@ -136,7 +136,7 @@ def Fire(component=None, command=None, name=None):
   if component_trace.show_trace and component_trace.show_help:
     output = ['Fire trace:\n{trace}\n'.format(trace=component_trace)]
     result = component_trace.GetResult()
-    help_string = helputils.HelpString(
+    help_string = helptext.HelpString(
         result, component_trace, component_trace.verbose)
     output.append(help_string)
     Display(output)
@@ -147,7 +147,7 @@ def Fire(component=None, command=None, name=None):
     raise FireExit(0, component_trace)
   if component_trace.show_help:
     result = component_trace.GetResult()
-    help_string = helputils.HelpString(
+    help_string = helptext.HelpString(
         result, component_trace, component_trace.verbose)
     output = [help_string]
     Display(output)
@@ -255,7 +255,7 @@ def _PrintResult(component_trace, verbose=False):
   elif isinstance(result, value_types.VALUE_TYPES):
     print(result)
   elif result is not None:
-    print(helputils.HelpString(result, component_trace, verbose))
+    print(helptext.HelpString(result, component_trace, verbose))
 
 
 def _DisplayError(component_trace):
@@ -269,8 +269,8 @@ def _DisplayError(component_trace):
       output.append(message)
   output.append('ERROR: ' + component_trace.elements[-1].ErrorAsStr())
   result = component_trace.GetResult()
-  help_string = helputils.HelpString(result, component_trace,
-                                     component_trace.verbose)
+  help_string = helptext.HelpString(result, component_trace,
+                                    component_trace.verbose)
   output.append(help_string)
   Display(output)
 
diff --git a/fire/core_test.py b/fire/core_test.py
index 2a906002..8a02411e 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -74,34 +74,34 @@ def testInteractiveModeVariablesWithName(self, mock_embed):
 
   # TODO(dbieber): Use parameterized tests to break up repetitive tests.
   def testHelpWithClass(self):
-    with self.assertRaisesFireExit(0, 'Usage:.*ARG1'):
+    with self.assertRaisesFireExit(0, 'SYNOPSIS.*ARG1'):
       core.Fire(tc.InstanceVars, command=['--', '--help'])
-    with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*ARG1'):
+    with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*ARG1'):
       core.Fire(tc.InstanceVars, command=['--help'])
-    with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*ARG1'):
+    with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*ARG1'):
       core.Fire(tc.InstanceVars, command=['-h'])
 
   def testHelpWithMember(self):
-    with self.assertRaisesFireExit(0, 'Usage:.*capitalize'):
+    with self.assertRaisesFireExit(0, 'SYNOPSIS.*capitalize'):
       core.Fire(tc.TypedProperties, command=['gamma', '--', '--help'])
-    with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*capitalize'):
+    with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*capitalize'):
       core.Fire(tc.TypedProperties, command=['gamma', '--help'])
-    with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*capitalize'):
+    with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*capitalize'):
       core.Fire(tc.TypedProperties, command=['gamma', '-h'])
-    with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*delta'):
+    with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*delta'):
       core.Fire(tc.TypedProperties, command=['delta', '--help'])
-    with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*echo'):
+    with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*echo'):
       core.Fire(tc.TypedProperties, command=['echo', '--help'])
 
   def testHelpOnErrorInConstructor(self):
-    with self.assertRaisesFireExit(0, 'Usage:.*[VALUE]'):
+    with self.assertRaisesFireExit(0, 'SYNOPSIS.*VALUE'):
       core.Fire(tc.ErrorInConstructor, command=['--', '--help'])
-    with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*[VALUE]'):
+    with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*VALUE'):
       core.Fire(tc.ErrorInConstructor, command=['--help'])
 
   def testHelpWithNamespaceCollision(self):
     # Tests cases when calling the help shortcut should not show help.
-    with self.assertOutputMatches(stdout='Docstring.*', stderr=None):
+    with self.assertOutputMatches(stdout='DESCRIPTION.*', stderr=None):
       core.Fire(tc.WithHelpArg, command=['--help', 'False'])
     with self.assertOutputMatches(stdout='help in a dict', stderr=None):
       core.Fire(tc.WithHelpArg, command=['dictionary', '__help'])
diff --git a/fire/fire_test.py b/fire/fire_test.py
index 734c123e..c4287bfb 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -60,7 +60,7 @@ def testFireDefaultName(self):
     with mock.patch.object(sys, 'argv',
                            [os.path.join('python-fire', 'fire',
                                          'base_filename.py')]):
-      with self.assertOutputMatches(stdout='Usage:       base_filename.py',
+      with self.assertOutputMatches(stdout='SYNOPSIS.*base_filename.py',
                                     stderr=None):
         fire.Fire(tc.Empty)
 
@@ -536,12 +536,12 @@ def testHelpFlag(self):
       fire.Fire(tc.BoolConverter, command=['--', '--help'])
 
   def testHelpFlagAndTraceFlag(self):
-    with self.assertRaisesFireExit(0, 'Fire trace:\n.*Usage:'):
+    with self.assertRaisesFireExit(0, 'Fire trace:\n.*SYNOPSIS'):
       fire.Fire(tc.BoolConverter,
                 command=['as-bool', 'True', '--', '--help', '--trace'])
-    with self.assertRaisesFireExit(0, 'Fire trace:\n.*Usage:'):
+    with self.assertRaisesFireExit(0, 'Fire trace:\n.*SYNOPSIS'):
       fire.Fire(tc.BoolConverter, command=['as-bool', 'True', '--', '-h', '-t'])
-    with self.assertRaisesFireExit(0, 'Fire trace:\n.*Usage:'):
+    with self.assertRaisesFireExit(0, 'Fire trace:\n.*SYNOPSIS'):
       fire.Fire(tc.BoolConverter, command=['--', '-h', '--trace'])
 
   def testTabCompletionNoName(self):
diff --git a/fire/helputils.py b/fire/helputils.py
deleted file mode 100644
index 2d906a11..00000000
--- a/fire/helputils.py
+++ /dev/null
@@ -1,263 +0,0 @@
-# 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.
-
-"""Utility for producing help strings for use in Fire CLIs.
-
-Can produce help strings suitable for display in Fire CLIs for any type of
-Python object, module, class, or function.
-
-There are two types of informative strings: Usage and Help screens.
-
-Usage screens are shown when the user accesses a group or accesses a command
-without calling it. A Usage screen shows information about how to use that group
-or command. Usage screens are typically short and show the minimal information
-necessary for the user to determine how to proceed.
-
-Help screens are shown when the user requests help with the help flag (--help).
-Help screens are shown in a less-style console view, and contain detailed help
-information.
-"""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import inspect
-
-from fire import completion
-from fire import docstrings
-from fire import inspectutils
-
-
-def _NormalizeField(field):
-  """Takes a field name and turns it into a human readable name for display.
-
-  Args:
-    field: The field name, used to index into the inspection dict.
-  Returns:
-    The human readable name, suitable for display in a help string.
-  """
-  if field == 'type_name':
-    field = 'type'
-  return (field[0].upper() + field[1:]).replace('_', ' ')
-
-
-def _DisplayValue(info, field, padding):
-  """Gets the value of field from the dict info for display.
-
-  Args:
-    info: The dict with information about the component.
-    field: The field to access for display.
-    padding: Number of spaces to indent text to line up with first-line text.
-  Returns:
-    The value of the field for display, or None if no value should be displayed.
-  """
-  value = info.get(field)
-
-  if value is None:
-    return None
-
-  skip_doc_types = ('dict', 'list', 'unicode', 'int', 'float', 'bool')
-
-  if field == 'docstring':
-    if info.get('type_name') in skip_doc_types:
-      # Don't show the boring default docstrings for these types.
-      return None
-    elif value == '<no docstring>':
-      return None
-
-  elif field == 'usage':
-    lines = []
-    for index, line in enumerate(value.split('\n')):
-      if index > 0:
-        line = ' ' * padding + line
-      lines.append(line)
-    return '\n'.join(lines)
-
-  return value
-
-
-def _GetFields(trace=None):
-  """Returns the field names to include in the help text for a component."""
-  del trace  # Unused.
-  return [
-      'type_name',
-      'string_form',
-      'file',
-      'line',
-      'docstring',
-      'init_docstring',
-      'class_docstring',
-      'call_docstring',
-      'length',
-      'usage',
-  ]
-
-
-def HelpString(component, trace=None, verbose=False):
-  """Returns a help string for a supplied component.
-
-  The component can be any Python class, object, function, module, etc.
-
-  Args:
-    component: The component to determine the help string for.
-    trace: The Fire trace leading to this component.
-    verbose: Whether to include private members in the help string.
-  Returns:
-    String suitable for display giving information about the component.
-  """
-  info = inspectutils.Info(component)
-  info['usage'] = UsageString(component, trace, verbose)
-  info['docstring_info'] = docstrings.parse(info['docstring'])
-
-  return _HelpText(info, trace)
-
-
-def _HelpText(info, trace=None):
-  """Returns help text.
-
-  This was a copy of previous HelpString function and will be removed once the
-  correct text formatters are implemented.
-
-  Args:
-    info: The IR object containing metadata of an object.
-    trace: The Fire trace object containing all metadata of current execution.
-  Returns:
-    String suitable for display giving information about the component.
-  """
-  fields = _GetFields(trace)
-
-  try:
-    max_size = max(
-        len(_NormalizeField(field)) + 1
-        for field in fields
-        if field in info and info[field])
-    format_string = '{{field:{max_size}s}} {{value}}'.format(max_size=max_size)
-  except ValueError:
-    return ''
-
-  lines = []
-  for field in fields:
-    value = _DisplayValue(info, field, padding=max_size + 1)
-    if value:
-      if lines and field == 'usage':
-        lines.append('')  # Ensure a blank line before usage.
-
-      lines.append(format_string.format(
-          field=_NormalizeField(field) + ':',
-          value=value,
-      ))
-  return '\n'.join(lines)
-
-
-def GetSummaryAndDescription(docstring_info):
-  """Retrieves summary and description for help text generation."""
-
-  # To handle both empty string and None
-  summary = docstring_info.summary if docstring_info.summary else None
-  description = (
-      docstring_info.description if docstring_info.description else None)
-  return summary, description
-
-
-def GetCurrentCommand(trace=None):
-  """Returns current command for the purpose of generating help text."""
-  if trace:
-    current_command = trace.GetCommand()
-  else:
-    current_command = ''
-
-  return current_command
-
-
-def _UsageStringFromFullArgSpec(command, spec):
-  """Get a usage string from the FullArgSpec for the given command.
-
-  The strings look like:
-  command --arg ARG [--opt OPT] [VAR ...] [--KWARGS ...]
-
-  Args:
-    command: The command leading up to the function.
-    spec: a FullArgSpec object describing the function.
-  Returns:
-    The usage string for the function.
-  """
-  num_required_args = len(spec.args) - len(spec.defaults)
-
-  help_flags = []
-  help_positional = []
-  for index, arg in enumerate(spec.args):
-    flag = arg.replace('_', '-')
-    if index < num_required_args:
-      help_flags.append('--{flag} {value}'.format(flag=flag, value=arg.upper()))
-      help_positional.append('{value}'.format(value=arg.upper()))
-    else:
-      help_flags.append('[--{flag} {value}]'.format(
-          flag=flag, value=arg.upper()))
-      help_positional.append('[{value}]'.format(value=arg.upper()))
-
-  if spec.varargs:
-    help_flags.append('[{var} ...]'.format(var=spec.varargs.upper()))
-    help_positional.append('[{var} ...]'.format(var=spec.varargs.upper()))
-
-  for arg in spec.kwonlyargs:
-    if arg in spec.kwonlydefaults:
-      arg_str = '[--{flag} {value}]'.format(flag=arg, value=arg.upper())
-    else:
-      arg_str = '--{flag} {value}'.format(flag=arg, value=arg.upper())
-    help_flags.append(arg_str)
-    help_positional.append(arg_str)
-
-  if spec.varkw:
-    help_flags.append('[--{kwarg} ...]'.format(kwarg=spec.varkw.upper()))
-    help_positional.append('[--{kwarg} ...]'.format(kwarg=spec.varkw.upper()))
-
-  commands_flags = command + ' '.join(help_flags)
-  commands_positional = command + ' '.join(help_positional)
-  commands = [commands_positional]
-
-  if commands_flags != commands_positional:
-    commands.append(commands_flags)
-
-  return '\n'.join(commands)
-
-
-def UsageString(component, trace=None, verbose=False):
-  """Returns a string showing how to use the component as a Fire command."""
-  if trace:
-    command = trace.GetCommand()
-  else:
-    command = None
-
-  if command:
-    command += ' '
-  else:
-    command = ''
-
-  if inspect.isroutine(component) or inspect.isclass(component):
-    spec = inspectutils.GetFullArgSpec(component)
-    return _UsageStringFromFullArgSpec(command, spec)
-
-  if isinstance(component, (list, tuple)):
-    length = len(component)
-    if length == 0:
-      return command
-    if length == 1:
-      return command + '[0]'
-    return command + '[0..{cap}]'.format(cap=length - 1)
-
-  completions = completion.Completions(component, verbose)
-  if command:
-    completions = [''] + completions
-  return '\n'.join(command + end for end in completions)
diff --git a/fire/helputils_test.py b/fire/helputils_test.py
deleted file mode 100644
index 08f7752c..00000000
--- a/fire/helputils_test.py
+++ /dev/null
@@ -1,131 +0,0 @@
-# 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.
-
-"""Tests for the helputils module."""
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import os
-
-from fire import helputils
-from fire import test_components as tc
-from fire import testutils
-import six
-
-
-class HelpUtilsTest(testutils.BaseTestCase):
-
-  def testHelpStringClass(self):
-    helpstring = helputils.HelpString(tc.NoDefaults)
-    self.assertIn('Type:        type', helpstring)
-    self.assertIn("String form: <class 'fire.test_components.NoDefaults'>",
-                  helpstring)
-    self.assertIn('test_components.py', helpstring)
-    self.assertIn('Line:        ', helpstring)
-    self.assertNotIn('Usage', helpstring)
-
-  def testHelpStringObject(self):
-    obj = tc.NoDefaults()
-    helpstring = helputils.HelpString(obj)
-    self.assertIn('Type:        NoDefaults', helpstring)
-    self.assertIn('String form: <fire.test_components.NoDefaults object at ',
-                  helpstring)
-    # TODO(dbieber): We comment this out since it only works with IPython:
-    # self.assertIn('test_components.py', helpstring)
-    self.assertIn('Usage:       double\n'
-                  '             triple', helpstring)
-
-  def testHelpStringFunction(self):
-    obj = tc.NoDefaults()
-    helpstring = helputils.HelpString(obj.double)
-    if six.PY2:
-      self.assertIn('Type:        instancemethod\n', helpstring)
-    else:
-      self.assertIn('Type:        method\n', helpstring)
-    self.assertIn(
-        'String form: <bound method NoDefaults.double of '
-        '<fire.test_components.NoDefaults object',
-        helpstring)
-    self.assertIn('test_components.py', helpstring)
-    self.assertIn('Line:        ', helpstring)
-    self.assertIn('Usage:       COUNT\n'
-                  '             --count COUNT', helpstring)
-
-  def testHelpStringFunctionWithDefaults(self):
-    obj = tc.WithDefaults()
-    helpstring = helputils.HelpString(obj.triple)
-    if six.PY2:
-      self.assertIn('Type:        instancemethod\n', helpstring)
-    else:
-      self.assertIn('Type:        method\n', helpstring)
-    self.assertIn(
-        'String form: <bound method WithDefaults.triple of '
-        '<fire.test_components.WithDefaults object',
-        helpstring)
-    self.assertIn('test_components.py', helpstring)
-    self.assertIn('Line:        ', helpstring)
-    self.assertIn('Usage:       [COUNT]\n'
-                  '             [--count COUNT]', helpstring)
-
-  def testHelpStringBuiltin(self):
-    helpstring = helputils.HelpString('test'.upper)
-    self.assertIn('Type:        builtin_function_or_method', helpstring)
-    self.assertIn('String form: <built-in method upper of', helpstring)
-
-  def testHelpStringIntType(self):
-    helpstring = helputils.HelpString(int)
-    self.assertIn('Type:        type', helpstring)
-    if six.PY2:
-      self.assertIn("String form: <type 'int'>", helpstring)
-    else:
-      self.assertIn("String form: <class 'int'>", helpstring)
-    self.assertNotIn('Usage', helpstring)
-
-  def testHelpStringEmptyList(self):
-    helpstring = helputils.HelpString([])
-    self.assertIn('Type:        list', helpstring)
-    self.assertIn('String form: []', helpstring)
-    self.assertIn('Length:      0', helpstring)
-
-  def testHelpStringShortList(self):
-    helpstring = helputils.HelpString([10])
-    self.assertIn('Type:        list', helpstring)
-    self.assertIn('String form: [10]', helpstring)
-    self.assertIn('Length:      1', helpstring)
-    self.assertIn('Usage:       [0]', helpstring)  # [] denotes optional.
-
-  def testHelpStringInt(self):
-    helpstring = helputils.HelpString(7)
-    self.assertIn('Type:        int', helpstring)
-    self.assertIn('String form: 7', helpstring)
-    self.assertIn('Usage:       bit-length\n'
-                  '             conjugate\n'
-                  '             denominator\n', helpstring)
-
-  def testHelpClassNoInit(self):
-    helpstring = helputils.HelpString(tc.OldStyleEmpty)
-    if six.PY2:
-      self.assertIn('Type:        classobj\n', helpstring)
-    else:
-      self.assertIn('Type:        type\n', helpstring)
-    self.assertIn('String form: ', helpstring)
-    self.assertIn('fire.test_components.OldStyleEmpty', helpstring)
-    self.assertIn(os.path.join('fire', 'test_components.py'), helpstring)
-    self.assertIn('Line:        ', helpstring)
-
-
-if __name__ == '__main__':
-  testutils.main()
diff --git a/setup.py b/setup.py
index 264e7336..0e34a55f 100644
--- a/setup.py
+++ b/setup.py
@@ -31,6 +31,7 @@
 
 DEPENDENCIES = [
     'six',
+    'termcolor',
 ]
 
 TEST_DEPENDENCIES = [

From 5b3b359fa9d897e455be8ba1f4c608a7f6444148 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 12 Jun 2019 20:37:49 -0700
Subject: [PATCH 106/324] Don't include dict in FireError when key cannot be
 found in dict. Just include the key.

PiperOrigin-RevId: 252954818
Change-Id: I4afa52c4b39bc71111968a37a9b8b7578a8ebb8d
---
 fire/core.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index 17871911..89232b7f 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -494,8 +494,7 @@ def _Fire(component, args, context, name=None):
             found_target = True
             break
         if not found_target:
-          error = FireError(
-              'Cannot find target in dict:', target, component)
+          error = FireError('Cannot find target in dict:', target)
           component_trace.AddError(error, initial_args)
           return component_trace
 

From 3e9e64d17f55fc158358645f1c2cef04f876cffb Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 12 Jun 2019 21:03:02 -0700
Subject: [PATCH 107/324] Remove extra newline from end of usage text.

PiperOrigin-RevId: 252957309
Change-Id: Ie5103998072ef19cb4a662712f022d5ba52f012b
---
 fire/helptext.py      |  6 ++----
 fire/helptext_test.py | 38 +++++++++++++-------------------------
 2 files changed, 15 insertions(+), 29 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index 134d7bd6..126e1389 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -440,8 +440,7 @@ def UsageTextForFunction(component, trace=None):
   output_template = """Usage: {current_command} {args_and_flags}
 {availability_lines}
 For detailed information on this command, run:
-  {current_command}{hyphen_hyphen} --help
-"""
+  {current_command}{hyphen_hyphen} --help"""
 
   if trace:
     command = trace.GetCommand()
@@ -507,8 +506,7 @@ def UsageTextForObject(component, trace=None, verbose=False):
   output_template = """Usage: {current_command}{possible_actions}
 {availability_lines}
 For detailed information on this command, run:
-  {current_command} --help
-"""
+  {current_command} --help"""
   if trace:
     command = trace.GetCommand()
   else:
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 343b5f59..1111738e 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -199,8 +199,7 @@ def testHelpScreen(self):
     VALUE is one of the following:
 
      message
-       The default message to print.
-"""
+       The default message to print."""
     self.assertEqual(textwrap.dedent(expected_output).strip(),
                      help_output.strip())
 
@@ -224,8 +223,7 @@ def testHelpScreenForFunctionDocstringWithLineBreak(self):
             The upper limit of the range to generate, from 0 to `n` - 1.
 
     NOTES
-        You could also use flags syntax for POSITIONAL ARGUMENTS
-    """
+        You could also use flags syntax for POSITIONAL ARGUMENTS"""
     self.assertEqual(textwrap.dedent(expected_output).strip(),
                      help_output.strip())
 
@@ -246,8 +244,7 @@ def testHelpScreenForFunctionFunctionWithDefaultArgs(self):
 
     FLAGS
         --count
-          Input number that you want to double.
-    """
+          Input number that you want to double."""
     self.assertEqual(textwrap.dedent(expected_output).strip(),
                      help_output.strip())
 
@@ -306,8 +303,7 @@ def testUsageOutput(self):
       available commands:    double | triple
 
     For detailed information on this command, run:
-      NoDefaults --help
-    '''
+      NoDefaults --help'''
 
     self.assertEqual(
         usage_output,
@@ -323,8 +319,7 @@ def testUsageOutputVerbose(self):
       available commands:    double | triple
 
     For detailed information on this command, run:
-      NoDefaults --help
-    '''
+      NoDefaults --help'''
     self.assertEqual(
         usage_output,
         textwrap.dedent(expected_output).lstrip('\n'))
@@ -339,8 +334,7 @@ def testUsageOutputMethod(self):
     Usage: NoDefaults double COUNT
 
     For detailed information on this command, run:
-      NoDefaults double --help
-    '''
+      NoDefaults double --help'''
     self.assertEqual(
         usage_output,
         textwrap.dedent(expected_output).lstrip('\n'))
@@ -356,8 +350,7 @@ def testUsageOutputFunctionWithHelp(self):
     Available flags: --help
 
     For detailed information on this command, run:
-      function_with_help -- --help
-    '''
+      function_with_help -- --help'''
     self.assertEqual(
         usage_output,
         textwrap.dedent(expected_output).lstrip('\n'))
@@ -373,8 +366,7 @@ def testUsageOutputFunctionWithDocstring(self):
     Available flags: --rate
 
     For detailed information on this command, run:
-      multiplier_with_docstring --help
-    '''
+      multiplier_with_docstring --help'''
     self.assertEqual(
         usage_output,
         textwrap.dedent(expected_output).lstrip('\n'))
@@ -386,7 +378,7 @@ def testUsageOutputCallable(self):
     t = trace.FireTrace(component, name='CallableWithKeywordArgument')
     info = inspectutils.Info(component)
     usage_output = helptext.UsageText(component, info, trace=t, verbose=True)
-    # TODO(zuhaohen): We need to handle the case for keyword args as well
+    # TODO(joejoevictor): We need to handle the case for keyword args as well
     # i.e. __call__ method of CallableWithKeywordArgument
     expected_output = '''
     Usage: CallableWithKeywordArgument <command>
@@ -394,8 +386,7 @@ def testUsageOutputCallable(self):
       Available commands:    print_msg
 
     For detailed information on this command, run:
-      CallableWithKeywordArgument -- --help
-    '''
+      CallableWithKeywordArgument -- --help'''
     self.assertEqual(
         usage_output,
         textwrap.dedent(expected_output).lstrip('\n'))
@@ -409,8 +400,7 @@ def testUsageOutputConstructorWithParameter(self):
     Usage: InstanceVars ARG1 ARG2
 
     For detailed information on this command, run:
-      InstanceVars --help
-    '''
+      InstanceVars --help'''
     self.assertEqual(
         usage_output,
         textwrap.dedent(expected_output).lstrip('\n'))
@@ -424,8 +414,7 @@ def testUsageOutputEmptyDict(self):
     Usage: EmptyDict
 
     For detailed information on this command, run:
-      EmptyDict --help
-    '''
+      EmptyDict --help'''
     self.assertEqual(
         usage_output,
         textwrap.dedent(expected_output).lstrip('\n'))
@@ -439,8 +428,7 @@ def testUsageOutputNone(self):
     Usage: None
 
     For detailed information on this command, run:
-      None --help
-    '''
+      None --help'''
     self.assertEqual(
         usage_output,
         textwrap.dedent(expected_output).lstrip('\n'))

From 76f2a87cd0a9f65eb4392093e141e3d1a7f91476 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 13 Jun 2019 07:19:37 -0700
Subject: [PATCH 108/324] Color the ERROR: label in red and bold it.

PiperOrigin-RevId: 253022905
Change-Id: I47610da2ad0e2644f96637ef62aa790d13e1c718
---
 fire/core.py       | 4 +++-
 fire/formatting.py | 4 ++++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/fire/core.py b/fire/core.py
index 89232b7f..76792e0d 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -64,6 +64,7 @@ def main(argv):
 
 from fire import completion
 from fire import decorators
+from fire import formatting
 from fire import helptext
 from fire import inspectutils
 from fire import interact
@@ -267,7 +268,8 @@ def _DisplayError(component_trace):
       message = 'INFO: Showing help with the command {cmd}.\n'.format(
           cmd=pipes.quote(command))
       output.append(message)
-  output.append('ERROR: ' + component_trace.elements[-1].ErrorAsStr())
+  output.append(formatting.Error('ERROR: ')
+                + component_trace.elements[-1].ErrorAsStr())
   result = component_trace.GetResult()
   help_string = helptext.HelpString(result, component_trace,
                                     component_trace.verbose)
diff --git a/fire/formatting.py b/fire/formatting.py
index 6619b246..880e2b18 100644
--- a/fire/formatting.py
+++ b/fire/formatting.py
@@ -61,3 +61,7 @@ def WrappedJoin(items, separator=' | ', width=80):
 
   lines.append(current_line)
   return lines
+
+
+def Error(text):
+  return termcolor.colored(text, color='red', attrs=['bold'])

From b300c6e057fc4d282928e1049c8dbde3e350f1f8 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 24 Jun 2019 10:59:01 -0700
Subject: [PATCH 109/324] Only determine the context if --interactive is set or
 if no component is supplied.

PiperOrigin-RevId: 254792343
Change-Id: I396e02dc7140ede46e5ce010c504763f65882bbc
---
 fire/core.py | 30 +++++++++++++++++-------------
 1 file changed, 17 insertions(+), 13 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index 76792e0d..f22be999 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -120,16 +120,22 @@ def Fire(component=None, command=None, name=None):
     raise ValueError('The command argument must be a string or a sequence of '
                      'arguments.')
 
-  # Determine the calling context.
-  caller = inspect.stack()[1]
-  caller_frame = caller[0]
-  caller_globals = caller_frame.f_globals
-  caller_locals = caller_frame.f_locals
+  args, flag_args = parser.SeparateFlagArgs(args)
+
+  argparser = parser.CreateParser()
+  parsed_flag_args, unused_args = argparser.parse_known_args(flag_args)
+
   context = {}
-  context.update(caller_globals)
-  context.update(caller_locals)
+  if parsed_flag_args.interactive or component is None:
+    # Determine the calling context.
+    caller = inspect.stack()[1]
+    caller_frame = caller[0]
+    caller_globals = caller_frame.f_globals
+    caller_locals = caller_frame.f_locals
+    context.update(caller_globals)
+    context.update(caller_locals)
 
-  component_trace = _Fire(component, args, context, name)
+  component_trace = _Fire(component, args, parsed_flag_args, context, name)
 
   if component_trace.HasError():
     _DisplayError(component_trace)
@@ -329,7 +335,7 @@ def _OneLineResult(result):
     return str(result).replace('\n', ' ')
 
 
-def _Fire(component, args, context, name=None):
+def _Fire(component, args, parsed_flag_args, context, name=None):
   """Execute a Fire command on a target component using the args supplied.
 
   Arguments that come after a final isolated '--' are treated as Flags, eg for
@@ -361,6 +367,8 @@ def _Fire(component, args, context, name=None):
     component: The target component for Fire.
     args: A list of args to consume in Firing on the component, usually from
         the command line.
+    parsed_flag_args: The values of the flag args (e.g. --verbose, --separator)
+        that are part of every Fire CLI.
     context: A dict with the local and global variables available at the call
         to Fire.
     name: Optional. The name of the command. Used in interactive mode and in
@@ -372,10 +380,6 @@ def _Fire(component, args, context, name=None):
     ValueError: If there are arguments that cannot be consumed.
     ValueError: If --completion is specified but no name available.
   """
-  args, flag_args = parser.SeparateFlagArgs(args)
-
-  argparser = parser.CreateParser()
-  parsed_flag_args, unused_args = argparser.parse_known_args(flag_args)
   verbose = parsed_flag_args.verbose
   interactive = parsed_flag_args.interactive
   separator = parsed_flag_args.separator

From 39db852d6642d7edb5a65bef9075c9cff17c0a36 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 26 Jun 2019 13:08:08 -0700
Subject: [PATCH 110/324] Display help when we say we're going to display help,
 even if there's also an error. Also fixes spacing issue in synopsis.

PiperOrigin-RevId: 255248550
Change-Id: I3861e780d4c91b83322e8f7e36a6f0ab6c91a4d1
---
 fire/core.py                | 51 +++++++++++++++++-----------
 fire/helptext.py            | 58 ++++++++++---------------------
 fire/helptext_test.py       | 68 ++++++++++---------------------------
 fire/test_components_bin.py | 33 ++++++++++++++++++
 4 files changed, 99 insertions(+), 111 deletions(-)
 create mode 100644 fire/test_components_bin.py

diff --git a/fire/core.py b/fire/core.py
index f22be999..83246522 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -143,21 +143,21 @@ def Fire(component=None, command=None, name=None):
   if component_trace.show_trace and component_trace.show_help:
     output = ['Fire trace:\n{trace}\n'.format(trace=component_trace)]
     result = component_trace.GetResult()
-    help_string = helptext.HelpString(
+    help_text = helptext.HelpText(
         result, component_trace, component_trace.verbose)
-    output.append(help_string)
-    Display(output)
+    output.append(help_text)
+    Display(output, out=sys.stderr)
     raise FireExit(0, component_trace)
   if component_trace.show_trace:
     output = ['Fire trace:\n{trace}'.format(trace=component_trace)]
-    Display(output)
+    Display(output, out=sys.stderr)
     raise FireExit(0, component_trace)
   if component_trace.show_help:
     result = component_trace.GetResult()
-    help_string = helptext.HelpString(
+    help_text = helptext.HelpText(
         result, component_trace, component_trace.verbose)
-    output = [help_string]
-    Display(output)
+    output = [help_text]
+    Display(output, out=sys.stderr)
     raise FireExit(0, component_trace)
 
   # The command succeeded normally; print the result.
@@ -166,9 +166,9 @@ def Fire(component=None, command=None, name=None):
   return result
 
 
-def Display(lines):
+def Display(lines, out):
   text = '\n'.join(lines) + '\n'
-  pager = console_pager.Pager(text, out=sys.stderr)
+  pager = console_pager.Pager(text, out=out)
   try:
     pager.Run()
   except:  # pylint: disable=bare-except
@@ -262,25 +262,36 @@ def _PrintResult(component_trace, verbose=False):
   elif isinstance(result, value_types.VALUE_TYPES):
     print(result)
   elif result is not None:
-    print(helptext.HelpString(result, component_trace, verbose))
+    help_text = helptext.HelpText(result, component_trace, verbose)
+    output = [help_text]
+    Display(output, out=sys.stdout)
 
 
 def _DisplayError(component_trace):
   """Prints the Fire trace and the error to stdout."""
+  result = component_trace.GetResult()
+
   output = []
+  show_help = False
   for help_flag in ('-h', '--help'):
     if help_flag in component_trace.elements[-1].args:
-      command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
-      message = 'INFO: Showing help with the command {cmd}.\n'.format(
-          cmd=pipes.quote(command))
-      output.append(message)
-  output.append(formatting.Error('ERROR: ')
-                + component_trace.elements[-1].ErrorAsStr())
-  result = component_trace.GetResult()
-  help_string = helptext.HelpString(result, component_trace,
+      show_help = True
+
+  if show_help:
+    command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
+    print('INFO: Showing help with the command {cmd}.\n'.format(
+        cmd=pipes.quote(command)), file=sys.stderr)
+    help_text = helptext.HelpText(result, component_trace,
+                                  component_trace.verbose)
+    output.append(help_text)
+  else:
+    output.append(formatting.Error('ERROR: ')
+                  + component_trace.elements[-1].ErrorAsStr())
+    error_text = helptext.UsageText(result, component_trace,
                                     component_trace.verbose)
-  output.append(help_string)
-  Display(output)
+    output.append(error_text)
+
+  Display(output, out=sys.stderr)
 
 
 def _DictAsString(result, verbose=False):
diff --git a/fire/helptext.py b/fire/helptext.py
index 126e1389..6c14bec4 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -45,30 +45,6 @@
 from fire import value_types
 
 
-def HelpString(component, trace=None, verbose=False):
-  """Returns the text to show for a supplied component.
-
-  The component can be any Python class, object, function, module, etc.
-
-  Args:
-    component: The component to determine the help string for.
-    trace: The Fire trace leading to this component.
-    verbose: Whether to include private members in the help string.
-  Returns:
-    String suitable for display giving information about the component.
-  """
-  info = inspectutils.Info(component)
-
-  is_error_screen = False
-  if trace:
-    is_error_screen = trace.HasError()
-
-  if is_error_screen:
-    return UsageText(component, info, trace, verbose=verbose)
-  else:
-    return HelpText(component, info, trace, verbose=verbose)
-
-
 def GetArgsAngFlags(component):
   """Returns all types of arguments and flags of a component."""
   spec = inspectutils.GetFullArgSpec(component)
@@ -103,11 +79,12 @@ def GetCurrentCommand(trace=None):
   return current_command
 
 
-def HelpText(component, info, trace=None, verbose=False):
+def HelpText(component, trace=None, verbose=False):
+  info = inspectutils.Info(component)
   if inspect.isroutine(component) or inspect.isclass(component):
-    return HelpTextForFunction(component, info, trace)
+    return HelpTextForFunction(component, info, trace=trace, verbose=verbose)
   else:
-    return HelpTextForObject(component, info, trace, verbose)
+    return HelpTextForObject(component, info, trace=trace, verbose=verbose)
 
 
 def GetDescriptionSectionText(summary, description):
@@ -163,19 +140,21 @@ def HelpTextForFunction(component, info, trace=None, verbose=False):
   name_section = name_section_template.format(
       current_command=current_command, command_summary=command_summary_str)
 
-  args_and_flags = ''
+  arg_and_flag_strings = []
   if args_with_no_defaults:
-    items = [formatting.Underline(arg.upper()) for arg in args_with_no_defaults]
-    args_and_flags = ' '.join(items)
+    arg_strings = [formatting.Underline(arg.upper())
+                   for arg in args_with_no_defaults]
+    arg_and_flag_strings.extend(arg_strings)
 
-  synopsis_flag_template = '[--{flag_name}={flag_name_upper}]'
+  flag_string_template = '[--{flag_name}={flag_name_upper}]'
   if flags:
-    items = [
-        synopsis_flag_template.format(
+    flag_strings = [
+        flag_string_template.format(
             flag_name=formatting.Underline(flag), flag_name_upper=flag.upper())
         for flag in flags
     ]
-    args_and_flags = args_and_flags + ' '.join(items)
+    arg_and_flag_strings.extend(flag_strings)
+  args_and_flags = ' '.join(arg_and_flag_strings)
 
   # Synopsis section
   synopsis_section_template = '{current_command} {args_and_flags}'
@@ -418,8 +397,7 @@ def _NewChoicesSection(name, choices):
       indent=1)
 
 
-def UsageText(component, info, trace=None, verbose=False):
-  del info  # Unused.
+def UsageText(component, trace=None, verbose=False):
   if inspect.isroutine(component) or inspect.isclass(component):
     return UsageTextForFunction(component, trace)
   else:
@@ -434,7 +412,7 @@ def UsageTextForFunction(component, trace=None):
     trace: The Fire trace object containing all metadata of current execution.
 
   Returns:
-    String suitable for display in error screen.
+    String suitable for display in an error screen.
   """
 
   output_template = """Usage: {current_command} {args_and_flags}
@@ -491,10 +469,10 @@ def _CreateAvailabilityLine(header, items,
 
 
 def UsageTextForObject(component, trace=None, verbose=False):
-  """Returns help text for usage screen for objects.
+  """Returns the usage text for the error screen for an object.
 
-  Construct help text for usage screen to inform the user about error occurred
-  and correct syntax for invoking the object.
+  Constructs the usage text for the error screen to inform the user about how
+  to use the current component.
 
   Args:
     component: The component to determine the usage text for.
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 1111738e..faf121f0 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -23,7 +23,6 @@
 
 from fire import formatting
 from fire import helptext
-from fire import inspectutils
 from fire import test_components as tc
 from fire import testutils
 from fire import trace
@@ -36,10 +35,8 @@ def setUp(self):
 
   def testHelpTextNoDefaults(self):
     component = tc.NoDefaults
-    info = inspectutils.Info(component)
     help_screen = helptext.HelpText(
         component=component,
-        info=info,
         trace=trace.FireTrace(component, name='NoDefaults'))
     self.assertIn('NAME\n    NoDefaults', help_screen)
     self.assertIn('SYNOPSIS\n    NoDefaults', help_screen)
@@ -48,10 +45,8 @@ def testHelpTextNoDefaults(self):
 
   def testHelpTextNoDefaultsObject(self):
     component = tc.NoDefaults()
-    info = inspectutils.Info(component)
     help_screen = helptext.HelpText(
         component=component,
-        info=info,
         trace=trace.FireTrace(component, name='NoDefaults'))
     self.assertIn('NAME\n    NoDefaults', help_screen)
     self.assertIn('SYNOPSIS\n    NoDefaults COMMAND', help_screen)
@@ -64,10 +59,8 @@ def testHelpTextNoDefaultsObject(self):
 
   def testHelpTextFunction(self):
     component = tc.NoDefaults().double
-    info = inspectutils.Info(component)
     help_screen = helptext.HelpText(
         component=component,
-        info=info,
         trace=trace.FireTrace(component, name='double'))
     self.assertIn('NAME\n    double', help_screen)
     self.assertIn('SYNOPSIS\n    double COUNT', help_screen)
@@ -79,10 +72,8 @@ def testHelpTextFunction(self):
 
   def testHelpTextFunctionWithDefaults(self):
     component = tc.WithDefaults().triple
-    info = inspectutils.Info(component)
     help_screen = helptext.HelpText(
         component=component,
-        info=info,
         trace=trace.FireTrace(component, name='triple'))
     self.assertIn('NAME\n    triple', help_screen)
     self.assertIn('SYNOPSIS\n    triple [--count=COUNT]', help_screen)
@@ -92,10 +83,8 @@ def testHelpTextFunctionWithDefaults(self):
 
   def testHelpTextFunctionWithBuiltin(self):
     component = 'test'.upper
-    info = inspectutils.Info(component)
     help_screen = helptext.HelpText(
         component=component,
-        info=info,
         trace=trace.FireTrace(component, 'upper'))
     self.assertIn('NAME\n    upper', help_screen)
     self.assertIn('SYNOPSIS\n    upper', help_screen)
@@ -106,9 +95,8 @@ def testHelpTextFunctionWithBuiltin(self):
 
   def testHelpTextFunctionIntType(self):
     component = int
-    info = inspectutils.Info(component)
     help_screen = helptext.HelpText(
-        component=component, info=info, trace=trace.FireTrace(component, 'int'))
+        component=component, trace=trace.FireTrace(component, 'int'))
     self.assertIn('NAME\n    int', help_screen)
     self.assertIn('SYNOPSIS\n    int', help_screen)
     # We don't check description content here since the content is python
@@ -117,10 +105,8 @@ def testHelpTextFunctionIntType(self):
 
   def testHelpTextEmptyList(self):
     component = []
-    info = inspectutils.Info(component)
     help_screen = helptext.HelpText(
         component=component,
-        info=info,
         trace=trace.FireTrace(component, 'list'))
     self.assertIn('NAME\n    list', help_screen)
     self.assertIn('SYNOPSIS\n    list COMMAND', help_screen)
@@ -134,10 +120,8 @@ def testHelpTextEmptyList(self):
 
   def testHelpTextShortList(self):
     component = [10]
-    info = inspectutils.Info(component)
     help_screen = helptext.HelpText(
         component=component,
-        info=info,
         trace=trace.FireTrace(component, 'list'))
     self.assertIn('NAME\n    list', help_screen)
     self.assertIn('SYNOPSIS\n    list COMMAND', help_screen)
@@ -154,9 +138,8 @@ def testHelpTextShortList(self):
 
   def testHelpTextInt(self):
     component = 7
-    info = inspectutils.Info(component)
     help_screen = helptext.HelpText(
-        component=component, info=info, trace=trace.FireTrace(component, '7'))
+        component=component, trace=trace.FireTrace(component, '7'))
     self.assertIn('NAME\n    7', help_screen)
     self.assertIn('SYNOPSIS\n    7 COMMAND | VALUE', help_screen)
     self.assertIn('DESCRIPTION\n', help_screen)
@@ -166,10 +149,8 @@ def testHelpTextInt(self):
 
   def testHelpTextNoInit(self):
     component = tc.OldStyleEmpty
-    info = inspectutils.Info(component)
     help_screen = helptext.HelpText(
         component=component,
-        info=info,
         trace=trace.FireTrace(component, 'OldStyleEmpty'))
     self.assertIn('NAME\n    OldStyleEmpty', help_screen)
     self.assertIn('SYNOPSIS\n    OldStyleEmpty', help_screen)
@@ -177,8 +158,7 @@ def testHelpTextNoInit(self):
   def testHelpScreen(self):
     component = tc.ClassWithDocstring()
     t = trace.FireTrace(component, name='ClassWithDocstring')
-    info = inspectutils.Info(component)
-    help_output = helptext.HelpText(component, info, t)
+    help_output = helptext.HelpText(component, t)
     expected_output = """
 NAME
     ClassWithDocstring - Test class for testing help text output.
@@ -206,8 +186,7 @@ def testHelpScreen(self):
   def testHelpScreenForFunctionDocstringWithLineBreak(self):
     component = tc.ClassWithMultilineDocstring.example_generator
     t = trace.FireTrace(component, name='example_generator')
-    info = inspectutils.Info(component)
-    help_output = helptext.HelpText(component, info, t)
+    help_output = helptext.HelpText(component, t)
     expected_output = """
     NAME
         example_generator - Generators have a ``Yields`` section instead of a ``Returns`` section.
@@ -230,8 +209,7 @@ def testHelpScreenForFunctionDocstringWithLineBreak(self):
   def testHelpScreenForFunctionFunctionWithDefaultArgs(self):
     component = tc.WithDefaults().double
     t = trace.FireTrace(component, name='double')
-    info = inspectutils.Info(component)
-    help_output = helptext.HelpText(component, info, t)
+    help_output = helptext.HelpText(component, t)
     expected_output = """
     NAME
         double - Returns the input multiplied by 2.
@@ -250,9 +228,8 @@ def testHelpScreenForFunctionFunctionWithDefaultArgs(self):
 
   def testHelpTextUnderlineFlag(self):
     component = tc.WithDefaults().triple
-    info = inspectutils.Info(component)
     t = trace.FireTrace(component, name='triple')
-    help_screen = helptext.HelpText(component, info, t)
+    help_screen = helptext.HelpText(component, t)
     self.assertIn(formatting.Bold('NAME') + '\n    triple', help_screen)
     self.assertIn(
         formatting.Bold('SYNOPSIS') + '\n    triple [--count=COUNT]',
@@ -263,9 +240,8 @@ def testHelpTextUnderlineFlag(self):
 
   def testHelpTextBoldCommandName(self):
     component = tc.ClassWithDocstring()
-    info = inspectutils.Info(component)
     t = trace.FireTrace(component, name='ClassWithDocstring')
-    help_screen = helptext.HelpText(component, info, t)
+    help_screen = helptext.HelpText(component, t)
     self.assertIn(
         formatting.Bold('NAME') + '\n    ClassWithDocstring', help_screen)
     self.assertIn(formatting.Bold('COMMANDS') + '\n', help_screen)
@@ -277,9 +253,8 @@ def testHelpTextBoldCommandName(self):
   def testHelpTextObjectWithGroupAndValues(self):
     component = tc.TypedProperties()
     t = trace.FireTrace(component, name='TypedProperties')
-    info = inspectutils.Info(component)
     help_screen = helptext.HelpText(
-        component=component, info=info, trace=t, verbose=True)
+        component=component, trace=t, verbose=True)
     print(help_screen)
     self.assertIn('GROUPS', help_screen)
     self.assertIn('GROUP is one of the following:', help_screen)
@@ -296,8 +271,7 @@ class UsageTest(testutils.BaseTestCase):
   def testUsageOutput(self):
     component = tc.NoDefaults()
     t = trace.FireTrace(component, name='NoDefaults')
-    info = inspectutils.Info(component)
-    usage_output = helptext.UsageText(component, info, trace=t, verbose=False)
+    usage_output = helptext.UsageText(component, trace=t, verbose=False)
     expected_output = '''
     Usage: NoDefaults <command>
       available commands:    double | triple
@@ -312,8 +286,7 @@ def testUsageOutput(self):
   def testUsageOutputVerbose(self):
     component = tc.NoDefaults()
     t = trace.FireTrace(component, name='NoDefaults')
-    info = inspectutils.Info(component)
-    usage_output = helptext.UsageText(component, info, trace=t, verbose=True)
+    usage_output = helptext.UsageText(component, trace=t, verbose=True)
     expected_output = '''
     Usage: NoDefaults <command>
       available commands:    double | triple
@@ -328,8 +301,7 @@ def testUsageOutputMethod(self):
     component = tc.NoDefaults().double
     t = trace.FireTrace(component, name='NoDefaults')
     t.AddAccessedProperty(component, 'double', ['double'], None, None)
-    info = inspectutils.Info(component)
-    usage_output = helptext.UsageText(component, info, trace=t, verbose=True)
+    usage_output = helptext.UsageText(component, trace=t, verbose=True)
     expected_output = '''
     Usage: NoDefaults double COUNT
 
@@ -342,8 +314,7 @@ def testUsageOutputMethod(self):
   def testUsageOutputFunctionWithHelp(self):
     component = tc.function_with_help
     t = trace.FireTrace(component, name='function_with_help')
-    info = inspectutils.Info(component)
-    usage_output = helptext.UsageText(component, info, trace=t, verbose=True)
+    usage_output = helptext.UsageText(component, trace=t, verbose=True)
     expected_output = '''
     Usage: function_with_help <flags>
 
@@ -358,8 +329,7 @@ def testUsageOutputFunctionWithHelp(self):
   def testUsageOutputFunctionWithDocstring(self):
     component = tc.multiplier_with_docstring
     t = trace.FireTrace(component, name='multiplier_with_docstring')
-    info = inspectutils.Info(component)
-    usage_output = helptext.UsageText(component, info, trace=t, verbose=True)
+    usage_output = helptext.UsageText(component, trace=t, verbose=True)
     expected_output = '''
     Usage: multiplier_with_docstring NUM <flags>
 
@@ -376,8 +346,7 @@ def testUsageOutputCallable(self):
     # This is both a group and a command!
     component = tc.CallableWithKeywordArgument
     t = trace.FireTrace(component, name='CallableWithKeywordArgument')
-    info = inspectutils.Info(component)
-    usage_output = helptext.UsageText(component, info, trace=t, verbose=True)
+    usage_output = helptext.UsageText(component, trace=t, verbose=True)
     # TODO(joejoevictor): We need to handle the case for keyword args as well
     # i.e. __call__ method of CallableWithKeywordArgument
     expected_output = '''
@@ -394,8 +363,7 @@ def testUsageOutputCallable(self):
   def testUsageOutputConstructorWithParameter(self):
     component = tc.InstanceVars
     t = trace.FireTrace(component, name='InstanceVars')
-    info = inspectutils.Info(component)
-    usage_output = helptext.UsageText(component, info, trace=t, verbose=True)
+    usage_output = helptext.UsageText(component, trace=t, verbose=True)
     expected_output = '''
     Usage: InstanceVars ARG1 ARG2
 
@@ -408,8 +376,7 @@ def testUsageOutputConstructorWithParameter(self):
   def testUsageOutputEmptyDict(self):
     component = {}
     t = trace.FireTrace(component, name='EmptyDict')
-    info = inspectutils.Info(component)
-    usage_output = helptext.UsageText(component, info, trace=t, verbose=True)
+    usage_output = helptext.UsageText(component, trace=t, verbose=True)
     expected_output = '''
     Usage: EmptyDict
 
@@ -422,8 +389,7 @@ def testUsageOutputEmptyDict(self):
   def testUsageOutputNone(self):
     component = None
     t = trace.FireTrace(component, name='None')
-    info = inspectutils.Info(component)
-    usage_output = helptext.UsageText(component, info, trace=t, verbose=True)
+    usage_output = helptext.UsageText(component, trace=t, verbose=True)
     expected_output = '''
     Usage: None
 
diff --git a/fire/test_components_bin.py b/fire/test_components_bin.py
new file mode 100644
index 00000000..3dc52ddd
--- /dev/null
+++ b/fire/test_components_bin.py
@@ -0,0 +1,33 @@
+# 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.
+
+"""Python Fire test components Fire CLI.
+
+This file is useful for replicating test results manually.
+"""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import fire
+from fire import test_components
+
+
+def main(argv):
+  del argv  # Unused.
+  fire.Fire(test_components)
+
+if __name__ == '__main__':
+  main()

From 453a64733a8e5d342ef68745134a1d821cfde283 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 26 Jun 2019 13:09:05 -0700
Subject: [PATCH 111/324] s/could/can in NOTES section of help screens

PiperOrigin-RevId: 255248712
Change-Id: I8e9f87ff2ccd1254a66340ee5dea136b0d4bfe46
---
 fire/helptext.py      | 2 +-
 fire/helptext_test.py | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index 6c14bec4..d990fe4d 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -185,7 +185,7 @@ def HelpTextForFunction(component, info, trace=None, verbose=False):
                                     '\n'.join(pos_arg_items).rstrip('\n'))
     args_and_flags_sections.append(positional_arguments_section)
     notes_sections.append(
-        ('NOTES', 'You could also use flags syntax for POSITIONAL ARGUMENTS')
+        ('NOTES', 'You can also use flags syntax for POSITIONAL ARGUMENTS')
     )
 
   flag_items = [
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index faf121f0..2a491779 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -67,7 +67,7 @@ def testHelpTextFunction(self):
     self.assertNotIn('DESCRIPTION', help_screen)
     self.assertIn('POSITIONAL ARGUMENTS\n    COUNT', help_screen)
     self.assertIn(
-        'NOTES\n    You could also use flags syntax for POSITIONAL ARGUMENTS',
+        'NOTES\n    You can also use flags syntax for POSITIONAL ARGUMENTS',
         help_screen)
 
   def testHelpTextFunctionWithDefaults(self):
@@ -202,7 +202,7 @@ def testHelpScreenForFunctionDocstringWithLineBreak(self):
             The upper limit of the range to generate, from 0 to `n` - 1.
 
     NOTES
-        You could also use flags syntax for POSITIONAL ARGUMENTS"""
+        You can also use flags syntax for POSITIONAL ARGUMENTS"""
     self.assertEqual(textwrap.dedent(expected_output).strip(),
                      help_output.strip())
 

From 4d136601175b623e1a50cb1757cf40ab8de70c85 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 26 Jun 2019 13:17:50 -0700
Subject: [PATCH 112/324] Use console_io rather than console_pager from console
 package.

PiperOrigin-RevId: 255250331
Change-Id: I6f1e5b3c10cbfe62056d5abcb3473950536ba5ec
---
 fire/console/console_attr.py    |  45 ++-
 fire/console/console_attr_os.py |   5 +-
 fire/console/console_io.py      | 108 +++++++
 fire/console/console_pager.py   |   2 +-
 fire/console/encoding.py        |   2 +-
 fire/console/files.py           | 116 ++++++++
 fire/console/platforms.py       | 481 ++++++++++++++++++++++++++++++++
 fire/console/text.py            | 103 +++++++
 fire/core.py                    |  10 +-
 9 files changed, 850 insertions(+), 22 deletions(-)
 create mode 100644 fire/console/console_io.py
 create mode 100644 fire/console/files.py
 create mode 100644 fire/console/platforms.py
 create mode 100644 fire/console/text.py

diff --git a/fire/console/console_attr.py b/fire/console/console_attr.py
index 94334f45..35c10fba 100644
--- a/fire/console/console_attr.py
+++ b/fire/console/console_attr.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*- #
 
-# Copyright 2015 Google Inc. All Rights Reserved.
+# Copyright 2015 Google LLC. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -95,12 +95,15 @@
 import sys
 import unicodedata
 
+# from fire.console import properties
 from fire.console import console_attr_os
 from fire.console import encoding as encoding_util
+from fire.console import text
 
 import six
 
 
+# TODO: Unify this logic with console.style.mappings
 class BoxLineCharacters(object):
   """Box/line drawing characters.
 
@@ -161,6 +164,18 @@ class BoxLineCharactersAscii(BoxLineCharacters):
   d_vr = '#'
 
 
+class BoxLineCharactersScreenReader(BoxLineCharactersAscii):
+  dl = ' '
+  dr = ' '
+  hd = ' '
+  hu = ' '
+  ul = ' '
+  ur = ' '
+  vh = ' '
+  vl = ' '
+  vr = ' '
+
+
 class ProgressTrackerSymbols(object):
   """Characters used by progress trackers."""
 
@@ -172,8 +187,8 @@ class ProgressTrackerSymbolsUnicode(ProgressTrackerSymbols):
   def spin_marks(self):
     return ['⠏', '⠛', '⠹', '⠼', '⠶', '⠧']
 
-  success = '✓'
-  failed = 'X'
+  success = text.TypedText(['✓'], text_type=text.TextTypes.PT_SUCCESS)
+  failed = text.TypedText(['X'], text_type=text.TextTypes.PT_FAILURE)
   interrupted = '-'
   not_started = '.'
   prefix_length = 2
@@ -232,7 +247,7 @@ class ConsoleAttr(object):
   _BULLETS_WINDOWS = ('■', '≡', '∞', 'Φ', '·')  # cp437 compatible unicode
   _BULLETS_ASCII = ('o', '*', '+', '-')
 
-  def __init__(self, encoding=None):
+  def __init__(self, encoding=None, suppress_output=False):
     """Constructor.
 
     Args:
@@ -240,6 +255,8 @@ def __init__(self, encoding=None):
         ascii -- ASCII art. This is the default.
         utf8 -- UTF-8 unicode.
         win -- Windows code page 437.
+      suppress_output: True to create a ConsoleAttr that doesn't want to output
+        anything.
     """
     # Normalize the encoding name.
     if not encoding:
@@ -247,7 +264,7 @@ def __init__(self, encoding=None):
     elif encoding == 'win':
       encoding = 'cp437'
     self._encoding = encoding or 'ascii'
-    self._term = os.getenv('TERM', '').lower()
+    self._term = '' if suppress_output else os.getenv('TERM', '').lower()
 
     # ANSI "standard" attributes.
     if self.SupportsAnsi():
@@ -263,23 +280,27 @@ def __init__(self, encoding=None):
       self._font_italic = ''
 
     # Encoded character attributes.
-    if self._encoding == 'utf8':
+    is_screen_reader = False
+    if self._encoding == 'utf8' and not is_screen_reader:
       self._box_line_characters = BoxLineCharactersUnicode()
       self._bullets = self._BULLETS_UNICODE
       self._progress_tracker_symbols = ProgressTrackerSymbolsUnicode()
-    elif self._encoding == 'cp437':
+    elif self._encoding == 'cp437' and not is_screen_reader:
       self._box_line_characters = BoxLineCharactersUnicode()
       self._bullets = self._BULLETS_WINDOWS
       # Windows does not suport the unicode characters used for the spinner.
       self._progress_tracker_symbols = ProgressTrackerSymbolsAscii()
     else:
       self._box_line_characters = BoxLineCharactersAscii()
+      if is_screen_reader:
+        self._box_line_characters = BoxLineCharactersScreenReader()
       self._bullets = self._BULLETS_ASCII
       self._progress_tracker_symbols = ProgressTrackerSymbolsAscii()
 
     # OS specific attributes.
     self._get_raw_key = [console_attr_os.GetRawKeyFunction()]
-    self._term_size = console_attr_os.GetTermSize()
+    self._term_size = (
+        (0, 0) if suppress_output else console_attr_os.GetTermSize())
 
     self._display_width_cache = {}
 
@@ -434,6 +455,14 @@ def GetRawKey(self):
     """
     return self._get_raw_key[0]()
 
+  def GetTermIdentifier(self):
+    """Returns the TERM envrionment variable for the console.
+
+    Returns:
+      str: A str that describes the console's text capabilities
+    """
+    return self._term
+
   def GetTermSize(self):
     """Returns the terminal (x, y) dimensions in characters.
 
diff --git a/fire/console/console_attr_os.py b/fire/console/console_attr_os.py
index 52decc2c..4c0c462e 100644
--- a/fire/console/console_attr_os.py
+++ b/fire/console/console_attr_os.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*- #
-# Copyright 2015 Google Inc. All Rights Reserved.
+# Copyright 2015 Google LLC. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,9 +14,6 @@
 # limitations under the License.
 
 """OS specific console_attr helper functions."""
-# This file contains platform specific code which is not currently handled
-# by pytype.
-# pytype: skip-file
 
 from __future__ import absolute_import
 from __future__ import division
diff --git a/fire/console/console_io.py b/fire/console/console_io.py
new file mode 100644
index 00000000..777f1f48
--- /dev/null
+++ b/fire/console/console_io.py
@@ -0,0 +1,108 @@
+# -*- coding: utf-8 -*- #
+# Copyright 2013 Google LLC. All Rights Reserved.
+#
+# 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.
+
+"""General console printing utilities used by the Cloud SDK."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import os
+import subprocess
+import sys
+
+from fire.console import console_attr
+from fire.console import console_pager
+from fire.console import encoding
+from fire.console import files
+
+
+def IsInteractive(output=False, error=False, heuristic=False):
+  """Determines if the current terminal session is interactive.
+
+  sys.stdin must be a terminal input stream.
+
+  Args:
+    output: If True then sys.stdout must also be a terminal output stream.
+    error: If True then sys.stderr must also be a terminal output stream.
+    heuristic: If True then we also do some additional heuristics to check if
+               we are in an interactive context. Checking home path for example.
+
+  Returns:
+    True if the current terminal session is interactive.
+  """
+  if not sys.stdin.isatty():
+    return False
+  if output and not sys.stdout.isatty():
+    return False
+  if error and not sys.stderr.isatty():
+    return False
+
+  if heuristic:
+    # Check the home path. Most startup scripts for example are executed by
+    # users that don't have a home path set. Home is OS dependent though, so
+    # check everything.
+    # *NIX OS usually sets the HOME env variable. It is usually '/home/user',
+    # but can also be '/root'. If it's just '/' we are most likely in an init
+    # script.
+    # Windows usually sets HOMEDRIVE and HOMEPATH. If they don't exist we are
+    # probably being run from a task scheduler context. HOMEPATH can be '\'
+    # when a user has a network mapped home directory.
+    # Cygwin has it all! Both Windows and Linux. Checking both is perfect.
+    home = os.getenv('HOME')
+    homepath = os.getenv('HOMEPATH')
+    if not homepath and (not home or home == '/'):
+      return False
+  return True
+
+
+def More(contents, out, prompt=None, check_pager=True):
+  """Run a user specified pager or fall back to the internal pager.
+
+  Args:
+    contents: The entire contents of the text lines to page.
+    out: The output stream.
+    prompt: The page break prompt.
+    check_pager: Checks the PAGER env var and uses it if True.
+  """
+  if not IsInteractive(output=True):
+    out.write(contents)
+    return
+  if check_pager:
+    pager = encoding.GetEncodedValue(os.environ, 'PAGER', None)
+    if pager == '-':
+      # Use the fallback Pager.
+      pager = None
+    elif not pager:
+      # Search for a pager that handles ANSI escapes.
+      for command in ('less', 'pager'):
+        if files.FindExecutableOnPath(command):
+          pager = command
+          break
+    if pager:
+      # If the pager is less(1) then instruct it to display raw ANSI escape
+      # sequences to enable colors and font embellishments.
+      less_orig = encoding.GetEncodedValue(os.environ, 'LESS', None)
+      less = '-R' + (less_orig or '')
+      encoding.SetEncodedValue(os.environ, 'LESS', less)
+      p = subprocess.Popen(pager, stdin=subprocess.PIPE, shell=True)
+      enc = console_attr.GetConsoleAttr().GetEncoding()
+      p.communicate(input=contents.encode(enc))
+      p.wait()
+      if less_orig is None:
+        encoding.SetEncodedValue(os.environ, 'LESS', None)
+      return
+  # Fall back to the internal pager.
+  console_pager.Pager(contents, out, prompt).Run()
diff --git a/fire/console/console_pager.py b/fire/console/console_pager.py
index c15a8ee5..044fcb37 100644
--- a/fire/console/console_pager.py
+++ b/fire/console/console_pager.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*- #
-# Copyright 2015 Google Inc. All Rights Reserved.
+# Copyright 2015 Google LLC. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/fire/console/encoding.py b/fire/console/encoding.py
index e8b1e571..780e5a28 100644
--- a/fire/console/encoding.py
+++ b/fire/console/encoding.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*- #
 
-# Copyright 2015 Google Inc. All Rights Reserved.
+# Copyright 2015 Google LLC. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/fire/console/files.py b/fire/console/files.py
new file mode 100644
index 00000000..69970f43
--- /dev/null
+++ b/fire/console/files.py
@@ -0,0 +1,116 @@
+# -*- coding: utf-8 -*- #
+# Copyright 2013 Google LLC. All Rights Reserved.
+#
+# 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.
+
+"""Some general file utilities used that can be used by the Cloud SDK."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import unicode_literals
+
+import os
+
+from fire.console import encoding as encoding_util
+from fire.console import platforms
+
+import six
+
+
+def _GetSystemPath():
+  """Returns properly encoded system PATH variable string."""
+  return encoding_util.GetEncodedValue(os.environ, 'PATH')
+
+
+def _FindExecutableOnPath(executable, path, pathext):
+  """Internal function to a find an executable.
+
+  Args:
+    executable: The name of the executable to find.
+    path: A list of directories to search separated by 'os.pathsep'.
+    pathext: An iterable of file name extensions to use.
+
+  Returns:
+    str, the path to a file on `path` with name `executable` + `p` for
+      `p` in `pathext`.
+
+  Raises:
+    ValueError: invalid input.
+  """
+
+  if isinstance(pathext, six.string_types):
+    raise ValueError('_FindExecutableOnPath(..., pathext=\'{0}\') failed '
+                     'because pathext must be an iterable of strings, but got '
+                     'a string.'.format(pathext))
+
+  # Prioritize preferred extension over earlier in path.
+  for ext in pathext:
+    for directory in path.split(os.pathsep):
+      # Windows can have paths quoted.
+      directory = directory.strip('"')
+      full = os.path.normpath(os.path.join(directory, executable) + ext)
+      # On Windows os.access(full, os.X_OK) is always True.
+      if os.path.isfile(full) and os.access(full, os.X_OK):
+        return full
+  return None
+
+
+def _PlatformExecutableExtensions(platform):
+  if platform == platforms.OperatingSystem.WINDOWS:
+    return ('.exe', '.cmd', '.bat', '.com', '.ps1')
+  else:
+    return ('', '.sh')
+
+
+def FindExecutableOnPath(executable, path=None, pathext=None,
+                         allow_extensions=False):
+  """Searches for `executable` in the directories listed in `path` or $PATH.
+
+  Executable must not contain a directory or an extension.
+
+  Args:
+    executable: The name of the executable to find.
+    path: A list of directories to search separated by 'os.pathsep'.  If None
+      then the system PATH is used.
+    pathext: An iterable of file name extensions to use.  If None then
+      platform specific extensions are used.
+    allow_extensions: A boolean flag indicating whether extensions in the
+      executable are allowed.
+
+  Returns:
+    The path of 'executable' (possibly with a platform-specific extension) if
+    found and executable, None if not found.
+
+  Raises:
+    ValueError: if executable has a path or an extension, and extensions are
+      not allowed, or if there's an internal error.
+  """
+
+  if not allow_extensions and os.path.splitext(executable)[1]:
+    raise ValueError('FindExecutableOnPath({0},...) failed because first '
+                     'argument must not have an extension.'.format(executable))
+
+  if os.path.dirname(executable):
+    raise ValueError('FindExecutableOnPath({0},...) failed because first '
+                     'argument must not have a path.'.format(executable))
+
+  if path is None:
+    effective_path = _GetSystemPath()
+  else:
+    effective_path = path
+  effective_pathext = (pathext if pathext is not None
+                       else _PlatformExecutableExtensions(
+                           platforms.OperatingSystem.Current()))
+
+  return _FindExecutableOnPath(executable, effective_path,
+                               effective_pathext)
diff --git a/fire/console/platforms.py b/fire/console/platforms.py
new file mode 100644
index 00000000..018eb89e
--- /dev/null
+++ b/fire/console/platforms.py
@@ -0,0 +1,481 @@
+# -*- coding: utf-8 -*- #
+# Copyright 2013 Google LLC. All Rights Reserved.
+#
+# 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.
+
+"""Utilities for determining the current platform and architecture."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import unicode_literals
+
+import os
+import platform
+import subprocess
+import sys
+
+
+class Error(Exception):
+  """Base class for exceptions in the platforms module."""
+  pass
+
+
+class InvalidEnumValue(Error):  # pylint: disable=g-bad-exception-name
+  """Exception for when a string could not be parsed to a valid enum value."""
+
+  def __init__(self, given, enum_type, options):
+    """Constructs a new exception.
+
+    Args:
+      given: str, The given string that could not be parsed.
+      enum_type: str, The human readable name of the enum you were trying to
+        parse.
+      options: list(str), The valid values for this enum.
+    """
+    super(InvalidEnumValue, self).__init__(
+        'Could not parse [{0}] into a valid {1}.  Valid values are [{2}]'
+        .format(given, enum_type, ', '.join(options)))
+
+
+class OperatingSystem(object):
+  """An enum representing the operating system you are running on."""
+
+  class _OS(object):
+    """A single operating system."""
+
+    # pylint: disable=redefined-builtin
+    def __init__(self, id, name, file_name):
+      self.id = id
+      self.name = name
+      self.file_name = file_name
+
+    def __str__(self):
+      return self.id
+
+    def __eq__(self, other):
+      return (isinstance(other, type(self)) and
+              self.id == other.id and
+              self.name == other.name and
+              self.file_name == other.file_name)
+
+    def __hash__(self):
+      return hash(self.id) + hash(self.name) + hash(self.file_name)
+
+    def __ne__(self, other):
+      return not self == other
+
+    @classmethod
+    def _CmpHelper(cls, x, y):
+      """Just a helper equivalent to the cmp() function in Python 2."""
+      return (x > y) - (x < y)
+
+    def __lt__(self, other):
+      return self._CmpHelper(
+          (self.id, self.name, self.file_name),
+          (other.id, other.name, other.file_name)) < 0
+
+    def __gt__(self, other):
+      return self._CmpHelper(
+          (self.id, self.name, self.file_name),
+          (other.id, other.name, other.file_name)) > 0
+
+    def __le__(self, other):
+      return not self.__gt__(other)
+
+    def __ge__(self, other):
+      return not self.__lt__(other)
+
+  WINDOWS = _OS('WINDOWS', 'Windows', 'windows')
+  MACOSX = _OS('MACOSX', 'Mac OS X', 'darwin')
+  LINUX = _OS('LINUX', 'Linux', 'linux')
+  CYGWIN = _OS('CYGWIN', 'Cygwin', 'cygwin')
+  MSYS = _OS('MSYS', 'Msys', 'msys')
+  _ALL = [WINDOWS, MACOSX, LINUX, CYGWIN, MSYS]
+
+  @staticmethod
+  def AllValues():
+    """Gets all possible enum values.
+
+    Returns:
+      list, All the enum values.
+    """
+    return list(OperatingSystem._ALL)
+
+  @staticmethod
+  def FromId(os_id, error_on_unknown=True):
+    """Gets the enum corresponding to the given operating system id.
+
+    Args:
+      os_id: str, The operating system id to parse
+      error_on_unknown: bool, True to raise an exception if the id is unknown,
+        False to just return None.
+
+    Raises:
+      InvalidEnumValue: If the given value cannot be parsed.
+
+    Returns:
+      OperatingSystemTuple, One of the OperatingSystem constants or None if the
+      input is None.
+    """
+    if not os_id:
+      return None
+    for operating_system in OperatingSystem._ALL:
+      if operating_system.id == os_id:
+        return operating_system
+    if error_on_unknown:
+      raise InvalidEnumValue(os_id, 'Operating System',
+                             [value.id for value in OperatingSystem._ALL])
+    return None
+
+  @staticmethod
+  def Current():
+    """Determines the current operating system.
+
+    Returns:
+      OperatingSystemTuple, One of the OperatingSystem constants or None if it
+      cannot be determined.
+    """
+    if os.name == 'nt':
+      return OperatingSystem.WINDOWS
+    elif 'linux' in sys.platform:
+      return OperatingSystem.LINUX
+    elif 'darwin' in sys.platform:
+      return OperatingSystem.MACOSX
+    elif 'cygwin' in sys.platform:
+      return OperatingSystem.CYGWIN
+    return None
+
+  @staticmethod
+  def IsWindows():
+    """Returns True if the current operating system is Windows."""
+    return OperatingSystem.Current() is OperatingSystem.WINDOWS
+
+
+class Architecture(object):
+  """An enum representing the system architecture you are running on."""
+
+  class _ARCH(object):
+    """A single architecture."""
+
+    # pylint: disable=redefined-builtin
+    def __init__(self, id, name, file_name):
+      self.id = id
+      self.name = name
+      self.file_name = file_name
+
+    def __str__(self):
+      return self.id
+
+    def __eq__(self, other):
+      return (isinstance(other, type(self)) and
+              self.id == other.id and
+              self.name == other.name and
+              self.file_name == other.file_name)
+
+    def __hash__(self):
+      return hash(self.id) + hash(self.name) + hash(self.file_name)
+
+    def __ne__(self, other):
+      return not self == other
+
+    @classmethod
+    def _CmpHelper(cls, x, y):
+      """Just a helper equivalent to the cmp() function in Python 2."""
+      return (x > y) - (x < y)
+
+    def __lt__(self, other):
+      return self._CmpHelper(
+          (self.id, self.name, self.file_name),
+          (other.id, other.name, other.file_name)) < 0
+
+    def __gt__(self, other):
+      return self._CmpHelper(
+          (self.id, self.name, self.file_name),
+          (other.id, other.name, other.file_name)) > 0
+
+    def __le__(self, other):
+      return not self.__gt__(other)
+
+    def __ge__(self, other):
+      return not self.__lt__(other)
+
+  x86 = _ARCH('x86', 'x86', 'x86')
+  x86_64 = _ARCH('x86_64', 'x86_64', 'x86_64')
+  ppc = _ARCH('PPC', 'PPC', 'ppc')
+  arm = _ARCH('arm', 'arm', 'arm')
+  _ALL = [x86, x86_64, ppc, arm]
+
+  # Possible values for `uname -m` and what arch they map to.
+  # Examples of possible values: https://en.wikipedia.org/wiki/Uname
+  _MACHINE_TO_ARCHITECTURE = {
+      'amd64': x86_64, 'x86_64': x86_64, 'i686-64': x86_64,
+      'i386': x86, 'i686': x86, 'x86': x86,
+      'ia64': x86,  # Itanium is different x64 arch, treat it as the common x86.
+      'powerpc': ppc, 'power macintosh': ppc, 'ppc64': ppc,
+      'armv6': arm, 'armv6l': arm, 'arm64': arm, 'armv7': arm, 'armv7l': arm}
+
+  @staticmethod
+  def AllValues():
+    """Gets all possible enum values.
+
+    Returns:
+      list, All the enum values.
+    """
+    return list(Architecture._ALL)
+
+  @staticmethod
+  def FromId(architecture_id, error_on_unknown=True):
+    """Gets the enum corresponding to the given architecture id.
+
+    Args:
+      architecture_id: str, The architecture id to parse
+      error_on_unknown: bool, True to raise an exception if the id is unknown,
+        False to just return None.
+
+    Raises:
+      InvalidEnumValue: If the given value cannot be parsed.
+
+    Returns:
+      ArchitectureTuple, One of the Architecture constants or None if the input
+      is None.
+    """
+    if not architecture_id:
+      return None
+    for arch in Architecture._ALL:
+      if arch.id == architecture_id:
+        return arch
+    if error_on_unknown:
+      raise InvalidEnumValue(architecture_id, 'Architecture',
+                             [value.id for value in Architecture._ALL])
+    return None
+
+  @staticmethod
+  def Current():
+    """Determines the current system architecture.
+
+    Returns:
+      ArchitectureTuple, One of the Architecture constants or None if it cannot
+      be determined.
+    """
+    return Architecture._MACHINE_TO_ARCHITECTURE.get(platform.machine().lower())
+
+
+class Platform(object):
+  """Holds an operating system and architecture."""
+
+  def __init__(self, operating_system, architecture):
+    """Constructs a new platform.
+
+    Args:
+      operating_system: OperatingSystem, The OS
+      architecture: Architecture, The machine architecture.
+    """
+    self.operating_system = operating_system
+    self.architecture = architecture
+
+  def __str__(self):
+    return '{}-{}'.format(self.operating_system, self.architecture)
+
+  @staticmethod
+  def Current(os_override=None, arch_override=None):
+    """Determines the current platform you are running on.
+
+    Args:
+      os_override: OperatingSystem, A value to use instead of the current.
+      arch_override: Architecture, A value to use instead of the current.
+
+    Returns:
+      Platform, The platform tuple of operating system and architecture.  Either
+      can be None if it could not be determined.
+    """
+    return Platform(
+        os_override if os_override else OperatingSystem.Current(),
+        arch_override if arch_override else Architecture.Current())
+
+  def UserAgentFragment(self):
+    """Generates the fragment of the User-Agent that represents the OS.
+
+    Examples:
+      (Linux 3.2.5-gg1236)
+      (Windows NT 6.1.7601)
+      (Macintosh; PPC Mac OS X 12.4.0)
+      (Macintosh; Intel Mac OS X 12.4.0)
+
+    Returns:
+      str, The fragment of the User-Agent string.
+    """
+    # Below, there are examples of the value of platform.uname() per platform.
+    # platform.release() is uname[2], platform.version() is uname[3].
+    if self.operating_system == OperatingSystem.LINUX:
+      # ('Linux', '<hostname goes here>', '3.2.5-gg1236',
+      # '#1 SMP Tue May 21 02:35:06 PDT 2013', 'x86_64', 'x86_64')
+      return '({name} {version})'.format(
+          name=self.operating_system.name, version=platform.release())
+    elif self.operating_system == OperatingSystem.WINDOWS:
+      # ('Windows', '<hostname goes here>', '7', '6.1.7601', 'AMD64',
+      # 'Intel64 Family 6 Model 45 Stepping 7, GenuineIntel')
+      return '({name} NT {version})'.format(
+          name=self.operating_system.name, version=platform.version())
+    elif self.operating_system == OperatingSystem.MACOSX:
+      # ('Darwin', '<hostname goes here>', '12.4.0',
+      # 'Darwin Kernel Version 12.4.0: Wed May  1 17:57:12 PDT 2013;
+      # root:xnu-2050.24.15~1/RELEASE_X86_64', 'x86_64', 'i386')
+      format_string = '(Macintosh; {name} Mac OS X {version})'
+      arch_string = (self.architecture.name
+                     if self.architecture == Architecture.ppc else 'Intel')
+      return format_string.format(
+          name=arch_string, version=platform.release())
+    else:
+      return '()'
+
+  def AsyncPopenArgs(self):
+    """Returns the args for spawning an async process using Popen on this OS.
+
+    Make sure the main process does not wait for the new process. On windows
+    this means setting the 0x8 creation flag to detach the process.
+
+    Killing a group leader kills the whole group. Setting creation flag 0x200 on
+    Windows or running setsid on *nix makes sure the new process is in a new
+    session with the new process the group leader. This means it can't be killed
+    if the parent is killed.
+
+    Finally, all file descriptors (FD) need to be closed so that waiting for the
+    output of the main process does not inadvertently wait for the output of the
+    new process, which means waiting for the termination of the new process.
+    If the new process wants to write to a file, it can open new FDs.
+
+    Returns:
+      {str:}, The args for spawning an async process using Popen on this OS.
+    """
+    args = {}
+    if self.operating_system == OperatingSystem.WINDOWS:
+      args['close_fds'] = True  # This is enough to close _all_ FDs on windows.
+      detached_process = 0x00000008
+      create_new_process_group = 0x00000200
+      # 0x008 | 0x200 == 0x208
+      args['creationflags'] = detached_process | create_new_process_group
+    else:
+      # Killing a group leader kills the whole group.
+      # Create a new session with the new process the group leader.
+      args['preexec_fn'] = os.setsid
+      args['close_fds'] = True  # This closes all FDs _except_ 0, 1, 2 on *nix.
+      args['stdin'] = subprocess.PIPE
+      args['stdout'] = subprocess.PIPE
+      args['stderr'] = subprocess.PIPE
+    return args
+
+
+class PythonVersion(object):
+  """Class to validate the Python version we are using.
+
+  The Cloud SDK officially supports Python 2.7.
+
+  However, many commands do work with Python 2.6, so we don't error out when
+  users are using this (we consider it sometimes "compatible" but not
+  "supported").
+  """
+
+  # See class docstring for descriptions of what these mean
+  MIN_REQUIRED_PY2_VERSION = (2, 6)
+  MIN_SUPPORTED_PY2_VERSION = (2, 7)
+  MIN_SUPPORTED_PY3_VERSION = (3, 4)
+  ENV_VAR_MESSAGE = """\
+
+If you have a compatible Python interpreter installed, you can use it by setting
+the CLOUDSDK_PYTHON environment variable to point to it.
+
+"""
+
+  def __init__(self, version=None):
+    if version:
+      self.version = version
+    elif hasattr(sys, 'version_info'):
+      self.version = sys.version_info[:2]
+    else:
+      self.version = None
+
+  def SupportedVersionMessage(self, allow_py3):
+    if allow_py3:
+      return 'Please use Python version {0}.{1}.x or {2}.{3} and up.'.format(
+          PythonVersion.MIN_SUPPORTED_PY2_VERSION[0],
+          PythonVersion.MIN_SUPPORTED_PY2_VERSION[1],
+          PythonVersion.MIN_SUPPORTED_PY3_VERSION[0],
+          PythonVersion.MIN_SUPPORTED_PY3_VERSION[1])
+    else:
+      return 'Please use Python version {0}.{1}.x.'.format(
+          PythonVersion.MIN_SUPPORTED_PY2_VERSION[0],
+          PythonVersion.MIN_SUPPORTED_PY2_VERSION[1])
+
+  def IsCompatible(self, allow_py3=False, raise_exception=False):
+    """Ensure that the Python version we are using is compatible.
+
+    This will print an error message if not compatible.
+
+    Compatible versions are 2.6 and 2.7 and > 3.4 if allow_py3 is True.
+    We don't guarantee support for 2.6 so we want to warn about it.
+
+    Args:
+      allow_py3: bool, True if we should allow a Python 3 interpreter to run
+        gcloud. If False, this returns an error for Python 3.
+      raise_exception: bool, True to raise an exception rather than printing
+        the error and exiting.
+
+    Raises:
+      Error: If not compatible and raise_exception is True.
+
+    Returns:
+      bool, True if the version is valid, False otherwise.
+    """
+    error = None
+    if not self.version:
+      # We don't know the version, not a good sign.
+      error = ('ERROR: Your current version of Python is not compatible with '
+               'the Google Cloud SDK. {0}\n'
+               .format(self.SupportedVersionMessage(allow_py3)))
+    else:
+      if self.version[0] < 3:
+        # Python 2 Mode
+        if self.version < PythonVersion.MIN_REQUIRED_PY2_VERSION:
+          error = ('ERROR: Python {0}.{1} is not compatible with the Google '
+                   'Cloud SDK. {2}\n'
+                   .format(self.version[0], self.version[1],
+                           self.SupportedVersionMessage(allow_py3)))
+      else:
+        # Python 3 Mode
+        if not allow_py3:
+          error = ('ERROR: Python 3 and later is not compatible with the '
+                   'Google Cloud SDK. {0}\n'
+                   .format(self.SupportedVersionMessage(allow_py3)))
+        elif self.version < PythonVersion.MIN_SUPPORTED_PY3_VERSION:
+          error = ('ERROR: Python {0}.{1} is not compatible with the Google '
+                   'Cloud SDK. {2}\n'
+                   .format(self.version[0], self.version[1],
+                           self.SupportedVersionMessage(allow_py3)))
+
+    if error:
+      if raise_exception:
+        raise Error(error)
+      sys.stderr.write(error)
+      sys.stderr.write(PythonVersion.ENV_VAR_MESSAGE)
+      return False
+
+    # Warn that 2.6 might not work.
+    if (self.version >= self.MIN_REQUIRED_PY2_VERSION and
+        self.version < self.MIN_SUPPORTED_PY2_VERSION):
+      sys.stderr.write("""\
+WARNING:  Python 2.6.x is no longer officially supported by the Google Cloud SDK
+and may not function correctly.  {0}
+{1}""".format(self.SupportedVersionMessage(allow_py3),
+              PythonVersion.ENV_VAR_MESSAGE))
+
+    return True
diff --git a/fire/console/text.py b/fire/console/text.py
new file mode 100644
index 00000000..73e68488
--- /dev/null
+++ b/fire/console/text.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*- #
+# Copyright 2018 Google LLC. All Rights Reserved.
+#
+# 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.
+"""Semantic text objects that are used for styled outputting."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import unicode_literals
+
+import enum
+
+
+class TextAttributes(object):
+  """Attributes to use to style text with."""
+
+  def __init__(self, format_str=None, color=None, attrs=None):
+    """Defines a set of attributes for a piece of text.
+
+    Args:
+      format_str: (str), string that will be used to format the text
+        with. For example '[{}]', to enclose text in brackets.
+      color: (Colors), the color the text should be formatted with.
+      attrs: (Attrs), the attributes to apply to text.
+    """
+    self._format_str = format_str
+    self._color = color
+    self._attrs = attrs or []
+
+  @property
+  def format_str(self):
+    return self._format_str
+
+  @property
+  def color(self):
+    return self._color
+
+  @property
+  def attrs(self):
+    return self._attrs
+
+
+class TypedText(object):
+  """Text with a semantic type that will be used for styling."""
+
+  def __init__(self, texts, text_type=None):
+    """String of text and a corresponding type to use to style that text.
+
+    Args:
+     texts: (list[str]), list of strs or TypedText objects
+       that should be styled using text_type.
+     text_type: (TextTypes), the semantic type of the text that
+       will be used to style text.
+    """
+    self.texts = texts
+    self.text_type = text_type
+
+  def __len__(self):
+    length = 0
+    for text in self.texts:
+      length += len(text)
+    return length
+
+  def __add__(self, other):
+    texts = [self, other]
+    return TypedText(texts)
+
+  def __radd__(self, other):
+    texts = [other, self]
+    return TypedText(texts)
+
+
+class _TextTypes(enum.Enum):
+  """Text types base class that defines base functionality."""
+
+  def __call__(self, *args):
+    """Returns a TypedText object using this style."""
+    return TypedText(list(args), self)
+
+
+# TODO: Add more types.
+class TextTypes(_TextTypes):
+  """Defines text types that can be used for styling text."""
+  RESOURCE_NAME = 1
+  URL = 2
+  USER_INPUT = 3
+  COMMAND = 4
+  INFO = 5
+  URI = 6
+  OUTPUT = 7
+  PT_SUCCESS = 8
+  PT_FAILURE = 9
+
diff --git a/fire/core.py b/fire/core.py
index 83246522..bec6f1a6 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -71,7 +71,7 @@ def main(argv):
 from fire import parser
 from fire import trace
 from fire import value_types
-from fire.console import console_pager
+from fire.console import console_io
 import six
 
 
@@ -168,13 +168,7 @@ def Fire(component=None, command=None, name=None):
 
 def Display(lines, out):
   text = '\n'.join(lines) + '\n'
-  pager = console_pager.Pager(text, out=out)
-  try:
-    pager.Run()
-  except:  # pylint: disable=bare-except
-    # pager.Run() fails with termios.error(25, 'Inappropriate ioctl for device')
-    # for outputs that don't fit on a single screen in our test environment.
-    pass
+  console_io.More(text, out=out)
 
 
 def CompletionScript(name, component, shell):

From d43cbf09b32eca2764158414d3484d4e4dbcf4ea Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 26 Jun 2019 13:19:01 -0700
Subject: [PATCH 113/324] Print, rather than Display, errors and usage screens.

PiperOrigin-RevId: 255250566
Change-Id: I77e331b41dcfa5d3fa7e0bb569f58cb60b91c350
---
 fire/core.py | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index bec6f1a6..3e889403 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -278,14 +278,14 @@ def _DisplayError(component_trace):
     help_text = helptext.HelpText(result, component_trace,
                                   component_trace.verbose)
     output.append(help_text)
+    Display(output, out=sys.stderr)
   else:
-    output.append(formatting.Error('ERROR: ')
-                  + component_trace.elements[-1].ErrorAsStr())
+    print(formatting.Error('ERROR: ')
+          + component_trace.elements[-1].ErrorAsStr(),
+          file=sys.stderr)
     error_text = helptext.UsageText(result, component_trace,
                                     component_trace.verbose)
-    output.append(error_text)
-
-  Display(output, out=sys.stderr)
+    print(error_text, file=sys.stderr)
 
 
 def _DictAsString(result, verbose=False):

From 294fd9549e5a30a2e3873f20a92f5333cd2166e9 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 26 Jun 2019 13:19:56 -0700
Subject: [PATCH 114/324] Support non-string value keys in helptext usage
 details sections.

PiperOrigin-RevId: 255250733
Change-Id: Ieadcbfc1513652872100e4ec6fde3f18e382e3fd
---
 fire/helptext.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index d990fe4d..8cf58229 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -378,13 +378,15 @@ def ValuesUsageDetailsSection(component, values):
   for value_name, value in values:
     del value
     init_info = inspectutils.Info(component.__class__.__init__)
-    value_item = value_name
+    value_item = None
     if 'docstring_info' in init_info:
       init_docstring_info = init_info['docstring_info']
       if init_docstring_info.args:
         for arg_info in init_docstring_info.args:
           if arg_info.name == value_name:
             value_item = _CreateItem(value_name, arg_info.description)
+    if value_item is None:
+      value_item = str(value_name)
     value_item_strings.append(value_item)
   return ('VALUES', _NewChoicesSection('VALUE', value_item_strings))
 

From 39fd897a7293c9deafd3b390c2378b250b6dea6e Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 26 Jun 2019 13:20:55 -0700
Subject: [PATCH 115/324] Fixes help text when key in map is non-string value.

PiperOrigin-RevId: 255250914
Change-Id: I2112653d25f07b1df11a2fc97e86aa242a753b15
---
 fire/helptext.py        | 1 +
 fire/test_components.py | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index 8cf58229..20a2d769 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -501,6 +501,7 @@ def UsageTextForObject(component, trace=None, verbose=False):
 
   members = completion._Members(component, verbose)  # pylint: disable=protected-access
   for member_name, member in members:
+    member_name = str(member_name)
     if value_types.IsGroup(member):
       groups.append(member_name)
     if value_types.IsCommand(member):
diff --git a/fire/test_components.py b/fire/test_components.py
index ad3eecd6..b3d2d3c4 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -201,7 +201,7 @@ def underscore_function(self, underscore_arg):
 class BoolConverter(object):
 
   def as_bool(self, arg=False):
-    return arg
+    return bool(arg)
 
 
 class ReturnsObj(object):

From b2fbfec5bc1bcf67fac5f7b0663b97322faadc9a Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 27 Jun 2019 05:37:40 -0700
Subject: [PATCH 116/324] use proper main method in binary

PiperOrigin-RevId: 255381948
Change-Id: I494dac9bb5db9c93fa32a830656d686fc148816e
---
 fire/test_components_bin.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/fire/test_components_bin.py b/fire/test_components_bin.py
index 3dc52ddd..fbb41952 100644
--- a/fire/test_components_bin.py
+++ b/fire/test_components_bin.py
@@ -25,8 +25,7 @@
 from fire import test_components
 
 
-def main(argv):
-  del argv  # Unused.
+def main():
   fire.Fire(test_components)
 
 if __name__ == '__main__':

From 1c9abf3b5a1c9fd722f15357c6731b9f6d4325d2 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 27 Jun 2019 11:50:51 -0700
Subject: [PATCH 117/324] Add pytype disable lines for console package.

PiperOrigin-RevId: 255447487
Change-Id: I1a41485b812ed94d6d0cb651b36b510db979d576
---
 fire/console/console_attr_os.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/fire/console/console_attr_os.py b/fire/console/console_attr_os.py
index 4c0c462e..8482c7bc 100644
--- a/fire/console/console_attr_os.py
+++ b/fire/console/console_attr_os.py
@@ -14,6 +14,9 @@
 # limitations under the License.
 
 """OS specific console_attr helper functions."""
+# This file contains platform specific code which is not currently handled
+# by pytype.
+# pytype: skip-file
 
 from __future__ import absolute_import
 from __future__ import division

From 0e1dfe50ccb6268805e619a5ccc5b4163065f047 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 1 Jul 2019 12:18:00 -0700
Subject: [PATCH 118/324] Allow multiple handlers to be triggered for a single
 object.

PiperOrigin-RevId: 256006046
Change-Id: Iee945b2875a63f3c8afe0c38b5d490f5a0c6ed15
---
 fire/core.py            | 169 +++++++++++++++++++++-------------------
 fire/core_test.py       |   9 +++
 fire/decorators.py      |   6 +-
 fire/fire_test.py       |  10 +++
 fire/test_components.py |  14 ++++
 5 files changed, 126 insertions(+), 82 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index 3e889403..0a56e5cf 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -356,9 +356,15 @@ def _Fire(component, args, parsed_flag_args, context, name=None):
 
   2. Start with component as the current component.
   2a. If the current component is a class, instantiate it using args from args.
-  2b. If the current component is a routine, call it using args from args.
-  2c. Otherwise access a member from component using an arg from args.
-  2d. Repeat 2a-2c until no args remain.
+  2b. If the component is a routine, call it using args from args.
+  2c. If the component is a sequence, index into it using an arg from
+      args.
+  2d. If possible, access a member from the component using an arg from args.
+  2e. If the component is a callable object, call it using args from args.
+  2f. Repeat 2a-2e until no args remain.
+  Note: Only the first applicable rule from 2a-2e is applied in each iteration.
+  After each iteration of step 2a-2e, the current component is updated to be the
+  result of the applied rule.
 
   3a. Embed into ipython REPL if interactive mode is selected.
   3b. Generate a completion script if that flag is provided.
@@ -427,101 +433,97 @@ def _Fire(component, args, parsed_flag_args, context, name=None):
       used_separator = True
     assert separator not in remaining_args
 
-    if inspect.isclass(component) or inspect.isroutine(component):
+    handled = False
+    candidate_errors = []
+
+    is_callable = inspect.isclass(component) or inspect.isroutine(component)
+    is_callable_object = callable(component) and not is_callable
+    is_sequence = isinstance(component, (list, tuple))
+    is_map = isinstance(component, dict) or inspectutils.IsNamedTuple(component)
+
+    if not handled and is_callable:
       # The component is a class or a routine; we'll try to initialize it or
       # call it.
-      isclass = inspect.isclass(component)
+      is_class = inspect.isclass(component)
 
       try:
         component, remaining_args = _CallAndUpdateTrace(
             component,
             remaining_args,
             component_trace,
-            treatment='class' if isclass else 'routine',
+            treatment='class' if is_class else 'routine',
             target=component.__name__)
+        handled = True
       except FireError as error:
-        component_trace.AddError(error, initial_args)
-        return component_trace
+        candidate_errors.append((error, initial_args))
 
-      if last_component is initial_component:
+      if handled and last_component is initial_component:
         # If the initial component is a class, keep an instance for use with -i.
         instance = component
 
-    elif (isinstance(component, (list, tuple)) and remaining_args
-          and not inspectutils.IsNamedTuple(component)):
+    if not handled and is_sequence and remaining_args:
       # The component is a tuple or list; we'll try to access a member.
       arg = remaining_args[0]
       try:
         index = int(arg)
         component = component[index]
+        handled = True
       except (ValueError, IndexError):
         error = FireError(
             'Unable to index into component with argument:', arg)
-        component_trace.AddError(error, initial_args)
-        return component_trace
-
-      remaining_args = remaining_args[1:]
-      filename = None
-      lineno = None
-      component_trace.AddAccessedProperty(
-          component, index, [arg], filename, lineno)
-
-    elif ((isinstance(component, dict) or inspectutils.IsNamedTuple(component))
-          and remaining_args):
-      # The component is a dict; we'll try to access a member.
+        candidate_errors.append((error, initial_args))
+
+      if handled:
+        remaining_args = remaining_args[1:]
+        filename = None
+        lineno = None
+        component_trace.AddAccessedProperty(
+            component, index, [arg], filename, lineno)
+
+    if not handled and is_map and remaining_args:
+      # The component is a dict or other key-value map; try to access a member.
       target = remaining_args[0]
 
-      # Allow indexing for namedtuples.
-      try:
-        index = int(target)
-        is_target_int = True
-      except ValueError:
-        is_target_int = False
-
-      if inspectutils.IsNamedTuple(component) and is_target_int:
-        try:
-          component = component[index]
-        except (ValueError, IndexError):
-          error = FireError(
-              'Unable to index into component with argument:', target)
-          component_trace.AddError(error, initial_args)
-          return component_trace
-      elif target in component:
-        component = component[target]
-      elif target.replace('-', '_') in component:
-        component = component[target.replace('-', '_')]
+      # Treat namedtuples as dicts when handling them as a map.
+      if inspectutils.IsNamedTuple(component):
+        component_dict = component._asdict()  # pytype: disable=attribute-error
+      else:
+        component_dict = component
+
+      if target in component_dict:
+        component = component_dict[target]
+        handled = True
+      elif target.replace('-', '_') in component_dict:
+        component = component_dict[target.replace('-', '_')]
+        handled = True
       else:
-        # The target isn't present in the dict as a string, but maybe it is as
-        # another type.
+        # The target isn't present in the dict as a string key, but maybe it is
+        # a key as another type.
         # TODO(dbieber): Consider alternatives for accessing non-string keys.
-        found_target = False
-        # If the component is a namedtuple, we need to convert it to dict to
-        # be able to use the .items() method.
-        if inspectutils.IsNamedTuple(component):
-          component = component._asdict()  # pytype: disable=attribute-error
-        for key, value in component.items():
+        for key, value in component_dict.items():
           if target == str(key):
             component = value
-            found_target = True
+            handled = True
             break
-        if not found_target:
-          error = FireError('Cannot find target in dict:', target)
-          component_trace.AddError(error, initial_args)
-          return component_trace
-
-      remaining_args = remaining_args[1:]
-      filename = None
-      lineno = None
-      component_trace.AddAccessedProperty(
-          component, target, [target], filename, lineno)
-
-    elif remaining_args:
-      # We'll try to access a member of the component.
+
+      if handled:
+        remaining_args = remaining_args[1:]
+        filename = None
+        lineno = None
+        component_trace.AddAccessedProperty(
+            component, target, [target], filename, lineno)
+      else:
+        error = FireError('Cannot find key:', target)
+        candidate_errors.append((error, initial_args))
+
+    if not handled and remaining_args:
+      # Object handler. We'll try to access a member of the component.
       try:
         target = remaining_args[0]
 
         component, consumed_args, remaining_args = _GetMember(
             component, remaining_args)
+        handled = True
 
         filename, lineno = inspectutils.GetFileAndLine(component)
 
@@ -529,19 +531,25 @@ def _Fire(component, args, parsed_flag_args, context, name=None):
             component, target, consumed_args, filename, lineno)
 
       except FireError as error:
-        if not callable(component):
-          component_trace.AddError(error, initial_args)
-          return component_trace
-
-        # If we can't access the member, try to treat component as a callable.
-        try:
-          component, remaining_args = _CallAndUpdateTrace(component,
-                                                          remaining_args,
-                                                          component_trace,
-                                                          treatment='callable')
-        except FireError as error:
-          component_trace.AddError(error, initial_args)
-          return component_trace
+        # Couldn't access member.
+        candidate_errors.append((error, initial_args))
+
+    if not handled and is_callable_object:
+      # The component is a callable object; we'll try to call it.
+      try:
+        component, remaining_args = _CallAndUpdateTrace(
+            component,
+            remaining_args,
+            component_trace,
+            treatment='callable')
+        handled = True
+      except FireError as error:
+        candidate_errors.append((error, initial_args))
+
+    if not handled and candidate_errors:
+      error, initial_args = candidate_errors[0]
+      component_trace.AddError(error, initial_args)
+      return component_trace
 
     if used_separator:
       # Add back in the arguments from after the separator.
@@ -644,8 +652,9 @@ def _CallAndUpdateTrace(component, args, component_trace, treatment='class',
   if not target:
     target = component
   filename, lineno = inspectutils.GetFileAndLine(component)
+  metadata = decorators.GetMetadata(component)
   fn = component.__call__ if treatment == 'callable' else component
-  parse = _MakeParseFn(fn)
+  parse = _MakeParseFn(fn, metadata)
   (varargs, kwargs), consumed_args, remaining_args, capacity = parse(args)
   component = fn(*varargs, **kwargs)
 
@@ -662,11 +671,12 @@ def _CallAndUpdateTrace(component, args, component_trace, treatment='class',
   return component, remaining_args
 
 
-def _MakeParseFn(fn):
+def _MakeParseFn(fn, metadata):
   """Creates a parse function for fn.
 
   Args:
     fn: The function or class to create the parse function for.
+    metadata: Additional metadata about the component the parse function is for.
   Returns:
     A parse function for fn. The parse function accepts a list of arguments
     and returns (varargs, kwargs), remaining_args. The original function fn
@@ -674,7 +684,6 @@ def _MakeParseFn(fn):
     the leftover args from the arguments to the parse function.
   """
   fn_spec = inspectutils.GetFullArgSpec(fn)
-  metadata = decorators.GetMetadata(fn)
 
   # Note: num_required_args is the number of positional arguments without
   # default values. All of these arguments are required.
diff --git a/fire/core_test.py b/fire/core_test.py
index 8a02411e..d2607666 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -144,6 +144,10 @@ def testPrintNamedTupleField(self):
     with self.assertOutputMatches(stdout='11', stderr=None):
       core.Fire(tc.NamedTuple, command=['point', 'x'])
 
+  def testPrintNamedTupleFieldNameEqualsValue(self):
+    with self.assertOutputMatches(stdout='x', stderr=None):
+      core.Fire(tc.NamedTuple, command=['matching_names', 'x'])
+
   def testPrintNamedTupleIndex(self):
     with self.assertOutputMatches(stdout='22', stderr=None):
       core.Fire(tc.NamedTuple, command=['point', '1'])
@@ -160,6 +164,11 @@ def testCallable(self):
     with self.assertOutputMatches(stdout=r'', stderr=None):
       core.Fire(tc.CallableWithKeywordArgument(), command=[])
 
+  def testCallableWithPositionalArgs(self):
+    with self.assertRaisesFireExit(2, ''):
+      # This does not give 7 since positional args are disallowed for callable
+      # objects.
+      core.Fire(tc.CallableWithPositionalArgs(), command=['3', '4'])
 
 if __name__ == '__main__':
   testutils.main()
diff --git a/fire/decorators.py b/fire/decorators.py
index 3fcc4b97..b7b3c660 100644
--- a/fire/decorators.py
+++ b/fire/decorators.py
@@ -60,7 +60,7 @@ def SetParseFns(*positional, **named):
   Python arguments with which to call the function.
 
   A parse function should accept a single string argument and return a value to
-  be used in it's place when calling the decorated function.
+  be used in its place when calling the decorated function.
 
   Args:
     *positional: The functions to be used for parsing positional arguments.
@@ -85,8 +85,10 @@ def _SetMetadata(fn, attribute, value):
 
 
 def GetMetadata(fn):
+  # Class __init__ functions and object __call__ functions require flag style
+  # arguments. Other methods and functions may accept positional args.
   default = {
-      ACCEPTS_POSITIONAL_ARGS: not inspect.isclass(fn),
+      ACCEPTS_POSITIONAL_ARGS: inspect.isroutine(fn),
   }
   return getattr(fn, FIRE_METADATA, default)
 
diff --git a/fire/fire_test.py b/fire/fire_test.py
index c4287bfb..a35af669 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -308,6 +308,16 @@ def testFireObjectWithTuple(self):
     self.assertEqual(fire.Fire(tc.TypedProperties, command=['fox', '1']),
                      'divide')
 
+  def testFireObjectWithListAsObject(self):
+    self.assertEqual(
+        fire.Fire(tc.TypedProperties, command=['echo', 'index', 'bethany']),
+        1)
+
+  def testFireObjectWithTupleAsObject(self):
+    self.assertEqual(
+        fire.Fire(tc.TypedProperties, command=['fox', 'count', 'divide']),
+        1)
+
   def testFireNoComponent(self):
     self.assertEqual(fire.Fire(command=['tc', 'WithDefaults', 'double', '10']),
                      20)
diff --git a/fire/test_components.py b/fire/test_components.py
index b3d2d3c4..b1f2f898 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -287,6 +287,7 @@ def non_empty(self):
 
 
 class NamedTuple(object):
+  """Functions returning named tuples used for testing."""
 
   def point(self):
     """Point example straight from Python docs."""
@@ -294,6 +295,19 @@ def point(self):
     Point = collections.namedtuple('Point', ['x', 'y'])
     return Point(11, y=22)
 
+  def matching_names(self):
+    """Field name equals value."""
+    # pylint: disable=invalid-name
+    Point = collections.namedtuple('Point', ['x', 'y'])
+    return Point(x='x', y='y')
+
+
+class CallableWithPositionalArgs(object):
+  """Test class for supporting callable."""
+
+  def __call__(self, x, y):
+    return x + y
+
 
 class CallableWithKeywordArgument(object):
   """Test class for supporting callable."""

From dea9c8de652d1c1fb4c195f9f3638ea6004b4d2d Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 1 Jul 2019 12:36:10 -0700
Subject: [PATCH 119/324] use count instead of index in test since index
 changes signature in Python 3.7.

PiperOrigin-RevId: 256009321
Change-Id: Ia054df210cf686c1a3c2d22c508a9d75393b1028
---
 fire/fire_test.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fire/fire_test.py b/fire/fire_test.py
index a35af669..8a2f1d4a 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -310,7 +310,7 @@ def testFireObjectWithTuple(self):
 
   def testFireObjectWithListAsObject(self):
     self.assertEqual(
-        fire.Fire(tc.TypedProperties, command=['echo', 'index', 'bethany']),
+        fire.Fire(tc.TypedProperties, command=['echo', 'count', 'bethany']),
         1)
 
   def testFireObjectWithTupleAsObject(self):

From cc6db7219012f4991286030a03df5e1d05141dc4 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 1 Jul 2019 13:03:31 -0700
Subject: [PATCH 120/324] Show frozensets the same way we show sets.

PiperOrigin-RevId: 256014966
Change-Id: I6dc36726c6ec75dd8350dbab64a272c4a31f9829
---
 fire/core.py            |  2 +-
 fire/core_test.py       |  8 ++++++++
 fire/fire_test.py       | 10 ++++++++++
 fire/test_components.py |  8 ++++++++
 4 files changed, 27 insertions(+), 1 deletion(-)

diff --git a/fire/core.py b/fire/core.py
index 0a56e5cf..db18ab65 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -244,7 +244,7 @@ def _PrintResult(component_trace, verbose=False):
   # and move serialization to its own module.
   result = component_trace.GetResult()
 
-  if isinstance(result, (list, set, types.GeneratorType)):
+  if isinstance(result, (list, set, frozenset, types.GeneratorType)):
     for i in result:
       print(_OneLineResult(i))
   elif inspect.isgeneratorfunction(result):
diff --git a/fire/core_test.py b/fire/core_test.py
index d2607666..171611a9 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -152,6 +152,14 @@ def testPrintNamedTupleIndex(self):
     with self.assertOutputMatches(stdout='22', stderr=None):
       core.Fire(tc.NamedTuple, command=['point', '1'])
 
+  def testPrintSet(self):
+    with self.assertOutputMatches(stdout='.*three.*', stderr=None):
+      core.Fire(tc.simple_set(), command=[])
+
+  def testPrintFrozenSet(self):
+    with self.assertOutputMatches(stdout='.*three.*', stderr=None):
+      core.Fire(tc.simple_frozenset(), command=[])
+
   def testPrintNamedTupleNegativeIndex(self):
     with self.assertOutputMatches(stdout='11', stderr=None):
       core.Fire(tc.NamedTuple, command=['point', '-2'])
diff --git a/fire/fire_test.py b/fire/fire_test.py
index 8a2f1d4a..43c2e363 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -290,6 +290,16 @@ def testFireObjectWithDict(self):
     self.assertEqual(
         fire.Fire(tc.TypedProperties, command=['delta', 'nest', '0']), 'a')
 
+  def testFireSet(self):
+    component = tc.simple_set()
+    result = fire.Fire(component, command=[])
+    self.assertLen(result, 3)
+
+  def testFireFrozenset(self):
+    component = tc.simple_frozenset()
+    result = fire.Fire(component, command=[])
+    self.assertLen(result, 3)
+
   def testFireList(self):
     component = ['zero', 'one', 'two', 'three']
     self.assertEqual(fire.Fire(component, command=['2']), 'two')
diff --git a/fire/test_components.py b/fire/test_components.py
index b1f2f898..8d0c41db 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -370,3 +370,11 @@ def example_generator(n):
     """
     for i in range(n):
       yield i
+
+
+def simple_set():
+  return {1, 2, 'three'}
+
+
+def simple_frozenset():
+  return frozenset({1, 2, 'three'})

From 7f3156bb0b321b4e31a49e887fcff7b35c5d304f Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 1 Jul 2019 13:05:07 -0700
Subject: [PATCH 121/324] For functions that require flag syntax, show this
 appropriately in the usage screen and help screen.

PiperOrigin-RevId: 256015316
Change-Id: I305d0d9c2f14822ddcaf5b5b1a048eaf9f0a492a
---
 fire/helptext.py        | 53 +++++++++++++++++++++++++----------------
 fire/helptext_test.py   |  9 ++++++-
 fire/test_components.py | 10 ++++++++
 3 files changed, 51 insertions(+), 21 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index 20a2d769..ce5dcf15 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -40,6 +40,7 @@
 import inspect
 
 from fire import completion
+from fire import decorators
 from fire import formatting
 from fire import inspectutils
 from fire import value_types
@@ -128,8 +129,6 @@ def HelpTextForFunction(component, info, trace=None, verbose=False):
 
   current_command = GetCurrentCommand(trace)
   summary, description = GetSummaryAndDescription(info['docstring_info'])
-  spec = inspectutils.GetFullArgSpec(component)
-  args = spec.args
 
   args_with_no_defaults, args_with_defaults, flags = GetArgsAngFlags(component)
   del args_with_defaults
@@ -140,10 +139,20 @@ def HelpTextForFunction(component, info, trace=None, verbose=False):
   name_section = name_section_template.format(
       current_command=current_command, command_summary=command_summary_str)
 
+  # Check if positional args are allowed. If not, require flag syntax for args.
+  metadata = decorators.GetMetadata(component)
+  accepts_positional_args = metadata.get(decorators.ACCEPTS_POSITIONAL_ARGS)
+
   arg_and_flag_strings = []
   if args_with_no_defaults:
-    arg_strings = [formatting.Underline(arg.upper())
-                   for arg in args_with_no_defaults]
+    if accepts_positional_args:
+      arg_strings = [formatting.Underline(arg.upper())
+                     for arg in args_with_no_defaults]
+    else:
+      arg_strings = [
+          '--{arg}={arg_upper}'.format(
+              arg=arg, arg_upper=formatting.Underline(arg.upper()))
+          for arg in args_with_no_defaults]
     arg_and_flag_strings.extend(arg_strings)
 
   flag_string_template = '[--{flag_name}={flag_name_upper}]'
@@ -158,9 +167,6 @@ def HelpTextForFunction(component, info, trace=None, verbose=False):
 
   # Synopsis section
   synopsis_section_template = '{current_command} {args_and_flags}'
-  positional_arguments = '|'.join(args)
-  if positional_arguments:
-    positional_arguments = ' ' + positional_arguments
   synopsis_section = synopsis_section_template.format(
       current_command=current_command, args_and_flags=args_and_flags)
 
@@ -175,18 +181,18 @@ def HelpTextForFunction(component, info, trace=None, verbose=False):
   args_and_flags_sections = []
   notes_sections = []
 
-  pos_arg_items = []
-  pos_arg_items = [
-      _CreatePositionalArgItem(arg, docstring_info)
+  arg_items = [
+      _CreateArgItem(arg, docstring_info)
       for arg in args_with_no_defaults
   ]
-  if pos_arg_items:
-    positional_arguments_section = ('POSITIONAL ARGUMENTS',
-                                    '\n'.join(pos_arg_items).rstrip('\n'))
-    args_and_flags_sections.append(positional_arguments_section)
-    notes_sections.append(
-        ('NOTES', 'You can also use flags syntax for POSITIONAL ARGUMENTS')
-    )
+  if arg_items:
+    title = 'POSITIONAL ARGUMENTS' if accepts_positional_args else 'ARGUMENTS'
+    arguments_section = (title, '\n'.join(arg_items).rstrip('\n'))
+    args_and_flags_sections.append(arguments_section)
+    if accepts_positional_args:
+      notes_sections.append(
+          ('NOTES', 'You can also use flags syntax for POSITIONAL ARGUMENTS')
+      )
 
   flag_items = [
       _CreateFlagItem(flag, docstring_info)
@@ -214,7 +220,7 @@ def _CreateOutputSection(name, content):
                     content=formatting.Indent(content, 4))
 
 
-def _CreatePositionalArgItem(arg, docstring_info):
+def _CreateArgItem(arg, docstring_info):
   """Returns a string describing a positional argument.
 
   Args:
@@ -416,7 +422,6 @@ def UsageTextForFunction(component, trace=None):
   Returns:
     String suitable for display in an error screen.
   """
-
   output_template = """Usage: {current_command} {args_and_flags}
 {availability_lines}
 For detailed information on this command, run:
@@ -442,7 +447,15 @@ def UsageTextForFunction(component, trace=None):
   args_with_defaults = args[len(args) - num_defaults:]
   flags = args_with_defaults + spec.kwonlyargs
 
-  items = [arg.upper() for arg in args_with_no_defaults]
+  # Check if positional args are allowed. If not, show flag syntax for args.
+  metadata = decorators.GetMetadata(component)
+  accepts_positional_args = metadata.get(decorators.ACCEPTS_POSITIONAL_ARGS)
+  if not accepts_positional_args:
+    items = ['--{arg}={upper}'.format(arg=arg, upper=arg.upper())
+             for arg in args_with_no_defaults]
+  else:
+    items = [arg.upper() for arg in args_with_no_defaults]
+
   if flags:
     items.append('<flags>')
     availability_lines = (
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 2a491779..4ca36497 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -365,7 +365,7 @@ def testUsageOutputConstructorWithParameter(self):
     t = trace.FireTrace(component, name='InstanceVars')
     usage_output = helptext.UsageText(component, trace=t, verbose=True)
     expected_output = '''
-    Usage: InstanceVars ARG1 ARG2
+    Usage: InstanceVars --arg1=ARG1 --arg2=ARG2
 
     For detailed information on this command, run:
       InstanceVars --help'''
@@ -399,6 +399,13 @@ def testUsageOutputNone(self):
         usage_output,
         textwrap.dedent(expected_output).lstrip('\n'))
 
+  @testutils.skip('Only passes in Python 3 for now.')
+  def testInitRequiresFlagSyntaxSubclassNamedTuple(self):
+    component = tc.SubPoint
+    t = trace.FireTrace(component, name='SubPoint')
+    usage_output = helptext.UsageText(component, trace=t, verbose=True)
+    expected_output = 'Usage: SubPoint --x=X --y=Y'
+    self.assertIn(expected_output, usage_output)
 
 if __name__ == '__main__':
   testutils.main()
diff --git a/fire/test_components.py b/fire/test_components.py
index 8d0c41db..47d343cf 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -309,6 +309,16 @@ def __call__(self, x, y):
     return x + y
 
 
+NamedTuplePoint = collections.namedtuple('NamedTuplePoint', ['x', 'y'])
+
+
+class SubPoint(NamedTuplePoint):
+  """Used for verifying subclasses of namedtuples behave as intended."""
+
+  def coordinate_sum(self):
+    return self.x + self.y
+
+
 class CallableWithKeywordArgument(object):
   """Test class for supporting callable."""
 

From b6f6b3ab7d16799a7f32b1bc278280dde2b7ebf6 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 1 Jul 2019 13:16:41 -0700
Subject: [PATCH 122/324] Use assertEqual instead of assertLen.

PiperOrigin-RevId: 256017892
Change-Id: I6b99b5907cdb87fef082753c05d961ecd232aba2
---
 fire/fire_test.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/fire/fire_test.py b/fire/fire_test.py
index 43c2e363..8cf121af 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -293,12 +293,12 @@ def testFireObjectWithDict(self):
   def testFireSet(self):
     component = tc.simple_set()
     result = fire.Fire(component, command=[])
-    self.assertLen(result, 3)
+    self.assertEqual(len(result), 3)
 
   def testFireFrozenset(self):
     component = tc.simple_frozenset()
     result = fire.Fire(component, command=[])
-    self.assertLen(result, 3)
+    self.assertEqual(len(result), 3)
 
   def testFireList(self):
     component = ['zero', 'one', 'two', 'three']

From 024fbad9424cfdb0d3c93c86c856aedbac0f9d48 Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Thu, 11 Jul 2019 09:55:15 -0700
Subject: [PATCH 123/324] Strip all separators from the current command name in
 NAME section.

PiperOrigin-RevId: 257626944
Change-Id: Ia407053299b4123c1ff50825d85ca2a00737f6a1
---
 fire/helptext.py      | 15 +++++++++------
 fire/helptext_test.py |  8 ++++++++
 fire/trace.py         |  9 ++++++---
 3 files changed, 23 insertions(+), 9 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index ce5dcf15..af3372e2 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -70,13 +70,12 @@ def GetSummaryAndDescription(docstring_info):
   return summary, description
 
 
-def GetCurrentCommand(trace=None):
+def GetCurrentCommand(trace=None, include_separators=True):
   """Returns current command for the purpose of generating help text."""
   if trace:
-    current_command = trace.GetCommand()
+    current_command = trace.GetCommand(include_separators=include_separators)
   else:
     current_command = ''
-
   return current_command
 
 
@@ -128,6 +127,8 @@ def HelpTextForFunction(component, info, trace=None, verbose=False):
   del verbose
 
   current_command = GetCurrentCommand(trace)
+  current_command_without_separator = GetCurrentCommand(
+      trace, include_separators=False)
   summary, description = GetSummaryAndDescription(info['docstring_info'])
 
   args_with_no_defaults, args_with_defaults, flags = GetArgsAngFlags(component)
@@ -137,7 +138,8 @@ def HelpTextForFunction(component, info, trace=None, verbose=False):
   name_section_template = '{current_command}{command_summary}'
   command_summary_str = ' - ' + summary if summary else ''
   name_section = name_section_template.format(
-      current_command=current_command, command_summary=command_summary_str)
+      current_command=current_command_without_separator,
+      command_summary=command_summary_str)
 
   # Check if positional args are allowed. If not, require flag syntax for args.
   metadata = decorators.GetMetadata(component)
@@ -285,7 +287,8 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
     Formatted help text for display.
   """
   current_command = GetCurrentCommand(trace)
-
+  current_command_without_separator = GetCurrentCommand(
+      trace, include_separators=False)
   docstring_info = info['docstring_info']
   command_summary = docstring_info.summary if docstring_info.summary else ''
   command_description = GetDescriptionSectionText(docstring_info.summary,
@@ -335,7 +338,7 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
     description_sections.append(('DESCRIPTION', command_description))
 
   name_line = '{current_command} - {command_summary}'.format(
-      current_command=current_command,
+      current_command=current_command_without_separator,
       command_summary=command_summary)
   output_sections = [
       ('NAME', name_line),
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 4ca36497..dfd92455 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -265,6 +265,14 @@ def testHelpTextObjectWithGroupAndValues(self):
     self.assertIn('VALUE is one of the following:', help_screen)
     self.assertIn('alpha', help_screen)
 
+  def testHelpTextNameSectionCommandWithSeparator(self):
+    component = 9
+    t = trace.FireTrace(component, name='int', separator='-')
+    t.AddSeparator()
+    help_screen = helptext.HelpText(component=component, trace=t, verbose=True)
+    self.assertIn('int -', help_screen)
+    self.assertNotIn('int - -', help_screen)
+
 
 class UsageTest(testutils.BaseTestCase):
 
diff --git a/fire/trace.py b/fire/trace.py
index 12cc4392..7174f994 100644
--- a/fire/trace.py
+++ b/fire/trace.py
@@ -169,9 +169,12 @@ def _Quote(self, arg):
       return pipes.quote(prefix) + '=' + pipes.quote(value)
     return pipes.quote(arg)
 
-  def GetCommand(self):
+  def GetCommand(self, include_separators=True):
     """Returns the command representing the trace up to this point.
 
+    Args:
+      include_separators: Whether or not to include separators in the command.
+
     Returns:
       A string representing a Fire CLI command that would produce this trace.
     """
@@ -184,10 +187,10 @@ def GetCommand(self):
         continue
       if element.args:
         args.extend(element.args)
-      if element.HasSeparator():
+      if element.HasSeparator() and include_separators:
         args.append(self.separator)
 
-    if self.NeedsSeparator():
+    if self.NeedsSeparator() and include_separators:
       args.append(self.separator)
 
     return ' '.join(self._Quote(arg) for arg in args)

From 979c7887286f276078a5380b27f14fca0a1f4f21 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 22 Jul 2019 10:39:12 -0700
Subject: [PATCH 124/324] Adds a subclass of dict as a test component.

PiperOrigin-RevId: 259358342
Change-Id: I4218e6b702ada87eac1248072ccdaa18977226ba
---
 fire/test_components.py | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/fire/test_components.py b/fire/test_components.py
index 47d343cf..a36e393f 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -388,3 +388,12 @@ def simple_set():
 
 def simple_frozenset():
   return frozenset({1, 2, 'three'})
+
+
+class Subdict(dict):
+
+  pass
+
+
+subdict = Subdict({1: 2,
+                   'red': 'blue'})

From 149380b6b963fbad4de98b13d694de67867144af Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 22 Jul 2019 10:41:09 -0700
Subject: [PATCH 125/324] Show help instead of value if any non-value
 non-collection (list, dict) objects are found

PiperOrigin-RevId: 259358731
Change-Id: I12b48571ee4f642b12ec937b768e3877956f2355
---
 fire/core.py        |  2 +-
 fire/value_types.py | 19 +++++++++++++++++++
 2 files changed, 20 insertions(+), 1 deletion(-)

diff --git a/fire/core.py b/fire/core.py
index db18ab65..5f253236 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -249,7 +249,7 @@ def _PrintResult(component_trace, verbose=False):
       print(_OneLineResult(i))
   elif inspect.isgeneratorfunction(result):
     raise NotImplementedError
-  elif isinstance(result, dict):
+  elif isinstance(result, dict) and value_types.IsSimpleGroup(result):
     print(_DictAsString(result, verbose))
   elif isinstance(result, tuple):
     print(_OneLineResult(result))
diff --git a/fire/value_types.py b/fire/value_types.py
index 77d05dc7..9cb34f86 100644
--- a/fire/value_types.py
+++ b/fire/value_types.py
@@ -37,3 +37,22 @@ def IsCommand(component):
 
 def IsValue(component):
   return isinstance(component, VALUE_TYPES)
+
+
+def IsSimpleGroup(component):
+  """If a group is simple enough, then we treat it as a value in PrintResult.
+
+  Only if a group contains all value types do we consider it simple enough to
+  print as a value.
+
+  Args:
+    component: The group to check for value-group status.
+  Returns:
+    A boolean indicating if the group should be treated as a value for printing
+    purposes.
+  """
+  assert isinstance(component, dict)
+  for unused_key, value in component.items():
+    if not IsValue(value) and not isinstance(value, (list, dict)):
+      return False
+  return True

From 0820e6551a93218362d0914abc68090566265466 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 22 Jul 2019 10:42:16 -0700
Subject: [PATCH 126/324] Treat Ellipsis, None, and NotImplemented as value
 types.

PiperOrigin-RevId: 259358998
Change-Id: I1afa4a0d97c57ce3b4e89f2b6614663dc2660dfa
---
 fire/core.py        | 5 +++--
 fire/value_types.py | 3 ++-
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index 5f253236..8e35b3cc 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -254,8 +254,9 @@ def _PrintResult(component_trace, verbose=False):
   elif isinstance(result, tuple):
     print(_OneLineResult(result))
   elif isinstance(result, value_types.VALUE_TYPES):
-    print(result)
-  elif result is not None:
+    if result is not None:
+      print(result)
+  else:
     help_text = helptext.HelpText(result, component_trace, verbose)
     output = [help_text]
     Display(output, out=sys.stdout)
diff --git a/fire/value_types.py b/fire/value_types.py
index 9cb34f86..ad50cb23 100644
--- a/fire/value_types.py
+++ b/fire/value_types.py
@@ -23,7 +23,8 @@
 import six
 
 
-VALUE_TYPES = (bool, six.string_types, six.integer_types, float, complex)
+VALUE_TYPES = (bool, six.string_types, six.integer_types, float, complex,
+               type(Ellipsis), type(None), type(NotImplemented))
 
 
 def IsGroup(component):

From ee210d16ef58b5bd9d80feef3754c0c56fdcbe72 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 22 Jul 2019 10:43:21 -0700
Subject: [PATCH 127/324] Handle printing functions and modules a little more
 cleanly.

PiperOrigin-RevId: 259359253
Change-Id: Ib61b3a2b9f0a4339cb396806e8c2aa77bee27223
---
 fire/core.py | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/fire/core.py b/fire/core.py
index 8e35b3cc..99ae2d8c 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -334,6 +334,14 @@ def _OneLineResult(result):
   if isinstance(result, six.string_types):
     return str(result).replace('\n', ' ')
 
+  # TODO(dbieber): Show a small amount of usage information about the function
+  # or module if it fits cleanly on the line.
+  if inspect.isfunction(result):
+    return '<function {name}>'.format(name=result.__name__)
+
+  if inspect.ismodule(result):
+    return '<module {name}>'.format(name=result.__name__)
+
   try:
     # Don't force conversion to ascii.
     return json.dumps(result, ensure_ascii=False)

From b9c32df6519a4cd49b9c68ab29f78f96ed9e092e Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 22 Jul 2019 10:44:36 -0700
Subject: [PATCH 128/324] Only sure Future Features in verbose mode, and only
 show six in verbose mode. Unifies verbosity logic.

PiperOrigin-RevId: 259359499
Change-Id: I44cec5968b54faf88fda9ddbe36c499b64d1f4a1
---
 fire/completion.py | 22 ++++++++++++++--------
 fire/core.py       | 12 ++----------
 fire/helptext.py   |  7 +++++--
 3 files changed, 21 insertions(+), 20 deletions(-)

diff --git a/fire/completion.py b/fire/completion.py
index cda7c936..705ef586 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -281,30 +281,36 @@ def _FishScript(name, commands, default_options=None):
   )
 
 
-def _IncludeMember(name, verbose):
+def MemberVisible(name, member, verbose):
   """Returns whether a member should be included in auto-completion or help.
 
   Determines whether a member of an object with the specified name should be
   included in auto-completion or help text(both usage and detailed help).
 
-  If the member starts with '__', it will always be excluded. If the member
+  If the member name starts with '__', it will always be excluded. If it
   starts with only one '_', it will be included for all non-string types. If
-  verbose is True, the members, including the private members, are always
-  included.
+  verbose is True, the members, including the private members, are included.
+
+  When not in verbose mode, some modules and functions are excluded as well.
 
   Args:
     name: The name of the member.
+    member: The member itself.
     verbose: Whether to include private members.
   Returns
     A boolean value indicating whether the member should be included.
-
   """
-  if isinstance(name, six.string_types) and name[:2] == '__':
+  if isinstance(name, six.string_types) and name.startswith('__'):
     return False
   if verbose:
     return True
+  if isinstance(member, type(absolute_import)):
+    return False
+  if inspect.ismodule(member) and member is six:
+    # TODO(dbieber): Determine more generally which modules to hide.
+    return False
   if isinstance(name, six.string_types):
-    return name and name[0] != '_'
+    return not name.startswith('_')
   return True  # Default to including the member
 
 
@@ -328,7 +334,7 @@ def _Members(component, verbose=False):
   return [
       (member_name, member)
       for member_name, member in members
-      if _IncludeMember(member_name, verbose)
+      if MemberVisible(member_name, member, verbose)
   ]
 
 
diff --git a/fire/core.py b/fire/core.py
index 99ae2d8c..3ba081c2 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -303,7 +303,7 @@ def _DictAsString(result, verbose=False):
   # 1) Getting visible items and the longest key for output formatting
   # 2) Actually construct the output lines
   result_visible = {key: value for key, value in result.items()
-                    if _ComponentVisible(key, verbose)}
+                    if completion.MemberVisible(key, value, verbose)}
 
   if not result_visible:
     return '{}'
@@ -313,21 +313,13 @@ def _DictAsString(result, verbose=False):
 
   lines = []
   for key, value in result.items():
-    if _ComponentVisible(key, verbose):
+    if completion.MemberVisible(key, value, verbose):
       line = format_string.format(key=str(key) + ':',
                                   value=_OneLineResult(value))
       lines.append(line)
   return '\n'.join(lines)
 
 
-def _ComponentVisible(component, verbose=False):
-  """Returns whether a component should be visible in the output."""
-  return (
-      verbose
-      or not isinstance(component, six.string_types)
-      or not component.startswith('_'))
-
-
 def _OneLineResult(result):
   """Returns result serialized to a single line string."""
   # TODO(dbieber): Ensure line is fewer than eg 120 characters.
diff --git a/fire/helptext.py b/fire/helptext.py
index af3372e2..23db4542 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -410,21 +410,24 @@ def _NewChoicesSection(name, choices):
 
 def UsageText(component, trace=None, verbose=False):
   if inspect.isroutine(component) or inspect.isclass(component):
-    return UsageTextForFunction(component, trace)
+    return UsageTextForFunction(component, trace, verbose)
   else:
     return UsageTextForObject(component, trace, verbose)
 
 
-def UsageTextForFunction(component, trace=None):
+def UsageTextForFunction(component, trace=None, verbose=False):
   """Returns usage text for function objects.
 
   Args:
     component: The component to determine the usage text for.
     trace: The Fire trace object containing all metadata of current execution.
+    verbose: Whether to display the usage text in verbose mode.
 
   Returns:
     String suitable for display in an error screen.
   """
+  del verbose  # Unused.
+
   output_template = """Usage: {current_command} {args_and_flags}
 {availability_lines}
 For detailed information on this command, run:

From 402ea3fc4cd6e9722e1e6137b170ed323b215603 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 22 Jul 2019 10:46:34 -0700
Subject: [PATCH 129/324] Add indexes section to help and usage information for
 lists and tuples (including namedtuples).

PiperOrigin-RevId: 259359939
Change-Id: I4bc6d8267cf2203bb1c252c3b245abe5d69d39c5
---
 fire/helptext.py | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/fire/helptext.py b/fire/helptext.py
index 23db4542..33642430 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -324,6 +324,16 @@ def HelpTextForObject(component, info, trace=None, verbose=False):
     usage_details_section = ValuesUsageDetailsSection(component, values)
     usage_details_sections.append(usage_details_section)
 
+  if isinstance(component, (list, tuple)) and component:
+    possible_actions.append('INDEX')
+    component_len = len(component)
+    if component_len < 10:
+      indexes_strings = [', '.join(str(x) for x in range(component_len))]
+    else:
+      indexes_strings = ['0..{max}'.format(max=component_len-1)]
+    usage_details_sections.append(
+        ('INDEXES', _NewChoicesSection('INDEX', indexes_strings)))
+
   possible_actions_string = ' | '.join(
       formatting.Underline(action) for action in possible_actions)
 
@@ -549,6 +559,18 @@ def UsageTextForObject(component, trace=None, verbose=False):
         items=values)
     availability_lines.append(values_text)
 
+  if isinstance(component, (list, tuple)) and component:
+    possible_actions.append('index')
+    component_len = len(component)
+    if component_len < 10:
+      indexes_strings = [str(x) for x in range(component_len)]
+    else:
+      indexes_strings = ['0..{max}'.format(max=component_len-1)]
+    indexes_text = _CreateAvailabilityLine(
+        header='available indexes:',
+        items=indexes_strings)
+    availability_lines.append(indexes_text)
+
   if possible_actions:
     possible_actions_string = ' <{actions}>'.format(
         actions='|'.join(possible_actions))

From 34c82659550d964b711a6d4fff734f7be13a2836 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 22 Jul 2019 11:22:06 -0700
Subject: [PATCH 130/324] Clean up subdict name for linter.

PiperOrigin-RevId: 259368017
Change-Id: I0dc86fed9107660d439c0d2fe155b9f1348283d3
---
 fire/test_components.py | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/fire/test_components.py b/fire/test_components.py
index a36e393f..f81c9a0f 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -391,9 +391,8 @@ def simple_frozenset():
 
 
 class Subdict(dict):
-
-  pass
+  """A subclass of dict, for testing purposes."""
 
 
-subdict = Subdict({1: 2,
-                   'red': 'blue'})
+# An example subdict.
+SUBDICT = Subdict({1: 2, 'red': 'blue'})

From b80d596ae8b20519c542d8cc2cb03318f7098b2d Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 22 Jul 2019 11:36:28 -0700
Subject: [PATCH 131/324] Unifies function and object help string building to
 make supporting callable functions more natural in a follow up CL. Refactor
 of helptext module to reduce redundant action collection logic and organize
 sections more consistently.

PiperOrigin-RevId: 259371261
Change-Id: I5959988e9c6d656bb0e2ac46c786be5dac750151
---
 fire/helptext.py      | 490 +++++++++++++++++++++---------------------
 fire/helptext_test.py |  10 +-
 2 files changed, 253 insertions(+), 247 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index 33642430..8f4fe97b 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -12,11 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""helptext is the new, work in progress, help text module for Fire.
-
-This is a fork of, and is intended to replace, helputils.
-
-Utility for producing help strings for use in Fire CLIs.
+"""Utilities for producing help strings for use in Fire CLIs.
 
 Can produce help strings suitable for display in Fire CLIs for any type of
 Python object, module, class, or function.
@@ -46,103 +42,201 @@
 from fire import value_types
 
 
-def GetArgsAngFlags(component):
-  """Returns all types of arguments and flags of a component."""
+def HelpText(component, trace=None, verbose=False):
+  """Gets the help string for the current component, suitalbe for a help screen.
+
+  Args:
+    component: The component to construct the help string for.
+    trace: The Fire trace of the command so far. The command executed so far
+      can be extracted from this trace.
+    verbose: Whether to include private members in the help screen.
+
+  Returns:
+    The full help screen as a string.
+  """
+  # Preprocessing needed to create the sections:
+  info = inspectutils.Info(component)
+  actions_grouped_by_kind = _GetActionsGroupedByKind(component, verbose=verbose)
   spec = inspectutils.GetFullArgSpec(component)
-  args = spec.args
-  if spec.defaults is None:
-    num_defaults = 0
-  else:
-    num_defaults = len(spec.defaults)
-  args_with_no_defaults = args[:len(args) - num_defaults]
-  args_with_defaults = args[len(args) - num_defaults:]
-  flags = args_with_defaults + spec.kwonlyargs
-  return args_with_no_defaults, args_with_defaults, flags
+  metadata = decorators.GetMetadata(component)
 
+  # Sections:
+  name_section = _NameSection(info, trace=trace, verbose=verbose)
+  synopsis_section = _SynopsisSection(
+      component, actions_grouped_by_kind, spec, metadata, trace=trace)
+  description_section = _DescriptionSection(info)
+  # TODO(dbieber): Add returns and raises sections for functions.
 
-def GetSummaryAndDescription(docstring_info):
-  """Retrieves summary and description for help text generation."""
+  if inspect.isroutine(component) or inspect.isclass(component):
+    # For functions (ARGUMENTS / POSITIONAL ARGUMENTS, FLAGS)
+    args_and_flags_sections, notes_sections = _ArgsAndFlagsSections(
+        info, spec, metadata)
+    usage_details_sections = []
+  else:
+    # For objects (GROUPS, COMMANDS, VALUES, INDEXES)
+    # TODO(dbieber): Show callable function usage in help text.
+    args_and_flags_sections = []
+    notes_sections = []
+    usage_details_sections = _UsageDetailsSections(component,
+                                                   actions_grouped_by_kind)
+
+  sections = (
+      [name_section, synopsis_section, description_section]
+      + args_and_flags_sections
+      + usage_details_sections
+      + notes_sections
+  )
+  return '\n\n'.join(
+      _CreateOutputSection(*section)
+      for section in sections if section is not None
+  )
 
-  # To handle both empty string and None
-  summary = docstring_info.summary if docstring_info.summary else None
-  description = (
-      docstring_info.description if docstring_info.description else None)
-  return summary, description
 
+def _NameSection(info, trace=None, verbose=False):
+  """The "Name" section of the help string."""
+  # Only include separators in the name in verbose mode.
+  current_command = _GetCurrentCommand(trace, include_separators=verbose)
+  summary = _GetSummary(info)
 
-def GetCurrentCommand(trace=None, include_separators=True):
-  """Returns current command for the purpose of generating help text."""
-  if trace:
-    current_command = trace.GetCommand(include_separators=include_separators)
+  if summary:
+    text = current_command + ' - ' + summary
   else:
-    current_command = ''
-  return current_command
+    text = current_command
+  return ('NAME', text)
 
 
-def HelpText(component, trace=None, verbose=False):
-  info = inspectutils.Info(component)
+def _SynopsisSection(component, actions_grouped_by_kind, spec, metadata,
+                     trace=None):
+  """The "Synopsis" section of the help string."""
+  current_command = _GetCurrentCommand(trace=trace, include_separators=True)
+
+  # TODO(dbieber): Support callable functions.
   if inspect.isroutine(component) or inspect.isclass(component):
-    return HelpTextForFunction(component, info, trace=trace, verbose=verbose)
+    # For function:
+    args_and_flags = _GetArgsAndFlagsString(spec, metadata)
+    synopsis_section_template = '{current_command} {args_and_flags}'
+    text = synopsis_section_template.format(
+        current_command=current_command, args_and_flags=args_and_flags)
+
   else:
-    return HelpTextForObject(component, info, trace=trace, verbose=verbose)
+    # For object:
+    possible_actions_string = _GetPossibleActionsString(actions_grouped_by_kind)
+    synopsis_template = '{current_command} {possible_actions}'
+    text = synopsis_template.format(
+        current_command=current_command,
+        possible_actions=possible_actions_string)
+
+  return ('SYNOPSIS', text)
+
+
+def _DescriptionSection(info):
+  """The "Description" sections of the help string."""
+  summary = _GetSummary(info)
+  description = _GetDescription(info)
+  # Returns the description if available. If not, returns the summary.
+  # If neither are available, returns None.
+  text = description or summary or None
+  if text:
+    return ('DESCRIPTION', text)
+  else:
+    return None
 
 
-def GetDescriptionSectionText(summary, description):
-  """Returns description section text based on the input docstring info.
+def _ArgsAndFlagsSections(info, spec, metadata):
+  """The "Args and Flags" sections of the help string."""
+  args_with_no_defaults = spec.args[:len(spec.args) - len(spec.defaults)]
+  args_with_defaults = spec.args[len(spec.args) - len(spec.defaults):]
+  flags = args_with_defaults + spec.kwonlyargs
 
-  Returns the string that should be used as description section based on the
-  input. The logic is the following: If there's description available, use it.
-  Otherwise, use summary if available. If neither description or summary is
-  available, returns None.
+  # Check if positional args are allowed. If not, require flag syntax for args.
+  accepts_positional_args = metadata.get(decorators.ACCEPTS_POSITIONAL_ARGS)
 
-  Args:
-    summary: summary found in object summary
-    description: description found in object docstring
+  args_and_flags_sections = []
+  notes_sections = []
 
-  Returns:
-    String for the description section in help screen.
-  """
-  if not (description or summary):
-    return None
+  docstring_info = info['docstring_info']
 
-  if description:
-    return description
-  else:
-    return summary
+  arg_items = [
+      _CreateArgItem(arg, docstring_info)
+      for arg in args_with_no_defaults
+  ]
+  if arg_items:
+    title = 'POSITIONAL ARGUMENTS' if accepts_positional_args else 'ARGUMENTS'
+    arguments_section = (title, '\n'.join(arg_items).rstrip('\n'))
+    args_and_flags_sections.append(arguments_section)
+    if accepts_positional_args:
+      notes_sections.append(
+          ('NOTES', 'You can also use flags syntax for POSITIONAL ARGUMENTS')
+      )
+
+  flag_items = [
+      _CreateFlagItem(flag, docstring_info)
+      for flag in flags
+  ]
 
+  if flag_items:
+    flags_section = ('FLAGS', '\n'.join(flag_items))
+    args_and_flags_sections.append(flags_section)
 
-def HelpTextForFunction(component, info, trace=None, verbose=False):
-  """Returns detail help text for a function component.
+  return args_and_flags_sections, notes_sections
 
-  Args:
-    component: Current component to generate help text for.
-    info: Info containing metadata of component.
-    trace: FireTrace object that leads to current component.
-    verbose: Whether to display help text in verbose mode.
 
-  Returns:
-    Formatted help text for display.
-  """
-  # TODO(joejoevictor): Implement verbose related output
-  del verbose
+def _UsageDetailsSections(component, actions_grouped_by_kind):
+  """The usage details sections of the help string."""
+  groups, commands, values, indexes = actions_grouped_by_kind
+
+  usage_details_sections = []
+
+  if groups:
+    usage_details_section = _GroupUsageDetailsSection(groups)
+    usage_details_sections.append(usage_details_section)
+  if commands:
+    usage_details_section = _CommandUsageDetailsSection(commands)
+    usage_details_sections.append(usage_details_section)
+  if values:
+    usage_details_section = _ValuesUsageDetailsSection(component, values)
+    usage_details_sections.append(usage_details_section)
+  if indexes:
+    usage_details_sections.append(
+        ('INDEXES', _NewChoicesSection('INDEX', [indexes])))
+
+  return usage_details_sections
 
-  current_command = GetCurrentCommand(trace)
-  current_command_without_separator = GetCurrentCommand(
-      trace, include_separators=False)
-  summary, description = GetSummaryAndDescription(info['docstring_info'])
 
-  args_with_no_defaults, args_with_defaults, flags = GetArgsAngFlags(component)
-  del args_with_defaults
+def _GetSummary(info):
+  docstring_info = info['docstring_info']
+  return docstring_info.summary if docstring_info.summary else None
+
 
-  # Name section
-  name_section_template = '{current_command}{command_summary}'
-  command_summary_str = ' - ' + summary if summary else ''
-  name_section = name_section_template.format(
-      current_command=current_command_without_separator,
-      command_summary=command_summary_str)
+def _GetDescription(info):
+  docstring_info = info['docstring_info']
+  return docstring_info.description if docstring_info.description else None
+
+
+def _GetArgsAndFlagsString(spec, metadata):
+  """The args and flags string for showing how to call a function.
+
+  If positional arguments are accepted, the args will be shown as positional.
+  E.g. "ARG1 ARG2 [--flag=FLAG]"
+
+  If positional arguments are disallowed, the args will be shown with flags
+  syntax.
+  E.g. "--arg1=ARG1 [--flag=FLAG]"
+
+  Args:
+    spec: The full arg spec for the component to construct the args and flags
+      string for.
+    metadata: Metadata for the component, including whether it accepts
+      positional arguments.
+
+  Returns:
+    The constructed args and flags string.
+  """
+  args_with_no_defaults = spec.args[:len(spec.args) - len(spec.defaults)]
+  args_with_defaults = spec.args[len(spec.args) - len(spec.defaults):]
+  flags = args_with_defaults + spec.kwonlyargs
 
   # Check if positional args are allowed. If not, require flag syntax for args.
-  metadata = decorators.GetMetadata(component)
   accepts_positional_args = metadata.get(decorators.ACCEPTS_POSITIONAL_ARGS)
 
   arg_and_flag_strings = []
@@ -159,61 +253,68 @@ def HelpTextForFunction(component, info, trace=None, verbose=False):
 
   flag_string_template = '[--{flag_name}={flag_name_upper}]'
   if flags:
-    flag_strings = [
-        flag_string_template.format(
-            flag_name=formatting.Underline(flag), flag_name_upper=flag.upper())
-        for flag in flags
-    ]
-    arg_and_flag_strings.extend(flag_strings)
-  args_and_flags = ' '.join(arg_and_flag_strings)
-
-  # Synopsis section
-  synopsis_section_template = '{current_command} {args_and_flags}'
-  synopsis_section = synopsis_section_template.format(
-      current_command=current_command, args_and_flags=args_and_flags)
-
-  # Description section
-  command_description = GetDescriptionSectionText(summary, description)
-  description_sections = []
-  if command_description:
-    description_sections.append(('DESCRIPTION', command_description))
-
-  # Positional arguments and flags section
-  docstring_info = info['docstring_info']
-  args_and_flags_sections = []
-  notes_sections = []
+    for flag in flags:
+      flag_string = flag_string_template.format(
+          flag_name=formatting.Underline(flag),
+          flag_name_upper=flag.upper())
+      arg_and_flag_strings.append(flag_string)
+  return ' '.join(arg_and_flag_strings)
 
-  arg_items = [
-      _CreateArgItem(arg, docstring_info)
-      for arg in args_with_no_defaults
-  ]
-  if arg_items:
-    title = 'POSITIONAL ARGUMENTS' if accepts_positional_args else 'ARGUMENTS'
-    arguments_section = (title, '\n'.join(arg_items).rstrip('\n'))
-    args_and_flags_sections.append(arguments_section)
-    if accepts_positional_args:
-      notes_sections.append(
-          ('NOTES', 'You can also use flags syntax for POSITIONAL ARGUMENTS')
-      )
 
-  flag_items = [
-      _CreateFlagItem(flag, docstring_info)
-      for flag in flags
-  ]
+def _GetPossibleActionsString(actions_grouped_by_kind):
+  """A help screen string listing the possible action kinds available."""
+  groups, commands, values, indexes = actions_grouped_by_kind
 
-  if flag_items:
-    flags_section = ('FLAGS', '\n'.join(flag_items))
-    args_and_flags_sections.append(flags_section)
+  possible_actions = []
+  if groups:
+    possible_actions.append('GROUP')
+  if commands:
+    possible_actions.append('COMMAND')
+  if values:
+    possible_actions.append('VALUE')
+  if indexes:
+    possible_actions.append('INDEX')
 
-  output_sections = [
-      ('NAME', name_section),
-      ('SYNOPSIS', synopsis_section),
-  ] + description_sections + args_and_flags_sections + notes_sections
+  possible_actions_string = ' | '.join(
+      formatting.Underline(action) for action in possible_actions)
+  return possible_actions_string
 
-  return '\n\n'.join(
-      _CreateOutputSection(name, content)
-      for name, content in output_sections
-  )
+
+def _GetActionsGroupedByKind(component, verbose=False):
+  """Gets lists of available actions, grouped by action kind."""
+  groups = []
+  commands = []
+  values = []
+
+  members = completion._Members(component, verbose)  # pylint: disable=protected-access
+  for member_name, member in members:
+    member_name = str(member_name)
+    if value_types.IsGroup(member):
+      groups.append((member_name, member))
+    if value_types.IsCommand(member):
+      commands.append((member_name, member))
+    if value_types.IsValue(member):
+      values.append((member_name, member))
+
+  indexes = None
+  if isinstance(component, (list, tuple)) and component:
+    component_len = len(component)
+    # WARNING: Note that indexes is a string, whereas the rest are lists.
+    if component_len < 10:
+      indexes = ', '.join(str(x) for x in range(component_len))
+    else:
+      indexes = '0..{max}'.format(max=component_len-1)
+
+  return groups, commands, values, indexes
+
+
+def _GetCurrentCommand(trace=None, include_separators=True):
+  """Returns current command for the purpose of generating help text."""
+  if trace:
+    current_command = trace.GetCommand(include_separators=include_separators)
+  else:
+    current_command = ''
+  return current_command
 
 
 def _CreateOutputSection(name, content):
@@ -274,94 +375,7 @@ def _CreateItem(name, description, indent=2):
                         description=formatting.Indent(description, indent))
 
 
-def HelpTextForObject(component, info, trace=None, verbose=False):
-  """Generates help text for python objects.
-
-  Args:
-    component: Current component to generate help text for.
-    info: Info containing metadata of component.
-    trace: FireTrace object that leads to current component.
-    verbose: Whether to display help text in verbose mode.
-
-  Returns:
-    Formatted help text for display.
-  """
-  current_command = GetCurrentCommand(trace)
-  current_command_without_separator = GetCurrentCommand(
-      trace, include_separators=False)
-  docstring_info = info['docstring_info']
-  command_summary = docstring_info.summary if docstring_info.summary else ''
-  command_description = GetDescriptionSectionText(docstring_info.summary,
-                                                  docstring_info.description)
-  groups = []
-  commands = []
-  values = []
-  members = completion._Members(component, verbose)  # pylint: disable=protected-access
-  for member_name, member in members:
-    if value_types.IsGroup(member):
-      groups.append((member_name, member))
-    if value_types.IsCommand(member):
-      commands.append((member_name, member))
-    if value_types.IsValue(member):
-      values.append((member_name, member))
-
-  usage_details_sections = []
-  possible_actions = []
-  # TODO(joejoevictor): Add global flags to here. Also, if it's a callable,
-  # there will be additional flags.
-  possible_flags = ''
-
-  if groups:
-    possible_actions.append('GROUP')
-    usage_details_section = GroupUsageDetailsSection(groups)
-    usage_details_sections.append(usage_details_section)
-  if commands:
-    possible_actions.append('COMMAND')
-    usage_details_section = CommandUsageDetailsSection(commands)
-    usage_details_sections.append(usage_details_section)
-  if values:
-    possible_actions.append('VALUE')
-    usage_details_section = ValuesUsageDetailsSection(component, values)
-    usage_details_sections.append(usage_details_section)
-
-  if isinstance(component, (list, tuple)) and component:
-    possible_actions.append('INDEX')
-    component_len = len(component)
-    if component_len < 10:
-      indexes_strings = [', '.join(str(x) for x in range(component_len))]
-    else:
-      indexes_strings = ['0..{max}'.format(max=component_len-1)]
-    usage_details_sections.append(
-        ('INDEXES', _NewChoicesSection('INDEX', indexes_strings)))
-
-  possible_actions_string = ' | '.join(
-      formatting.Underline(action) for action in possible_actions)
-
-  synopsis_template = '{current_command} {possible_actions}{possible_flags}'
-  synopsis_string = synopsis_template.format(
-      current_command=current_command,
-      possible_actions=possible_actions_string,
-      possible_flags=possible_flags)
-
-  description_sections = []
-  if command_description:
-    description_sections.append(('DESCRIPTION', command_description))
-
-  name_line = '{current_command} - {command_summary}'.format(
-      current_command=current_command_without_separator,
-      command_summary=command_summary)
-  output_sections = [
-      ('NAME', name_line),
-      ('SYNOPSIS', synopsis_string),
-  ] + description_sections + usage_details_sections
-
-  return '\n\n'.join(
-      _CreateOutputSection(name, content)
-      for name, content in output_sections
-  )
-
-
-def GroupUsageDetailsSection(groups):
+def _GroupUsageDetailsSection(groups):
   """Creates a section tuple for the groups section of the usage details."""
   group_item_strings = []
   for group_name, group in groups:
@@ -376,7 +390,7 @@ def GroupUsageDetailsSection(groups):
   return ('GROUPS', _NewChoicesSection('GROUP', group_item_strings))
 
 
-def CommandUsageDetailsSection(commands):
+def _CommandUsageDetailsSection(commands):
   """Creates a section tuple for the commands section of the usage details."""
   command_item_strings = []
   for command_name, command in commands:
@@ -391,7 +405,7 @@ def CommandUsageDetailsSection(commands):
   return ('COMMANDS', _NewChoicesSection('COMMAND', command_item_strings))
 
 
-def ValuesUsageDetailsSection(component, values):
+def _ValuesUsageDetailsSection(component, values):
   """Creates a section tuple for the values section of the usage details."""
   value_item_strings = []
   for value_name, value in values:
@@ -490,15 +504,6 @@ def UsageTextForFunction(component, trace=None, verbose=False):
       hyphen_hyphen=hyphen_hyphen)
 
 
-def _CreateAvailabilityLine(header, items,
-                            header_indent=2, items_indent=25, line_length=80):
-  items_width = line_length - items_indent
-  items_text = '\n'.join(formatting.WrappedJoin(items, width=items_width))
-  indented_items_text = formatting.Indent(items_text, spaces=items_indent)
-  indented_header = formatting.Indent(header, spaces=header_indent)
-  return indented_header + indented_items_text[len(indented_header):] + '\n'
-
-
 def UsageTextForObject(component, trace=None, verbose=False):
   """Returns the usage text for the error screen for an object.
 
@@ -524,19 +529,8 @@ def UsageTextForObject(component, trace=None, verbose=False):
   if not command:
     command = ''
 
-  groups = []
-  commands = []
-  values = []
-
-  members = completion._Members(component, verbose)  # pylint: disable=protected-access
-  for member_name, member in members:
-    member_name = str(member_name)
-    if value_types.IsGroup(member):
-      groups.append(member_name)
-    if value_types.IsCommand(member):
-      commands.append(member_name)
-    if value_types.IsValue(member):
-      values.append(member_name)
+  actions_grouped_by_kind = _GetActionsGroupedByKind(component, verbose=verbose)
+  groups, commands, values, indexes = actions_grouped_by_kind
 
   possible_actions = []
   availability_lines = []
@@ -558,17 +552,11 @@ def UsageTextForObject(component, trace=None, verbose=False):
         header='available values:',
         items=values)
     availability_lines.append(values_text)
-
-  if isinstance(component, (list, tuple)) and component:
+  if indexes:
     possible_actions.append('index')
-    component_len = len(component)
-    if component_len < 10:
-      indexes_strings = [str(x) for x in range(component_len)]
-    else:
-      indexes_strings = ['0..{max}'.format(max=component_len-1)]
     indexes_text = _CreateAvailabilityLine(
         header='available indexes:',
-        items=indexes_strings)
+        items=[(indexes, None)])
     availability_lines.append(indexes_text)
 
   if possible_actions:
@@ -583,3 +571,13 @@ def UsageTextForObject(component, trace=None, verbose=False):
       current_command=command,
       possible_actions=possible_actions_string,
       availability_lines=availability_lines_string)
+
+
+def _CreateAvailabilityLine(header, items,
+                            header_indent=2, items_indent=25, line_length=80):
+  items_width = line_length - items_indent
+  item_names = [item[0] for item in items]
+  items_text = '\n'.join(formatting.WrappedJoin(item_names, width=items_width))
+  indented_items_text = formatting.Indent(items_text, spaces=items_indent)
+  indented_header = formatting.Indent(header, spaces=header_indent)
+  return indented_header + indented_items_text[len(indented_header):] + '\n'
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index dfd92455..b0b545dc 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -269,10 +269,18 @@ def testHelpTextNameSectionCommandWithSeparator(self):
     component = 9
     t = trace.FireTrace(component, name='int', separator='-')
     t.AddSeparator()
-    help_screen = helptext.HelpText(component=component, trace=t, verbose=True)
+    help_screen = helptext.HelpText(component=component, trace=t, verbose=False)
     self.assertIn('int -', help_screen)
     self.assertNotIn('int - -', help_screen)
 
+  def testHelpTextNameSectionCommandWithSeparatorVerbobse(self):
+    component = 9
+    t = trace.FireTrace(component, name='int', separator='-')
+    t.AddSeparator()
+    help_screen = helptext.HelpText(component=component, trace=t, verbose=True)
+    self.assertIn('int -', help_screen)
+    self.assertIn('int - -', help_screen)
+
 
 class UsageTest(testutils.BaseTestCase):
 

From f547c68c11960a602827e45bbc55e866c137d5a8 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 22 Jul 2019 13:26:28 -0700
Subject: [PATCH 132/324] Fix broken b-key induced typo.

PiperOrigin-RevId: 259393920
Change-Id: Ia44913e38741a42174497e686f50d6fe105778c8
---
 fire/helptext_test.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index b0b545dc..21f88614 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -273,7 +273,7 @@ def testHelpTextNameSectionCommandWithSeparator(self):
     self.assertIn('int -', help_screen)
     self.assertNotIn('int - -', help_screen)
 
-  def testHelpTextNameSectionCommandWithSeparatorVerbobse(self):
+  def testHelpTextNameSectionCommandWithSeparatorVerbose(self):
     component = 9
     t = trace.FireTrace(component, name='int', separator='-')
     t.AddSeparator()

From 46d100e1fcb446a5481fbe148a4419c1e31777bc Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 22 Jul 2019 17:41:24 -0700
Subject: [PATCH 133/324] First step toward custom descriptions and summaries
 for primitive types with messy builtin docstrings.

PiperOrigin-RevId: 259442516
Change-Id: I7e6013af7c9f893af1f80291fdcde5643f2d2830
---
 fire/custom_descriptions.py | 71 +++++++++++++++++++++++++++++++++++++
 fire/docstrings.py          |  2 +-
 fire/helptext.py            | 39 +++++++++++++++-----
 fire/helptext_test.py       | 21 ++++++-----
 4 files changed, 113 insertions(+), 20 deletions(-)
 create mode 100644 fire/custom_descriptions.py

diff --git a/fire/custom_descriptions.py b/fire/custom_descriptions.py
new file mode 100644
index 00000000..70bd666f
--- /dev/null
+++ b/fire/custom_descriptions.py
@@ -0,0 +1,71 @@
+# 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.
+
+"""Custom descriptions and summaries for the builtin types.
+
+The docstrings for objects of primitive types reflect the type of the object,
+rather than the object itself. For example, the docstring for any dict is this:
+
+> print({'key': 'value'}.__doc__)
+dict() -> new empty dictionary
+dict(mapping) -> new dictionary initialized from a mapping object's
+    (key, value) pairs
+dict(iterable) -> new dictionary initialized as if via:
+    d = {}
+    for k, v in iterable:
+        d[k] = v
+dict(**kwargs) -> new dictionary initialized with the name=value pairs
+    in the keyword argument list.  For example:  dict(one=1, two=2)
+
+As you can see, this docstring is more pertinant to the function `dict` and
+would be suitable as the result of `dict.__doc__`, but is wholely unsuitable
+as a description for the dict `{'key': 'value'}`.
+
+This modules aims to resolve that problem, providing custom summaries and
+descriptions for primitive typed values.
+"""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import six
+
+
+def NeedsCustomDescription(component):
+  """Whether the component should use a custom description and summary.
+
+  Components of primitive type, such as ints, floats, dicts, lists, and others
+  have messy builtin docstrings. These are inappropriate for display as
+  descriptions and summaries in a CLI. This function determines whether the
+  provided component has one of these docstrings.
+
+  Note that an object such as `int` has the same docstring as an int like `3`.
+  The docstring is OK for `int`, but is inappropriate as a docstring for `3`.
+
+  Args:
+    component: The component of interest.
+  Returns:
+    Whether the component should use a custom description and summary.
+  """
+  type_ = type(component)
+  if (type_ in six.string_types
+      or type_ in six.integer_types
+      or type_ is six.text_type
+      or type_ is six.binary_type
+      or type_ in (float, complex, bool)
+      or type_ in (dict, tuple, list, set)
+     ):
+    return True
+  return False
diff --git a/fire/docstrings.py b/fire/docstrings.py
index 4f0ffd93..6613d853 100644
--- a/fire/docstrings.py
+++ b/fire/docstrings.py
@@ -424,7 +424,7 @@ def _consume_line(line_info, state):
     elif state.section.format == Formats.NUMPY:
       line_stripped = line_info.remaining.strip()
       if _is_arg_name(line_stripped):
-        # Token on it's own line can either be the last word of the description
+        # Token on its own line can either be the last word of the description
         # of the previous arg, or a new arg. TODO: Whitespace can distinguish.
         arg = _get_or_create_arg_by_name(state, line_stripped)
         state.current_arg = arg
diff --git a/fire/helptext.py b/fire/helptext.py
index 8f4fe97b..257b9286 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -36,11 +36,14 @@
 import inspect
 
 from fire import completion
+from fire import custom_descriptions
 from fire import decorators
 from fire import formatting
 from fire import inspectutils
 from fire import value_types
 
+LINE_LENGTH = 80
+
 
 def HelpText(component, trace=None, verbose=False):
   """Gets the help string for the current component, suitalbe for a help screen.
@@ -61,10 +64,10 @@ def HelpText(component, trace=None, verbose=False):
   metadata = decorators.GetMetadata(component)
 
   # Sections:
-  name_section = _NameSection(info, trace=trace, verbose=verbose)
+  name_section = _NameSection(component, info, trace=trace, verbose=verbose)
   synopsis_section = _SynopsisSection(
       component, actions_grouped_by_kind, spec, metadata, trace=trace)
-  description_section = _DescriptionSection(info)
+  description_section = _DescriptionSection(component, info)
   # TODO(dbieber): Add returns and raises sections for functions.
 
   if inspect.isroutine(component) or inspect.isclass(component):
@@ -92,12 +95,18 @@ def HelpText(component, trace=None, verbose=False):
   )
 
 
-def _NameSection(info, trace=None, verbose=False):
+def _NameSection(component, info, trace=None, verbose=False):
   """The "Name" section of the help string."""
+
   # Only include separators in the name in verbose mode.
   current_command = _GetCurrentCommand(trace, include_separators=verbose)
   summary = _GetSummary(info)
 
+  # If the docstring is one of the messy builtin docstrings, don't show summary.
+  # TODO(dbieber): In follow up commits we can add in replacement summaries.
+  if custom_descriptions.NeedsCustomDescription(component):
+    summary = None
+
   if summary:
     text = current_command + ' - ' + summary
   else:
@@ -129,13 +138,26 @@ def _SynopsisSection(component, actions_grouped_by_kind, spec, metadata,
   return ('SYNOPSIS', text)
 
 
-def _DescriptionSection(info):
-  """The "Description" sections of the help string."""
+def _DescriptionSection(component, info):
+  """The "Description" sections of the help string.
+
+  Args:
+    component: The component to produce the description section for.
+    info: The info dict for the component of interest.
+
+  Returns:
+    Returns the description if available. If not, returns the summary.
+    If neither are available, returns None.
+  """
+  # If the docstring is one of the messy builtin docstrings, set it to None.
+  # TODO(dbieber): In follow up commits we can add in replacement docstrings.
+  if custom_descriptions.NeedsCustomDescription(component):
+    return None
+
   summary = _GetSummary(info)
   description = _GetDescription(info)
-  # Returns the description if available. If not, returns the summary.
-  # If neither are available, returns None.
   text = description or summary or None
+
   if text:
     return ('DESCRIPTION', text)
   else:
@@ -574,7 +596,8 @@ def UsageTextForObject(component, trace=None, verbose=False):
 
 
 def _CreateAvailabilityLine(header, items,
-                            header_indent=2, items_indent=25, line_length=80):
+                            header_indent=2, items_indent=25,
+                            line_length=LINE_LENGTH):
   items_width = line_length - items_indent
   item_names = [item[0] for item in items]
   items_text = '\n'.join(formatting.WrappedJoin(item_names, width=items_width))
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 21f88614..9e93d98f 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -110,9 +110,8 @@ def testHelpTextEmptyList(self):
         trace=trace.FireTrace(component, 'list'))
     self.assertIn('NAME\n    list', help_screen)
     self.assertIn('SYNOPSIS\n    list COMMAND', help_screen)
-    # We don't check description content here since the content could be python
-    # version dependent.
-    self.assertIn('DESCRIPTION\n', help_screen)
+    # The list docstring is messy, so it is not shown.
+    self.assertNotIn('DESCRIPTION', help_screen)
     # We don't check the listed commands either since the list API could
     # potentially change between Python versions.
     self.assertIn('COMMANDS\n    COMMAND is one of the following:\n',
@@ -125,9 +124,8 @@ def testHelpTextShortList(self):
         trace=trace.FireTrace(component, 'list'))
     self.assertIn('NAME\n    list', help_screen)
     self.assertIn('SYNOPSIS\n    list COMMAND', help_screen)
-    # We don't check description content here since the content could be python
-    # version dependent.
-    self.assertIn('DESCRIPTION\n', help_screen)
+    # The list docstring is messy, so it is not shown.
+    self.assertNotIn('DESCRIPTION', help_screen)
 
     # We don't check the listed commands comprehensively since the list API
     # could potentially change between Python versions. Check a few
@@ -142,7 +140,8 @@ def testHelpTextInt(self):
         component=component, trace=trace.FireTrace(component, '7'))
     self.assertIn('NAME\n    7', help_screen)
     self.assertIn('SYNOPSIS\n    7 COMMAND | VALUE', help_screen)
-    self.assertIn('DESCRIPTION\n', help_screen)
+    # The int docstring is messy, so it is not shown.
+    self.assertNotIn('DESCRIPTION', help_screen)
     self.assertIn('COMMANDS\n    COMMAND is one of the following:\n',
                   help_screen)
     self.assertIn('VALUES\n    VALUE is one of the following:\n', help_screen)
@@ -274,12 +273,12 @@ def testHelpTextNameSectionCommandWithSeparator(self):
     self.assertNotIn('int - -', help_screen)
 
   def testHelpTextNameSectionCommandWithSeparatorVerbose(self):
-    component = 9
-    t = trace.FireTrace(component, name='int', separator='-')
+    component = tc.WithDefaults().double
+    t = trace.FireTrace(component, name='double', separator='-')
     t.AddSeparator()
     help_screen = helptext.HelpText(component=component, trace=t, verbose=True)
-    self.assertIn('int -', help_screen)
-    self.assertIn('int - -', help_screen)
+    self.assertIn('double -', help_screen)
+    self.assertIn('double - -', help_screen)
 
 
 class UsageTest(testutils.BaseTestCase):

From a9b4819cb72cd272ea25d38f1e964d55af6c8fc6 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Tue, 23 Jul 2019 09:47:48 -0700
Subject: [PATCH 134/324] Adds the oft-neglected frozenset to the list of types
 needing custom descriptions.

PiperOrigin-RevId: 259554805
Change-Id: I24e8586bf911f406468f858b6b9c4bdfa909ff16
---
 fire/custom_descriptions.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fire/custom_descriptions.py b/fire/custom_descriptions.py
index 70bd666f..695b01b3 100644
--- a/fire/custom_descriptions.py
+++ b/fire/custom_descriptions.py
@@ -65,7 +65,7 @@ def NeedsCustomDescription(component):
       or type_ is six.text_type
       or type_ is six.binary_type
       or type_ in (float, complex, bool)
-      or type_ in (dict, tuple, list, set)
+      or type_ in (dict, tuple, list, set, frozenset)
      ):
     return True
   return False

From 337342e236cb32694f481d3ab84592a52c83029a Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Tue, 23 Jul 2019 15:38:05 -0700
Subject: [PATCH 135/324] Support for namedtuples in Python 2.

PiperOrigin-RevId: 259626143
Change-Id: If87c68d56e5885aa43c9f7ce782e00cd77aa32ad
---
 fire/inspectutils.py      | 21 +++++++++++++++++++--
 fire/inspectutils_test.py | 20 ++++++++++++++++++++
 fire/test_components.py   |  7 +++++++
 3 files changed, 46 insertions(+), 2 deletions(-)

diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index 645b7b18..5cd8d770 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -86,6 +86,7 @@ class with an __init__ method.
 def GetFullArgSpec(fn):
   """Returns a FullArgSpec describing the given callable."""
 
+  original_fn = fn
   fn, skip_arg = _GetArgSpecInfo(fn)
 
   try:
@@ -100,10 +101,26 @@ def GetFullArgSpec(fn):
   except TypeError:
     # If we can't get the argspec, how do we know if the fn should take args?
     # 1. If it's a builtin, it can take args.
-    # 2. If it's an implicit __init__ function (a 'slot wrapper'), take no args.
-    # Are there other cases?
+    # 2. If it's an implicit __init__ function (a 'slot wrapper'), that comes
+    # from a namedtuple, use _fields to determine the args.
+    # 3. If it's another slot wrapper (that comes from not subclassing object in
+    # Python 2), then there are no args.
+    # Are there other cases? We just don't know.
+
+    # Case 1: Builtins accept args.
     if inspect.isbuiltin(fn):
       return FullArgSpec(varargs='vars', varkw='kwargs')
+
+    # Case 2: namedtuples store their args in their _fields attribute.
+    # TODO(dbieber): Determine if there's a way to detect false positives.
+    # In Python 2, a class that does not subclass anything, does not define
+    # __init__, and has an attribute named _fields will cause Fire to think it
+    # expects args for its constructor when in fact it does not.
+    fields = getattr(original_fn, '_fields', None)
+    if fields:
+      return FullArgSpec(args=list(fields))
+
+    # Case 3: Other known slot wrappers do not accept args.
     return FullArgSpec()
 
   if skip_arg and args:
diff --git a/fire/inspectutils_test.py b/fire/inspectutils_test.py
index 0ebd4059..ea8eb0e2 100644
--- a/fire/inspectutils_test.py
+++ b/fire/inspectutils_test.py
@@ -70,6 +70,26 @@ def testGetFullArgSpecFromSlotWrapper(self):
     self.assertEqual(spec.kwonlydefaults, {})
     self.assertEqual(spec.annotations, {})
 
+  def testGetFullArgSpecFromNamedTuple(self):
+    spec = inspectutils.GetFullArgSpec(tc.NamedTuplePoint)
+    self.assertEqual(spec.args, ['x', 'y'])
+    self.assertEqual(spec.defaults, ())
+    self.assertEqual(spec.varargs, None)
+    self.assertEqual(spec.varkw, None)
+    self.assertEqual(spec.kwonlyargs, [])
+    self.assertEqual(spec.kwonlydefaults, {})
+    self.assertEqual(spec.annotations, {})
+
+  def testGetFullArgSpecFromNamedTupleSubclass(self):
+    spec = inspectutils.GetFullArgSpec(tc.SubPoint)
+    self.assertEqual(spec.args, ['x', 'y'])
+    self.assertEqual(spec.defaults, ())
+    self.assertEqual(spec.varargs, None)
+    self.assertEqual(spec.varkw, None)
+    self.assertEqual(spec.kwonlyargs, [])
+    self.assertEqual(spec.kwonlydefaults, {})
+    self.assertEqual(spec.annotations, {})
+
   def testGetFullArgSpecFromClassNoInit(self):
     spec = inspectutils.GetFullArgSpec(tc.OldStyleEmpty)
     self.assertEqual(spec.args, [])
diff --git a/fire/test_components.py b/fire/test_components.py
index f81c9a0f..fd868758 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -20,6 +20,7 @@
 
 import collections
 
+import enum
 import six
 
 if six.PY3:
@@ -396,3 +397,9 @@ class Subdict(dict):
 
 # An example subdict.
 SUBDICT = Subdict({1: 2, 'red': 'blue'})
+
+
+class Color(enum.Enum):
+  RED = 1
+  GREEN = 2
+  BLUE = 3

From 73dde2ca4a01a70f14c4f89e28cccad1a81392cd Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 24 Jul 2019 11:18:28 -0700
Subject: [PATCH 136/324] more explicit check for fields

PiperOrigin-RevId: 259778984
Change-Id: I615eb97beccb1c5e20449ef97a7710c3dc0d5bb0
---
 fire/inspectutils.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index 5cd8d770..18a1f1ae 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -117,7 +117,7 @@ def GetFullArgSpec(fn):
     # __init__, and has an attribute named _fields will cause Fire to think it
     # expects args for its constructor when in fact it does not.
     fields = getattr(original_fn, '_fields', None)
-    if fields:
+    if fields is not None:
       return FullArgSpec(args=list(fields))
 
     # Case 3: Other known slot wrappers do not accept args.

From 01651a2841db5a3e45e1b3cf62b208730f80f3e4 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 24 Jul 2019 11:20:22 -0700
Subject: [PATCH 137/324] Adds test for static and class methods.

PiperOrigin-RevId: 259779362
Change-Id: I5b95f90793f99ef4d682d2b94a2e8d3a2cc725c8
---
 fire/core_test.py       | 14 ++++++++++++++
 fire/test_components.py | 17 +++++++++++++++++
 2 files changed, 31 insertions(+)

diff --git a/fire/core_test.py b/fire/core_test.py
index 171611a9..2f2df3fd 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -178,5 +178,19 @@ def testCallableWithPositionalArgs(self):
       # objects.
       core.Fire(tc.CallableWithPositionalArgs(), command=['3', '4'])
 
+  def testStaticMethod(self):
+    self.assertEqual(
+        core.Fire(tc.HasStaticAndClassMethods,
+                  command=['static_fn', 'alpha']),
+        'alpha',
+    )
+
+  def testClassMethod(self):
+    self.assertEqual(
+        core.Fire(tc.HasStaticAndClassMethods,
+                  command=['class_fn', '6']),
+        7,
+    )
+
 if __name__ == '__main__':
   testutils.main()
diff --git a/fire/test_components.py b/fire/test_components.py
index fd868758..1bf7ccce 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -403,3 +403,20 @@ class Color(enum.Enum):
   RED = 1
   GREEN = 2
   BLUE = 3
+
+
+class HasStaticAndClassMethods(object):
+  """A class with a static method and a class method."""
+
+  CLASS_STATE = 1
+
+  def __init__(self, instance_state):
+    self.instance_state = instance_state
+
+  @staticmethod
+  def static_fn(args):
+    return args
+
+  @classmethod
+  def class_fn(cls, args):
+    return args + cls.CLASS_STATE

From 970d292f1d04fde282a7a3063d3c0f4b8b763348 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 24 Jul 2019 11:28:11 -0700
Subject: [PATCH 138/324] Include varargs in help screens and usage screens.

PiperOrigin-RevId: 259780867
Change-Id: Ibbfe7ca14ea136e25b29a75be12a4a4aa149c5b7
---
 fire/helptext.py        | 26 +++++++++++++++++++++-----
 fire/helptext_test.py   |  2 +-
 fire/test_components.py | 19 +++++++++++++++++++
 3 files changed, 41 insertions(+), 6 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index 257b9286..f1866392 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -182,11 +182,17 @@ def _ArgsAndFlagsSections(info, spec, metadata):
       _CreateArgItem(arg, docstring_info)
       for arg in args_with_no_defaults
   ]
+
+  if spec.varargs:
+    arg_items.append(
+        _CreateArgItem(spec.varargs, docstring_info)
+    )
+
   if arg_items:
     title = 'POSITIONAL ARGUMENTS' if accepts_positional_args else 'ARGUMENTS'
     arguments_section = (title, '\n'.join(arg_items).rstrip('\n'))
     args_and_flags_sections.append(arguments_section)
-    if accepts_positional_args:
+    if args_with_no_defaults and accepts_positional_args:
       notes_sections.append(
           ('NOTES', 'You can also use flags syntax for POSITIONAL ARGUMENTS')
       )
@@ -277,9 +283,15 @@ def _GetArgsAndFlagsString(spec, metadata):
   if flags:
     for flag in flags:
       flag_string = flag_string_template.format(
-          flag_name=formatting.Underline(flag),
-          flag_name_upper=flag.upper())
+          flag_name=flag,
+          flag_name_upper=formatting.Underline(flag.upper()))
       arg_and_flag_strings.append(flag_string)
+
+  if spec.varargs:
+    varargs_string = '[{varargs}]...'.format(
+        varargs=formatting.Underline(spec.varargs.upper()))
+    arg_and_flag_strings.append(varargs_string)
+
   return ' '.join(arg_and_flag_strings)
 
 
@@ -358,7 +370,7 @@ def _CreateArgItem(arg, docstring_info):
   description = None
   if docstring_info.args:
     for arg_in_docstring in docstring_info.args:
-      if arg_in_docstring.name == arg:
+      if arg_in_docstring.name in (arg, '*' + arg, '**' + arg):
         description = arg_in_docstring.description
 
   arg = arg.upper()
@@ -387,7 +399,7 @@ def _CreateFlagItem(flag, docstring_info):
 
   flag = '--{flag}'.format(flag=formatting.Underline(flag))
   if description:
-    return _CreateItem(flag, description, indent=2)
+    return _CreateItem(flag, description, indent=4)
   return flag
 
 
@@ -515,6 +527,10 @@ def UsageTextForFunction(component, trace=None, verbose=False):
         + ' | '.join('--' + flag for flag in flags) + '\n')
   else:
     availability_lines = ''
+
+  if spec.varargs:
+    items.append('[{varargs}]...'.format(varargs=spec.varargs.upper()))
+
   args_and_flags = ' '.join(items)
 
   hyphen_hyphen = ' --' if needs_separating_hyphen_hyphen else ''
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 9e93d98f..5e517860 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -221,7 +221,7 @@ def testHelpScreenForFunctionFunctionWithDefaultArgs(self):
 
     FLAGS
         --count
-          Input number that you want to double."""
+            Input number that you want to double."""
     self.assertEqual(textwrap.dedent(expected_output).strip(),
                      help_output.strip())
 
diff --git a/fire/test_components.py b/fire/test_components.py
index 1bf7ccce..04603b93 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -420,3 +420,22 @@ def static_fn(args):
   @classmethod
   def class_fn(cls, args):
     return args + cls.CLASS_STATE
+
+
+def function_with_varargs(arg1, arg2, arg3=1, *varargs):  # pylint: disable=keyword-arg-before-vararg
+  """Function with varargs.
+
+  Args:
+    arg1: Position arg docstring.
+    arg2: Position arg docstring.
+    arg3: Flags docstring.
+    *varargs: Accepts unlimited positional args.
+  Returns:
+    The unlimited positional args.
+  """
+  del arg1, arg2, arg3  # Unused.
+  return varargs
+
+
+def function_with_keyword_arguments(**kwargs):
+  return kwargs

From ce030f66b41279343b62e31a355f11c214c0cd99 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 24 Jul 2019 11:48:24 -0700
Subject: [PATCH 139/324] call super in setUp to make internal linter happy

PiperOrigin-RevId: 259785116
Change-Id: I1adab292e7fca7b27222d0f4f59278f38d9c37bb
---
 fire/helptext_test.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 5e517860..7fc6ce12 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -31,6 +31,7 @@
 class HelpTest(testutils.BaseTestCase):
 
   def setUp(self):
+    super(HelpTest, self).setUp()
     os.environ['ANSI_COLORS_DISABLED'] = '1'
 
   def testHelpTextNoDefaults(self):

From c1a4acf17433701b8048e6ff34beb3cd9c31121c Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 24 Jul 2019 13:42:02 -0700
Subject: [PATCH 140/324] Include kwargs in help screens and usage screens.

PiperOrigin-RevId: 259807919
Change-Id: I3fb16fa529e1e4085f2fecaf7c1b0d95368d9c20
---
 fire/helptext.py        | 138 +++++++++++++++++++++++-----------------
 fire/helptext_test.py   |  14 ++--
 fire/test_components.py |   5 +-
 3 files changed, 89 insertions(+), 68 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index f1866392..5fd7080a 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -168,7 +168,6 @@ def _ArgsAndFlagsSections(info, spec, metadata):
   """The "Args and Flags" sections of the help string."""
   args_with_no_defaults = spec.args[:len(spec.args) - len(spec.defaults)]
   args_with_defaults = spec.args[len(spec.args) - len(spec.defaults):]
-  flags = args_with_defaults + spec.kwonlyargs
 
   # Check if positional args are allowed. If not, require flag syntax for args.
   accepts_positional_args = metadata.get(decorators.ACCEPTS_POSITIONAL_ARGS)
@@ -197,10 +196,23 @@ def _ArgsAndFlagsSections(info, spec, metadata):
           ('NOTES', 'You can also use flags syntax for POSITIONAL ARGUMENTS')
       )
 
-  flag_items = [
-      _CreateFlagItem(flag, docstring_info)
-      for flag in flags
+  optional_flag_items = [
+      _CreateFlagItem(flag, docstring_info, required=False)
+      for flag in args_with_defaults
   ]
+  required_flag_items = [
+      _CreateFlagItem(flag, docstring_info, required=True)
+      for flag in spec.kwonlyargs
+  ]
+  flag_items = optional_flag_items + required_flag_items
+
+  if spec.varkw:
+    description = _GetArgDescription(spec.varkw, docstring_info)
+    message = ('Additional flags are accepted.'
+               if flag_items else
+               'Flags are accepted.')
+    item = _CreateItem(message, description, indent=4)
+    flag_items.append(item)
 
   if flag_items:
     flags_section = ('FLAGS', '\n'.join(flag_items))
@@ -262,7 +274,6 @@ def _GetArgsAndFlagsString(spec, metadata):
   """
   args_with_no_defaults = spec.args[:len(spec.args) - len(spec.defaults)]
   args_with_defaults = spec.args[len(spec.args) - len(spec.defaults):]
-  flags = args_with_defaults + spec.kwonlyargs
 
   # Check if positional args are allowed. If not, require flag syntax for args.
   accepts_positional_args = metadata.get(decorators.ACCEPTS_POSITIONAL_ARGS)
@@ -279,13 +290,9 @@ def _GetArgsAndFlagsString(spec, metadata):
           for arg in args_with_no_defaults]
     arg_and_flag_strings.extend(arg_strings)
 
-  flag_string_template = '[--{flag_name}={flag_name_upper}]'
-  if flags:
-    for flag in flags:
-      flag_string = flag_string_template.format(
-          flag_name=flag,
-          flag_name_upper=formatting.Underline(flag.upper()))
-      arg_and_flag_strings.append(flag_string)
+  # If there are any arguments that are treated as flags:
+  if args_with_defaults or spec.kwonlyargs or spec.varkw:
+    arg_and_flag_strings.append('<flags>')
 
   if spec.varargs:
     varargs_string = '[{varargs}]...'.format(
@@ -367,48 +374,51 @@ def _CreateArgItem(arg, docstring_info):
   Returns:
     A string to be used in constructing the help screen for the function.
   """
-  description = None
-  if docstring_info.args:
-    for arg_in_docstring in docstring_info.args:
-      if arg_in_docstring.name in (arg, '*' + arg, '**' + arg):
-        description = arg_in_docstring.description
+  description = _GetArgDescription(arg, docstring_info)
 
   arg = arg.upper()
-  if description:
-    return _CreateItem(formatting.BoldUnderline(arg), description, indent=4)
-  else:
-    return formatting.BoldUnderline(arg)
+  return _CreateItem(formatting.BoldUnderline(arg), description, indent=4)
 
 
-def _CreateFlagItem(flag, docstring_info):
+def _CreateFlagItem(flag, docstring_info, required=False):
   """Returns a string describing a flag using information from the docstring.
 
   Args:
     flag: The name of the flag.
     docstring_info: A docstrings.DocstringInfo namedtuple with information about
       the containing function's docstring.
+    required: Whether the flag is required. Keyword-only arguments (only in
+      Python 3) become required flags, whereas normal keyword arguments become
+      optional flags.
   Returns:
     A string to be used in constructing the help screen for the function.
   """
-  description = None
-  if docstring_info.args:
-    for arg_in_docstring in docstring_info.args:
-      if arg_in_docstring.name == flag:
-        description = arg_in_docstring.description
-        break
+  description = _GetArgDescription(flag, docstring_info)
 
-  flag = '--{flag}'.format(flag=formatting.Underline(flag))
-  if description:
-    return _CreateItem(flag, description, indent=4)
-  return flag
+  flag_string_template = '--{flag_name}={flag_name_upper}'
+  flag = flag_string_template.format(
+      flag_name=flag,
+      flag_name_upper=formatting.Underline(flag.upper()))
+  if not required:
+    flag += ' (optional)'
+  return _CreateItem(flag, description, indent=4)
 
 
 def _CreateItem(name, description, indent=2):
+  if not description:
+    return name
   return """{name}
 {description}""".format(name=name,
                         description=formatting.Indent(description, indent))
 
 
+def _GetArgDescription(name, docstring_info):
+  if docstring_info.args:
+    for arg_in_docstring in docstring_info.args:
+      if arg_in_docstring.name in (name, '*' + name, '**' + name):
+        return arg_in_docstring.description
+
+
 def _GroupUsageDetailsSection(groups):
   """Creates a section tuple for the groups section of the usage details."""
   group_item_strings = []
@@ -417,9 +427,8 @@ def _GroupUsageDetailsSection(groups):
     group_item = group_name
     if 'docstring_info' in group_info:
       group_docstring_info = group_info['docstring_info']
-      if group_docstring_info and group_docstring_info.summary:
-        group_item = _CreateItem(group_name,
-                                 group_docstring_info.summary)
+      if group_docstring_info:
+        group_item = _CreateItem(group_name, group_docstring_info.summary)
     group_item_strings.append(group_item)
   return ('GROUPS', _NewChoicesSection('GROUP', group_item_strings))
 
@@ -432,9 +441,8 @@ def _CommandUsageDetailsSection(commands):
     command_item = command_name
     if 'docstring_info' in command_info:
       command_docstring_info = command_info['docstring_info']
-      if command_docstring_info and command_docstring_info.summary:
-        command_item = _CreateItem(command_name,
-                                   command_docstring_info.summary)
+      if command_docstring_info:
+        command_item = _CreateItem(command_name, command_docstring_info.summary)
     command_item_strings.append(command_item)
   return ('COMMANDS', _NewChoicesSection('COMMAND', command_item_strings))
 
@@ -502,14 +510,8 @@ def UsageTextForFunction(component, trace=None, verbose=False):
     command = ''
 
   spec = inspectutils.GetFullArgSpec(component)
-  args = spec.args
-  if spec.defaults is None:
-    num_defaults = 0
-  else:
-    num_defaults = len(spec.defaults)
-  args_with_no_defaults = args[:len(args) - num_defaults]
-  args_with_defaults = args[len(args) - num_defaults:]
-  flags = args_with_defaults + spec.kwonlyargs
+  args_with_no_defaults = spec.args[:len(spec.args) - len(spec.defaults)]
+  args_with_defaults = spec.args[len(spec.args) - len(spec.defaults):]
 
   # Check if positional args are allowed. If not, show flag syntax for args.
   metadata = decorators.GetMetadata(component)
@@ -520,13 +522,32 @@ def UsageTextForFunction(component, trace=None, verbose=False):
   else:
     items = [arg.upper() for arg in args_with_no_defaults]
 
-  if flags:
+  # If there are any arguments that are treated as flags:
+  if args_with_defaults or spec.kwonlyargs or spec.varkw:
     items.append('<flags>')
-    availability_lines = (
-        '\nAvailable flags: '
-        + ' | '.join('--' + flag for flag in flags) + '\n')
-  else:
-    availability_lines = ''
+
+  optional_flags = [('--' + flag) for flag in args_with_defaults]
+  required_flags = [('--' + flag) for flag in spec.kwonlyargs]
+
+  # Flags section:
+  availability_lines = []
+  if optional_flags:
+    availability_lines.append(
+        _CreateAvailabilityLine(header='Optional flags:', items=optional_flags,
+                                header_indent=0))
+  if required_flags:
+    availability_lines.append(
+        _CreateAvailabilityLine(header='Required flags:', items=required_flags,
+                                header_indent=0))
+  if spec.varkw:
+    additional_flags = ('Additional flags are accepted.'
+                        if optional_flags or required_flags else
+                        'Flags are accepted.')
+    availability_lines.append(additional_flags + '\n')
+
+  if availability_lines:
+    # Start the section with blank lines.
+    availability_lines.insert(0, '\n')
 
   if spec.varargs:
     items.append('[{varargs}]...'.format(varargs=spec.varargs.upper()))
@@ -538,7 +559,7 @@ def UsageTextForFunction(component, trace=None, verbose=False):
   return output_template.format(
       current_command=command,
       args_and_flags=args_and_flags,
-      availability_lines=availability_lines,
+      availability_lines=''.join(availability_lines),
       hyphen_hyphen=hyphen_hyphen)
 
 
@@ -576,25 +597,25 @@ def UsageTextForObject(component, trace=None, verbose=False):
     possible_actions.append('group')
     groups_text = _CreateAvailabilityLine(
         header='available groups:',
-        items=groups)
+        items=[name for name, _ in groups])
     availability_lines.append(groups_text)
   if commands:
     possible_actions.append('command')
     commands_text = _CreateAvailabilityLine(
         header='available commands:',
-        items=commands)
+        items=[name for name, _ in commands])
     availability_lines.append(commands_text)
   if values:
     possible_actions.append('value')
     values_text = _CreateAvailabilityLine(
         header='available values:',
-        items=values)
+        items=[name for name, _ in values])
     availability_lines.append(values_text)
   if indexes:
     possible_actions.append('index')
     indexes_text = _CreateAvailabilityLine(
         header='available indexes:',
-        items=[(indexes, None)])
+        items=indexes)
     availability_lines.append(indexes_text)
 
   if possible_actions:
@@ -615,8 +636,7 @@ def _CreateAvailabilityLine(header, items,
                             header_indent=2, items_indent=25,
                             line_length=LINE_LENGTH):
   items_width = line_length - items_indent
-  item_names = [item[0] for item in items]
-  items_text = '\n'.join(formatting.WrappedJoin(item_names, width=items_width))
+  items_text = '\n'.join(formatting.WrappedJoin(items, width=items_width))
   indented_items_text = formatting.Indent(items_text, spaces=items_indent)
   indented_header = formatting.Indent(header, spaces=header_indent)
   return indented_header + indented_items_text[len(indented_header):] + '\n'
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 7fc6ce12..46f5b851 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -77,9 +77,9 @@ def testHelpTextFunctionWithDefaults(self):
         component=component,
         trace=trace.FireTrace(component, name='triple'))
     self.assertIn('NAME\n    triple', help_screen)
-    self.assertIn('SYNOPSIS\n    triple [--count=COUNT]', help_screen)
+    self.assertIn('SYNOPSIS\n    triple <flags>', help_screen)
     self.assertNotIn('DESCRIPTION', help_screen)
-    self.assertIn('FLAGS\n    --count', help_screen)
+    self.assertIn('FLAGS\n    --count=COUNT (optional)', help_screen)
     self.assertNotIn('NOTES', help_screen)
 
   def testHelpTextFunctionWithBuiltin(self):
@@ -215,13 +215,13 @@ def testHelpScreenForFunctionFunctionWithDefaultArgs(self):
         double - Returns the input multiplied by 2.
 
     SYNOPSIS
-        double [--count=COUNT]
+        double <flags>
 
     DESCRIPTION
         Returns the input multiplied by 2.
 
     FLAGS
-        --count
+        --count=COUNT (optional)
             Input number that you want to double."""
     self.assertEqual(textwrap.dedent(expected_output).strip(),
                      help_output.strip())
@@ -232,7 +232,7 @@ def testHelpTextUnderlineFlag(self):
     help_screen = helptext.HelpText(component, t)
     self.assertIn(formatting.Bold('NAME') + '\n    triple', help_screen)
     self.assertIn(
-        formatting.Bold('SYNOPSIS') + '\n    triple [--count=COUNT]',
+        formatting.Bold('SYNOPSIS') + '\n    triple <flags>',
         help_screen)
     self.assertIn(
         formatting.Bold('FLAGS') + '\n    --' + formatting.Underline('count'),
@@ -334,7 +334,7 @@ def testUsageOutputFunctionWithHelp(self):
     expected_output = '''
     Usage: function_with_help <flags>
 
-    Available flags: --help
+    Optional flags:          --help
 
     For detailed information on this command, run:
       function_with_help -- --help'''
@@ -349,7 +349,7 @@ def testUsageOutputFunctionWithDocstring(self):
     expected_output = '''
     Usage: multiplier_with_docstring NUM <flags>
 
-    Available flags: --rate
+    Optional flags:          --rate
 
     For detailed information on this command, run:
       multiplier_with_docstring --help'''
diff --git a/fire/test_components.py b/fire/test_components.py
index 04603b93..650d279a 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -437,5 +437,6 @@ def function_with_varargs(arg1, arg2, arg3=1, *varargs):  # pylint: disable=keyw
   return varargs
 
 
-def function_with_keyword_arguments(**kwargs):
-  return kwargs
+def function_with_keyword_arguments(arg1, arg2=3, **kwargs):
+  del arg2  # Unused.
+  return arg1, kwargs

From 97a401e435183420e83f85b4ae6e82ee4ed43b06 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 24 Jul 2019 14:02:02 -0700
Subject: [PATCH 141/324] switch from marking optional flags to marking
 required flags.

PiperOrigin-RevId: 259812051
Change-Id: I81809f372dd4f5afd46128bd70c69e6b572e7d0c
---
 fire/helptext.py      | 4 ++--
 fire/helptext_test.py | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index 5fd7080a..ecfe10ed 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -399,8 +399,8 @@ def _CreateFlagItem(flag, docstring_info, required=False):
   flag = flag_string_template.format(
       flag_name=flag,
       flag_name_upper=formatting.Underline(flag.upper()))
-  if not required:
-    flag += ' (optional)'
+  if required:
+    flag += ' (required)'
   return _CreateItem(flag, description, indent=4)
 
 
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 46f5b851..33676420 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -79,7 +79,7 @@ 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 (optional)', help_screen)
+    self.assertIn('FLAGS\n    --count=COUNT', help_screen)
     self.assertNotIn('NOTES', help_screen)
 
   def testHelpTextFunctionWithBuiltin(self):
@@ -221,7 +221,7 @@ def testHelpScreenForFunctionFunctionWithDefaultArgs(self):
         Returns the input multiplied by 2.
 
     FLAGS
-        --count=COUNT (optional)
+        --count=COUNT
             Input number that you want to double."""
     self.assertEqual(textwrap.dedent(expected_output).strip(),
                      help_output.strip())

From eea89511daeaded98edea91a808d348b89c16fe1 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 24 Jul 2019 14:26:20 -0700
Subject: [PATCH 142/324] Adds hello world example to readme. Shows that --help
 works for help without isolated --.

PiperOrigin-RevId: 259817005
Change-Id: I5c079d60dcac234fa1cb53d16f498fb06edc117b
---
 README.md     | 38 +++++++++++++++++++++++++++++---------
 docs/index.md | 38 +++++++++++++++++++++++++++++---------
 2 files changed, 58 insertions(+), 18 deletions(-)

diff --git a/README.md b/README.md
index 9aea0877..862f5f23 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,26 @@ You can call `Fire` on any Python object:<br>
 functions, classes, modules, objects, dictionaries, lists, tuples, etc.
 They all work!
 
+Here's an example of calling Fire on a function.
+
+```python
+import fire
+
+def hello(name="World"):
+  return "Hello %s!" % name
+
+if __name__ == '__main__':
+  fire.Fire(hello)
+```
+
+Then, from the command line, you can run:
+
+```bash
+python hello.py  # Hello World!
+python hello.py --name=David  # Hello David!
+python hello.py --help  # Shows usage information.
+```
+
 Here's an example of calling Fire on a class.
 
 ```python
@@ -77,16 +97,16 @@ Please see [The Python Fire Guide](docs/guide.md).
 | Call           | `fire.Fire()`          | Turns the current module into a Fire CLI.
 | Call           | `fire.Fire(component)` | Turns `component` into a Fire CLI.
 
-| Using a CLI    | Command                    | Notes
-| :------------- | :------------------------- | :---------
-| [Help](docs/using-cli.md#help-flag) | `command -- --help` |
-| [REPL](docs/using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
-| [Separator](docs/using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
-| [Completion](docs/using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI.
-| [Trace](docs/using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
-| [Verbose](docs/using-cli.md#verbose-flag) | `command -- --verbose` |
+Using a CLI                                     | Command                                 | Notes
+:---------------------------------------------- | :-------------------------------------- | :----
+[Help](docs/using-cli.md#help-flag)             | `command --help` or `command -- --help` |
+[REPL](docs/using-cli.md#interactive-flag)      | `command -- --interactive`              | Enters interactive mode.
+[Separator](docs/using-cli.md#separator-flag)   | `command -- --separator=X`              | Sets the separator to `X`. The default separator is `-`.
+[Completion](docs/using-cli.md#completion-flag) | `command -- --completion [shell]`       | Generates a completion script for the CLI.
+[Trace](docs/using-cli.md#trace-flag)           | `command -- --trace`                    | Gets a Fire trace for the command.
+[Verbose](docs/using-cli.md#verbose-flag)       | `command -- --verbose`                  |
 
-_Note that flags are separated from the Fire command by an isolated `--` arg._
+_Note that these flags are separated from the Fire command by an isolated `--`._
 
 ## License
 
diff --git a/docs/index.md b/docs/index.md
index f6503c0b..171ae26e 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -27,6 +27,26 @@ You can call `Fire` on any Python object:<br>
 functions, classes, modules, objects, dictionaries, lists, tuples, etc.
 They all work!
 
+Here's an example of calling Fire on a function.
+
+```python
+import fire
+
+def hello(name="World"):
+  return "Hello %s!" % name
+
+if __name__ == '__main__':
+  fire.Fire(hello)
+```
+
+Then, from the command line, you can run:
+
+```bash
+python hello.py  # Hello World!
+python hello.py --name=David  # Hello David!
+python hello.py --help  # Shows usage information.
+```
+
 Here's an example of calling Fire on a class.
 
 ```python
@@ -77,16 +97,16 @@ Please see [The Python Fire Guide](guide.md).
 | Call           | `fire.Fire()`          | Turns the current module into a Fire CLI.
 | Call           | `fire.Fire(component)` | Turns `component` into a Fire CLI.
 
-| Using a CLI    | Command                    | Notes
-| :------------- | :------------------------- | :---------
-| [Help](using-cli.md#help-flag) | `command -- --help` |
-| [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
-| [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
-| [Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI.
-| [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
-| [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` |
+Using a CLI                                     | Command                                 | Notes
+:---------------------------------------------- | :-------------------------------------- | :----
+[Help](using-cli.md#help-flag)             | `command --help` or `command -- --help` |
+[REPL](using-cli.md#interactive-flag)      | `command -- --interactive`              | Enters interactive mode.
+[Separator](using-cli.md#separator-flag)   | `command -- --separator=X`              | Sets the separator to `X`. The default separator is `-`.
+[Completion](using-cli.md#completion-flag) | `command -- --completion [shell]`       | Generates a completion script for the CLI.
+[Trace](using-cli.md#trace-flag)           | `command -- --trace`                    | Gets a Fire trace for the command.
+[Verbose](using-cli.md#verbose-flag)       | `command -- --verbose`                  |
 
-_Note that flags are separated from the Fire command by an isolated `--` arg._
+_Note that these flags are separated from the Fire command by an isolated `--`._
 
 ## License
 

From ed3cd74b4d24b2f94c6fb620d15ab00c9c22c9ca Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 24 Jul 2019 14:50:01 -0700
Subject: [PATCH 143/324] resolve inconsistent-return-statements lint error.

PiperOrigin-RevId: 259821764
Change-Id: I3983037f00f0d620b84363cd1bfc348c085ac8a2
---
 fire/helptext.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/fire/helptext.py b/fire/helptext.py
index ecfe10ed..56c1ff7b 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -417,6 +417,7 @@ def _GetArgDescription(name, docstring_info):
     for arg_in_docstring in docstring_info.args:
       if arg_in_docstring.name in (name, '*' + name, '**' + name):
         return arg_in_docstring.description
+  return None
 
 
 def _GroupUsageDetailsSection(groups):

From dbc8022386a48462074c8a5ddd01055bfd690606 Mon Sep 17 00:00:00 2001
From: Evan Juarez <evanjuv@google.com>
Date: Thu, 25 Jul 2019 15:13:18 -0700
Subject: [PATCH 144/324] Don't strip whitespace when collecting descriptions
 in docstring

PiperOrigin-RevId: 260030371
Change-Id: Id307fa5ca4c77e07290928daea6b3e4291e1c0e6
---
 fire/docstrings.py      | 38 ++++++++++++++++++++++++++++++++++++--
 fire/docstrings_test.py | 14 ++++++++++----
 fire/test_components.py | 17 +++++++++++++++++
 3 files changed, 63 insertions(+), 6 deletions(-)

diff --git a/fire/docstrings.py b/fire/docstrings.py
index 6613d853..b15cddce 100644
--- a/fire/docstrings.py
+++ b/fire/docstrings.py
@@ -56,6 +56,7 @@
 
 import collections
 import re
+import textwrap
 
 import enum
 
@@ -179,7 +180,10 @@ def parse(docstring):
     _consume_line(line_info, state)
 
   summary = ' '.join(state.summary.lines) if state.summary.lines else None
-  description = _join_lines(state.description.lines)
+  state.description.lines = _strip_blank_lines(state.description.lines)
+  description = textwrap.dedent('\n'.join(state.description.lines))
+  if not description:
+    description = None
   returns = _join_lines(state.returns.lines)
   yields = _join_lines(state.yields.lines)
   raises = _join_lines(state.raises.lines)
@@ -203,6 +207,32 @@ def parse(docstring):
   )
 
 
+def _strip_blank_lines(lines):
+  """Removes lines containing only blank characters before and after the text.
+
+  Args:
+    lines: A list of lines.
+  Returns:
+    A list of lines without trailing or leading blank lines.
+  """
+  # Find the first non-blank line.
+  start = 0
+  while lines and _is_blank(lines[start]):
+    start += 1
+
+  lines = lines[start:]
+
+  # Remove trailing blank lines.
+  while lines and _is_blank(lines[-1]):
+    lines.pop()
+
+  return lines
+
+
+def _is_blank(line):
+  return not line or line.isspace()
+
+
 def _join_lines(lines):
   """Joins lines with the appropriate connective whitespace.
 
@@ -391,7 +421,7 @@ def _consume_line(line_info, state):
     else:
       # We're past the end of the summary.
       # Additions now contribute to the description.
-      state.description.lines.append(line_info.remaining)
+      state.description.lines.append(line_info.remaining_raw)
   else:
     state.summary.permitted = False
 
@@ -470,6 +500,7 @@ def _create_line_info(line, next_line):
   line_info = Namespace()  # TODO(dbieber): Switch to an explicit class.
   line_info.line = line
   line_info.stripped = line.strip()
+  line_info.remaining_raw = line_info.line
   line_info.remaining = line_info.stripped
   line_info.indentation = len(line) - len(line.lstrip())
   line_info.next.line = next_line
@@ -497,6 +528,7 @@ def _update_section_state(line_info, state):
     state.section.format = Formats.GOOGLE
     state.section.title = google_section
     line_info.remaining = _get_after_google_header(line_info)
+    line_info.remaining_raw = line_info.remaining
     section_updated = True
 
   rst_section = _rst_section(line_info)
@@ -504,6 +536,7 @@ def _update_section_state(line_info, state):
     state.section.format = Formats.RST
     state.section.title = rst_section
     line_info.remaining = _get_after_directive(line_info)
+    line_info.remaining_raw = line_info.remaining
     section_updated = True
 
   numpy_section = _numpy_section(line_info)
@@ -511,6 +544,7 @@ def _update_section_state(line_info, state):
     state.section.format = Formats.NUMPY
     state.section.title = numpy_section
     line_info.remaining = ''
+    line_info.remaining_raw = line_info.remaining
     section_updated = True
 
   if section_updated:
diff --git a/fire/docstrings_test.py b/fire/docstrings_test.py
index 96810c7e..5f7eeeb1 100644
--- a/fire/docstrings_test.py
+++ b/fire/docstrings_test.py
@@ -131,7 +131,7 @@ def test_google_format_typed_args_and_returns(self):
     expected_docstring_info = DocstringInfo(
         summary='Docstring summary.',
         description='This is a longer description of the docstring. It spans '
-        'multiple lines, as is allowed.',
+        'multiple lines, as\nis allowed.',
         args=[
             ArgInfo(name='param1', type='int',
                     description='The first parameter.'),
@@ -159,7 +159,7 @@ def test_rst_format_typed_args_and_returns(self):
     expected_docstring_info = DocstringInfo(
         summary='Docstring summary.',
         description='This is a longer description of the docstring. It spans '
-        'across multiple lines.',
+        'across multiple\nlines.',
         args=[
             ArgInfo(name='arg1', type='str',
                     description='Description of arg1.'),
@@ -193,7 +193,7 @@ def test_numpy_format_typed_args_and_returns(self):
     expected_docstring_info = DocstringInfo(
         summary='Docstring summary.',
         description='This is a longer description of the docstring. It spans '
-        'across multiple lines.',
+        'across multiple\nlines.',
         args=[
             ArgInfo(name='param1', type='int',
                     description='The first parameter.'),
@@ -217,7 +217,7 @@ def test_multisection_docstring(self):
     expected_docstring_info = DocstringInfo(
         summary='Docstring summary.',
         description='This is the first section of a docstring description.\n\n'
-        'This is the second section of a docstring description. This docstring '
+        'This is the second section of a docstring description. This docstring\n'
         'description has just two sections.',
     )
     self.assertEqual(docstring_info, expected_docstring_info)
@@ -232,6 +232,12 @@ def test_ill_formed_docstring(self):
     """
     docstrings.parse(docstring)
 
+  def test_strip_blank_lines(self):
+    lines = ['   ', '  foo  ', '   ']
+    expected_output = ['  foo  ']
+
+    self.assertEqual(docstrings._strip_blank_lines(lines), expected_output)
+
 
 if __name__ == '__main__':
   testutils.main()
diff --git a/fire/test_components.py b/fire/test_components.py
index 650d279a..1ca39c14 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -440,3 +440,20 @@ def function_with_varargs(arg1, arg2, arg3=1, *varargs):  # pylint: disable=keyw
 def function_with_keyword_arguments(arg1, arg2=3, **kwargs):
   del arg2  # Unused.
   return arg1, kwargs
+
+
+def fn_with_code_in_docstring():
+  """This has code in the docstring.
+
+
+
+  Example:
+    x = fn_with_code_in_docstring()
+    indentation_matters = True
+
+
+
+  Returns:
+    True.
+  """
+  return True

From 3d74ae8e486054d391c23650da201371b89de90c Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 25 Jul 2019 15:24:04 -0700
Subject: [PATCH 145/324] Show both call syntax and members for callable
 objects in help and usage screens.

PiperOrigin-RevId: 260032359
Change-Id: I91963f1eb1fa570c15a1accd4588f53e1a77cabe
---
 fire/completion.py      |  12 ++-
 fire/core.py            |   4 +-
 fire/helptext.py        | 203 ++++++++++++++++++----------------------
 fire/inspectutils.py    |   3 +
 fire/test_components.py |   5 +
 5 files changed, 109 insertions(+), 118 deletions(-)

diff --git a/fire/completion.py b/fire/completion.py
index 705ef586..f1be1558 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -281,7 +281,7 @@ def _FishScript(name, commands, default_options=None):
   )
 
 
-def MemberVisible(name, member, verbose):
+def MemberVisible(component, name, member, verbose):
   """Returns whether a member should be included in auto-completion or help.
 
   Determines whether a member of an object with the specified name should be
@@ -294,6 +294,7 @@ def MemberVisible(name, member, verbose):
   When not in verbose mode, some modules and functions are excluded as well.
 
   Args:
+    component: The component containing the member.
     name: The name of the member.
     member: The member itself.
     verbose: Whether to include private members.
@@ -309,6 +310,13 @@ def MemberVisible(name, member, verbose):
   if inspect.ismodule(member) and member is six:
     # TODO(dbieber): Determine more generally which modules to hide.
     return False
+  if (six.PY2 and inspect.isfunction(component)
+      and name in ('func_closure', 'func_code', 'func_defaults',
+                   'func_dict', 'func_doc', 'func_globals', 'func_name')):
+    return False
+  if (six.PY2 and inspect.ismethod(component)
+      and name in ('im_class', 'im_func', 'im_self')):
+    return False
   if isinstance(name, six.string_types):
     return not name.startswith('_')
   return True  # Default to including the member
@@ -334,7 +342,7 @@ def _Members(component, verbose=False):
   return [
       (member_name, member)
       for member_name, member in members
-      if MemberVisible(member_name, member, verbose)
+      if MemberVisible(component, member_name, member, verbose)
   ]
 
 
diff --git a/fire/core.py b/fire/core.py
index 3ba081c2..d3d51d93 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -303,7 +303,7 @@ def _DictAsString(result, verbose=False):
   # 1) Getting visible items and the longest key for output formatting
   # 2) Actually construct the output lines
   result_visible = {key: value for key, value in result.items()
-                    if completion.MemberVisible(key, value, verbose)}
+                    if completion.MemberVisible(result, key, value, verbose)}
 
   if not result_visible:
     return '{}'
@@ -313,7 +313,7 @@ def _DictAsString(result, verbose=False):
 
   lines = []
   for key, value in result.items():
-    if completion.MemberVisible(key, value, verbose):
+    if completion.MemberVisible(result, key, value, verbose):
       line = format_string.format(key=str(key) + ':',
                                   value=_OneLineResult(value))
       lines.append(line)
diff --git a/fire/helptext.py b/fire/helptext.py
index 56c1ff7b..7ae9bb0c 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -70,18 +70,14 @@ def HelpText(component, trace=None, verbose=False):
   description_section = _DescriptionSection(component, info)
   # TODO(dbieber): Add returns and raises sections for functions.
 
-  if inspect.isroutine(component) or inspect.isclass(component):
-    # For functions (ARGUMENTS / POSITIONAL ARGUMENTS, FLAGS)
+  if callable(component):
     args_and_flags_sections, notes_sections = _ArgsAndFlagsSections(
         info, spec, metadata)
-    usage_details_sections = []
   else:
-    # For objects (GROUPS, COMMANDS, VALUES, INDEXES)
-    # TODO(dbieber): Show callable function usage in help text.
     args_and_flags_sections = []
     notes_sections = []
-    usage_details_sections = _UsageDetailsSections(component,
-                                                   actions_grouped_by_kind)
+  usage_details_sections = _UsageDetailsSections(component,
+                                                 actions_grouped_by_kind)
 
   sections = (
       [name_section, synopsis_section, description_section]
@@ -119,21 +115,19 @@ def _SynopsisSection(component, actions_grouped_by_kind, spec, metadata,
   """The "Synopsis" section of the help string."""
   current_command = _GetCurrentCommand(trace=trace, include_separators=True)
 
-  # TODO(dbieber): Support callable functions.
-  if inspect.isroutine(component) or inspect.isclass(component):
-    # For function:
-    args_and_flags = _GetArgsAndFlagsString(spec, metadata)
-    synopsis_section_template = '{current_command} {args_and_flags}'
-    text = synopsis_section_template.format(
-        current_command=current_command, args_and_flags=args_and_flags)
+  possible_actions = _GetPossibleActions(actions_grouped_by_kind)
 
-  else:
-    # For object:
-    possible_actions_string = _GetPossibleActionsString(actions_grouped_by_kind)
-    synopsis_template = '{current_command} {possible_actions}'
-    text = synopsis_template.format(
-        current_command=current_command,
-        possible_actions=possible_actions_string)
+  continuations = []
+  if possible_actions:
+    continuations.append(_GetPossibleActionsString(possible_actions))
+  if callable(component):
+    continuations.append(_GetArgsAndFlagsString(spec, metadata))
+  continuation = ' | '.join(continuations)
+
+  synopsis_template = '{current_command} {continuation}'
+  text = synopsis_template.format(
+      current_command=current_command,
+      continuation=continuation)
 
   return ('SYNOPSIS', text)
 
@@ -225,22 +219,17 @@ def _UsageDetailsSections(component, actions_grouped_by_kind):
   """The usage details sections of the help string."""
   groups, commands, values, indexes = actions_grouped_by_kind
 
-  usage_details_sections = []
+  sections = []
+  if groups.members:
+    sections.append(_MakeUsageDetailsSection(groups))
+  if commands.members:
+    sections.append(_MakeUsageDetailsSection(commands))
+  if values.members:
+    sections.append(_ValuesUsageDetailsSection(component, values))
+  if indexes.members:
+    sections.append(('INDEXES', _NewChoicesSection('INDEX', indexes.names)))
 
-  if groups:
-    usage_details_section = _GroupUsageDetailsSection(groups)
-    usage_details_sections.append(usage_details_section)
-  if commands:
-    usage_details_section = _CommandUsageDetailsSection(commands)
-    usage_details_sections.append(usage_details_section)
-  if values:
-    usage_details_section = _ValuesUsageDetailsSection(component, values)
-    usage_details_sections.append(usage_details_section)
-  if indexes:
-    usage_details_sections.append(
-        ('INDEXES', _NewChoicesSection('INDEX', [indexes])))
-
-  return usage_details_sections
+  return sections
 
 
 def _GetSummary(info):
@@ -302,51 +291,46 @@ def _GetArgsAndFlagsString(spec, metadata):
   return ' '.join(arg_and_flag_strings)
 
 
-def _GetPossibleActionsString(actions_grouped_by_kind):
-  """A help screen string listing the possible action kinds available."""
-  groups, commands, values, indexes = actions_grouped_by_kind
-
+def _GetPossibleActions(actions_grouped_by_kind):
+  """The list of possible action kinds."""
   possible_actions = []
-  if groups:
-    possible_actions.append('GROUP')
-  if commands:
-    possible_actions.append('COMMAND')
-  if values:
-    possible_actions.append('VALUE')
-  if indexes:
-    possible_actions.append('INDEX')
+  for action_group in actions_grouped_by_kind:
+    if action_group.members:
+      possible_actions.append(action_group.name)
+  return possible_actions
+
 
-  possible_actions_string = ' | '.join(
-      formatting.Underline(action) for action in possible_actions)
-  return possible_actions_string
+def _GetPossibleActionsString(possible_actions):
+  """A help screen string listing the possible action kinds available."""
+  return ' | '.join(formatting.Underline(action.upper())
+                    for action in possible_actions)
 
 
 def _GetActionsGroupedByKind(component, verbose=False):
   """Gets lists of available actions, grouped by action kind."""
-  groups = []
-  commands = []
-  values = []
+  groups = ActionGroup(name='group', plural='groups')
+  commands = ActionGroup(name='command', plural='commands')
+  values = ActionGroup(name='value', plural='values')
+  indexes = ActionGroup(name='index', plural='indexes')
 
   members = completion._Members(component, verbose)  # pylint: disable=protected-access
   for member_name, member in members:
     member_name = str(member_name)
     if value_types.IsGroup(member):
-      groups.append((member_name, member))
+      groups.Add(name=member_name, member=member)
     if value_types.IsCommand(member):
-      commands.append((member_name, member))
+      commands.Add(name=member_name, member=member)
     if value_types.IsValue(member):
-      values.append((member_name, member))
+      values.Add(name=member_name, member=member)
 
-  indexes = None
   if isinstance(component, (list, tuple)) and component:
     component_len = len(component)
-    # WARNING: Note that indexes is a string, whereas the rest are lists.
     if component_len < 10:
-      indexes = ', '.join(str(x) for x in range(component_len))
+      indexes.Add(name=', '.join(str(x) for x in range(component_len)))
     else:
-      indexes = '0..{max}'.format(max=component_len-1)
+      indexes.Add(name='0..{max}'.format(max=component_len-1))
 
-  return groups, commands, values, indexes
+  return [groups, commands, values, indexes]
 
 
 def _GetCurrentCommand(trace=None, include_separators=True):
@@ -420,38 +404,28 @@ def _GetArgDescription(name, docstring_info):
   return None
 
 
-def _GroupUsageDetailsSection(groups):
-  """Creates a section tuple for the groups section of the usage details."""
-  group_item_strings = []
-  for group_name, group in groups:
-    group_info = inspectutils.Info(group)
-    group_item = group_name
-    if 'docstring_info' in group_info:
-      group_docstring_info = group_info['docstring_info']
-      if group_docstring_info:
-        group_item = _CreateItem(group_name, group_docstring_info.summary)
-    group_item_strings.append(group_item)
-  return ('GROUPS', _NewChoicesSection('GROUP', group_item_strings))
-
-
-def _CommandUsageDetailsSection(commands):
-  """Creates a section tuple for the commands section of the usage details."""
-  command_item_strings = []
-  for command_name, command in commands:
-    command_info = inspectutils.Info(command)
-    command_item = command_name
-    if 'docstring_info' in command_info:
-      command_docstring_info = command_info['docstring_info']
-      if command_docstring_info:
-        command_item = _CreateItem(command_name, command_docstring_info.summary)
-    command_item_strings.append(command_item)
-  return ('COMMANDS', _NewChoicesSection('COMMAND', command_item_strings))
+def _MakeUsageDetailsSection(action_group):
+  """Creates a usage details section for the provided action group."""
+  item_strings = []
+  for name, member in action_group.GetItems():
+    info = inspectutils.Info(member)
+    item = name
+    docstring_info = info.get('docstring_info')
+    if (docstring_info
+        and not custom_descriptions.NeedsCustomDescription(member)):
+      summary = docstring_info.summary
+    else:
+      summary = None
+    item = _CreateItem(name, summary)
+    item_strings.append(item)
+  return (action_group.plural.upper(),
+          _NewChoicesSection(action_group.name.upper(), item_strings))
 
 
 def _ValuesUsageDetailsSection(component, values):
   """Creates a section tuple for the values section of the usage details."""
   value_item_strings = []
-  for value_name, value in values:
+  for value_name, value in values.GetItems():
     del value
     init_info = inspectutils.Info(component.__class__.__init__)
     value_item = None
@@ -590,34 +564,17 @@ def UsageTextForObject(component, trace=None, verbose=False):
     command = ''
 
   actions_grouped_by_kind = _GetActionsGroupedByKind(component, verbose=verbose)
-  groups, commands, values, indexes = actions_grouped_by_kind
 
   possible_actions = []
   availability_lines = []
-  if groups:
-    possible_actions.append('group')
-    groups_text = _CreateAvailabilityLine(
-        header='available groups:',
-        items=[name for name, _ in groups])
-    availability_lines.append(groups_text)
-  if commands:
-    possible_actions.append('command')
-    commands_text = _CreateAvailabilityLine(
-        header='available commands:',
-        items=[name for name, _ in commands])
-    availability_lines.append(commands_text)
-  if values:
-    possible_actions.append('value')
-    values_text = _CreateAvailabilityLine(
-        header='available values:',
-        items=[name for name, _ in values])
-    availability_lines.append(values_text)
-  if indexes:
-    possible_actions.append('index')
-    indexes_text = _CreateAvailabilityLine(
-        header='available indexes:',
-        items=indexes)
-    availability_lines.append(indexes_text)
+  for action_group in actions_grouped_by_kind:
+    if action_group.members:
+      possible_actions.append(action_group.name)
+      availability_line = _CreateAvailabilityLine(
+          header='available {plural}:'.format(plural=action_group.plural),
+          items=action_group.names
+      )
+      availability_lines.append(availability_line)
 
   if possible_actions:
     possible_actions_string = ' <{actions}>'.format(
@@ -641,3 +598,21 @@ def _CreateAvailabilityLine(header, items,
   indented_items_text = formatting.Indent(items_text, spaces=items_indent)
   indented_header = formatting.Indent(header, spaces=header_indent)
   return indented_header + indented_items_text[len(indented_header):] + '\n'
+
+
+class ActionGroup(object):
+  """A group of actions of the same kind."""
+
+  def __init__(self, name, plural):
+    self.name = name
+    self.plural = plural
+    self.names = []
+    self.members = []
+
+  def Add(self, name, member=None):
+    self.names.append(name)
+    self.members.append(member)
+
+  def GetItems(self):
+    return zip(self.names, self.members)
+
diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index 18a1f1ae..66a106f5 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -80,6 +80,9 @@ class with an __init__ method.
   elif inspect.isbuiltin(fn):
     # If the function is a bound builtin, we skip the `self` argument.
     skip_arg = fn.__self__ is not None
+  elif not inspect.isfunction(fn):
+    # The purpose of this else clause is to set skip_arg for callable objects.
+    skip_arg = True
   return fn, skip_arg
 
 
diff --git a/fire/test_components.py b/fire/test_components.py
index 1ca39c14..a011919c 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -306,9 +306,14 @@ def matching_names(self):
 class CallableWithPositionalArgs(object):
   """Test class for supporting callable."""
 
+  TEST = 1
+
   def __call__(self, x, y):
     return x + y
 
+  def foo(self, x):
+    return x + 1
+
 
 NamedTuplePoint = collections.namedtuple('NamedTuplePoint', ['x', 'y'])
 

From 88999fd5664d24e52ab8477306b30aefa54369ae Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 25 Jul 2019 15:25:27 -0700
Subject: [PATCH 146/324] Print "3+4j" instead of "(3+4j)".

PiperOrigin-RevId: 260032586
Change-Id: I42c2a36bc65ea637435450b412ef3c634677ff85
---
 fire/core.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/fire/core.py b/fire/core.py
index d3d51d93..7dbf6740 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -253,6 +253,9 @@ def _PrintResult(component_trace, verbose=False):
     print(_DictAsString(result, verbose))
   elif isinstance(result, tuple):
     print(_OneLineResult(result))
+  elif isinstance(result, complex):
+    # Print "3+4j" instead of "(3+4j)".
+    print(str(result).strip('()'))
   elif isinstance(result, value_types.VALUE_TYPES):
     if result is not None:
       print(result)

From 8d47ddb0ff2eac9e5f61ab7f1063c5e7e476ae2f Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 25 Jul 2019 15:40:43 -0700
Subject: [PATCH 147/324] Show a separator as the continuation if no args are
 needed by a callable.

PiperOrigin-RevId: 260035298
Change-Id: Ia5f54a1b78b45e11b00ecf2a2444724205811b96
---
 fire/helptext.py | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index 7ae9bb0c..2b9d2de3 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -121,7 +121,13 @@ def _SynopsisSection(component, actions_grouped_by_kind, spec, metadata,
   if possible_actions:
     continuations.append(_GetPossibleActionsString(possible_actions))
   if callable(component):
-    continuations.append(_GetArgsAndFlagsString(spec, metadata))
+    callable_continuation = _GetArgsAndFlagsString(spec, metadata)
+    if callable_continuation:
+      continuations.append(callable_continuation)
+    elif trace:
+      # This continuation might be blank if no args are needed.
+      # In this case, show a separator.
+      continuations.append(trace.separator)
   continuation = ' | '.join(continuations)
 
   synopsis_template = '{current_command} {continuation}'

From b05ae0d891657d924ab67ffa5da3fbc0a00f22d7 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 25 Jul 2019 15:42:15 -0700
Subject: [PATCH 148/324] Use kwargs for calling HelpText and UsageText.

PiperOrigin-RevId: 260035604
Change-Id: If680eea5e105b618ddc30068023c4b4ad72c9836
---
 fire/core.py | 15 ++++++++-------
 1 file changed, 8 insertions(+), 7 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index 7dbf6740..f2ce54ad 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -144,7 +144,7 @@ def Fire(component=None, command=None, name=None):
     output = ['Fire trace:\n{trace}\n'.format(trace=component_trace)]
     result = component_trace.GetResult()
     help_text = helptext.HelpText(
-        result, component_trace, component_trace.verbose)
+        result, trace=component_trace, verbose=component_trace.verbose)
     output.append(help_text)
     Display(output, out=sys.stderr)
     raise FireExit(0, component_trace)
@@ -155,7 +155,7 @@ def Fire(component=None, command=None, name=None):
   if component_trace.show_help:
     result = component_trace.GetResult()
     help_text = helptext.HelpText(
-        result, component_trace, component_trace.verbose)
+        result, trace=component_trace, verbose=component_trace.verbose)
     output = [help_text]
     Display(output, out=sys.stderr)
     raise FireExit(0, component_trace)
@@ -260,7 +260,8 @@ def _PrintResult(component_trace, verbose=False):
     if result is not None:
       print(result)
   else:
-    help_text = helptext.HelpText(result, component_trace, verbose)
+    help_text = helptext.HelpText(
+        result, trace=component_trace, verbose=verbose)
     output = [help_text]
     Display(output, out=sys.stdout)
 
@@ -279,16 +280,16 @@ def _DisplayError(component_trace):
     command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
     print('INFO: Showing help with the command {cmd}.\n'.format(
         cmd=pipes.quote(command)), file=sys.stderr)
-    help_text = helptext.HelpText(result, component_trace,
-                                  component_trace.verbose)
+    help_text = helptext.HelpText(result, trace=component_trace,
+                                  verbose=component_trace.verbose)
     output.append(help_text)
     Display(output, out=sys.stderr)
   else:
     print(formatting.Error('ERROR: ')
           + component_trace.elements[-1].ErrorAsStr(),
           file=sys.stderr)
-    error_text = helptext.UsageText(result, component_trace,
-                                    component_trace.verbose)
+    error_text = helptext.UsageText(result, trace=component_trace,
+                                    verbose=component_trace.verbose)
     print(error_text, file=sys.stderr)
 
 

From c3d6b2d18fac35eef081b3e6065eb804000a1c6c Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 25 Jul 2019 15:54:50 -0700
Subject: [PATCH 149/324] fix line-too-long lint error

PiperOrigin-RevId: 260037806
Change-Id: I7989608bdb1d4e75822630729e58d3ac6e03eced
---
 fire/docstrings_test.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/fire/docstrings_test.py b/fire/docstrings_test.py
index 5f7eeeb1..5c805461 100644
--- a/fire/docstrings_test.py
+++ b/fire/docstrings_test.py
@@ -216,8 +216,10 @@ def test_multisection_docstring(self):
     docstring_info = docstrings.parse(docstring)
     expected_docstring_info = DocstringInfo(
         summary='Docstring summary.',
-        description='This is the first section of a docstring description.\n\n'
-        'This is the second section of a docstring description. This docstring\n'
+        description='This is the first section of a docstring description.'
+        '\n\n'
+        'This is the second section of a docstring description. This docstring'
+        '\n'
         'description has just two sections.',
     )
     self.assertEqual(docstring_info, expected_docstring_info)

From 283e3a7c7589354576598af611a11e6c23018129 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 25 Jul 2019 16:10:05 -0700
Subject: [PATCH 150/324] disable protected-access check for docstrings_test

PiperOrigin-RevId: 260040819
Change-Id: If6b6eae54f59db76818fe662c24a857d77100b9a
---
 fire/docstrings_test.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fire/docstrings_test.py b/fire/docstrings_test.py
index 5c805461..23c46c5a 100644
--- a/fire/docstrings_test.py
+++ b/fire/docstrings_test.py
@@ -238,7 +238,7 @@ def test_strip_blank_lines(self):
     lines = ['   ', '  foo  ', '   ']
     expected_output = ['  foo  ']
 
-    self.assertEqual(docstrings._strip_blank_lines(lines), expected_output)
+    self.assertEqual(docstrings._strip_blank_lines(lines), expected_output)  # pylint: disable=protected-access
 
 
 if __name__ == '__main__':

From ba22c7841071c911cf691c7132d67409ef13a670 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 25 Jul 2019 16:11:24 -0700
Subject: [PATCH 151/324] fix external lint errors.

PiperOrigin-RevId: 260041059
Change-Id: Ic258e2fed908a36ccdbede96650c71625b16b61b
---
 fire/helptext.py        | 1 -
 fire/test_components.py | 2 +-
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index 2b9d2de3..1b04596e 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -621,4 +621,3 @@ def Add(self, name, member=None):
 
   def GetItems(self):
     return zip(self.names, self.members)
-
diff --git a/fire/test_components.py b/fire/test_components.py
index a011919c..9c7ae0e5 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -311,7 +311,7 @@ class CallableWithPositionalArgs(object):
   def __call__(self, x, y):
     return x + y
 
-  def foo(self, x):
+  def fn(self, x):
     return x + 1
 
 

From 8de88360b6bf83aa2bb76cc64d117d0daa5e4dfc Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 25 Jul 2019 16:12:50 -0700
Subject: [PATCH 152/324] Support for callable objects in usage test. Refactor
 of usage text.

PiperOrigin-RevId: 260041306
Change-Id: Id14ddd3a935627a068ff4f4e3523e9cd9d90ce89
---
 fire/completion.py    |  47 ++++++++++--
 fire/core.py          |  11 ++-
 fire/helptext.py      | 173 ++++++++++++++++++++----------------------
 fire/helptext_test.py |  48 +++++++-----
 fire/inspectutils.py  |   2 +
 5 files changed, 160 insertions(+), 121 deletions(-)

diff --git a/fire/completion.py b/fire/completion.py
index f1be1558..e18a27cc 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -281,7 +281,18 @@ def _FishScript(name, commands, default_options=None):
   )
 
 
-def MemberVisible(component, name, member, verbose):
+def GetClassAttrsDict(component):
+  """Gets the attributes of the component class, as a dict with name keys."""
+  if not inspect.isclass(component):
+    return None
+  class_attrs_list = inspect.classify_class_attrs(component)
+  return {
+      class_attr.name: class_attr
+      for class_attr in class_attrs_list
+  }
+
+
+def MemberVisible(component, name, member, class_attrs=None, verbose=False):
   """Returns whether a member should be included in auto-completion or help.
 
   Determines whether a member of an object with the specified name should be
@@ -297,6 +308,8 @@ def MemberVisible(component, name, member, verbose):
     component: The component containing the member.
     name: The name of the member.
     member: The member itself.
+    class_attrs: (optional) If component is a class, provide this as:
+      GetClassAttrsDict(component). If not provided, it will be computed.
     verbose: Whether to include private members.
   Returns
     A boolean value indicating whether the member should be included.
@@ -310,6 +323,13 @@ def MemberVisible(component, name, member, verbose):
   if inspect.ismodule(member) and member is six:
     # TODO(dbieber): Determine more generally which modules to hide.
     return False
+  if inspect.isclass(component):
+    # If class_attrs has not been provided, compute it.
+    if class_attrs is None:
+      class_attrs = GetClassAttrsDict(class_attrs)
+    class_attr = class_attrs.get(name)
+    if class_attr and class_attr.kind == 'method':
+      return False
   if (six.PY2 and inspect.isfunction(component)
       and name in ('func_closure', 'func_code', 'func_defaults',
                    'func_dict', 'func_doc', 'func_globals', 'func_name')):
@@ -322,7 +342,7 @@ def MemberVisible(component, name, member, verbose):
   return True  # Default to including the member
 
 
-def _Members(component, verbose=False):
+def VisibleMembers(component, class_attrs=None, verbose=False):
   """Returns a list of the members of the given component.
 
   If verbose is True, then members starting with _ (normally ignored) are
@@ -330,6 +350,12 @@ def _Members(component, verbose=False):
 
   Args:
     component: The component whose members to list.
+    class_attrs: (optional) If component is a class, you may provide this as:
+      GetClassAttrsDict(component). If not provided, it will be computed.
+      If provided, this determines how class members will be treated for
+      visibility. In particular, methods are generally hidden for
+      non-instantiated classes, but if you wish them to be shown (e.g. for
+      completion scripts) then pass in a different class_attr for them.
     verbose: Whether to include private members.
   Returns:
     A list of tuples (member_name, member) of all members of the component.
@@ -339,10 +365,13 @@ def _Members(component, verbose=False):
   else:
     members = inspect.getmembers(component)
 
+  # If class_attrs has not been provided, compute it.
+  if class_attrs is None:
+    class_attrs = GetClassAttrsDict(component)
   return [
-      (member_name, member)
-      for member_name, member in members
-      if MemberVisible(component, member_name, member, verbose)
+      (member_name, member) for member_name, member in members
+      if MemberVisible(component, member_name, member, class_attrs=class_attrs,
+                       verbose=verbose)
   ]
 
 
@@ -386,7 +415,7 @@ def Completions(component, verbose=False):
 
   return [
       _FormatForCommand(member_name)
-      for member_name, unused_member in _Members(component, verbose)
+      for member_name, _ in VisibleMembers(component, verbose=verbose)
   ]
 
 
@@ -427,7 +456,7 @@ def _Commands(component, depth=3):
     Only traverses the member DAG up to a depth of depth.
   """
   if inspect.isroutine(component) or inspect.isclass(component):
-    for completion in Completions(component):
+    for completion in Completions(component, verbose=False):
       yield (completion,)
   if inspect.isroutine(component):
     return  # Don't descend into routines.
@@ -435,7 +464,9 @@ def _Commands(component, depth=3):
   if depth < 1:
     return
 
-  for member_name, member in _Members(component):
+  # By setting class_attrs={} we don't hide methods in completion.
+  for member_name, member in VisibleMembers(component, class_attrs={},
+                                            verbose=False):
     # TODO(dbieber): Also skip components we've already seen.
     member_name = _FormatForCommand(member_name)
 
diff --git a/fire/core.py b/fire/core.py
index f2ce54ad..6903ae97 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -306,8 +306,12 @@ def _DictAsString(result, verbose=False):
   # We need to do 2 iterations over the items in the result dict
   # 1) Getting visible items and the longest key for output formatting
   # 2) Actually construct the output lines
-  result_visible = {key: value for key, value in result.items()
-                    if completion.MemberVisible(result, key, value, verbose)}
+  class_attrs = completion.GetClassAttrsDict(result)
+  result_visible = {
+      key: value for key, value in result.items()
+      if completion.MemberVisible(result, key, value,
+                                  class_attrs=class_attrs, verbose=verbose)
+  }
 
   if not result_visible:
     return '{}'
@@ -317,7 +321,8 @@ def _DictAsString(result, verbose=False):
 
   lines = []
   for key, value in result.items():
-    if completion.MemberVisible(result, key, value, verbose):
+    if completion.MemberVisible(result, key, value, class_attrs=class_attrs,
+                                verbose=verbose):
       line = format_string.format(key=str(key) + ':',
                                   value=_OneLineResult(value))
       lines.append(line)
diff --git a/fire/helptext.py b/fire/helptext.py
index 1b04596e..68dbfe7a 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -33,8 +33,6 @@
 from __future__ import division
 from __future__ import print_function
 
-import inspect
-
 from fire import completion
 from fire import custom_descriptions
 from fire import decorators
@@ -319,7 +317,7 @@ def _GetActionsGroupedByKind(component, verbose=False):
   values = ActionGroup(name='value', plural='values')
   indexes = ActionGroup(name='index', plural='indexes')
 
-  members = completion._Members(component, verbose)  # pylint: disable=protected-access
+  members = completion.VisibleMembers(component, verbose=verbose)
   for member_name, member in members:
     member_name = str(member_name)
     if value_types.IsGroup(member):
@@ -456,14 +454,7 @@ def _NewChoicesSection(name, choices):
 
 
 def UsageText(component, trace=None, verbose=False):
-  if inspect.isroutine(component) or inspect.isclass(component):
-    return UsageTextForFunction(component, trace, verbose)
-  else:
-    return UsageTextForObject(component, trace, verbose)
-
-
-def UsageTextForFunction(component, trace=None, verbose=False):
-  """Returns usage text for function objects.
+  """Returns usage text for the given component.
 
   Args:
     component: The component to determine the usage text for.
@@ -473,13 +464,12 @@ def UsageTextForFunction(component, trace=None, verbose=False):
   Returns:
     String suitable for display in an error screen.
   """
-  del verbose  # Unused.
-
-  output_template = """Usage: {current_command} {args_and_flags}
+  output_template = """Usage: {continued_command}
 {availability_lines}
 For detailed information on this command, run:
-  {current_command}{hyphen_hyphen} --help"""
+  {help_command}"""
 
+  # Get the command so far:
   if trace:
     command = trace.GetCommand()
     needs_separating_hyphen_hyphen = trace.NeedsSeparatingHyphenHyphen()
@@ -490,13 +480,67 @@ def UsageTextForFunction(component, trace=None, verbose=False):
   if not command:
     command = ''
 
+  # Build the continuations for the command:
+  continued_command = command
+
   spec = inspectutils.GetFullArgSpec(component)
+  metadata = decorators.GetMetadata(component)
+
+  # Usage for objects.
+  actions_grouped_by_kind = _GetActionsGroupedByKind(component, verbose=verbose)
+  possible_actions = _GetPossibleActions(actions_grouped_by_kind)
+
+  continuations = []
+  if possible_actions:
+    continuations.append(_GetPossibleActionsUsageString(possible_actions))
+
+  availability_lines = _UsageAvailabilityLines(actions_grouped_by_kind)
+
+  if callable(component):
+    callable_items = _GetCallableUsageItems(spec, metadata)
+    continuations.append(' '.join(callable_items))
+    availability_lines.extend(_GetCallableAvailabilityLines(spec))
+
+  if continuations:
+    continued_command += ' ' + ' | '.join(continuations)
+  help_command = (
+      command
+      + (' -- ' if needs_separating_hyphen_hyphen else ' ')
+      + '--help'
+  )
+
+  return output_template.format(
+      continued_command=continued_command,
+      availability_lines=''.join(availability_lines),
+      help_command=help_command)
+
+
+def _GetPossibleActionsUsageString(possible_actions):
+  if possible_actions:
+    return '<{actions}>'.format(actions='|'.join(possible_actions))
+  return None
+
+
+def _UsageAvailabilityLines(actions_grouped_by_kind):
+  availability_lines = []
+  for action_group in actions_grouped_by_kind:
+    if action_group.members:
+      availability_line = _CreateAvailabilityLine(
+          header='available {plural}:'.format(plural=action_group.plural),
+          items=action_group.names
+      )
+      availability_lines.append(availability_line)
+  return availability_lines
+
+
+def _GetCallableUsageItems(spec, metadata):
+  """A list of elements that comprise the usage summary for a callable."""
   args_with_no_defaults = spec.args[:len(spec.args) - len(spec.defaults)]
   args_with_defaults = spec.args[len(spec.args) - len(spec.defaults):]
 
   # Check if positional args are allowed. If not, show flag syntax for args.
-  metadata = decorators.GetMetadata(component)
   accepts_positional_args = metadata.get(decorators.ACCEPTS_POSITIONAL_ARGS)
+
   if not accepts_positional_args:
     items = ['--{arg}={upper}'.format(arg=arg, upper=arg.upper())
              for arg in args_with_no_defaults]
@@ -507,6 +551,17 @@ def UsageTextForFunction(component, trace=None, verbose=False):
   if args_with_defaults or spec.kwonlyargs or spec.varkw:
     items.append('<flags>')
 
+  if spec.varargs:
+    items.append('[{varargs}]...'.format(varargs=spec.varargs.upper()))
+
+  return items
+
+
+def _GetCallableAvailabilityLines(spec):
+  """The list of availability lines for a callable for use in a usage string."""
+  args_with_defaults = spec.args[len(spec.args) - len(spec.defaults):]
+
+  # TODO(dbieber): Handle args_with_no_defaults if not accepts_positional_args.
   optional_flags = [('--' + flag) for flag in args_with_defaults]
   required_flags = [('--' + flag) for flag in spec.kwonlyargs]
 
@@ -514,86 +569,20 @@ def UsageTextForFunction(component, trace=None, verbose=False):
   availability_lines = []
   if optional_flags:
     availability_lines.append(
-        _CreateAvailabilityLine(header='Optional flags:', items=optional_flags,
-                                header_indent=0))
+        _CreateAvailabilityLine(header='optional flags:', items=optional_flags,
+                                header_indent=2))
   if required_flags:
     availability_lines.append(
-        _CreateAvailabilityLine(header='Required flags:', items=required_flags,
-                                header_indent=0))
+        _CreateAvailabilityLine(header='required flags:', items=required_flags,
+                                header_indent=2))
   if spec.varkw:
-    additional_flags = ('Additional flags are accepted.'
+    additional_flags = ('additional flags are accepted'
                         if optional_flags or required_flags else
-                        'Flags are accepted.')
-    availability_lines.append(additional_flags + '\n')
-
-  if availability_lines:
-    # Start the section with blank lines.
-    availability_lines.insert(0, '\n')
-
-  if spec.varargs:
-    items.append('[{varargs}]...'.format(varargs=spec.varargs.upper()))
-
-  args_and_flags = ' '.join(items)
-
-  hyphen_hyphen = ' --' if needs_separating_hyphen_hyphen else ''
-
-  return output_template.format(
-      current_command=command,
-      args_and_flags=args_and_flags,
-      availability_lines=''.join(availability_lines),
-      hyphen_hyphen=hyphen_hyphen)
-
-
-def UsageTextForObject(component, trace=None, verbose=False):
-  """Returns the usage text for the error screen for an object.
-
-  Constructs the usage text for the error screen to inform the user about how
-  to use the current component.
-
-  Args:
-    component: The component to determine the usage text for.
-    trace: The Fire trace object containing all metadata of current execution.
-    verbose: Whether to include private members in the usage text.
-  Returns:
-    String suitable for display in error screen.
-  """
-  output_template = """Usage: {current_command}{possible_actions}
-{availability_lines}
-For detailed information on this command, run:
-  {current_command} --help"""
-  if trace:
-    command = trace.GetCommand()
-  else:
-    command = None
-
-  if not command:
-    command = ''
-
-  actions_grouped_by_kind = _GetActionsGroupedByKind(component, verbose=verbose)
-
-  possible_actions = []
-  availability_lines = []
-  for action_group in actions_grouped_by_kind:
-    if action_group.members:
-      possible_actions.append(action_group.name)
-      availability_line = _CreateAvailabilityLine(
-          header='available {plural}:'.format(plural=action_group.plural),
-          items=action_group.names
-      )
-      availability_lines.append(availability_line)
-
-  if possible_actions:
-    possible_actions_string = ' <{actions}>'.format(
-        actions='|'.join(possible_actions))
-  else:
-    possible_actions_string = ''
-
-  availability_lines_string = ''.join(availability_lines)
-
-  return output_template.format(
-      current_command=command,
-      possible_actions=possible_actions_string,
-      availability_lines=availability_lines_string)
+                        'flags are accepted')
+    availability_lines.append(
+        _CreateAvailabilityLine(header=additional_flags, items=[],
+                                header_indent=2))
+  return availability_lines
 
 
 def _CreateAvailabilityLine(header, items,
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 33676420..188bdb28 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -317,7 +317,7 @@ def testUsageOutputMethod(self):
     component = tc.NoDefaults().double
     t = trace.FireTrace(component, name='NoDefaults')
     t.AddAccessedProperty(component, 'double', ['double'], None, None)
-    usage_output = helptext.UsageText(component, trace=t, verbose=True)
+    usage_output = helptext.UsageText(component, trace=t, verbose=False)
     expected_output = '''
     Usage: NoDefaults double COUNT
 
@@ -330,11 +330,10 @@ def testUsageOutputMethod(self):
   def testUsageOutputFunctionWithHelp(self):
     component = tc.function_with_help
     t = trace.FireTrace(component, name='function_with_help')
-    usage_output = helptext.UsageText(component, trace=t, verbose=True)
+    usage_output = helptext.UsageText(component, trace=t, verbose=False)
     expected_output = '''
     Usage: function_with_help <flags>
-
-    Optional flags:          --help
+      optional flags:        --help
 
     For detailed information on this command, run:
       function_with_help -- --help'''
@@ -345,17 +344,16 @@ def testUsageOutputFunctionWithHelp(self):
   def testUsageOutputFunctionWithDocstring(self):
     component = tc.multiplier_with_docstring
     t = trace.FireTrace(component, name='multiplier_with_docstring')
-    usage_output = helptext.UsageText(component, trace=t, verbose=True)
+    usage_output = helptext.UsageText(component, trace=t, verbose=False)
     expected_output = '''
     Usage: multiplier_with_docstring NUM <flags>
-
-    Optional flags:          --rate
+      optional flags:        --rate
 
     For detailed information on this command, run:
       multiplier_with_docstring --help'''
     self.assertEqual(
-        usage_output,
-        textwrap.dedent(expected_output).lstrip('\n'))
+        textwrap.dedent(expected_output).lstrip('\n'),
+        usage_output)
 
   @testutils.skip('The functionality is not implemented yet')
   def testUsageOutputCallable(self):
@@ -373,21 +371,35 @@ def testUsageOutputCallable(self):
     For detailed information on this command, run:
       CallableWithKeywordArgument -- --help'''
     self.assertEqual(
-        usage_output,
-        textwrap.dedent(expected_output).lstrip('\n'))
+        textwrap.dedent(expected_output).lstrip('\n'),
+        usage_output)
 
   def testUsageOutputConstructorWithParameter(self):
     component = tc.InstanceVars
     t = trace.FireTrace(component, name='InstanceVars')
-    usage_output = helptext.UsageText(component, trace=t, verbose=True)
+    usage_output = helptext.UsageText(component, trace=t, verbose=False)
     expected_output = '''
     Usage: InstanceVars --arg1=ARG1 --arg2=ARG2
 
     For detailed information on this command, run:
       InstanceVars --help'''
     self.assertEqual(
-        usage_output,
-        textwrap.dedent(expected_output).lstrip('\n'))
+        textwrap.dedent(expected_output).lstrip('\n'),
+        usage_output)
+
+  def testUsageOutputConstructorWithParameterVerbose(self):
+    component = tc.InstanceVars
+    t = trace.FireTrace(component, name='InstanceVars')
+    usage_output = helptext.UsageText(component, trace=t, verbose=True)
+    expected_output = '''
+    Usage: InstanceVars <command> | --arg1=ARG1 --arg2=ARG2
+      available commands:    run
+
+    For detailed information on this command, run:
+      InstanceVars --help'''
+    self.assertEqual(
+        textwrap.dedent(expected_output).lstrip('\n'),
+        usage_output)
 
   def testUsageOutputEmptyDict(self):
     component = {}
@@ -399,8 +411,8 @@ def testUsageOutputEmptyDict(self):
     For detailed information on this command, run:
       EmptyDict --help'''
     self.assertEqual(
-        usage_output,
-        textwrap.dedent(expected_output).lstrip('\n'))
+        textwrap.dedent(expected_output).lstrip('\n'),
+        usage_output)
 
   def testUsageOutputNone(self):
     component = None
@@ -412,8 +424,8 @@ def testUsageOutputNone(self):
     For detailed information on this command, run:
       None --help'''
     self.assertEqual(
-        usage_output,
-        textwrap.dedent(expected_output).lstrip('\n'))
+        textwrap.dedent(expected_output).lstrip('\n'),
+        usage_output)
 
   @testutils.skip('Only passes in Python 3 for now.')
   def testInitRequiresFlagSyntaxSubclassNamedTuple(self):
diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index 66a106f5..b612b859 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -112,6 +112,8 @@ def GetFullArgSpec(fn):
 
     # Case 1: Builtins accept args.
     if inspect.isbuiltin(fn):
+      # TODO(dbieber): Try parsing the docstring, if available.
+      # TODO(dbieber): Use known argspecs, like set.add and namedtuple.count.
       return FullArgSpec(varargs='vars', varkw='kwargs')
 
     # Case 2: namedtuples store their args in their _fields attribute.

From 05080ec3afa6614703770bd06ade01d042a68e8c Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 25 Jul 2019 16:21:37 -0700
Subject: [PATCH 153/324] Don't show properties on uninstantiated classes
 outside verbose mode / completion scripts.

PiperOrigin-RevId: 260042848
Change-Id: Ia773c9922a1138693b8fb17d7f5595289ea5d35b
---
 fire/completion.py    | 4 +++-
 fire/helptext_test.py | 3 +--
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/fire/completion.py b/fire/completion.py
index e18a27cc..19d068fd 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -328,7 +328,9 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False):
     if class_attrs is None:
       class_attrs = GetClassAttrsDict(class_attrs)
     class_attr = class_attrs.get(name)
-    if class_attr and class_attr.kind == 'method':
+    if class_attr and class_attr.kind in ('method', 'property'):
+      # methods and properties should be accessed on instantiated objects,
+      # not uninstantiated classes.
       return False
   if (six.PY2 and inspect.isfunction(component)
       and name in ('func_closure', 'func_code', 'func_defaults',
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 188bdb28..00bdcc2a 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -427,11 +427,10 @@ def testUsageOutputNone(self):
         textwrap.dedent(expected_output).lstrip('\n'),
         usage_output)
 
-  @testutils.skip('Only passes in Python 3 for now.')
   def testInitRequiresFlagSyntaxSubclassNamedTuple(self):
     component = tc.SubPoint
     t = trace.FireTrace(component, name='SubPoint')
-    usage_output = helptext.UsageText(component, trace=t, verbose=True)
+    usage_output = helptext.UsageText(component, trace=t, verbose=False)
     expected_output = 'Usage: SubPoint --x=X --y=Y'
     self.assertIn(expected_output, usage_output)
 

From 88f392319afd22d95f342fc7e0b09a204873a0b0 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 25 Jul 2019 16:38:17 -0700
Subject: [PATCH 154/324] enable test for callable object usage

PiperOrigin-RevId: 260045817
Change-Id: I700cef5d0676fae7c79221beafcf07373f268a46
---
 fire/helptext.py        |  5 ++++-
 fire/helptext_test.py   | 18 ++++++++----------
 fire/inspectutils.py    | 12 +++++++++++-
 fire/test_components.py |  3 +++
 4 files changed, 26 insertions(+), 12 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index 68dbfe7a..2c360d2b 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -498,7 +498,10 @@ def UsageText(component, trace=None, verbose=False):
 
   if callable(component):
     callable_items = _GetCallableUsageItems(spec, metadata)
-    continuations.append(' '.join(callable_items))
+    if callable_items:
+      continuations.append(' '.join(callable_items))
+    elif trace:
+      continuations.append(trace.separator)
     availability_lines.extend(_GetCallableAvailabilityLines(spec))
 
   if continuations:
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 00bdcc2a..d3850cc7 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -355,18 +355,16 @@ def testUsageOutputFunctionWithDocstring(self):
         textwrap.dedent(expected_output).lstrip('\n'),
         usage_output)
 
-  @testutils.skip('The functionality is not implemented yet')
   def testUsageOutputCallable(self):
-    # This is both a group and a command!
-    component = tc.CallableWithKeywordArgument
-    t = trace.FireTrace(component, name='CallableWithKeywordArgument')
-    usage_output = helptext.UsageText(component, trace=t, verbose=True)
-    # TODO(joejoevictor): We need to handle the case for keyword args as well
-    # i.e. __call__ method of CallableWithKeywordArgument
+    # This is both a group and a command.
+    component = tc.CallableWithKeywordArgument()
+    t = trace.FireTrace(component, name='CallableWithKeywordArgument',
+                        separator='@')
+    usage_output = helptext.UsageText(component, trace=t, verbose=False)
     expected_output = '''
-    Usage: CallableWithKeywordArgument <command>
-
-      Available commands:    print_msg
+    Usage: CallableWithKeywordArgument <command> | <flags>
+      available commands:    print_msg
+      flags are accepted
 
     For detailed information on this command, run:
       CallableWithKeywordArgument -- --help'''
diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index b612b859..34c92317 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -86,6 +86,16 @@ class with an __init__ method.
   return fn, skip_arg
 
 
+def Py2GetArgSpec(fn):
+  """A wrapper around getargspec that tries both fn and fn.__call__."""
+  try:
+    return inspect.getargspec(fn)  # pylint: disable=deprecated-method
+  except TypeError:
+    if hasattr(fn, '__call__'):
+      return inspect.getargspec(fn.__call__)  # pylint: disable=deprecated-method
+    raise
+
+
 def GetFullArgSpec(fn):
   """Returns a FullArgSpec describing the given callable."""
 
@@ -94,7 +104,7 @@ def GetFullArgSpec(fn):
 
   try:
     if six.PY2:
-      args, varargs, varkw, defaults = inspect.getargspec(fn)  # pylint: disable=deprecated-method
+      args, varargs, varkw, defaults = Py2GetArgSpec(fn)
       kwonlyargs = kwonlydefaults = None
       annotations = getattr(fn, '__annotations__', None)
     else:
diff --git a/fire/test_components.py b/fire/test_components.py
index 9c7ae0e5..6e3b7015 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -336,6 +336,9 @@ def print_msg(self, msg):
     print(msg)
 
 
+callable_with_keyword_argument = CallableWithKeywordArgument()
+
+
 class ClassWithDocstring(object):
   """Test class for testing help text output.
 

From 86e2c355517d51488488068cf833185442e91d3c Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 25 Jul 2019 16:39:33 -0700
Subject: [PATCH 155/324] Set version to 0.2.0.

PiperOrigin-RevId: 260046043
Change-Id: I1f6f28c2b6984cb8645fa1b8f34931fcb0c30e9f
---
 fire/__init__.py | 2 +-
 setup.py         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/fire/__init__.py b/fire/__init__.py
index e15450db..1e92b531 100644
--- a/fire/__init__.py
+++ b/fire/__init__.py
@@ -21,4 +21,4 @@
 from fire.core import Fire
 
 __all__ = ['Fire']
-__version__ = '0.1.4'
+__version__ = '0.2.0'
diff --git a/setup.py b/setup.py
index 0e34a55f..68103649 100644
--- a/setup.py
+++ b/setup.py
@@ -40,7 +40,7 @@
     'python-Levenshtein',
 ]
 
-VERSION = '0.1.4'
+VERSION = '0.2.0'
 URL = 'https://github.com/google/python-fire'
 
 setup(

From 962907495556305385ee73eaecf9ff6e83ebe6cd Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 25 Jul 2019 16:56:53 -0700
Subject: [PATCH 156/324] Triple double-quotes, not triple single-quotes and 
 linter error for callable_with_keyword_argument

PiperOrigin-RevId: 260049158
Change-Id: I2b0cd024f2f700d73a60d972a0365c6674112c0f
---
 fire/helptext_test.py   | 40 ++++++++++++++++++++--------------------
 fire/test_components.py |  2 +-
 2 files changed, 21 insertions(+), 21 deletions(-)

diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index d3850cc7..1371e2a1 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -288,12 +288,12 @@ def testUsageOutput(self):
     component = tc.NoDefaults()
     t = trace.FireTrace(component, name='NoDefaults')
     usage_output = helptext.UsageText(component, trace=t, verbose=False)
-    expected_output = '''
+    expected_output = """
     Usage: NoDefaults <command>
       available commands:    double | triple
 
     For detailed information on this command, run:
-      NoDefaults --help'''
+      NoDefaults --help"""
 
     self.assertEqual(
         usage_output,
@@ -303,12 +303,12 @@ def testUsageOutputVerbose(self):
     component = tc.NoDefaults()
     t = trace.FireTrace(component, name='NoDefaults')
     usage_output = helptext.UsageText(component, trace=t, verbose=True)
-    expected_output = '''
+    expected_output = """
     Usage: NoDefaults <command>
       available commands:    double | triple
 
     For detailed information on this command, run:
-      NoDefaults --help'''
+      NoDefaults --help"""
     self.assertEqual(
         usage_output,
         textwrap.dedent(expected_output).lstrip('\n'))
@@ -318,11 +318,11 @@ def testUsageOutputMethod(self):
     t = trace.FireTrace(component, name='NoDefaults')
     t.AddAccessedProperty(component, 'double', ['double'], None, None)
     usage_output = helptext.UsageText(component, trace=t, verbose=False)
-    expected_output = '''
+    expected_output = """
     Usage: NoDefaults double COUNT
 
     For detailed information on this command, run:
-      NoDefaults double --help'''
+      NoDefaults double --help"""
     self.assertEqual(
         usage_output,
         textwrap.dedent(expected_output).lstrip('\n'))
@@ -331,12 +331,12 @@ def testUsageOutputFunctionWithHelp(self):
     component = tc.function_with_help
     t = trace.FireTrace(component, name='function_with_help')
     usage_output = helptext.UsageText(component, trace=t, verbose=False)
-    expected_output = '''
+    expected_output = """
     Usage: function_with_help <flags>
       optional flags:        --help
 
     For detailed information on this command, run:
-      function_with_help -- --help'''
+      function_with_help -- --help"""
     self.assertEqual(
         usage_output,
         textwrap.dedent(expected_output).lstrip('\n'))
@@ -345,12 +345,12 @@ def testUsageOutputFunctionWithDocstring(self):
     component = tc.multiplier_with_docstring
     t = trace.FireTrace(component, name='multiplier_with_docstring')
     usage_output = helptext.UsageText(component, trace=t, verbose=False)
-    expected_output = '''
+    expected_output = """
     Usage: multiplier_with_docstring NUM <flags>
       optional flags:        --rate
 
     For detailed information on this command, run:
-      multiplier_with_docstring --help'''
+      multiplier_with_docstring --help"""
     self.assertEqual(
         textwrap.dedent(expected_output).lstrip('\n'),
         usage_output)
@@ -361,13 +361,13 @@ def testUsageOutputCallable(self):
     t = trace.FireTrace(component, name='CallableWithKeywordArgument',
                         separator='@')
     usage_output = helptext.UsageText(component, trace=t, verbose=False)
-    expected_output = '''
+    expected_output = """
     Usage: CallableWithKeywordArgument <command> | <flags>
       available commands:    print_msg
       flags are accepted
 
     For detailed information on this command, run:
-      CallableWithKeywordArgument -- --help'''
+      CallableWithKeywordArgument -- --help"""
     self.assertEqual(
         textwrap.dedent(expected_output).lstrip('\n'),
         usage_output)
@@ -376,11 +376,11 @@ def testUsageOutputConstructorWithParameter(self):
     component = tc.InstanceVars
     t = trace.FireTrace(component, name='InstanceVars')
     usage_output = helptext.UsageText(component, trace=t, verbose=False)
-    expected_output = '''
+    expected_output = """
     Usage: InstanceVars --arg1=ARG1 --arg2=ARG2
 
     For detailed information on this command, run:
-      InstanceVars --help'''
+      InstanceVars --help"""
     self.assertEqual(
         textwrap.dedent(expected_output).lstrip('\n'),
         usage_output)
@@ -389,12 +389,12 @@ def testUsageOutputConstructorWithParameterVerbose(self):
     component = tc.InstanceVars
     t = trace.FireTrace(component, name='InstanceVars')
     usage_output = helptext.UsageText(component, trace=t, verbose=True)
-    expected_output = '''
+    expected_output = """
     Usage: InstanceVars <command> | --arg1=ARG1 --arg2=ARG2
       available commands:    run
 
     For detailed information on this command, run:
-      InstanceVars --help'''
+      InstanceVars --help"""
     self.assertEqual(
         textwrap.dedent(expected_output).lstrip('\n'),
         usage_output)
@@ -403,11 +403,11 @@ def testUsageOutputEmptyDict(self):
     component = {}
     t = trace.FireTrace(component, name='EmptyDict')
     usage_output = helptext.UsageText(component, trace=t, verbose=True)
-    expected_output = '''
+    expected_output = """
     Usage: EmptyDict
 
     For detailed information on this command, run:
-      EmptyDict --help'''
+      EmptyDict --help"""
     self.assertEqual(
         textwrap.dedent(expected_output).lstrip('\n'),
         usage_output)
@@ -416,11 +416,11 @@ def testUsageOutputNone(self):
     component = None
     t = trace.FireTrace(component, name='None')
     usage_output = helptext.UsageText(component, trace=t, verbose=True)
-    expected_output = '''
+    expected_output = """
     Usage: None
 
     For detailed information on this command, run:
-      None --help'''
+      None --help"""
     self.assertEqual(
         textwrap.dedent(expected_output).lstrip('\n'),
         usage_output)
diff --git a/fire/test_components.py b/fire/test_components.py
index 6e3b7015..a3e049b7 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -336,7 +336,7 @@ def print_msg(self, msg):
     print(msg)
 
 
-callable_with_keyword_argument = CallableWithKeywordArgument()
+CALLABLE_WITH_KEYWORD_ARGUMENT = CallableWithKeywordArgument()
 
 
 class ClassWithDocstring(object):

From 18ba635631b9aca7670f9b7a7979c0bf684ef1d9 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 25 Jul 2019 21:10:44 -0700
Subject: [PATCH 157/324] Allow custom types to define their own serialization
 with __str__.

PiperOrigin-RevId: 260078541
Change-Id: I2999e0caaaf962708ab1de4272b1df58327b2ada
---
 docs/guide.md        | 12 +++++++++++-
 docs/using-cli.md    |  2 ++
 fire/core.py         | 12 +++++++++---
 fire/inspectutils.py |  1 -
 4 files changed, 22 insertions(+), 5 deletions(-)

diff --git a/docs/guide.md b/docs/guide.md
index 7f610699..2e59ce8a 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -413,7 +413,7 @@ if __name__ == '__main__':
 Now we can draw stuff :).
 
 ```bash
-$ python example.py move 3 3 on move 3 6 on move 6 3 on move 6 6 on move 7 4 on move 7 5 on __str__
+$ python example.py move 3 3 on move 3 6 on move 6 3 on move 6 6 on move 7 4 on move 7 5 on
 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0
@@ -428,6 +428,16 @@ $ python example.py move 3 3 on move 3 6 on move 6 3 on move 6 6 on move 7 4 on
 
 It's supposed to be a smiley face.
 
+### Custom Serialization
+
+You'll notice in the BinaryCanvas example, the canvas with the smiley face was
+printed to the screen. You can determine how a component will be serialized by
+defining its `__str__` method.
+
+If a custom `__str__` method is present on the final component, the object is
+serialized and printed. If there's no custom `__str__` method, then the help
+screen for the object is shown instead.
+
 ### Can we make an even simpler example than Hello World?
 
 Yes, this program is even simpler than our original Hello World example.
diff --git a/docs/using-cli.md b/docs/using-cli.md
index 236a8228..4cd95e14 100644
--- a/docs/using-cli.md
+++ b/docs/using-cli.md
@@ -91,6 +91,8 @@ the arguments of the class's \_\_init\_\_ function. Arguments must be specified
 by name, using the flags syntax. See the section on
 [calling a function](#calling-a-function) for more details.
 
+Similarly, when passing arguments to a callable object (an object with a custom
+`__call__` function), those arguments must be passed using flags syntax.
 
 ## Using Flags with Fire CLIs <a name="using-flags"></a>
 
diff --git a/fire/core.py b/fire/core.py
index 6903ae97..dabfb865 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -244,6 +244,15 @@ def _PrintResult(component_trace, verbose=False):
   # and move serialization to its own module.
   result = component_trace.GetResult()
 
+  if hasattr(result, '__str__'):
+    # If the object has a custom __str__ method, rather than one inherited from
+    # object, then we use that to serialize the object.
+    class_attrs = completion.GetClassAttrsDict(type(result))
+    str_attr = class_attrs.get('__str__')
+    if str_attr and str_attr.defining_class is not object:
+      print(str(result))
+      return
+
   if isinstance(result, (list, set, frozenset, types.GeneratorType)):
     for i in result:
       print(_OneLineResult(i))
@@ -253,9 +262,6 @@ def _PrintResult(component_trace, verbose=False):
     print(_DictAsString(result, verbose))
   elif isinstance(result, tuple):
     print(_OneLineResult(result))
-  elif isinstance(result, complex):
-    # Print "3+4j" instead of "(3+4j)".
-    print(str(result).strip('()'))
   elif isinstance(result, value_types.VALUE_TYPES):
     if result is not None:
       print(result)
diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index 34c92317..aa130e05 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -98,7 +98,6 @@ def Py2GetArgSpec(fn):
 
 def GetFullArgSpec(fn):
   """Returns a FullArgSpec describing the given callable."""
-
   original_fn = fn
   fn, skip_arg = _GetArgSpecInfo(fn)
 

From d77453938a4b7a5a2bb71d9cb40397ee8bbc2e0a Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 26 Jul 2019 09:10:44 -0700
Subject: [PATCH 158/324] make pytype happy w/ __str__ feature.

PiperOrigin-RevId: 260155248
Change-Id: Iac4795a8177df8bf9108efe916035f94507e8902
---
 fire/core.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fire/core.py b/fire/core.py
index dabfb865..07993de9 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -247,7 +247,7 @@ def _PrintResult(component_trace, verbose=False):
   if hasattr(result, '__str__'):
     # If the object has a custom __str__ method, rather than one inherited from
     # object, then we use that to serialize the object.
-    class_attrs = completion.GetClassAttrsDict(type(result))
+    class_attrs = completion.GetClassAttrsDict(type(result)) or {}
     str_attr = class_attrs.get('__str__')
     if str_attr and str_attr.defining_class is not object:
       print(str(result))

From a54ef58d16168d1e337a360a6f978072c8514117 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 26 Jul 2019 11:51:36 -0700
Subject: [PATCH 159/324] Adds BinaryCanvas as test component.

PiperOrigin-RevId: 260185289
Change-Id: I8c8354359486b9f0c63a7c524eb9a904de6c3249
---
 fire/test_components.py | 33 +++++++++++++++++++++++++++++++++
 1 file changed, 33 insertions(+)

diff --git a/fire/test_components.py b/fire/test_components.py
index a3e049b7..e35027c7 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -465,3 +465,36 @@ def fn_with_code_in_docstring():
     True.
   """
   return True
+
+
+class BinaryCanvas(object):
+  """A canvas with which to make binary art, one bit at a time."""
+
+  def __init__(self, size=10):
+    self.pixels = [[0] * size for _ in range(size)]
+    self._size = size
+    self._row = 0  # The row of the cursor.
+    self._col = 0  # The column of the cursor.
+
+  def __str__(self):
+    return '\n'.join(
+        ' '.join(str(pixel) for pixel in row) for row in self.pixels)
+
+  def show(self):
+    print(self)
+    return self
+
+  def move(self, row, col):
+    self._row = row % self._size
+    self._col = col % self._size
+    return self
+
+  def on(self):
+    return self.set(1)
+
+  def off(self):
+    return self.set(0)
+
+  def set(self, value):
+    self.pixels[self._row][self._col] = value
+    return self

From 0fa5c42ac8c1dddd15c82ca3607bc6923f971cc9 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 29 Jul 2019 15:32:39 -0700
Subject: [PATCH 160/324] robustify docstring parser to blank lines after
 section starts. Resolves https://github.com/google/python-fire/issues/183

PiperOrigin-RevId: 260593779
Change-Id: Iab9052b9d56b24d65df9beb3ed8add68e2288926
---
 fire/docstrings.py      | 17 +++++++----------
 fire/docstrings_test.py | 37 +++++++++++++++++++++++++------------
 2 files changed, 32 insertions(+), 22 deletions(-)

diff --git a/fire/docstrings.py b/fire/docstrings.py
index b15cddce..d14c4544 100644
--- a/fire/docstrings.py
+++ b/fire/docstrings.py
@@ -188,14 +188,9 @@ def parse(docstring):
   yields = _join_lines(state.yields.lines)
   raises = _join_lines(state.raises.lines)
 
-  args = [
-      ArgInfo(
-          name=arg.name,
-          type=_cast_to_known_type(_join_lines(arg.type.lines)),
-          description=_join_lines(arg.description.lines),
-      )
-      for arg in state.args
-  ]
+  args = [ArgInfo(
+      name=arg.name, type=_cast_to_known_type(_join_lines(arg.type.lines)),
+      description=_join_lines(arg.description.lines)) for arg in state.args]
 
   return DocstringInfo(
       summary=summary,
@@ -503,10 +498,12 @@ def _create_line_info(line, next_line):
   line_info.remaining_raw = line_info.line
   line_info.remaining = line_info.stripped
   line_info.indentation = len(line) - len(line.lstrip())
+  # TODO(dbieber): If next_line is blank, use the next non-blank line.
   line_info.next.line = next_line
-  line_info.next.stripped = next_line.strip() if next_line else None
+  next_line_exists = next_line is not None
+  line_info.next.stripped = next_line.strip() if next_line_exists else None
   line_info.next.indentation = (
-      len(next_line) - len(next_line.lstrip()) if next_line else None)
+      len(next_line) - len(next_line.lstrip()) if next_line_exists else None)
   # Note: This counts all whitespace equally.
   return line_info
 
diff --git a/fire/docstrings_test.py b/fire/docstrings_test.py
index 23c46c5a..adb89492 100644
--- a/fire/docstrings_test.py
+++ b/fire/docstrings_test.py
@@ -34,7 +34,7 @@ def test_one_line_simple(self):
     expected_docstring_info = DocstringInfo(
         summary='A simple one line docstring.',
     )
-    self.assertEqual(docstring_info, expected_docstring_info)
+    self.assertEqual(expected_docstring_info, docstring_info)
 
   def test_one_line_simple_whitespace(self):
     docstring = """
@@ -44,7 +44,7 @@ def test_one_line_simple_whitespace(self):
     expected_docstring_info = DocstringInfo(
         summary='A simple one line docstring.',
     )
-    self.assertEqual(docstring_info, expected_docstring_info)
+    self.assertEqual(expected_docstring_info, docstring_info)
 
   def test_one_line_too_long(self):
     # pylint: disable=line-too-long
@@ -57,7 +57,7 @@ def test_one_line_too_long(self):
         'a little too long so it keeps going well beyond a reasonable length '
         'for a one-liner.',
     )
-    self.assertEqual(docstring_info, expected_docstring_info)
+    self.assertEqual(expected_docstring_info, docstring_info)
 
   def test_one_line_runs_over(self):
     # pylint: disable=line-too-long
@@ -70,7 +70,7 @@ def test_one_line_runs_over(self):
         summary='A one line docstring thats both a little too verbose and '
         'a little too long so it runs onto a second line.',
     )
-    self.assertEqual(docstring_info, expected_docstring_info)
+    self.assertEqual(expected_docstring_info, docstring_info)
 
   def test_one_line_runs_over_whitespace(self):
     docstring = """
@@ -82,7 +82,7 @@ def test_one_line_runs_over_whitespace(self):
         summary='A one line docstring thats both a little too verbose and '
         'a little too long so it runs onto a second line.',
     )
-    self.assertEqual(docstring_info, expected_docstring_info)
+    self.assertEqual(expected_docstring_info, docstring_info)
 
   def test_google_format_args_only(self):
     docstring = """One line description.
@@ -99,7 +99,7 @@ def test_google_format_args_only(self):
             ArgInfo(name='arg2', description='arg2_description'),
         ]
     )
-    self.assertEqual(docstring_info, expected_docstring_info)
+    self.assertEqual(expected_docstring_info, docstring_info)
 
   def test_google_format_arg_named_args(self):
     docstring = """
@@ -112,7 +112,7 @@ def test_google_format_arg_named_args(self):
             ArgInfo(name='args', description='arg_description'),
         ]
     )
-    self.assertEqual(docstring_info, expected_docstring_info)
+    self.assertEqual(expected_docstring_info, docstring_info)
 
   def test_google_format_typed_args_and_returns(self):
     docstring = """Docstring summary.
@@ -140,7 +140,7 @@ def test_google_format_typed_args_and_returns(self):
         ],
         returns='bool: The return value. True for success, False otherwise.'
     )
-    self.assertEqual(docstring_info, expected_docstring_info)
+    self.assertEqual(expected_docstring_info, docstring_info)
 
   def test_rst_format_typed_args_and_returns(self):
     docstring = """Docstring summary.
@@ -169,7 +169,7 @@ def test_rst_format_typed_args_and_returns(self):
         returns='int -- description of the return value.',
         raises='AttributeError, KeyError',
     )
-    self.assertEqual(docstring_info, expected_docstring_info)
+    self.assertEqual(expected_docstring_info, docstring_info)
 
   def test_numpy_format_typed_args_and_returns(self):
     docstring = """Docstring summary.
@@ -203,7 +203,7 @@ def test_numpy_format_typed_args_and_returns(self):
         # TODO(dbieber): Support return type.
         returns='bool True if successful, False otherwise.',
     )
-    self.assertEqual(docstring_info, expected_docstring_info)
+    self.assertEqual(expected_docstring_info, docstring_info)
 
   def test_multisection_docstring(self):
     docstring = """Docstring summary.
@@ -222,7 +222,20 @@ def test_multisection_docstring(self):
         '\n'
         'description has just two sections.',
     )
-    self.assertEqual(docstring_info, expected_docstring_info)
+    self.assertEqual(expected_docstring_info, docstring_info)
+
+  def test_google_section_with_blank_first_line(self):
+    docstring = """Inspired by requests HTTPAdapter docstring.
+
+    :param x: Simple param.
+
+    Usage:
+
+      >>> import requests
+    """
+    docstring_info = docstrings.parse(docstring)
+    self.assertEqual('Inspired by requests HTTPAdapter docstring.',
+                     docstring_info.summary)
 
   def test_ill_formed_docstring(self):
     docstring = """Docstring summary.
@@ -238,7 +251,7 @@ def test_strip_blank_lines(self):
     lines = ['   ', '  foo  ', '   ']
     expected_output = ['  foo  ']
 
-    self.assertEqual(docstrings._strip_blank_lines(lines), expected_output)  # pylint: disable=protected-access
+    self.assertEqual(expected_output, docstrings._strip_blank_lines(lines))  # pylint: disable=protected-access
 
 
 if __name__ == '__main__':

From c1a7e2f21fb95e37d80d3e32c7ecbefa12140edf Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Tue, 30 Jul 2019 11:28:50 -0700
Subject: [PATCH 161/324] Set version to 0.2.1.

PiperOrigin-RevId: 260758966
Change-Id: I46fd2a5fbbbfa33c86782a25c9e92b387c6fb03d
---
 fire/__init__.py | 2 +-
 setup.py         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/fire/__init__.py b/fire/__init__.py
index 1e92b531..2d8f61b5 100644
--- a/fire/__init__.py
+++ b/fire/__init__.py
@@ -21,4 +21,4 @@
 from fire.core import Fire
 
 __all__ = ['Fire']
-__version__ = '0.2.0'
+__version__ = '0.2.1'
diff --git a/setup.py b/setup.py
index 68103649..44b7d7be 100644
--- a/setup.py
+++ b/setup.py
@@ -40,7 +40,7 @@
     'python-Levenshtein',
 ]
 
-VERSION = '0.2.0'
+VERSION = '0.2.1'
 URL = 'https://github.com/google/python-fire'
 
 setup(

From f01aad347632791e3438c1a753e42a514520d690 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 5 Aug 2019 15:37:25 -0700
Subject: [PATCH 162/324] Prevent error when all lines are blank in
 _strip_blank_lines.

PiperOrigin-RevId: 261784747
Change-Id: I40ac24b553b2b4fa081de3fe0b1250472e1011a7
---
 fire/docstrings.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/fire/docstrings.py b/fire/docstrings.py
index d14c4544..a58a42fe 100644
--- a/fire/docstrings.py
+++ b/fire/docstrings.py
@@ -212,7 +212,8 @@ def _strip_blank_lines(lines):
   """
   # Find the first non-blank line.
   start = 0
-  while lines and _is_blank(lines[start]):
+  num_lines = len(lines)
+  while lines and start < num_lines and _is_blank(lines[start]):
     start += 1
 
   lines = lines[start:]

From a7810582f81fef2e8fc99db4917f6eed8b74d886 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 8 Aug 2019 12:08:43 -0700
Subject: [PATCH 163/324] Set version number to 0.2.2

PiperOrigin-RevId: 262405343
Change-Id: Ia1e7b79b492618f7a965ef6ce8113fb5d4a437c9
---
 fire/__init__.py | 2 +-
 setup.py         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/fire/__init__.py b/fire/__init__.py
index 2d8f61b5..9eff51c4 100644
--- a/fire/__init__.py
+++ b/fire/__init__.py
@@ -21,4 +21,4 @@
 from fire.core import Fire
 
 __all__ = ['Fire']
-__version__ = '0.2.1'
+__version__ = '0.2.2'
diff --git a/setup.py b/setup.py
index 44b7d7be..5ddbb483 100644
--- a/setup.py
+++ b/setup.py
@@ -40,7 +40,7 @@
     'python-Levenshtein',
 ]
 
-VERSION = '0.2.1'
+VERSION = '0.2.2'
 URL = 'https://github.com/google/python-fire'
 
 setup(

From 720c0aa209c86bdb77b748a92b239a0edd9c2261 Mon Sep 17 00:00:00 2001
From: williamfzc <178894043@qq.com>
Date: Fri, 6 Sep 2019 01:23:31 +0800
Subject: [PATCH 164/324] add pypi version badge in README (#192)

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 862f5f23..8e4a3ec8 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/fire.svg?style=plastic)](https://github.com/google/python-fire)
+# Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/fire.svg?style=plastic)](https://github.com/google/python-fire) [![PyPI version](https://badge.fury.io/py/fire.svg)](https://badge.fury.io/py/fire)
 _Python Fire is a library for automatically generating command line interfaces
 (CLIs) from absolutely any Python object._
 

From 8d372e2a67d1ad8135eca074629b52ddbca96f25 Mon Sep 17 00:00:00 2001
From: Geoff Bacon <bacon@berkeley.edu>
Date: Sat, 7 Sep 2019 18:27:42 -0700
Subject: [PATCH 165/324] Exposes builtin functions from the standard library
 (#193)

* fixes #187
---
 fire/inspectutils.py | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index aa130e05..d0e74e33 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -19,6 +19,7 @@
 from __future__ import print_function
 
 import inspect
+import types
 from fire import docstrings
 
 import six
@@ -70,7 +71,7 @@ class with an __init__ method.
   """
   skip_arg = False
   if inspect.isclass(fn):
-    # If the function is a class, we try to use it's init method.
+    # If the function is a class, we try to use its init method.
     skip_arg = True
     if six.PY2 and hasattr(fn, '__init__'):
       fn = fn.__init__
@@ -78,8 +79,11 @@ class with an __init__ method.
     # If the function is a bound method, we skip the `self` argument.
     skip_arg = fn.__self__ is not None
   elif inspect.isbuiltin(fn):
-    # If the function is a bound builtin, we skip the `self` argument.
-    skip_arg = fn.__self__ is not None
+    # If the function is a bound builtin, we skip the `self` argument, unless
+    # the function is from a standard library module in which case its __self__
+    # attribute is that module.
+    if not isinstance(fn.__self__, types.ModuleType):
+      skip_arg = True
   elif not inspect.isfunction(fn):
     # The purpose of this else clause is to set skip_arg for callable objects.
     skip_arg = True

From 4695f34429d22d78aab5d6bfc7b155b23a7165d5 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 5 Dec 2019 21:30:16 +0000
Subject: [PATCH 166/324] Add disable=import-outside-toplevel pylint lines.

PiperOrigin-RevId: 282375002
Change-Id: I83c9354df82baa8d31fb068cf06a02df35341c50
---
 fire/inspectutils.py | 2 +-
 fire/interact.py     | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index d0e74e33..bf197f3f 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -195,7 +195,7 @@ def Info(component):
     A dict with information about the component.
   """
   try:
-    from IPython.core import oinspect  # pylint: disable=g-import-not-at-top
+    from IPython.core import oinspect  # pylint: disable=import-outside-toplevel,g-import-not-at-top
     inspector = oinspect.Inspector()
     info = inspector.info(component)
 
diff --git a/fire/interact.py b/fire/interact.py
index 9f0a01e6..7df32841 100644
--- a/fire/interact.py
+++ b/fire/interact.py
@@ -89,11 +89,11 @@ def _EmbedIPython(variables, argv=None):
         Values are variable values.
     argv: The argv to use for starting ipython. Defaults to an empty list.
   """
-  import IPython  # pylint: disable=g-import-not-at-top
+  import IPython  # pylint: disable=import-outside-toplevel,g-import-not-at-top
   argv = argv or []
   IPython.start_ipython(argv=argv, user_ns=variables)
 
 
 def _EmbedCode(variables):
-  import code  # pylint: disable=g-import-not-at-top
+  import code  # pylint: disable=import-outside-toplevel,g-import-not-at-top
   code.InteractiveConsole(variables).interact()

From 48038f55cb6ff2a302443bc0102ea3efcec95c53 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Tue, 3 Dec 2019 16:11:41 -0800
Subject: [PATCH 167/324] sync internal and external

PiperOrigin-RevId: 283644109
Change-Id: I81f1648de1ff03a7f1a5e5c64aa1763dae875990
---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 8e4a3ec8..862f5f23 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/fire.svg?style=plastic)](https://github.com/google/python-fire) [![PyPI version](https://badge.fury.io/py/fire.svg)](https://badge.fury.io/py/fire)
+# Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/fire.svg?style=plastic)](https://github.com/google/python-fire)
 _Python Fire is a library for automatically generating command line interfaces
 (CLIs) from absolutely any Python object._
 

From 15a6f6b59703ff292892321260da5625cbeb1160 Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Fri, 13 Dec 2019 11:20:06 -0800
Subject: [PATCH 168/324] Install enum 34 if python version is lower than 3.4.

PiperOrigin-RevId: 285436201
Change-Id: Idacf0059d86010c50bac61ee32449c575d5c53b0
---
 setup.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/setup.py b/setup.py
index 5ddbb483..2e7a679d 100644
--- a/setup.py
+++ b/setup.py
@@ -14,9 +14,9 @@
 
 """The setup.py file for Python Fire."""
 
+import sys
 from setuptools import setup
 
-
 LONG_DESCRIPTION = """
 Python Fire is a library for automatically generating command line interfaces
 (CLIs) with a single line of code.
@@ -32,7 +32,7 @@
 DEPENDENCIES = [
     'six',
     'termcolor',
-]
+] + (['enum34'] if sys.version < 3.4 else [])
 
 TEST_DEPENDENCIES = [
     'hypothesis',

From cd95ae25814636111a3acb3b18d14db053643f97 Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Fri, 13 Dec 2019 14:09:06 -0800
Subject: [PATCH 169/324] Fix the type in version comparison in setup script.

PiperOrigin-RevId: 285468373
Change-Id: I9e7d2afe8fcf899aee975b20eebba873ab68bb62
---
 setup.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.py b/setup.py
index 2e7a679d..c0898a1f 100644
--- a/setup.py
+++ b/setup.py
@@ -32,7 +32,7 @@
 DEPENDENCIES = [
     'six',
     'termcolor',
-] + (['enum34'] if sys.version < 3.4 else [])
+] + (['enum34'] if sys.version < '3.4' else [])
 
 TEST_DEPENDENCIES = [
     'hypothesis',

From 074f4b25906b4c7bc309a64feeba8873ec5e849c Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Fri, 17 Jan 2020 10:01:31 -0800
Subject: [PATCH 170/324] Ignoring SIGINT in pager process and fire process
 while the pager process is alive.

PiperOrigin-RevId: 290290435
Change-Id: Ia34edcce58619be8a7ff4296a388b5b6a46c8544
---
 fire/console/console_io.py | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/fire/console/console_io.py b/fire/console/console_io.py
index 777f1f48..784369b7 100644
--- a/fire/console/console_io.py
+++ b/fire/console/console_io.py
@@ -20,6 +20,7 @@
 from __future__ import print_function
 
 import os
+import signal
 import subprocess
 import sys
 
@@ -68,6 +69,10 @@ def IsInteractive(output=False, error=False, heuristic=False):
   return True
 
 
+def PreexecFunc():
+  signal.signal(signal.SIGINT, signal.SIG_IGN)
+
+
 def More(contents, out, prompt=None, check_pager=True):
   """Run a user specified pager or fall back to the internal pager.
 
@@ -97,10 +102,19 @@ def More(contents, out, prompt=None, check_pager=True):
       less_orig = encoding.GetEncodedValue(os.environ, 'LESS', None)
       less = '-R' + (less_orig or '')
       encoding.SetEncodedValue(os.environ, 'LESS', less)
-      p = subprocess.Popen(pager, stdin=subprocess.PIPE, shell=True)
+      # Ignores SIGINT from this point on since the child process has started
+      # and we don't want to terminate either one when the child is still alive.
+      signal.signal(signal.SIGINT, signal.SIG_IGN)
+      # Runs PreexecFunc before starting the child so SIGINT is ignored for the
+      # child process as well.
+      p = subprocess.Popen(
+          pager, stdin=subprocess.PIPE, shell=True, preexec_fn=PreexecFunc)
       enc = console_attr.GetConsoleAttr().GetEncoding()
       p.communicate(input=contents.encode(enc))
       p.wait()
+      # Starts using default disposition for SIGINT again after the child has
+      # exited.
+      signal.signal(signal.SIGINT, signal.SIG_DFL)
       if less_orig is None:
         encoding.SetEncodedValue(os.environ, 'LESS', None)
       return

From 420ced0454f97b204f3ba178edf77344997df31e Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Fri, 24 Jan 2020 13:08:34 -0800
Subject: [PATCH 171/324] Print out predefined summary and descriptions for
 primitive types instead of the real docstring in the object since the
 docstring of the builtin objects are usually not very useful.

PiperOrigin-RevId: 291428068
Change-Id: Ica21b25aff6a8f95fc1ec7bd2fae8672fcf13ebb
---
 fire/custom_descriptions.py      | 80 ++++++++++++++++++++++++++++++++
 fire/custom_descriptions_test.py | 73 +++++++++++++++++++++++++++++
 fire/docstrings.py               |  1 +
 fire/formatting.py               | 28 +++++++++++
 fire/formatting_test.py          | 26 +++++++++++
 fire/helptext.py                 | 34 +++++++++-----
 fire/helptext_test.py            |  9 ++--
 7 files changed, 236 insertions(+), 15 deletions(-)
 create mode 100644 fire/custom_descriptions_test.py

diff --git a/fire/custom_descriptions.py b/fire/custom_descriptions.py
index 695b01b3..191e8b29 100644
--- a/fire/custom_descriptions.py
+++ b/fire/custom_descriptions.py
@@ -40,8 +40,12 @@
 from __future__ import division
 from __future__ import print_function
 
+from fire import formatting
 import six
 
+TWO_DOUBLE_QUOTES = '""'
+STRING_DESC_PREFIX = 'The string '
+
 
 def NeedsCustomDescription(component):
   """Whether the component should use a custom description and summary.
@@ -69,3 +73,79 @@ def NeedsCustomDescription(component):
      ):
     return True
   return False
+
+
+def GetStringTypeSummary(obj, available_space, line_length):
+  """Returns a custom summary for string type objects.
+
+  This function constructs a summary for string type objects by double quoting
+  the string value. The double quoted string value will be potentially truncated
+  with ellipsis depending on whether it has enough space available to show the
+  full string value.
+
+  Args:
+    obj: The object to generate summary for.
+    available_space: Number of character spaces available.
+    line_length: The full width of the terminal, default is 80.
+
+  Returns:
+    A summary for the input object.
+  """
+  if len(obj) + len(TWO_DOUBLE_QUOTES) <= available_space:
+    content = obj
+  else:
+    additional_len_needed = len(TWO_DOUBLE_QUOTES) + len(formatting.ELLIPSIS)
+    if available_space < additional_len_needed:
+      available_space = line_length
+    content = formatting.EllipsisTruncate(
+        obj, available_space - len(TWO_DOUBLE_QUOTES), line_length)
+  return formatting.DoubleQuote(content)
+
+
+def GetStringTypeDescription(obj, available_space, line_length):
+  """Returns the predefined description for string obj.
+
+  This function constructs a description for string type objects in the format
+  of 'The string "<string_value>"'. <string_value> could be potentially
+  truncated depending on whether it has enough space available to show the full
+  string value.
+
+  Args:
+    obj: The object to generate description for.
+    available_space: Number of character spaces available.
+    line_length: The full width of the terminal, default if 80.
+
+  Returns:
+    A description for input object.
+  """
+  additional_len_needed = len(STRING_DESC_PREFIX) + len(
+      TWO_DOUBLE_QUOTES) + len(formatting.ELLIPSIS)
+  if available_space < additional_len_needed:
+    available_space = line_length
+
+  return STRING_DESC_PREFIX + formatting.DoubleQuote(
+      formatting.EllipsisTruncate(
+          obj, available_space - len(STRING_DESC_PREFIX) -
+          len(TWO_DOUBLE_QUOTES), line_length))
+
+
+CUSTOM_DESC_SUM_FN_DICT = {
+    'str': (GetStringTypeSummary, GetStringTypeDescription),
+    'unicode': (GetStringTypeSummary, GetStringTypeDescription),
+}
+
+
+def GetSummary(obj, available_space, line_length):
+  obj_type_name = type(obj).__name__
+  if obj_type_name in CUSTOM_DESC_SUM_FN_DICT.keys():
+    return CUSTOM_DESC_SUM_FN_DICT.get(obj_type_name)[0](obj, available_space,
+                                                         line_length)
+  return None
+
+
+def GetDescription(obj, available_space, line_length):
+  obj_type_name = type(obj).__name__
+  if obj_type_name in CUSTOM_DESC_SUM_FN_DICT.keys():
+    return CUSTOM_DESC_SUM_FN_DICT.get(obj_type_name)[1](obj, available_space,
+                                                         line_length)
+  return None
diff --git a/fire/custom_descriptions_test.py b/fire/custom_descriptions_test.py
new file mode 100644
index 00000000..79d7c7a1
--- /dev/null
+++ b/fire/custom_descriptions_test.py
@@ -0,0 +1,73 @@
+# 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.
+
+"""Tests for custom description module."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+from fire import custom_descriptions
+from fire import testutils
+
+LINE_LENGTH = 80
+
+
+class CustomDescriptionTest(testutils.BaseTestCase):
+
+  def test_string_type_summary_enough_space(self):
+    component = 'Test'
+    summary = custom_descriptions.GetSummary(
+        obj=component, available_space=80, line_length=LINE_LENGTH)
+    self.assertEqual(summary, '"Test"')
+
+  def test_string_type_summary_not_enough_space_truncated(self):
+    component = 'Test'
+    summary = custom_descriptions.GetSummary(
+        obj=component, available_space=5, line_length=LINE_LENGTH)
+    self.assertEqual(summary, '"..."')
+
+  def test_string_type_summary_not_enough_space_new_line(self):
+    component = 'Test'
+    summary = custom_descriptions.GetSummary(
+        obj=component, available_space=4, line_length=LINE_LENGTH)
+    self.assertEqual(summary, '"Test"')
+
+  def test_string_type_summary_not_enough_space_long_truncated(self):
+    component = 'Lorem ipsum dolor sit amet'
+    summary = custom_descriptions.GetSummary(
+        obj=component, available_space=10, line_length=LINE_LENGTH)
+    self.assertEqual(summary, '"Lorem..."')
+
+  def test_string_type_description_enough_space(self):
+    component = 'Test'
+    description = custom_descriptions.GetDescription(
+        obj=component, available_space=80, line_length=LINE_LENGTH)
+    self.assertEqual(description, 'The string "Test"')
+
+  def test_string_type_description_not_enough_space_truncated(self):
+    component = 'Lorem ipsum dolor sit amet'
+    description = custom_descriptions.GetDescription(
+        obj=component, available_space=20, line_length=LINE_LENGTH)
+    self.assertEqual(description, 'The string "Lore..."')
+
+  def test_string_type_description_not_enough_space_new_line(self):
+    component = 'Lorem ipsum dolor sit amet'
+    description = custom_descriptions.GetDescription(
+        obj=component, available_space=10, line_length=LINE_LENGTH)
+    self.assertEqual(description, 'The string "Lorem ipsum dolor sit amet"')
+
+
+if __name__ == '__main__':
+  testutils.main()
diff --git a/fire/docstrings.py b/fire/docstrings.py
index a58a42fe..edacf53c 100644
--- a/fire/docstrings.py
+++ b/fire/docstrings.py
@@ -149,6 +149,7 @@ def parse(docstring):
 
   Args:
     docstring: The docstring to parse.
+
   Returns:
     A DocstringInfo containing information about the docstring.
   """
diff --git a/fire/formatting.py b/fire/formatting.py
index 880e2b18..2c7cb335 100644
--- a/fire/formatting.py
+++ b/fire/formatting.py
@@ -20,6 +20,8 @@
 
 import termcolor
 
+ELLIPSIS = '...'
+
 
 def Indent(text, spaces=2):
   lines = text.split('\n')
@@ -65,3 +67,29 @@ def WrappedJoin(items, separator=' | ', width=80):
 
 def Error(text):
   return termcolor.colored(text, color='red', attrs=['bold'])
+
+
+def EllipsisTruncate(text, available_space, line_length):
+  """Truncate text from the end with ellipsis."""
+  if available_space < len(ELLIPSIS):
+    available_space = line_length
+  # No need to truncate
+  if len(text) <= available_space:
+    return text
+  return text[:available_space - len(ELLIPSIS)] + ELLIPSIS
+
+
+def EllipsisMiddleTruncate(text, available_space, line_length):
+  """Truncates text from the middle with ellipsis."""
+  if available_space < len(ELLIPSIS):
+    available_space = line_length
+  if len(text) < available_space:
+    return text
+  available_string_len = available_space - len(ELLIPSIS)
+  first_half_len = int(available_string_len / 2)  # start from middle
+  second_half_len = available_string_len - first_half_len
+  return text[:first_half_len] + ELLIPSIS + text[-second_half_len:]
+
+
+def DoubleQuote(text):
+  return '"%s"' % text
diff --git a/fire/formatting_test.py b/fire/formatting_test.py
index c19db054..61cce0e8 100644
--- a/fire/formatting_test.py
+++ b/fire/formatting_test.py
@@ -21,6 +21,8 @@
 from fire import formatting
 from fire import testutils
 
+LINE_LENGTH = 80
+
 
 class FormattingTest(testutils.BaseTestCase):
 
@@ -51,6 +53,30 @@ def test_wrap_multiple_items(self):
                       'chicken |',
                       'cheese'], lines)
 
+  def test_ellipsis_truncate(self):
+    text = 'This is a string'
+    truncated_text = formatting.EllipsisTruncate(
+        text=text, available_space=10, line_length=LINE_LENGTH)
+    self.assertEqual('This is...', truncated_text)
+
+  def test_ellipsis_truncate_not_enough_space(self):
+    text = 'This is a string'
+    truncated_text = formatting.EllipsisTruncate(
+        text=text, available_space=2, line_length=LINE_LENGTH)
+    self.assertEqual('This is a string', truncated_text)
+
+  def test_ellipsis_middle_truncate(self):
+    text = '1000000000L'
+    truncated_text = formatting.EllipsisMiddleTruncate(
+        text=text, available_space=7, line_length=LINE_LENGTH)
+    self.assertEqual('10...0L', truncated_text)
+
+  def test_ellipsis_middle_truncate_not_enough_space(self):
+    text = '1000000000L'
+    truncated_text = formatting.EllipsisMiddleTruncate(
+        text=text, available_space=2, line_length=LINE_LENGTH)
+    self.assertEqual('1000000000L', truncated_text)
+
 
 if __name__ == '__main__':
   testutils.main()
diff --git a/fire/helptext.py b/fire/helptext.py
index 2c360d2b..fd38cb48 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -41,6 +41,7 @@
 from fire import value_types
 
 LINE_LENGTH = 80
+SECTION_INDENTATION = 4
 
 
 def HelpText(component, trace=None, verbose=False):
@@ -96,10 +97,12 @@ def _NameSection(component, info, trace=None, verbose=False):
   current_command = _GetCurrentCommand(trace, include_separators=verbose)
   summary = _GetSummary(info)
 
-  # If the docstring is one of the messy builtin docstrings, don't show summary.
-  # TODO(dbieber): In follow up commits we can add in replacement summaries.
+  # If the docstring is one of the messy builtin docstrings, show custom one.
   if custom_descriptions.NeedsCustomDescription(component):
-    summary = None
+    available_space = LINE_LENGTH - SECTION_INDENTATION - len(current_command +
+                                                              ' - ')
+    summary = custom_descriptions.GetSummary(component, available_space,
+                                             LINE_LENGTH)
 
   if summary:
     text = current_command + ' - ' + summary
@@ -147,15 +150,17 @@ def _DescriptionSection(component, info):
     Returns the description if available. If not, returns the summary.
     If neither are available, returns None.
   """
-  # If the docstring is one of the messy builtin docstrings, set it to None.
-  # TODO(dbieber): In follow up commits we can add in replacement docstrings.
   if custom_descriptions.NeedsCustomDescription(component):
-    return None
-
-  summary = _GetSummary(info)
-  description = _GetDescription(info)
+    available_space = LINE_LENGTH - SECTION_INDENTATION
+    description = custom_descriptions.GetDescription(component, available_space,
+                                                     LINE_LENGTH)
+    summary = custom_descriptions.GetSummary(component, available_space,
+                                             LINE_LENGTH)
+  else:
+    description = _GetDescription(info)
+    summary = _GetSummary(info)
+  # Fall back to summary if description is not available.
   text = description or summary or None
-
   if text:
     return ('DESCRIPTION', text)
   else:
@@ -348,8 +353,9 @@ def _GetCurrentCommand(trace=None, include_separators=True):
 
 def _CreateOutputSection(name, content):
   return """{name}
-{content}""".format(name=formatting.Bold(name),
-                    content=formatting.Indent(content, 4))
+{content}""".format(
+    name=formatting.Bold(name),
+    content=formatting.Indent(content, SECTION_INDENTATION))
 
 
 def _CreateArgItem(arg, docstring_info):
@@ -359,6 +365,7 @@ def _CreateArgItem(arg, docstring_info):
     arg: The name of the positional argument.
     docstring_info: A docstrings.DocstringInfo namedtuple with information about
       the containing function's docstring.
+
   Returns:
     A string to be used in constructing the help screen for the function.
   """
@@ -418,6 +425,9 @@ def _MakeUsageDetailsSection(action_group):
     if (docstring_info
         and not custom_descriptions.NeedsCustomDescription(member)):
       summary = docstring_info.summary
+    elif custom_descriptions.NeedsCustomDescription(member):
+      summary = custom_descriptions.GetSummary(
+          member, LINE_LENGTH - SECTION_INDENTATION, LINE_LENGTH)
     else:
       summary = None
     item = _CreateItem(name, summary)
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 1371e2a1..a866ee4d 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -111,7 +111,8 @@ def testHelpTextEmptyList(self):
         trace=trace.FireTrace(component, 'list'))
     self.assertIn('NAME\n    list', help_screen)
     self.assertIn('SYNOPSIS\n    list COMMAND', help_screen)
-    # The list docstring is messy, so it is not shown.
+    # TODO(zuhaochen): Change assertion after custom description is
+    # implemented for list type.
     self.assertNotIn('DESCRIPTION', help_screen)
     # We don't check the listed commands either since the list API could
     # potentially change between Python versions.
@@ -125,7 +126,8 @@ def testHelpTextShortList(self):
         trace=trace.FireTrace(component, 'list'))
     self.assertIn('NAME\n    list', help_screen)
     self.assertIn('SYNOPSIS\n    list COMMAND', help_screen)
-    # The list docstring is messy, so it is not shown.
+    # TODO(zuhaochen): Change assertion after custom description is
+    # implemented for list type.
     self.assertNotIn('DESCRIPTION', help_screen)
 
     # We don't check the listed commands comprehensively since the list API
@@ -141,7 +143,8 @@ def testHelpTextInt(self):
         component=component, trace=trace.FireTrace(component, '7'))
     self.assertIn('NAME\n    7', help_screen)
     self.assertIn('SYNOPSIS\n    7 COMMAND | VALUE', help_screen)
-    # The int docstring is messy, so it is not shown.
+    # TODO(zuhaochen): Change assertion after implementing custom
+    # description for int.
     self.assertNotIn('DESCRIPTION', help_screen)
     self.assertIn('COMMANDS\n    COMMAND is one of the following:\n',
                   help_screen)

From 192c020d301fac2bf5fbd02b28c5b4ce36495cba Mon Sep 17 00:00:00 2001
From: David Caron <david.caron@crim.ca>
Date: Tue, 28 Jan 2020 16:23:58 -0500
Subject: [PATCH 172/324] fix typo in guide (#218)

* fix typo in guide

- `{name: David}` -> `{name:David}` (no space)
---
 docs/guide.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/guide.md b/docs/guide.md
index 2e59ce8a..b02f37b5 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -595,7 +595,7 @@ $ python example.py [1,2]
 list
 $ python example.py True
 bool
-$ python example.py {name: David}
+$ python example.py {name:David}
 dict
 ```
 

From 1ac5105a0437518785033f47dd78746678e5133a Mon Sep 17 00:00:00 2001
From: Mehmood Deshmukh <meshde.md@gmail.com>
Date: Sat, 22 Feb 2020 00:26:34 +0530
Subject: [PATCH 173/324] Use "dir()" to get available methods of object (#215)

* Use dir() instead of inspect.getmembers() to get available methods of the object
* Add test case for #149

Ref: #149
---
 fire/core.py            |  4 ++--
 fire/fire_test.py       |  5 +++++
 fire/test_components.py | 10 ++++++++++
 3 files changed, 17 insertions(+), 2 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index 07993de9..083163b9 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -632,7 +632,7 @@ def _GetMember(component, args):
   Raises:
     FireError: If we cannot consume an argument to get a member.
   """
-  members = dict(inspect.getmembers(component))
+  members = dir(component)
   arg = args[0]
   arg_names = [
       arg,
@@ -641,7 +641,7 @@ def _GetMember(component, args):
 
   for arg_name in arg_names:
     if arg_name in members:
-      return members[arg_name], [arg], args[1:]
+      return getattr(component, arg_name), [arg], args[1:]
 
   raise FireError('Could not consume arg:', arg)
 
diff --git a/fire/fire_test.py b/fire/fire_test.py
index 8cf121af..63302d26 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -703,6 +703,11 @@ def testTraceErrors(self):
     with self.assertRaisesFireExit(2):
       fire.Fire(tc.InstanceVars, command=['--arg1=a1', '--arg2=a2', '-', 'jog'])
 
+  def testClassWithDefaultMethod(self):
+    self.assertEqual(
+        fire.Fire(tc.DefaultMethod, command=['double', '10']), 20
+    )
+
 
 if __name__ == '__main__':
   testutils.main()
diff --git a/fire/test_components.py b/fire/test_components.py
index e35027c7..e88ae907 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -498,3 +498,13 @@ def off(self):
   def set(self, value):
     self.pixels[self._row][self._col] = value
     return self
+
+class DefaultMethod(object):
+
+  def double(self, number):
+    return 2 * number
+
+  def __getattr__(self, name):
+    def _missing():
+      return "Undefined Function"
+    return _missing

From 34a10f9218e04b343ff1579b3505a3cbe8da4824 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Tue, 25 Feb 2020 13:16:32 -0800
Subject: [PATCH 174/324] Add InvalidProperty test and sync with GitHub

PiperOrigin-RevId: 297186473
Change-Id: Id23c439ce057b9bc1c7e7c9fd02dd3fb3f3d6a49
---
 docs/guide.md           | 2 +-
 fire/test_components.py | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/docs/guide.md b/docs/guide.md
index b02f37b5..2e59ce8a 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -595,7 +595,7 @@ $ python example.py [1,2]
 list
 $ python example.py True
 bool
-$ python example.py {name:David}
+$ python example.py {name: David}
 dict
 ```
 
diff --git a/fire/test_components.py b/fire/test_components.py
index e88ae907..d29dfa2d 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -499,6 +499,7 @@ def set(self, value):
     self.pixels[self._row][self._col] = value
     return self
 
+
 class DefaultMethod(object):
 
   def double(self, number):
@@ -508,3 +509,4 @@ def __getattr__(self, name):
     def _missing():
       return "Undefined Function"
     return _missing
+

From 297a1adfae74cb7e688985d24a8683d808781826 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Tue, 25 Feb 2020 13:28:36 -0800
Subject: [PATCH 175/324] Quote lint formatting and fix accidentally reverted
 docs change.

PiperOrigin-RevId: 297189113
Change-Id: I6db02e4be001d280b0d98afaeb9e201f837cbecb
---
 docs/guide.md           | 2 +-
 fire/test_components.py | 3 +--
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/docs/guide.md b/docs/guide.md
index 2e59ce8a..b02f37b5 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -595,7 +595,7 @@ $ python example.py [1,2]
 list
 $ python example.py True
 bool
-$ python example.py {name: David}
+$ python example.py {name:David}
 dict
 ```
 
diff --git a/fire/test_components.py b/fire/test_components.py
index d29dfa2d..73217051 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -507,6 +507,5 @@ def double(self, number):
 
   def __getattr__(self, name):
     def _missing():
-      return "Undefined Function"
+      return 'Undefined function'
     return _missing
-

From 04e2434001e809bdf05e6f47d71452d1e8614b78 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Tue, 25 Feb 2020 17:42:34 -0800
Subject: [PATCH 176/324] Add InvalidProperty test and fix the travis error
 resulting from pytype not being supported in python 3.4

PiperOrigin-RevId: 297245710
Change-Id: I0b0f6110f2aeb1951a585be3532e8d49c22e41c7
---
 .travis.yml             |  9 ++++++---
 fire/fire_test.py       |  4 ++++
 fire/test_components.py | 10 ++++++++++
 3 files changed, 20 insertions(+), 3 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 44edde6e..16246cb8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -23,12 +23,15 @@ script:
   - pip install ipython
   - python -m pytest  # Now run the tests with IPython.
   - pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console
-  - pip install pytype
+  - if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then
+      pip install pytype;
+    fi
   # Run type-checking, excluding files that define or use py3 features in py2.
   - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then
       pytype -x
         fire/fire_test.py
         fire/inspectutils_test.py
         fire/test_components_py3.py;
-    else
-      pytype; fi
+    elif [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then
+      pytype;
+    fi
diff --git a/fire/fire_test.py b/fire/fire_test.py
index 63302d26..e5c7fe3a 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -708,6 +708,10 @@ def testClassWithDefaultMethod(self):
         fire.Fire(tc.DefaultMethod, command=['double', '10']), 20
     )
 
+  def testClassWithInvalidProperty(self):
+    self.assertEqual(
+        fire.Fire(tc.InvalidProperty, command=['double', '10']), 20
+    )
 
 if __name__ == '__main__':
   testutils.main()
diff --git a/fire/test_components.py b/fire/test_components.py
index 73217051..ffd02b37 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -509,3 +509,13 @@ def __getattr__(self, name):
     def _missing():
       return 'Undefined function'
     return _missing
+
+
+class InvalidProperty(object):
+
+  def double(self, number):
+    return 2 * number
+
+  @property
+  def prop(self):
+    raise ValueError('test')

From a5dba1301c64b179c39b3cfb214aefe954706cf0 Mon Sep 17 00:00:00 2001
From: Pranav Gupta <pranavgupta4321@gmail.com>
Date: Thu, 27 Feb 2020 09:13:26 -0800
Subject: [PATCH 177/324] Copybara import of the project:

--
9d5f581e13deee9f654fbec0be02944b1d7e6a90 by Pranav Gupta <pranavgupta4321@gmail.com>:

Update using-cli.md to reflect support for --help

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/194 from pranavgupta1234:patch-1 9d5f581e13deee9f654fbec0be02944b1d7e6a90
PiperOrigin-RevId: 297612634
Change-Id: I89eed909ad09226a9642d968497b413d12512dae
---
 docs/using-cli.md | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/docs/using-cli.md b/docs/using-cli.md
index 4cd95e14..0f369a9a 100644
--- a/docs/using-cli.md
+++ b/docs/using-cli.md
@@ -9,10 +9,13 @@ arguments. This command corresponds to the Python component you called the
 `Fire` function on. If you did not supply an object in the call to `Fire`, then
 the context in which `Fire` was called will be used as the Python component.
 
-You can append `-- --help` to any command to see what Python component it
+You can append `--help` or `-h` to a command to see what Python component it
 corresponds to, as well as the various ways in which you can extend the command.
-Flags are always separated from the Fire command by an isolated `--` in order
-to distinguish between flags and named arguments.
+
+Flags to Fire should be separated from the Fire command by an isolated `--` in
+order to distinguish between flags and named arguments. So, for example, to
+enter interactive mode append `-- -i` or `-- --interactive` to any command. To
+use Fire in verbose mode, append `-- --verbose`.
 
 Given a Fire command that corresponds to a Python object, you can extend that
 command to access a member of that object, call it with arguments if it is a

From 7d87eb260456bda43518dd1259f0677c7b2a6d39 Mon Sep 17 00:00:00 2001
From: Jared Trog <jared.trog@gmail.com>
Date: Fri, 28 Feb 2020 09:10:40 -0800
Subject: [PATCH 178/324] Copybara import of the project:

--
9061166c98474e4dd001844b2bb821b428c148a9 by Jared Trog <jared.trog@gmail.com>:

Support printing classes with overridden str method as SimpleGroups #197

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/224 from jaredtrog:fix-issue-197 9061166c98474e4dd001844b2bb821b428c148a9
PiperOrigin-RevId: 297857231
Change-Id: Ibf2c344eda89f5a042726bde003bb4653ae3835e
---
 fire/value_types.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/fire/value_types.py b/fire/value_types.py
index ad50cb23..39a0e9f3 100644
--- a/fire/value_types.py
+++ b/fire/value_types.py
@@ -37,7 +37,8 @@ def IsCommand(component):
 
 
 def IsValue(component):
-  return isinstance(component, VALUE_TYPES)
+  return isinstance(component, VALUE_TYPES) or (
+      hasattr(component, '__str__') and inspect.ismethod(component.__str__))
 
 
 def IsSimpleGroup(component):

From 1c1afd16c6cf5943a0c62c0db654d5da570ae65f Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 28 Feb 2020 09:13:42 -0800
Subject: [PATCH 179/324] Treat objects with custom __str__ methods as Values /
 SimpleGroups.

PiperOrigin-RevId: 297857807
Change-Id: I9ab3f50336cc30e6cdeccc2965a50821838507a3
---
 fire/core.py        |  9 +++------
 fire/value_types.py | 29 +++++++++++++++++++++++++++--
 2 files changed, 30 insertions(+), 8 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index 083163b9..be064c1c 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -244,14 +244,11 @@ def _PrintResult(component_trace, verbose=False):
   # and move serialization to its own module.
   result = component_trace.GetResult()
 
-  if hasattr(result, '__str__'):
+  if value_types.HasCustomStr(result):
     # If the object has a custom __str__ method, rather than one inherited from
     # object, then we use that to serialize the object.
-    class_attrs = completion.GetClassAttrsDict(type(result)) or {}
-    str_attr = class_attrs.get('__str__')
-    if str_attr and str_attr.defining_class is not object:
-      print(str(result))
-      return
+    print(str(result))
+    return
 
   if isinstance(result, (list, set, frozenset, types.GeneratorType)):
     for i in result:
diff --git a/fire/value_types.py b/fire/value_types.py
index 39a0e9f3..9ee63384 100644
--- a/fire/value_types.py
+++ b/fire/value_types.py
@@ -20,6 +20,7 @@
 
 import inspect
 
+from fire import completion
 import six
 
 
@@ -37,8 +38,7 @@ def IsCommand(component):
 
 
 def IsValue(component):
-  return isinstance(component, VALUE_TYPES) or (
-      hasattr(component, '__str__') and inspect.ismethod(component.__str__))
+  return isinstance(component, VALUE_TYPES) or HasCustomStr(component)
 
 
 def IsSimpleGroup(component):
@@ -58,3 +58,28 @@ def IsSimpleGroup(component):
     if not IsValue(value) and not isinstance(value, (list, dict)):
       return False
   return True
+
+
+def HasCustomStr(component):
+  """Determines if a component has a custom __str__ method.
+
+  Uses inspect.classify_class_attrs to determine the origin of the object's
+  __str__ method, if one is present. If it defined by `object` itself, then
+  it is not considered custom. Otherwise it is. This means that the __str__
+  methods of primitives like ints and floats are considered custom.
+
+  Objects with custom __str__ methods are treated as values and can be
+  serialized in places where more complex objects would have their help screen
+  shown instead.
+
+  Args:
+    component: The object to check for a custom __str__ method.
+  Returns:
+    Whether `component` has a custom __str__ method.
+  """
+  if hasattr(component, '__str__'):
+    class_attrs = completion.GetClassAttrsDict(type(component)) or {}
+    str_attr = class_attrs.get('__str__')
+    if str_attr and str_attr.defining_class is not object:
+      return True
+  return False

From c8ea506b067ac13465f0331d54afce35e495da3f Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 28 Feb 2020 09:14:48 -0800
Subject: [PATCH 180/324] Hide future imports in MemberVisible

The reason for this is that in PY34 and beyond, future imports have their own type that can be detected and those imports can be filtered out.
However, in Python 2 the type of these imports is just "instance" which is too aggressive and filters out objects unnecessarily.

PiperOrigin-RevId: 297858001
Change-Id: I86d10505455d708f4058ec2e05091a6405425f84
---
 fire/completion.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/fire/completion.py b/fire/completion.py
index 19d068fd..bf7678e5 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -318,7 +318,9 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False):
     return False
   if verbose:
     return True
-  if isinstance(member, type(absolute_import)):
+  if member in (absolute_import, division, print_function):
+    return False
+  if isinstance(member, type(absolute_import)) and six.PY34:
     return False
   if inspect.ismodule(member) and member is six:
     # TODO(dbieber): Determine more generally which modules to hide.

From c830177ce00b1c6f123316e3cae81cbaec8377ca Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 28 Feb 2020 09:37:23 -0800
Subject: [PATCH 181/324] Move GetClassAttrsDict to inspectutils.

PiperOrigin-RevId: 297862432
Change-Id: I8b37f6715f70085a9b967b183e27aaa031480ad1
---
 fire/completion.py   | 21 +++++----------------
 fire/core.py         |  2 +-
 fire/inspectutils.py | 11 +++++++++++
 fire/value_types.py  |  4 ++--
 4 files changed, 19 insertions(+), 19 deletions(-)

diff --git a/fire/completion.py b/fire/completion.py
index bf7678e5..73360715 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -281,17 +281,6 @@ def _FishScript(name, commands, default_options=None):
   )
 
 
-def GetClassAttrsDict(component):
-  """Gets the attributes of the component class, as a dict with name keys."""
-  if not inspect.isclass(component):
-    return None
-  class_attrs_list = inspect.classify_class_attrs(component)
-  return {
-      class_attr.name: class_attr
-      for class_attr in class_attrs_list
-  }
-
-
 def MemberVisible(component, name, member, class_attrs=None, verbose=False):
   """Returns whether a member should be included in auto-completion or help.
 
@@ -328,7 +317,7 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False):
   if inspect.isclass(component):
     # If class_attrs has not been provided, compute it.
     if class_attrs is None:
-      class_attrs = GetClassAttrsDict(class_attrs)
+      class_attrs = inspectutils.GetClassAttrsDict(class_attrs)
     class_attr = class_attrs.get(name)
     if class_attr and class_attr.kind in ('method', 'property'):
       # methods and properties should be accessed on instantiated objects,
@@ -355,9 +344,9 @@ def VisibleMembers(component, class_attrs=None, verbose=False):
   Args:
     component: The component whose members to list.
     class_attrs: (optional) If component is a class, you may provide this as:
-      GetClassAttrsDict(component). If not provided, it will be computed.
-      If provided, this determines how class members will be treated for
-      visibility. In particular, methods are generally hidden for
+      inspectutils.GetClassAttrsDict(component). If not provided, it will be
+      computed. If provided, this determines how class members will be treated
+      for visibility. In particular, methods are generally hidden for
       non-instantiated classes, but if you wish them to be shown (e.g. for
       completion scripts) then pass in a different class_attr for them.
     verbose: Whether to include private members.
@@ -371,7 +360,7 @@ def VisibleMembers(component, class_attrs=None, verbose=False):
 
   # If class_attrs has not been provided, compute it.
   if class_attrs is None:
-    class_attrs = GetClassAttrsDict(component)
+    class_attrs = inspectutils.GetClassAttrsDict(component)
   return [
       (member_name, member) for member_name, member in members
       if MemberVisible(component, member_name, member, class_attrs=class_attrs,
diff --git a/fire/core.py b/fire/core.py
index be064c1c..763b3d13 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -309,7 +309,7 @@ def _DictAsString(result, verbose=False):
   # We need to do 2 iterations over the items in the result dict
   # 1) Getting visible items and the longest key for output formatting
   # 2) Actually construct the output lines
-  class_attrs = completion.GetClassAttrsDict(result)
+  class_attrs = inspectutils.GetClassAttrsDict(result)
   result_visible = {
       key: value for key, value in result.items()
       if completion.MemberVisible(result, key, value,
diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index bf197f3f..f3e54a98 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -268,3 +268,14 @@ def IsNamedTuple(component):
 
   has_fields = bool(getattr(component, '_fields', None))
   return has_fields
+
+
+def GetClassAttrsDict(component):
+  """Gets the attributes of the component class, as a dict with name keys."""
+  if not inspect.isclass(component):
+    return None
+  class_attrs_list = inspect.classify_class_attrs(component)
+  return {
+      class_attr.name: class_attr
+      for class_attr in class_attrs_list
+  }
diff --git a/fire/value_types.py b/fire/value_types.py
index 9ee63384..c0a137fd 100644
--- a/fire/value_types.py
+++ b/fire/value_types.py
@@ -20,7 +20,7 @@
 
 import inspect
 
-from fire import completion
+from fire import inspectutils
 import six
 
 
@@ -78,7 +78,7 @@ def HasCustomStr(component):
     Whether `component` has a custom __str__ method.
   """
   if hasattr(component, '__str__'):
-    class_attrs = completion.GetClassAttrsDict(type(component)) or {}
+    class_attrs = inspectutils.GetClassAttrsDict(type(component)) or {}
     str_attr = class_attrs.get('__str__')
     if str_attr and str_attr.defining_class is not object:
       return True

From 86ac2685a694b13da120ab52010ed22a1117c898 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 28 Feb 2020 13:23:17 -0800
Subject: [PATCH 182/324] Satisfy pytype

PiperOrigin-RevId: 297916197
Change-Id: I718ce9ffa5a4c137d2d111eaf3ca842287cef114
---
 fire/completion.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/fire/completion.py b/fire/completion.py
index 73360715..451e2021 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -298,7 +298,8 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False):
     name: The name of the member.
     member: The member itself.
     class_attrs: (optional) If component is a class, provide this as:
-      GetClassAttrsDict(component). If not provided, it will be computed.
+      inspectutils.GetClassAttrsDict(component). If not provided, it will be
+      computed.
     verbose: Whether to include private members.
   Returns
     A boolean value indicating whether the member should be included.
@@ -317,7 +318,7 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False):
   if inspect.isclass(component):
     # If class_attrs has not been provided, compute it.
     if class_attrs is None:
-      class_attrs = inspectutils.GetClassAttrsDict(class_attrs)
+      class_attrs = inspectutils.GetClassAttrsDict(class_attrs) or {}
     class_attr = class_attrs.get(name)
     if class_attr and class_attr.kind in ('method', 'property'):
       # methods and properties should be accessed on instantiated objects,

From deb33cc34f94cad86a875a9993864ac0acf93828 Mon Sep 17 00:00:00 2001
From: Vincent Barbaresi <vincent.barbaresi@gmail.com>
Date: Fri, 28 Feb 2020 14:24:21 -0800
Subject: [PATCH 183/324] use a custom getfullargspec that follows wrapped
 chains #159

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/203 from vbarbaresi:bugfix_159 27e024114a4cb663e7eb65623ba6443a7670f72e
PiperOrigin-RevId: 297930634
Change-Id: Ieebd9769e44a7ca85688711da771f2fded2df720
---
 fire/core_test.py           | 13 ++++++
 fire/fire_test.py           | 12 +++++-
 fire/inspectutils.py        | 85 ++++++++++++++++++++++++++++++++++---
 fire/test_components.py     | 13 ++++++
 fire/test_components_py3.py | 16 +++++++
 fire/testutils.py           |  1 +
 6 files changed, 132 insertions(+), 8 deletions(-)

diff --git a/fire/core_test.py b/fire/core_test.py
index 2f2df3fd..97389a2d 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -24,6 +24,8 @@
 from fire import trace
 import mock
 
+import six
+
 
 class CoreTest(testutils.BaseTestCase):
 
@@ -192,5 +194,16 @@ def testClassMethod(self):
         7,
     )
 
+  @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.')
+  def testLruCacheDecoratorBoundArg(self):
+    self.assertEqual(core.Fire(tc.py3.LruCacheDecoratedMethod,
+                               command=['lru_cache_in_class', 'foo']), 'foo')
+
+  @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.')
+  def testLruCacheDecorator(self):
+    self.assertEqual(
+        core.Fire(tc.py3.lru_cache_decorated, command=['foo']), 'foo')
+
+
 if __name__ == '__main__':
   testutils.main()
diff --git a/fire/fire_test.py b/fire/fire_test.py
index e5c7fe3a..2c7a580b 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -20,7 +20,6 @@
 
 import os
 import sys
-import unittest
 
 import fire
 from fire import test_components as tc
@@ -185,7 +184,7 @@ def testFireAnnotatedArgs(self):
     self.assertEqual(fire.Fire(tc.Annotations, command=['double', '5']), 10)
     self.assertEqual(fire.Fire(tc.Annotations, command=['triple', '5']), 15)
 
-  @unittest.skipIf(six.PY2, 'Keyword-only arguments not in Python 2.')
+  @testutils.skipIf(six.PY2, 'Keyword-only arguments not in Python 2.')
   def testFireKeywordOnlyArgs(self):
     with self.assertRaisesFireExit(2):
       # Keyword arguments must be passed with flag syntax.
@@ -713,5 +712,14 @@ def testClassWithInvalidProperty(self):
         fire.Fire(tc.InvalidProperty, command=['double', '10']), 20
     )
 
+  @testutils.skipIf(six.PY2, 'Cannot inspect wrapped signatures in Python 2.')
+  def testHelpKwargsDecorator(self):
+    # Issue #190, follow the wrapped method instead of crashing.
+    with self.assertRaisesFireExit(0):
+      fire.Fire(tc.decorated_method, command=['-h'])
+    with self.assertRaisesFireExit(0):
+      fire.Fire(tc.decorated_method, command=['--help'])
+
+
 if __name__ == '__main__':
   testutils.main()
diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index f3e54a98..a979b4ea 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -100,19 +100,92 @@ def Py2GetArgSpec(fn):
     raise
 
 
+def Py3GetFullArgSpec(fn):
+  """A alternative to the builtin getfullargspec.
+
+  The builtin inspect.getfullargspec uses:
+  `skip_bound_args=False, follow_wrapped_chains=False`
+  in order to be backwards compatible.
+
+  This function instead skips bound args (self) and follows wrapped chains.
+
+  Args:
+    fn: The function or class of interest.
+  Returns:
+    An inspect.FullArgSpec namedtuple with the full arg spec of the function.
+  """
+  # pytype: disable=module-attr
+  try:
+    sig = inspect._signature_from_callable(  # pylint: disable=protected-access
+        fn,
+        skip_bound_arg=True,
+        follow_wrapper_chains=True,
+        sigcls=inspect.Signature)
+  except Exception:
+    # 'signature' can raise ValueError (most common), AttributeError, and
+    # possibly others. We catch all exceptions here, and reraise a TypeError.
+    raise TypeError('Unsupported callable.')
+
+  args = []
+  varargs = None
+  varkw = None
+  kwonlyargs = []
+  defaults = ()
+  annotations = {}
+  defaults = ()
+  kwdefaults = {}
+
+  if sig.return_annotation is not sig.empty:
+    annotations['return'] = sig.return_annotation
+
+  for param in sig.parameters.values():
+    kind = param.kind
+    name = param.name
+
+    # pylint: disable=protected-access
+    if kind is inspect._POSITIONAL_ONLY:
+      args.append(name)
+    elif kind is  inspect._POSITIONAL_OR_KEYWORD:
+      args.append(name)
+      if param.default is not param.empty:
+        defaults += (param.default,)
+    elif kind is  inspect._VAR_POSITIONAL:
+      varargs = name
+    elif kind is  inspect._KEYWORD_ONLY:
+      kwonlyargs.append(name)
+      if param.default is not param.empty:
+        kwdefaults[name] = param.default
+    elif kind is  inspect._VAR_KEYWORD:
+      varkw = name
+    if param.annotation is not param.empty:
+      annotations[name] = param.annotation
+    # pylint: enable=protected-access
+
+  if not kwdefaults:
+    # compatibility with 'func.__kwdefaults__'
+    kwdefaults = None
+
+  if not defaults:
+    # compatibility with 'func.__defaults__'
+    defaults = None
+  return inspect.FullArgSpec(args, varargs, varkw, defaults,
+                             kwonlyargs, kwdefaults, annotations)
+  # pytype: enable=module-attr
+
+
 def GetFullArgSpec(fn):
   """Returns a FullArgSpec describing the given callable."""
   original_fn = fn
   fn, skip_arg = _GetArgSpecInfo(fn)
 
   try:
-    if six.PY2:
+    if six.PY3:
+      (args, varargs, varkw, defaults,
+       kwonlyargs, kwonlydefaults, annotations) = Py3GetFullArgSpec(fn)
+    else:  # six.PY2
       args, varargs, varkw, defaults = Py2GetArgSpec(fn)
       kwonlyargs = kwonlydefaults = None
       annotations = getattr(fn, '__annotations__', None)
-    else:
-      (args, varargs, varkw, defaults,
-       kwonlyargs, kwonlydefaults, annotations) = inspect.getfullargspec(fn)  # pylint: disable=deprecated-method,no-member
 
   except TypeError:
     # If we can't get the argspec, how do we know if the fn should take args?
@@ -141,9 +214,9 @@ def GetFullArgSpec(fn):
     # Case 3: Other known slot wrappers do not accept args.
     return FullArgSpec()
 
-  if skip_arg and args:
+  if six.PY2 and skip_arg and args:
+    # In Python 3, Py3GetFullArgSpec uses skip_bound_arg=True already.
     args.pop(0)  # Remove 'self' or 'cls' from the list of arguments.
-
   return FullArgSpec(args, varargs, varkw, defaults,
                      kwonlyargs, kwonlydefaults, annotations)
 
diff --git a/fire/test_components.py b/fire/test_components.py
index ffd02b37..a25ba7af 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -19,6 +19,7 @@
 from __future__ import print_function
 
 import collections
+import functools
 
 import enum
 import six
@@ -519,3 +520,15 @@ def double(self, number):
   @property
   def prop(self):
     raise ValueError('test')
+
+
+def simple_decorator(f):
+  @functools.wraps(f)
+  def wrapper(*args, **kwargs):
+    return f(*args, **kwargs)
+  return wrapper
+
+
+@simple_decorator
+def decorated_method(name='World'):
+  return 'Hello %s' % name
diff --git a/fire/test_components_py3.py b/fire/test_components_py3.py
index d705c43a..e714be30 100644
--- a/fire/test_components_py3.py
+++ b/fire/test_components_py3.py
@@ -12,9 +12,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+# Lint as: python3
 """This module has components that use Python 3 specific syntax."""
 
+import functools
 
+
+# pylint: disable=keyword-arg-before-vararg
 def identity(arg1, arg2: int, arg3=10, arg4: int = 20, *arg5,
              arg6, arg7: int, arg8=30, arg9: int = 40, **arg10):
   return arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10
@@ -27,3 +31,15 @@ def double(self, *, count):
 
   def triple(self, *, count):
     return count * 3
+
+
+class LruCacheDecoratedMethod(object):
+
+  @functools.lru_cache()
+  def lru_cache_in_class(self, arg1):
+    return arg1
+
+
+@functools.lru_cache()
+def lru_cache_decorated(arg1):
+  return arg1
diff --git a/fire/testutils.py b/fire/testutils.py
index 0541a9a5..3463fd50 100644
--- a/fire/testutils.py
+++ b/fire/testutils.py
@@ -98,4 +98,5 @@ def assertRaisesFireExit(self, code, regexp='.*'):
 # pylint: disable=invalid-name
 main = unittest.main
 skip = unittest.skip
+skipIf = unittest.skipIf
 # pylint: enable=invalid-name

From b1f3f95d736e7e36126f2e6c457c52f995f587dc Mon Sep 17 00:00:00 2001
From: Vincent Barbaresi <vincent.barbaresi@gmail.com>
Date: Fri, 28 Feb 2020 14:31:51 -0800
Subject: [PATCH 184/324] handle NumPy style docstring params containing a
 colon #189

The problematic case was:

    name : str
        name, default: World

The second line wasn't treated as a description if it contained a colon
and ended up ignored

We parse line by line, and used to keep track only of the next line.
I added tracking of the previous line so that I can compare the indentations.
That allows to distinguish descriptions and a parameter definitions.

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/201 from vbarbaresi:bugfix_189 8e6baa4f976d707ffdeeff951fdecd1eac7d09ca
PiperOrigin-RevId: 297932310
Change-Id: Ia5ee774890a713c8ec1d5b3a895d2cb0163849c3
---
 fire/docstrings.py      | 40 ++++++++++++++++++++++++++++++++++++----
 fire/docstrings_test.py | 26 ++++++++++++++++++++++++++
 2 files changed, 62 insertions(+), 4 deletions(-)

diff --git a/fire/docstrings.py b/fire/docstrings.py
index edacf53c..e173d192 100644
--- a/fire/docstrings.py
+++ b/fire/docstrings.py
@@ -176,8 +176,9 @@ def parse(docstring):
 
   for index, line in enumerate(lines):
     has_next = index + 1 < lines_len
+    previous_line = lines[index - 1] if index > 0 else None
     next_line = lines[index + 1] if has_next else None
-    line_info = _create_line_info(line, next_line)
+    line_info = _create_line_info(line, next_line, previous_line)
     _consume_line(line_info, state)
 
   summary = ' '.join(state.summary.lines) if state.summary.lines else None
@@ -455,7 +456,7 @@ def _consume_line(line_info, state):
         # of the previous arg, or a new arg. TODO: Whitespace can distinguish.
         arg = _get_or_create_arg_by_name(state, line_stripped)
         state.current_arg = arg
-      elif ':' in line_stripped:
+      elif _line_is_numpy_parameter_type(line_info):
         possible_args, type_data = line_stripped.split(':', 1)
         arg_names = _as_arg_names(possible_args)  # re.split(' |,', s)
         if arg_names:
@@ -492,8 +493,8 @@ def _consume_line(line_info, state):
       pass
 
 
-def _create_line_info(line, next_line):
-  """Returns information about the current and next line of the docstring."""
+def _create_line_info(line, next_line, previous_line):
+  """Returns information about the current line and surrounding lines."""
   line_info = Namespace()  # TODO(dbieber): Switch to an explicit class.
   line_info.line = line
   line_info.stripped = line.strip()
@@ -506,6 +507,11 @@ def _create_line_info(line, next_line):
   line_info.next.stripped = next_line.strip() if next_line_exists else None
   line_info.next.indentation = (
       len(next_line) - len(next_line.lstrip()) if next_line_exists else None)
+  line_info.previous.line = previous_line
+  previous_line_exists = previous_line is not None
+  line_info.previous.indentation = (
+      len(previous_line) -
+      len(previous_line.lstrip()) if previous_line_exists else None)
   # Note: This counts all whitespace equally.
   return line_info
 
@@ -726,3 +732,29 @@ def _numpy_section(line_info):
     return _section_from_possible_title(possible_title)
   else:
     return None
+
+
+def _line_is_numpy_parameter_type(line_info):
+  """Returns whether the line contains a numpy style parameter type definition.
+
+  We look for a line of the form:
+  x : type
+
+  And we have to exclude false positives on argument descriptions containing a
+  colon by checking the indentation of the line above.
+
+  Args:
+    line_info: Information about the current line.
+  Returns:
+    True if the line is a numpy parameter type definition, False otherwise.
+  """
+  line_stripped = line_info.remaining.strip()
+  if ':' in line_stripped:
+    previous_indent = line_info.previous.indentation
+    current_indent = line_info.indentation
+    if ':' in line_info.previous.line and current_indent > previous_indent:
+      # The parameter type was the previous line; this is the description.
+      return False
+    else:
+      return True
+  return False
diff --git a/fire/docstrings_test.py b/fire/docstrings_test.py
index adb89492..8b1d7685 100644
--- a/fire/docstrings_test.py
+++ b/fire/docstrings_test.py
@@ -253,6 +253,32 @@ def test_strip_blank_lines(self):
 
     self.assertEqual(expected_output, docstrings._strip_blank_lines(lines))  # pylint: disable=protected-access
 
+  def test_numpy_colon_in_description(self):
+    docstring = """
+     Greets name.
+
+     Arguments
+     ---------
+     name : str
+         name, default : World
+     arg2 : int
+         arg2, default:None
+     arg3 : bool
+     """
+    docstring_info = docstrings.parse(docstring)
+    expected_docstring_info = DocstringInfo(
+        summary='Greets name.',
+        description=None,
+        args=[
+            ArgInfo(name='name', type='str',
+                    description='name, default : World'),
+            ArgInfo(name='arg2', type='int',
+                    description='arg2, default:None'),
+            ArgInfo(name='arg3', type='bool', description=None),
+        ]
+    )
+    self.assertEqual(expected_docstring_info, docstring_info)
+
 
 if __name__ == '__main__':
   testutils.main()

From 4de6607fa0c9c077ff31962ae029f97689f946bf Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 28 Feb 2020 14:40:08 -0800
Subject: [PATCH 185/324] Disable pytype in core_test

PiperOrigin-RevId: 297936591
Change-Id: I1fe0379ce4c8a9e09f971a464b010862eada580c
---
 fire/core_test.py | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/fire/core_test.py b/fire/core_test.py
index 97389a2d..84a63864 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -196,13 +196,15 @@ def testClassMethod(self):
 
   @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.')
   def testLruCacheDecoratorBoundArg(self):
-    self.assertEqual(core.Fire(tc.py3.LruCacheDecoratedMethod,
-                               command=['lru_cache_in_class', 'foo']), 'foo')
+    self.assertEqual(
+        core.Fire(tc.py3.LruCacheDecoratedMethod,  # pylint: disable=module-attr
+                  command=['lru_cache_in_class', 'foo']), 'foo')
 
   @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.')
   def testLruCacheDecorator(self):
     self.assertEqual(
-        core.Fire(tc.py3.lru_cache_decorated, command=['foo']), 'foo')
+        core.Fire(tc.py3.lru_cache_decorated,  # pylint: disable=module-attr
+                  command=['foo']), 'foo')
 
 
 if __name__ == '__main__':

From 780a360a6d4231b2fb171c3ae5da4e18bf3f7ccb Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 28 Feb 2020 15:09:13 -0800
Subject: [PATCH 186/324] pytype: disable=module-attr

PiperOrigin-RevId: 297944246
Change-Id: I23e8871ba0a968dbd20325883f2322114c5d8971
---
 fire/core_test.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/fire/core_test.py b/fire/core_test.py
index 84a63864..27c9f418 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -197,13 +197,13 @@ def testClassMethod(self):
   @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.')
   def testLruCacheDecoratorBoundArg(self):
     self.assertEqual(
-        core.Fire(tc.py3.LruCacheDecoratedMethod,  # pylint: disable=module-attr
+        core.Fire(tc.py3.LruCacheDecoratedMethod,  # pytype: disable=module-attr
                   command=['lru_cache_in_class', 'foo']), 'foo')
 
   @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.')
   def testLruCacheDecorator(self):
     self.assertEqual(
-        core.Fire(tc.py3.lru_cache_decorated,  # pylint: disable=module-attr
+        core.Fire(tc.py3.lru_cache_decorated,  # pytype: disable=module-attr
                   command=['foo']), 'foo')
 
 

From afc34abdfb3c1f6ce7de9b9b92e2a07e5a6b4c89 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 28 Feb 2020 23:30:23 +0000
Subject: [PATCH 187/324] Restore support for Python 3.4 and lint for Python
 2.7.

PiperOrigin-RevId: 297948749
Change-Id: I5879e911b0e52404bc2ad7676340ce8d5e7aeae6
---
 fire/inspectutils.py | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index a979b4ea..cf9dae84 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -19,7 +19,9 @@
 from __future__ import print_function
 
 import inspect
+import sys
 import types
+
 from fire import docstrings
 
 import six
@@ -114,6 +116,7 @@ def Py3GetFullArgSpec(fn):
   Returns:
     An inspect.FullArgSpec namedtuple with the full arg spec of the function.
   """
+  # pylint: disable=no-member
   # pytype: disable=module-attr
   try:
     sig = inspect._signature_from_callable(  # pylint: disable=protected-access
@@ -170,6 +173,7 @@ def Py3GetFullArgSpec(fn):
     defaults = None
   return inspect.FullArgSpec(args, varargs, varkw, defaults,
                              kwonlyargs, kwdefaults, annotations)
+  # pylint: enable=no-member
   # pytype: enable=module-attr
 
 
@@ -179,9 +183,12 @@ def GetFullArgSpec(fn):
   fn, skip_arg = _GetArgSpecInfo(fn)
 
   try:
-    if six.PY3:
+    if sys.version_info[0:2] >= (3, 5):
       (args, varargs, varkw, defaults,
        kwonlyargs, kwonlydefaults, annotations) = Py3GetFullArgSpec(fn)
+    elif six.PY3:  # Specifically Python 3.4.
+      (args, varargs, varkw, defaults,
+       kwonlyargs, kwonlydefaults, annotations) = inspect.getfullargspec(fn)  # pylint: disable=deprecated-method,no-member
     else:  # six.PY2
       args, varargs, varkw, defaults = Py2GetArgSpec(fn)
       kwonlyargs = kwonlydefaults = None

From bb90d16e1725fe9679988087f96306ed98b16e5f Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 28 Feb 2020 16:29:03 -0800
Subject: [PATCH 188/324] Use skip_arg for Python 3.4 again.

PiperOrigin-RevId: 297961705
Change-Id: Ia5499dcdf01a55f476ca8fd59cf5ae5fa0f6b294
---
 fire/inspectutils.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index cf9dae84..c7559ae6 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -221,8 +221,9 @@ def GetFullArgSpec(fn):
     # Case 3: Other known slot wrappers do not accept args.
     return FullArgSpec()
 
-  if six.PY2 and skip_arg and args:
-    # In Python 3, Py3GetFullArgSpec uses skip_bound_arg=True already.
+  # In Python 3.5+ Py3GetFullArgSpec uses skip_bound_arg=True already.
+  skip_arg_required = six.PY2 or sys.version_info[0:2] == (3, 4)
+  if skip_arg_required and skip_arg and args:
     args.pop(0)  # Remove 'self' or 'cls' from the list of arguments.
   return FullArgSpec(args, varargs, varkw, defaults,
                      kwonlyargs, kwonlydefaults, annotations)

From b77f5aefbf5986391ab6cb9f51376c6d93d757cf Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 28 Feb 2020 20:04:23 -0800
Subject: [PATCH 189/324] Disable wrapped function test for Python 3.4

PiperOrigin-RevId: 297990663
Change-Id: I0c985fa611d04c764aed34ebfa9af05704d58332
---
 fire/fire_test.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/fire/fire_test.py b/fire/fire_test.py
index 2c7a580b..4f2ffb9b 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -712,7 +712,8 @@ def testClassWithInvalidProperty(self):
         fire.Fire(tc.InvalidProperty, command=['double', '10']), 20
     )
 
-  @testutils.skipIf(six.PY2, 'Cannot inspect wrapped signatures in Python 2.')
+  @testutils.skipIf(sys.version_info[0:2] <= (3, 4),
+                    'Cannot inspect wrapped signatures in Python 2 or 3.4.')
   def testHelpKwargsDecorator(self):
     # Issue #190, follow the wrapped method instead of crashing.
     with self.assertRaisesFireExit(0):

From 4a5b7c348cf3f5cff92093eab647a57d556651be Mon Sep 17 00:00:00 2001
From: Jared Trog <jared.trog@gmail.com>
Date: Fri, 6 Mar 2020 13:08:48 -0800
Subject: [PATCH 190/324] Update formatting to use colorama on windows if
 available.

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/232 from jaredtrog:issue-186 61f67405fd842a52a3ba6353789804713190cb43
PiperOrigin-RevId: 299419699
Change-Id: I72d9ac020e22b9b3c1eadfad466072ce6d2dca3f
---
 fire/formatting.py | 36 ++++++++++++++++++++++++++++++++++++
 1 file changed, 36 insertions(+)

diff --git a/fire/formatting.py b/fire/formatting.py
index 2c7cb335..6c7c253a 100644
--- a/fire/formatting.py
+++ b/fire/formatting.py
@@ -18,10 +18,46 @@
 from __future__ import division
 from __future__ import print_function
 
+import os
+import platform
+import sys
 import termcolor
 
 ELLIPSIS = '...'
 
+# Enable ANSI processing on Windows or disable entirely.
+if sys.platform.startswith('win'):
+  try:
+    import colorama  # pylint: disable=g-import-not-at-top,  # pytype: disable=import-error
+    HAS_COLORAMA = True
+  except ImportError:
+    HAS_COLORAMA = False
+
+  if HAS_COLORAMA:
+    SHOULD_WRAP = True
+    if sys.stdout.isatty() and platform.release() == '10':
+      # Enables native ANSI sequences in console.
+      # Windows 10, 2016, and 2019 only.
+      import ctypes  # pylint: disable=g-import-not-at-top
+      import subprocess  # pylint: disable=g-import-not-at-top
+
+      SHOULD_WRAP = False
+      KERNEL32 = ctypes.windll.kernel32  # pytype: disable=module-attr
+      ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04
+      OUT_HANDLE = KERNEL32.GetStdHandle(subprocess.STD_OUTPUT_HANDLE)  # pytype: disable=module-attr
+      # GetConsoleMode fails if the terminal isn't native.
+      MODE = ctypes.wintypes.DWORD()
+      if KERNEL32.GetConsoleMode(OUT_HANDLE, ctypes.byref(MODE)) == 0:
+        SHOULD_WRAP = True
+      if not MODE.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING:
+        if KERNEL32.SetConsoleMode(
+            OUT_HANDLE, MODE.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0:
+          # kernel32.SetConsoleMode to enable ANSI sequences failed
+          SHOULD_WRAP = True
+    colorama.init(wrap=SHOULD_WRAP)
+  else:
+    os.environ['ANSI_COLORS_DISABLED'] = '1'
+
 
 def Indent(text, spaces=2):
   lines = text.split('\n')

From d2aa542ca79f3c4f6a8e58b73770ca7ff858fc09 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 6 Mar 2020 22:17:08 +0000
Subject: [PATCH 191/324] Separate module for formatting_windows.

PiperOrigin-RevId: 299434969
Change-Id: I071b2dd4e50e504bea3bf60b5599b74b7e230f1e
---
 fire/formatting.py         | 38 ++----------------------
 fire/formatting_windows.py | 60 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 62 insertions(+), 36 deletions(-)
 create mode 100644 fire/formatting_windows.py

diff --git a/fire/formatting.py b/fire/formatting.py
index 6c7c253a..faef8047 100644
--- a/fire/formatting.py
+++ b/fire/formatting.py
@@ -18,45 +18,11 @@
 from __future__ import division
 from __future__ import print_function
 
-import os
-import platform
-import sys
+from fire import formatting_windows  # pylint: disable=unused-import
 import termcolor
 
-ELLIPSIS = '...'
 
-# Enable ANSI processing on Windows or disable entirely.
-if sys.platform.startswith('win'):
-  try:
-    import colorama  # pylint: disable=g-import-not-at-top,  # pytype: disable=import-error
-    HAS_COLORAMA = True
-  except ImportError:
-    HAS_COLORAMA = False
-
-  if HAS_COLORAMA:
-    SHOULD_WRAP = True
-    if sys.stdout.isatty() and platform.release() == '10':
-      # Enables native ANSI sequences in console.
-      # Windows 10, 2016, and 2019 only.
-      import ctypes  # pylint: disable=g-import-not-at-top
-      import subprocess  # pylint: disable=g-import-not-at-top
-
-      SHOULD_WRAP = False
-      KERNEL32 = ctypes.windll.kernel32  # pytype: disable=module-attr
-      ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04
-      OUT_HANDLE = KERNEL32.GetStdHandle(subprocess.STD_OUTPUT_HANDLE)  # pytype: disable=module-attr
-      # GetConsoleMode fails if the terminal isn't native.
-      MODE = ctypes.wintypes.DWORD()
-      if KERNEL32.GetConsoleMode(OUT_HANDLE, ctypes.byref(MODE)) == 0:
-        SHOULD_WRAP = True
-      if not MODE.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING:
-        if KERNEL32.SetConsoleMode(
-            OUT_HANDLE, MODE.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0:
-          # kernel32.SetConsoleMode to enable ANSI sequences failed
-          SHOULD_WRAP = True
-    colorama.init(wrap=SHOULD_WRAP)
-  else:
-    os.environ['ANSI_COLORS_DISABLED'] = '1'
+ELLIPSIS = '...'
 
 
 def Indent(text, spaces=2):
diff --git a/fire/formatting_windows.py b/fire/formatting_windows.py
new file mode 100644
index 00000000..9d7204d7
--- /dev/null
+++ b/fire/formatting_windows.py
@@ -0,0 +1,60 @@
+# 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.
+
+"""This module is used for enabling formatting on Windows."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import ctypes
+import os
+import platform
+import subprocess
+import sys
+
+
+def initialize_or_disable():
+  """Enables ANSI processing on Windows or disables it as needed."""
+  try:
+    import colorama  # pylint: disable=g-import-not-at-top,  # pytype: disable=import-error
+    has_colorama = True
+  except ImportError:
+    has_colorama = False
+
+  if has_colorama:
+    wrap = True
+    if sys.stdout.isatty() and platform.release() == '10':
+      # Enables native ANSI sequences in console.
+      # Windows 10, 2016, and 2019 only.
+
+      wrap = False
+      kernel32 = ctypes.windll.kernel32  # pytype: disable=module-attr
+      enable_virtual_terminal_processing = 0x04
+      out_handle = kernel32.GetStdHandle(subprocess.STD_OUTPUT_HANDLE)  # pylint: disable=line-too-long,  # pytype: disable=module-attr
+      # GetConsoleMode fails if the terminal isn't native.
+      mode = ctypes.wintypes.DWORD()
+      if kernel32.GetConsoleMode(out_handle, ctypes.byref(mode)) == 0:
+        wrap = True
+      if not mode.value & enable_virtual_terminal_processing:
+        if kernel32.SetConsoleMode(
+            out_handle, mode.value | enable_virtual_terminal_processing) == 0:
+          # kernel32.SetConsoleMode to enable ANSI sequences failed
+          wrap = True
+    colorama.init(wrap=wrap)
+  else:
+    os.environ['ANSI_COLORS_DISABLED'] = '1'
+
+if sys.platform.startswith('win'):
+  initialize_or_disable()

From 41297c7b1d77474ddee579e5d4c7dcf38b80bf68 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 6 Mar 2020 22:37:53 +0000
Subject: [PATCH 192/324] Fix typo in HelpText docstring.

PiperOrigin-RevId: 299439548
Change-Id: I7a35a5991da24402e56de17b19754618c9ef4395
---
 fire/helptext.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index fd38cb48..8a1d6881 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -45,7 +45,7 @@
 
 
 def HelpText(component, trace=None, verbose=False):
-  """Gets the help string for the current component, suitalbe for a help screen.
+  """Gets the help string for the current component, suitable for a help screen.
 
   Args:
     component: The component to construct the help string for.

From 0be2244c6a6a900d687d4d0bc2ddfd00acf7b84f Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 6 Mar 2020 15:25:33 -0800
Subject: [PATCH 193/324] Move colorama import to top

PiperOrigin-RevId: 299448843
Change-Id: I398dacae20798132646e5e9d30dc218ea4a24850
---
 fire/formatting_windows.py | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/fire/formatting_windows.py b/fire/formatting_windows.py
index 9d7204d7..2b85820d 100644
--- a/fire/formatting_windows.py
+++ b/fire/formatting_windows.py
@@ -24,16 +24,16 @@
 import subprocess
 import sys
 
+try:
+  import colorama  # pylint: disable=g-import-not-at-top,  # pytype: disable=import-error
+  HAS_COLORAMA = True
+except ImportError:
+  HAS_COLORAMA = False
+
 
 def initialize_or_disable():
   """Enables ANSI processing on Windows or disables it as needed."""
-  try:
-    import colorama  # pylint: disable=g-import-not-at-top,  # pytype: disable=import-error
-    has_colorama = True
-  except ImportError:
-    has_colorama = False
-
-  if has_colorama:
+  if HAS_COLORAMA:
     wrap = True
     if sys.stdout.isatty() and platform.release() == '10':
       # Enables native ANSI sequences in console.

From 598937e0741e1e8e61f7afa75437a5ff860e7386 Mon Sep 17 00:00:00 2001
From: Jeff Tratner <jtratner@counsyl.com>
Date: Fri, 6 Mar 2020 15:35:13 -0800
Subject: [PATCH 194/324] Enable python -m fire <module> <args> to work

Let users run fire as the main function to use fire to create CLIs
around *any* python module. This allows you to wrap third party
libraries with fire without writing any code! Exciting! :D

E.g.:

```
$ python -m fire tempfile mkdtemp
/var/folders/fm/sjgpzb856kld04jvq82bxrcw0000gp/T/tmpCbfg_1
```

or

```
$ python -m fire urllib unquote genetics%3Dawesome%26editor%3Dcrispr
genetics=awesome&editor=crispr
```

This follows normal python import semantics, so if you have a script
file, it'll work if named just as a module:

```
 # myscript.py
def fn1(x, y):
    return x
```

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/110 from jtratner:main-fire dbe70e23a3de6a85f23a509cfa32cca182ac0bb2
PiperOrigin-RevId: 299450705
Change-Id: I9ebead46b141657d826ca81a787e15d6f8d9c9b8
---
 fire/__main__.py  | 34 ++++++++++++++++++++++++++++++++++
 fire/main_test.py | 42 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 76 insertions(+)
 create mode 100644 fire/__main__.py
 create mode 100644 fire/main_test.py

diff --git a/fire/__main__.py b/fire/__main__.py
new file mode 100644
index 00000000..949632ef
--- /dev/null
+++ b/fire/__main__.py
@@ -0,0 +1,34 @@
+# 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.
+
+# pylint: disable=invalid-name
+"""Enables use of Python Fire as a "main" function (i.e. `python -m fire`).
+
+This allows using Fire with third-party libraries without modifying their code.
+"""
+
+import importlib
+import sys
+
+import fire
+
+
+def main(args):
+  module_name = args[1]
+  module = importlib.import_module(module_name)
+  fire.Fire(module, name=module_name, command=args[2:])
+
+
+if __name__ == '__main__':
+  main(sys.argv)
diff --git a/fire/main_test.py b/fire/main_test.py
new file mode 100644
index 00000000..9da8f54f
--- /dev/null
+++ b/fire/main_test.py
@@ -0,0 +1,42 @@
+# 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.
+
+"""Test using Fire via `python -m fire`."""
+
+import os
+
+from fire import __main__
+from fire import testutils
+
+
+class MainModuleTest(testutils.BaseTestCase):
+  """Tests to verify the behavior of __main__ (python -m fire)."""
+
+  def testNameSetting(self):
+    # Confirm one of the usage lines has the gettempdir member.
+    with self.assertOutputMatches('gettempdir'):
+      __main__.main(['__main__.py', 'tempfile'])
+
+  def testArgPassing(self):
+    expected = os.path.join('part1', 'part2', 'part3')
+    with self.assertOutputMatches('%s\n' % expected):
+      __main__.main(['__main__.py', 'os.path', 'join', 'part1', 'part2',
+                     'part3'])
+    with self.assertOutputMatches('%s\n' % expected):
+      __main__.main(['__main__.py', 'os', 'path', '-', 'join', 'part1',
+                     'part2', 'part3'])
+
+
+if __name__ == '__main__':
+  testutils.main()

From 335f6e9ab24b0b14200453d21f936e55c43cafa9 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 6 Mar 2020 15:52:05 -0800
Subject: [PATCH 195/324] Set version number in preparation for release.

PiperOrigin-RevId: 299454075
Change-Id: Id29897414a82d3be7e4990e6b99094a439702eca
---
 fire/__init__.py | 2 +-
 setup.py         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/fire/__init__.py b/fire/__init__.py
index 9eff51c4..b97eeb82 100644
--- a/fire/__init__.py
+++ b/fire/__init__.py
@@ -21,4 +21,4 @@
 from fire.core import Fire
 
 __all__ = ['Fire']
-__version__ = '0.2.2'
+__version__ = '0.3.0'
diff --git a/setup.py b/setup.py
index c0898a1f..8887b7d7 100644
--- a/setup.py
+++ b/setup.py
@@ -40,7 +40,7 @@
     'python-Levenshtein',
 ]
 
-VERSION = '0.2.2'
+VERSION = '0.3.0'
 URL = 'https://github.com/google/python-fire'
 
 setup(

From cd008fb42bcfec5daf4215105a92e95a40dde12b Mon Sep 17 00:00:00 2001
From: Daiki Katsuragawa <50144563+daikikatsuragawa@users.noreply.github.com>
Date: Mon, 9 Mar 2020 11:16:29 -0700
Subject: [PATCH 196/324] Update README.md formatting

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/233 from daikikatsuragawa:master 5150b412a88af0d75b56188ffd30d4efa30817ef
PiperOrigin-RevId: 299886851
Change-Id: Idf29b844e05b3caaeb07b1e205a93eb9b199d326
---
 README.md     | 22 +++++++++-------------
 docs/index.md | 22 +++++++++-------------
 2 files changed, 18 insertions(+), 26 deletions(-)

diff --git a/README.md b/README.md
index 862f5f23..eeb7dc37 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
 # Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/fire.svg?style=plastic)](https://github.com/google/python-fire)
+
 _Python Fire is a library for automatically generating command line interfaces
 (CLIs) from absolutely any Python object._
 
@@ -10,7 +11,6 @@ into a CLI. [[3]](docs/benefits.md#exploring)
 - Python Fire makes using a Python REPL easier by setting up the REPL with the
 modules and variables you'll need already imported and created. [[5]](docs/benefits.md#repl)
 
-
 ## Installation
 
 To install Python Fire with pip, run: `pip install fire`
@@ -20,7 +20,6 @@ To install Python Fire with conda, run: `conda install fire -c conda-forge`
 To install Python Fire from source, first clone the repository and then run:
 `python setup.py install`
 
-
 ## Basic Usage
 
 You can call `Fire` on any Python object:<br>
@@ -74,17 +73,14 @@ about Fire's other features, see the [Using a Fire CLI page](docs/using-cli.md).
 
 For additional examples, see [The Python Fire Guide](docs/guide.md).
 
-
 ## Why is it called Fire?
 
 When you call `Fire`, it fires off (executes) your command.
 
-
 ## Where can I learn more?
 
 Please see [The Python Fire Guide](docs/guide.md).
 
-
 ## Reference
 
 | Setup   | Command             | Notes
@@ -97,14 +93,14 @@ Please see [The Python Fire Guide](docs/guide.md).
 | Call           | `fire.Fire()`          | Turns the current module into a Fire CLI.
 | Call           | `fire.Fire(component)` | Turns `component` into a Fire CLI.
 
-Using a CLI                                     | Command                                 | Notes
-:---------------------------------------------- | :-------------------------------------- | :----
-[Help](docs/using-cli.md#help-flag)             | `command --help` or `command -- --help` |
-[REPL](docs/using-cli.md#interactive-flag)      | `command -- --interactive`              | Enters interactive mode.
-[Separator](docs/using-cli.md#separator-flag)   | `command -- --separator=X`              | Sets the separator to `X`. The default separator is `-`.
-[Completion](docs/using-cli.md#completion-flag) | `command -- --completion [shell]`       | Generates a completion script for the CLI.
-[Trace](docs/using-cli.md#trace-flag)           | `command -- --trace`                    | Gets a Fire trace for the command.
-[Verbose](docs/using-cli.md#verbose-flag)       | `command -- --verbose`                  |
+| Using a CLI                                     | Command                                 | Notes
+| :---------------------------------------------- | :-------------------------------------- | :----
+| [Help](docs/using-cli.md#help-flag)             | `command --help` or `command -- --help` |
+| [REPL](docs/using-cli.md#interactive-flag)      | `command -- --interactive`              | Enters interactive mode.
+| [Separator](docs/using-cli.md#separator-flag)   | `command -- --separator=X`              | Sets the separator to `X`. The default separator is `-`.
+| [Completion](docs/using-cli.md#completion-flag) | `command -- --completion [shell]`       | Generates a completion script for the CLI.
+| [Trace](docs/using-cli.md#trace-flag)           | `command -- --trace`                    | Gets a Fire trace for the command.
+| [Verbose](docs/using-cli.md#verbose-flag)       | `command -- --verbose`                  |
 
 _Note that these flags are separated from the Fire command by an isolated `--`._
 
diff --git a/docs/index.md b/docs/index.md
index 171ae26e..7dcbb0f1 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,4 +1,5 @@
 # Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/fire.svg?style=plastic)](https://github.com/google/python-fire)
+
 _Python Fire is a library for automatically generating command line interfaces
 (CLIs) from absolutely any Python object._
 
@@ -10,7 +11,6 @@ into a CLI. [[3]](benefits.md#exploring)
 - Python Fire makes using a Python REPL easier by setting up the REPL with the
 modules and variables you'll need already imported and created. [[5]](benefits.md#repl)
 
-
 ## Installation
 
 To install Python Fire with pip, run: `pip install fire`
@@ -20,7 +20,6 @@ To install Python Fire with conda, run: `conda install fire -c conda-forge`
 To install Python Fire from source, first clone the repository and then run:
 `python setup.py install`
 
-
 ## Basic Usage
 
 You can call `Fire` on any Python object:<br>
@@ -74,17 +73,14 @@ about Fire's other features, see the [Using a Fire CLI page](using-cli.md).
 
 For additional examples, see [The Python Fire Guide](guide.md).
 
-
 ## Why is it called Fire?
 
 When you call `Fire`, it fires off (executes) your command.
 
-
 ## Where can I learn more?
 
 Please see [The Python Fire Guide](guide.md).
 
-
 ## Reference
 
 | Setup   | Command             | Notes
@@ -97,14 +93,14 @@ Please see [The Python Fire Guide](guide.md).
 | Call           | `fire.Fire()`          | Turns the current module into a Fire CLI.
 | Call           | `fire.Fire(component)` | Turns `component` into a Fire CLI.
 
-Using a CLI                                     | Command                                 | Notes
-:---------------------------------------------- | :-------------------------------------- | :----
-[Help](using-cli.md#help-flag)             | `command --help` or `command -- --help` |
-[REPL](using-cli.md#interactive-flag)      | `command -- --interactive`              | Enters interactive mode.
-[Separator](using-cli.md#separator-flag)   | `command -- --separator=X`              | Sets the separator to `X`. The default separator is `-`.
-[Completion](using-cli.md#completion-flag) | `command -- --completion [shell]`       | Generates a completion script for the CLI.
-[Trace](using-cli.md#trace-flag)           | `command -- --trace`                    | Gets a Fire trace for the command.
-[Verbose](using-cli.md#verbose-flag)       | `command -- --verbose`                  |
+| Using a CLI                                     | Command                                 | Notes
+| :---------------------------------------------- | :-------------------------------------- | :----
+| [Help](using-cli.md#help-flag)             | `command --help` or `command -- --help` |
+| [REPL](using-cli.md#interactive-flag)      | `command -- --interactive`              | Enters interactive mode.
+| [Separator](using-cli.md#separator-flag)   | `command -- --separator=X`              | Sets the separator to `X`. The default separator is `-`.
+| [Completion](using-cli.md#completion-flag) | `command -- --completion [shell]`       | Generates a completion script for the CLI.
+| [Trace](using-cli.md#trace-flag)           | `command -- --trace`                    | Gets a Fire trace for the command.
+| [Verbose](using-cli.md#verbose-flag)       | `command -- --verbose`                  |
 
 _Note that these flags are separated from the Fire command by an isolated `--`._
 

From aa72776fdeff7ea0dbfe67606bcf89545ef81027 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 13 Mar 2020 11:49:37 -0700
Subject: [PATCH 197/324] Documentation updates for '--help' and 'python -m
 fire'

PiperOrigin-RevId: 300798279
Change-Id: I21bfe6fe5596546b092c2968c9234febf357d4ba
---
 README.md     | 18 +++++++++------
 docs/api.md   | 63 +++++++++++++++++++++++++++++++++++++++++++--------
 docs/guide.md | 21 +++++++++++++++--
 docs/index.md | 18 +++++++++------
 4 files changed, 95 insertions(+), 25 deletions(-)

diff --git a/README.md b/README.md
index eeb7dc37..1482d56d 100644
--- a/README.md
+++ b/README.md
@@ -3,13 +3,17 @@
 _Python Fire is a library for automatically generating command line interfaces
 (CLIs) from absolutely any Python object._
 
-- Python Fire is a simple way to create a CLI in Python. [[1]](docs/benefits.md#simple-cli)
-- Python Fire is a helpful tool for developing and debugging Python code. [[2]](docs/benefits.md#debugging)
-- Python Fire helps with exploring existing code or turning other people's code
-into a CLI. [[3]](docs/benefits.md#exploring)
-- Python Fire makes transitioning between Bash and Python easier. [[4]](docs/benefits.md#bash)
-- Python Fire makes using a Python REPL easier by setting up the REPL with the
-modules and variables you'll need already imported and created. [[5]](docs/benefits.md#repl)
+-   Python Fire is a simple way to create a CLI in Python.
+    [[1]](docs/benefits.md#simple-cli)
+-   Python Fire is a helpful tool for developing and debugging Python code.
+    [[2]](docs/benefits.md#debugging)
+-   Python Fire helps with exploring existing code or turning other people's
+    code into a CLI. [[3]](docs/benefits.md#exploring)
+-   Python Fire makes transitioning between Bash and Python easier.
+    [[4]](docs/benefits.md#bash)
+-   Python Fire makes using a Python REPL easier by setting up the REPL with the
+    modules and variables you'll need already imported and created.
+    [[5]](docs/benefits.md#repl)
 
 ## Installation
 
diff --git a/docs/api.md b/docs/api.md
index c3bb2ef6..aa918160 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -1,3 +1,5 @@
+## Python Fire Quick Reference
+
 | Setup   | Command             | Notes
 | :------ | :------------------ | :---------
 | install | `pip install fire`  |
@@ -8,13 +10,56 @@
 | Call           | `fire.Fire()`          | Turns the current module into a Fire CLI.
 | Call           | `fire.Fire(component)` | Turns `component` into a Fire CLI.
 
-| Using a CLI    | Command                    | Notes
-| :------------- | :------------------------- | :---------
-| [Help](using-cli.md#help-flag) | `command -- --help` |
-| [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
-| [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
-| [Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI.
-| [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
-| [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` |
+| Using a CLI                                | Command        | Notes          |
+| :----------------------------------------- | :------------- | :------------- |
+| [Help](using-cli.md#help-flag)             | `command       | Show the help  |
+:                                            : --help`        : screen.        :
+| [REPL](using-cli.md#interactive-flag)      | `command --    | Enters         |
+:                                            : --interactive` : interactive    :
+:                                            :                : mode.          :
+| [Separator](using-cli.md#separator-flag)   | `command --    | This sets the  |
+:                                            : --separator=X` : separator to   :
+:                                            :                : `X`. The       :
+:                                            :                : default        :
+:                                            :                : separator is   :
+:                                            :                : `-`.           :
+| [Completion](using-cli.md#completion-flag) | `command --    | Generate a     |
+:                                            : --completion   : completion     :
+:                                            : [shell]`       : script for the :
+:                                            :                : CLI.           :
+| [Trace](using-cli.md#trace-flag)           | `command --    | Gets a Fire    |
+:                                            : --trace`       : trace for the  :
+:                                            :                : command.       :
+| [Verbose](using-cli.md#verbose-flag)       | `command --    |                |
+:                                            : --verbose`     :                :
+
+_Note that flags are separated from the Fire command by an isolated `--` arg.
+Help is an exception; the isolated `--` is optional for getting help._
+
+## Arguments for Calling fire.Fire()
+
+| Argument  | Usage                     | Notes                                |
+| :-------- | :------------------------ | :----------------------------------- |
+| component | `fire.Fire(component)`    | If omitted, defaults to a dict of    |
+:           :                           : all locals and globals.              :
+| command   | `fire.Fire(command='hello | Either a string or a list of         |
+:           : --name=5')`               : arguments. If a string is provided,  :
+:           :                           : it is split to determine the         :
+:           :                           : arguments. If a list or tuple is     :
+:           :                           : provided, they are the arguments. If :
+:           :                           : `command` is omitted, then           :
+:           :                           : `sys.argv[1\:]` (the arguments from  :
+:           :                           : the command line) are used by        :
+:           :                           : default.                             :
+| name      | `fire.Fire(name='tool')`  | The name of the CLI, ideally the     |
+:           :                           : name users will enter to run the     :
+:           :                           : CLI. This name will be used in the   :
+:           :                           : CLI's help screens. If the argument  :
+:           :                           : is omitted, it will be inferred      :
+:           :                           : automatically.                       :
+
+## Using a Fire CLI without modifying any code
+
+`python -m fire <module> <arguments>`
 
-_Note that flags are separated from the Fire command by an isolated `--` arg._
+For example, `python -m fire calendar -h`.
diff --git a/docs/guide.md b/docs/guide.md
index b02f37b5..5f693a40 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -99,6 +99,22 @@ def main():
   fire.Fire(hello)
 ```
 
+##### Version 4: Fire Without Code Changes
+
+If you have a file `example.py` that doesn't even import fire:
+
+```python
+def hello(name):
+  return 'Hello {name}!'.format(name=name)
+```
+
+Then you can use it with Fire like this:
+
+```bash
+$ python -m fire example hello --name=World
+Hello World!
+```
+
 ### Exposing Multiple Commands
 
 In the previous example, we exposed a single function to the command line. Now
@@ -685,8 +701,9 @@ You can add the help flag to any command to see help and usage information. Fire
 incorporates your docstrings into the help and usage information that it
 generates. Fire will try to provide help even if you omit the isolated `--`
 separating the flags from the Fire command, but may not always be able to, since
-`help` is a valid argument name. Use this feature like this:
-`python example.py -- --help`.
+`help` is a valid argument name. Use this feature like this: `python
+example.py -- --help` or `python example.py --help` (or even `python example.py
+-h`).
 
 The complete set of flags available is shown below, in the reference section.
 
diff --git a/docs/index.md b/docs/index.md
index 7dcbb0f1..4eb114b1 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -3,13 +3,17 @@
 _Python Fire is a library for automatically generating command line interfaces
 (CLIs) from absolutely any Python object._
 
-- Python Fire is a simple way to create a CLI in Python. [[1]](benefits.md#simple-cli)
-- Python Fire is a helpful tool for developing and debugging Python code. [[2]](benefits.md#debugging)
-- Python Fire helps with exploring existing code or turning other people's code
-into a CLI. [[3]](benefits.md#exploring)
-- Python Fire makes transitioning between Bash and Python easier. [[4]](benefits.md#bash)
-- Python Fire makes using a Python REPL easier by setting up the REPL with the
-modules and variables you'll need already imported and created. [[5]](benefits.md#repl)
+-   Python Fire is a simple way to create a CLI in Python.
+    [[1]](benefits.md#simple-cli)
+-   Python Fire is a helpful tool for developing and debugging Python code.
+    [[2]](benefits.md#debugging)
+-   Python Fire helps with exploring existing code or turning other people's
+    code into a CLI. [[3]](benefits.md#exploring)
+-   Python Fire makes transitioning between Bash and Python easier.
+    [[4]](benefits.md#bash)
+-   Python Fire makes using a Python REPL easier by setting up the REPL with the
+    modules and variables you'll need already imported and created.
+    [[5]](benefits.md#repl)
 
 ## Installation
 

From b97816196071901b714eb70258a6818a59782379 Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Mon, 16 Mar 2020 17:21:33 +0000
Subject: [PATCH 198/324] Fix the issue where keyword only arguments with
 default value is incorrectly marked as required.

PiperOrigin-RevId: 301185283
Change-Id: I0d7e2df1d5411769db6250f819f776c955c788f9
---
 fire/helptext.py            | 31 ++++++++++++++++++++++---------
 fire/helptext_test.py       | 22 ++++++++++++++++++++++
 fire/test_components_py3.py |  3 +++
 3 files changed, 47 insertions(+), 9 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index 8a1d6881..1d165b37 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -33,6 +33,8 @@
 from __future__ import division
 from __future__ import print_function
 
+import itertools
+
 from fire import completion
 from fire import custom_descriptions
 from fire import decorators
@@ -167,6 +169,11 @@ def _DescriptionSection(component, info):
     return None
 
 
+def _CreateKeywordOnlyFlagItem(flag, docstring_info, spec):
+  return _CreateFlagItem(
+      flag, docstring_info, required=flag not in spec.kwonlydefaults)
+
+
 def _ArgsAndFlagsSections(info, spec, metadata):
   """The "Args and Flags" sections of the help string."""
   args_with_no_defaults = spec.args[:len(spec.args) - len(spec.defaults)]
@@ -199,15 +206,15 @@ def _ArgsAndFlagsSections(info, spec, metadata):
           ('NOTES', 'You can also use flags syntax for POSITIONAL ARGUMENTS')
       )
 
-  optional_flag_items = [
+  positional_flag_items = [
       _CreateFlagItem(flag, docstring_info, required=False)
       for flag in args_with_defaults
   ]
-  required_flag_items = [
-      _CreateFlagItem(flag, docstring_info, required=True)
+  kwonly_flag_items = [
+      _CreateKeywordOnlyFlagItem(flag, docstring_info, spec)
       for flag in spec.kwonlyargs
   ]
-  flag_items = optional_flag_items + required_flag_items
+  flag_items = positional_flag_items + kwonly_flag_items
 
   if spec.varkw:
     description = _GetArgDescription(spec.varkw, docstring_info)
@@ -382,9 +389,7 @@ def _CreateFlagItem(flag, docstring_info, required=False):
     flag: The name of the flag.
     docstring_info: A docstrings.DocstringInfo namedtuple with information about
       the containing function's docstring.
-    required: Whether the flag is required. Keyword-only arguments (only in
-      Python 3) become required flags, whereas normal keyword arguments become
-      optional flags.
+    required: Whether the flag is required.
   Returns:
     A string to be used in constructing the help screen for the function.
   """
@@ -570,13 +575,21 @@ def _GetCallableUsageItems(spec, metadata):
   return items
 
 
+def _KeywordOnlyArguments(spec, required=True):
+  return (flag for flag in spec.kwonlyargs
+          if required == (flag in spec.kwonlydefaults))
+
+
 def _GetCallableAvailabilityLines(spec):
   """The list of availability lines for a callable for use in a usage string."""
   args_with_defaults = spec.args[len(spec.args) - len(spec.defaults):]
 
   # TODO(dbieber): Handle args_with_no_defaults if not accepts_positional_args.
-  optional_flags = [('--' + flag) for flag in args_with_defaults]
-  required_flags = [('--' + flag) for flag in spec.kwonlyargs]
+  optional_flags = [('--' + flag) for flag in itertools.chain(
+      args_with_defaults, _KeywordOnlyArguments(spec, required=False))]
+  required_flags = [
+      ('--' + flag) for flag in _KeywordOnlyArguments(spec, required=True)
+  ]
 
   # Flags section:
   availability_lines = []
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index a866ee4d..00e88c2d 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -20,12 +20,14 @@
 
 import os
 import textwrap
+import unittest
 
 from fire import formatting
 from fire import helptext
 from fire import test_components as tc
 from fire import testutils
 from fire import trace
+import six
 
 
 class HelpTest(testutils.BaseTestCase):
@@ -158,6 +160,26 @@ def testHelpTextNoInit(self):
     self.assertIn('NAME\n    OldStyleEmpty', help_screen)
     self.assertIn('SYNOPSIS\n    OldStyleEmpty', help_screen)
 
+  @unittest.skipIf(
+      six.PY2,
+      'Python 2 does not support single asterisk in function definition')
+  def testHelpTextKeywordOnlyArgumentsWithDefault(self):
+    component = tc.py3.KeywordOnly.with_default
+    output = helptext.HelpText(
+        component=component, trace=trace.FireTrace(component, 'with_default'))
+    self.assertIn('NAME\n    with_default', output)
+    self.assertIn('FLAGS\n    --x=X', output)
+
+  @unittest.skipIf(
+      six.PY2,
+      'Python 2 does not support single asterisk in function definition')
+  def testHelpTextKeywordOnlyArgumentsWithoutDefault(self):
+    component = tc.py3.KeywordOnly.double
+    output = helptext.HelpText(
+        component=component, trace=trace.FireTrace(component, 'double'))
+    self.assertIn('NAME\n    double', output)
+    self.assertIn('FLAGS\n    --count=COUNT (required)', output)
+
   def testHelpScreen(self):
     component = tc.ClassWithDocstring()
     t = trace.FireTrace(component, name='ClassWithDocstring')
diff --git a/fire/test_components_py3.py b/fire/test_components_py3.py
index e714be30..dc37c3b1 100644
--- a/fire/test_components_py3.py
+++ b/fire/test_components_py3.py
@@ -32,6 +32,9 @@ def double(self, *, count):
   def triple(self, *, count):
     return count * 3
 
+  def with_default(self, *, x="x"):
+    print("x: " + x)
+
 
 class LruCacheDecoratedMethod(object):
 

From 61785e614c9ee1f0f16f8d8f1fd18b92a1981b4a Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 19 Mar 2020 12:47:16 -0700
Subject: [PATCH 199/324] Switch from unittest to testutils.

PiperOrigin-RevId: 301875411
Change-Id: Ie5df8560ce1a17f3cbf30b8087fb807398eb5ee0
---
 fire/helptext_test.py | 15 ++++++---------
 1 file changed, 6 insertions(+), 9 deletions(-)

diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 00e88c2d..3619fb06 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -20,7 +20,6 @@
 
 import os
 import textwrap
-import unittest
 
 from fire import formatting
 from fire import helptext
@@ -160,21 +159,19 @@ def testHelpTextNoInit(self):
     self.assertIn('NAME\n    OldStyleEmpty', help_screen)
     self.assertIn('SYNOPSIS\n    OldStyleEmpty', help_screen)
 
-  @unittest.skipIf(
-      six.PY2,
-      'Python 2 does not support single asterisk in function definition')
+  @testutils.skipIf(
+      six.PY2, 'Python 2 does not support keyword-only arguments.')
   def testHelpTextKeywordOnlyArgumentsWithDefault(self):
-    component = tc.py3.KeywordOnly.with_default
+    component = tc.py3.KeywordOnly.with_default  # pytype: disable=module-attr
     output = helptext.HelpText(
         component=component, trace=trace.FireTrace(component, 'with_default'))
     self.assertIn('NAME\n    with_default', output)
     self.assertIn('FLAGS\n    --x=X', output)
 
-  @unittest.skipIf(
-      six.PY2,
-      'Python 2 does not support single asterisk in function definition')
+  @testutils.skipIf(
+      six.PY2, 'Python 2 does not support keyword-only arguments.')
   def testHelpTextKeywordOnlyArgumentsWithoutDefault(self):
-    component = tc.py3.KeywordOnly.double
+    component = tc.py3.KeywordOnly.double  # pytype: disable=module-attr
     output = helptext.HelpText(
         component=component, trace=trace.FireTrace(component, 'double'))
     self.assertIn('NAME\n    double', output)

From af9d8488d595e71a73b95373e5a23cdf1ba54f2e Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 3 Apr 2020 17:41:42 +0000
Subject: [PATCH 200/324] Remove preexec_fn from subprocess call in
 console_io.py

PiperOrigin-RevId: 304646913
Change-Id: I16b76e90490984fd9f751823bf1304d75eb087a0
---
 fire/console/console_io.py | 16 ++++------------
 1 file changed, 4 insertions(+), 12 deletions(-)

diff --git a/fire/console/console_io.py b/fire/console/console_io.py
index 784369b7..3d3b9f81 100644
--- a/fire/console/console_io.py
+++ b/fire/console/console_io.py
@@ -69,10 +69,6 @@ def IsInteractive(output=False, error=False, heuristic=False):
   return True
 
 
-def PreexecFunc():
-  signal.signal(signal.SIGINT, signal.SIG_IGN)
-
-
 def More(contents, out, prompt=None, check_pager=True):
   """Run a user specified pager or fall back to the internal pager.
 
@@ -102,18 +98,14 @@ def More(contents, out, prompt=None, check_pager=True):
       less_orig = encoding.GetEncodedValue(os.environ, 'LESS', None)
       less = '-R' + (less_orig or '')
       encoding.SetEncodedValue(os.environ, 'LESS', less)
-      # Ignores SIGINT from this point on since the child process has started
-      # and we don't want to terminate either one when the child is still alive.
+      # Ignore SIGINT while the pager is running.
+      # We don't want to terminate the parent while the child is still alive.
       signal.signal(signal.SIGINT, signal.SIG_IGN)
-      # Runs PreexecFunc before starting the child so SIGINT is ignored for the
-      # child process as well.
-      p = subprocess.Popen(
-          pager, stdin=subprocess.PIPE, shell=True, preexec_fn=PreexecFunc)
+      p = subprocess.Popen(pager, stdin=subprocess.PIPE, shell=True)
       enc = console_attr.GetConsoleAttr().GetEncoding()
       p.communicate(input=contents.encode(enc))
       p.wait()
-      # Starts using default disposition for SIGINT again after the child has
-      # exited.
+      # Start using default signal handling for SIGINT again.
       signal.signal(signal.SIGINT, signal.SIG_DFL)
       if less_orig is None:
         encoding.SetEncodedValue(os.environ, 'LESS', None)

From fb7ee3a716020f6a04c0e55967612f9322d16893 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 3 Apr 2020 11:04:51 -0700
Subject: [PATCH 201/324] Bump version number to 0.3.1.

PiperOrigin-RevId: 304652311
Change-Id: I7e7e7c1d8cce1eb2c9dd59130b97ccc873b537fb
---
 fire/__init__.py | 2 +-
 setup.py         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/fire/__init__.py b/fire/__init__.py
index b97eeb82..2bacc8fe 100644
--- a/fire/__init__.py
+++ b/fire/__init__.py
@@ -21,4 +21,4 @@
 from fire.core import Fire
 
 __all__ = ['Fire']
-__version__ = '0.3.0'
+__version__ = '0.3.1'
diff --git a/setup.py b/setup.py
index 8887b7d7..0047fa95 100644
--- a/setup.py
+++ b/setup.py
@@ -40,7 +40,7 @@
     'python-Levenshtein',
 ]
 
-VERSION = '0.3.0'
+VERSION = '0.3.1'
 URL = 'https://github.com/google/python-fire'
 
 setup(

From 457b1562b88ae8f018c7b2bed46028035a75cccf Mon Sep 17 00:00:00 2001
From: uburuntu <bekbulatov.ramzan@ya.ru>
Date: Fri, 12 Jun 2020 10:51:40 -0700
Subject: [PATCH 202/324] Support for Python 3.8

- Enables Travis for 3.8
- Fixes namedtuple test for 3.8

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/263 from uburuntu:master efaf65fe5b0986121f0e1b76a949a07169dd354f
PiperOrigin-RevId: 316134191
Change-Id: I5e3ef20b488fa917b81525d5936a8e147b0458e1
---
 .travis.yml        | 11 +++--------
 fire/completion.py | 14 ++++++++++----
 setup.py           |  1 +
 3 files changed, 14 insertions(+), 12 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 16246cb8..30f0cad6 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,14 +3,9 @@ python:
   - "2.7"
   - "3.4"
   - "3.5"
-  - "3.6"
-# Workaround for testing Python 3.7:
-# https://github.com/travis-ci/travis-ci/issues/9815
-matrix:
-  include:
-    - python: 3.7
-      dist: xenial
-      sudo: yes
+  - "3.7"
+  - "3.8"
+
 before_install:
   - pip install --upgrade setuptools pip
   - pip install --upgrade pylint pytest pytest-pylint pytest-runner
diff --git a/fire/completion.py b/fire/completion.py
index 451e2021..2c9f15c0 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -320,10 +320,16 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False):
     if class_attrs is None:
       class_attrs = inspectutils.GetClassAttrsDict(class_attrs) or {}
     class_attr = class_attrs.get(name)
-    if class_attr and class_attr.kind in ('method', 'property'):
-      # methods and properties should be accessed on instantiated objects,
-      # not uninstantiated classes.
-      return False
+    if class_attr:
+      # Methods and properties should only be accessible on instantiated
+      # objects, not on uninstantiated classes.
+      if class_attr.kind in ('method', 'property'):
+        return False
+      # Backward compatibility notes: Before Python 3.8, namedtuple attributes
+      # were properties. In Python 3.8, they have type tuplegetter.
+      tuplegetter = getattr(collections, '_tuplegetter', type(None))
+      if isinstance(class_attr.object, tuplegetter):
+        return False
   if (six.PY2 and inspect.isfunction(component)
       and name in ('func_closure', 'func_code', 'func_defaults',
                    'func_dict', 'func_doc', 'func_globals', 'func_name')):
diff --git a/setup.py b/setup.py
index 0047fa95..efd3f766 100644
--- a/setup.py
+++ b/setup.py
@@ -70,6 +70,7 @@
         'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: 3.7',
+        'Programming Language :: Python :: 3.8',
 
         'Operating System :: OS Independent',
         'Operating System :: POSIX',

From 79d85df2706b2dab5dd5075f2a76953743ee9bf3 Mon Sep 17 00:00:00 2001
From: Michael Garbutt <michael.c.garbutt@gmail.com>
Date: Tue, 14 Jul 2020 09:00:56 -0700
Subject: [PATCH 203/324] Added types and defaults to function help text.

PiperOrigin-RevId: 321168198
Change-Id: I228b706f9db615ac10ad0865b9c5cc82107f3fb7
---
 fire/helptext.py            | 122 ++++++++++++++++++++++++++++++++----
 fire/helptext_test.py       |  92 ++++++++++++++++++++++++++-
 fire/test_components.py     |   9 ++-
 fire/test_components_py3.py |  41 ++++++++++++
 setup.cfg                   |   3 +-
 5 files changed, 252 insertions(+), 15 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index 1d165b37..ecb6bbb4 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -34,6 +34,7 @@
 from __future__ import print_function
 
 import itertools
+import sys
 
 from fire import completion
 from fire import custom_descriptions
@@ -44,6 +45,7 @@
 
 LINE_LENGTH = 80
 SECTION_INDENTATION = 4
+SUBSECTION_INDENTATION = 4
 
 
 def HelpText(component, trace=None, verbose=False):
@@ -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):
@@ -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:
@@ -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 = [
@@ -365,43 +367,139 @@ 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.
   """
+  # pylint: disable=g-bad-todo
+  # TODO(MichaelCG8): Get type and default information from docstrings if it is
+  # not available in FullArgSpec. This will require updating
+  # fire.docstrings.parser().
+
+  # 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):
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 3619fb06..d9a4dab8 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -19,6 +19,7 @@
 from __future__ import print_function
 
 import os
+import sys
 import textwrap
 
 from fire import formatting
@@ -80,9 +81,97 @@ 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.py3.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.py3.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.py3.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.py3.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)
+    # TODO(dbieber): Assert type is displayed correctly. Type displays
+    # differently in Travis vs in Google.
+    # 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(
@@ -244,6 +333,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())
diff --git a/fire/test_components.py b/fire/test_components.py
index a25ba7af..824e73fc 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -19,9 +19,9 @@
 from __future__ import print_function
 
 import collections
+import enum
 import functools
 
-import enum
 import six
 
 if six.PY3:
@@ -105,6 +105,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
 
diff --git a/fire/test_components_py3.py b/fire/test_components_py3.py
index dc37c3b1..56f64356 100644
--- a/fire/test_components_py3.py
+++ b/fire/test_components_py3.py
@@ -16,6 +16,7 @@
 """This module has components that use Python 3 specific syntax."""
 
 import functools
+from typing import Tuple
 
 
 # pylint: disable=keyword-arg-before-vararg
@@ -46,3 +47,43 @@ def lru_cache_in_class(self, arg1):
 @functools.lru_cache()
 def lru_cache_decorated(arg1):
   return arg1
+
+
+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
diff --git a/setup.cfg b/setup.cfg
index 058f329c..7a980136 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -8,7 +8,8 @@ universal = 1
 test = pytest
 
 [tool:pytest]
-addopts = --ignore=fire/test_components_py3.py --ignore=fire/parser_fuzz_test.py
+addopts = --ignore=fire/test_components_py3.py
+          --ignore=fire/parser_fuzz_test.py
 
 [pytype]
 inputs = .

From b5d6341d3034efc594421783d45620551c286a37 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Tue, 14 Jul 2020 11:01:11 -0700
Subject: [PATCH 204/324] Add module-attr disable hints.

PiperOrigin-RevId: 321192380
Change-Id: Ifead01df416bb2fbdd6cc220efd948f237c2120b
---
 fire/helptext_test.py | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index d9a4dab8..1ec16295 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -103,7 +103,8 @@ def testHelpTextFunctionWithLongDefaults(self):
       sys.version_info[0:2] < (3, 5),
       'Python < 3.5 does not support type hints.')
   def testHelpTextFunctionWithDefaultsAndTypes(self):
-    component = tc.py3.WithDefaultsAndTypes().double
+    component = (
+        tc.py3.WithDefaultsAndTypes().double)  # pytype: disable=module-attr
     help_screen = helptext.HelpText(
         component=component,
         trace=trace.FireTrace(component, name='double'))
@@ -119,7 +120,8 @@ def testHelpTextFunctionWithDefaultsAndTypes(self):
       sys.version_info[0:2] < (3, 5),
       'Python < 3.5 does not support type hints.')
   def testHelpTextFunctionWithTypesAndDefaultNone(self):
-    component = tc.py3.WithDefaultsAndTypes().get_int
+    component = (
+        tc.py3.WithDefaultsAndTypes().get_int)  # pytype: disable=module-attr
     help_screen = helptext.HelpText(
         component=component,
         trace=trace.FireTrace(component, name='get_int'))
@@ -136,7 +138,7 @@ def testHelpTextFunctionWithTypesAndDefaultNone(self):
       sys.version_info[0:2] < (3, 5),
       'Python < 3.5 does not support type hints.')
   def testHelpTextFunctionWithTypes(self):
-    component = tc.py3.WithTypes().double
+    component = tc.py3.WithTypes().double  # pytype: disable=module-attr
     help_screen = helptext.HelpText(
         component=component,
         trace=trace.FireTrace(component, name='double'))
@@ -154,7 +156,7 @@ def testHelpTextFunctionWithTypes(self):
       sys.version_info[0:2] < (3, 5),
       'Python < 3.5 does not support type hints.')
   def testHelpTextFunctionWithLongTypes(self):
-    component = tc.py3.WithTypes().long_type
+    component = tc.py3.WithTypes().long_type  # pytype: disable=module-attr
     help_screen = helptext.HelpText(
         component=component,
         trace=trace.FireTrace(component, name='long_type'))

From c3789064e2e161992e3c5a23eb0c47b66072707f Mon Sep 17 00:00:00 2001
From: Bradley D'Amato <53579156+bradleydamato@users.noreply.github.com>
Date: Fri, 2 Oct 2020 11:15:43 -0700
Subject: [PATCH 205/324] Change member future check to fix issue 272
 bb58f2611469851d842f95a65da1bf926c0c85c3 by bradleydamato
 <bradleydamato@gmail.com> ebc446fa79702ce532fb2e0355b69335e6ef027a by
 bradleydamato <bradleydamato@gmail.com>
 98b131f15d2f9540628942f975699a00a89d0c87 by bradleydamato
 <bradleydamato@gmail.com>

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/290 from bradleydamato:Issue272Fix 98b131f15d2f9540628942f975699a00a89d0c87
PiperOrigin-RevId: 335063289
Change-Id: I1715092509aad11b0bbb681b76a1db7ab48e9a78
---
 fire/completion.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/fire/completion.py b/fire/completion.py
index 2c9f15c0..ed7a1b61 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -308,7 +308,9 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False):
     return False
   if verbose:
     return True
-  if member in (absolute_import, division, print_function):
+  if (member is absolute_import
+      or member is division
+      or member is print_function):
     return False
   if isinstance(member, type(absolute_import)) and six.PY34:
     return False

From 7d5011ad0562966ee470a44cf0abcfee05825295 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 2 Oct 2020 13:40:56 -0700
Subject: [PATCH 206/324] disable lint checks super-with-arguments and
 raise-missing-from

PiperOrigin-RevId: 335092048
Change-Id: I3d6328a9f4b4704e4c665d657b032cada0463594
---
 pylintrc | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pylintrc b/pylintrc
index 37bfa447..1b1c5cc2 100644
--- a/pylintrc
+++ b/pylintrc
@@ -32,7 +32,7 @@ enable=indexing-exception,old-raise-syntax
 # Disable the message, report, category or checker with the given id(s). You
 # can either give multiple identifier separated by comma (,) or put this option
 # multiple time.
-disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return
+disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return,super-with-arguments,raise-missing-from
 
 
 [REPORTS]

From 878b8d86f488ef2606cffdf58297dd2781708316 Mon Sep 17 00:00:00 2001
From: Daniel Zheng <dzheng256@gmail.com>
Date: Mon, 5 Oct 2020 11:20:49 -0700
Subject: [PATCH 207/324] Fix inclusion of enum34 if setup.py.
 7d100c145a5a0afc44394e36c4c5c02afba288dc by Daniel Zheng
 <dzheng256@gmail.com>

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/289 from dzheng256:patch-1 7d100c145a5a0afc44394e36c4c5c02afba288dc
PiperOrigin-RevId: 335459087
Change-Id: Ifafd1ed3793604ed78b487189a0be908d699bd51
---
 setup.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/setup.py b/setup.py
index efd3f766..572fb3c6 100644
--- a/setup.py
+++ b/setup.py
@@ -14,7 +14,6 @@
 
 """The setup.py file for Python Fire."""
 
-import sys
 from setuptools import setup
 
 LONG_DESCRIPTION = """
@@ -32,7 +31,8 @@
 DEPENDENCIES = [
     'six',
     'termcolor',
-] + (['enum34'] if sys.version < '3.4' else [])
+    'enum34; python_version < "3.4"'
+]
 
 TEST_DEPENDENCIES = [
     'hypothesis',

From 5311d21b1b4ddbc6330396ce2d9fd1eb9642ff97 Mon Sep 17 00:00:00 2001
From: Jacob Austin <jaaustin@google.com>
Date: Thu, 29 Oct 2020 10:24:31 -0700
Subject: [PATCH 208/324] Added better error handling for module imports and

PiperOrigin-RevId: 339695282
Change-Id: I28e98d51a9d1ee20aeb69d1cb667980e3c7607cf
---
 fire/__main__.py  | 69 +++++++++++++++++++++++++++++++++++++++++++++--
 fire/main_test.py | 58 ++++++++++++++++++++++++++++++++++++---
 fire/testutils.py | 18 +++++++++++--
 3 files changed, 137 insertions(+), 8 deletions(-)

diff --git a/fire/__main__.py b/fire/__main__.py
index 949632ef..f23c7d9a 100644
--- a/fire/__main__.py
+++ b/fire/__main__.py
@@ -19,14 +19,79 @@
 """
 
 import importlib
+import os
 import sys
 
 import fire
 
+cli_string = """usage: python -m fire [module] [arg] ..."
 
-def main(args):
-  module_name = args[1]
+Python Fire is a library for creating CLIs from absolutely any Python
+object or program. To run Python Fire from the command line on an
+existing Python file, it can be invoked with "python -m fire [module]"
+and passed a Python module using module notation:
+
+"python -m fire packageA.packageB.module"
+
+or with a file path:
+
+"python -m fire packageA/packageB/module.py" """
+
+
+def import_from_file_path(path):
+  """Performs a module import given the filename."""
+  module_name = os.path.basename(path)
+
+  if sys.version_info.major == 3:
+    from importlib import util  # pylint: disable=g-import-not-at-top
+    spec = util.spec_from_file_location(module_name, path)
+
+    if spec is None:
+      raise IOError('Unable to load module from specified path.')
+
+    module = util.module_from_spec(spec)
+    spec.loader.exec_module(module)  # pytype: disable=attribute-error
+  else:
+    import imp  # pylint: disable=g-import-not-at-top
+    module = imp.load_source(module_name, path)
+
+  return module, module_name
+
+
+def import_from_module_name(module_name):
   module = importlib.import_module(module_name)
+  return module, module_name
+
+
+def import_module(module_or_filename):
+  """Imports a given module or filename."""
+
+  if os.path.exists(module_or_filename):
+    # importlib.util.spec_from_file_location requires .py
+    if not module_or_filename.endswith('.py'):
+      try:  # try as module instead
+        return import_from_module_name(module_or_filename)
+      except ImportError:
+        raise ValueError('Fire can only be called on .py files.')
+
+    return import_from_file_path(module_or_filename)
+
+  if os.path.sep in module_or_filename:  # Use / to detect if it was a filename.
+    raise IOError('Fire was passed a filename which could not be found.')
+
+  return import_from_module_name(module_or_filename)  # Assume it's a module.
+
+
+def main(args):
+  """Entrypoint for fire when invoked as a module with python -m fire."""
+
+  if len(args) < 2:
+    print(cli_string)
+    exit(1)
+
+  module_or_filename = args[1]
+  module, module_name = import_module(module_or_filename)
+
   fire.Fire(module, name=module_name, command=args[2:])
 
 
diff --git a/fire/main_test.py b/fire/main_test.py
index 9da8f54f..478e370a 100644
--- a/fire/main_test.py
+++ b/fire/main_test.py
@@ -15,6 +15,7 @@
 """Test using Fire via `python -m fire`."""
 
 import os
+import tempfile
 
 from fire import __main__
 from fire import testutils
@@ -31,11 +32,60 @@ def testNameSetting(self):
   def testArgPassing(self):
     expected = os.path.join('part1', 'part2', 'part3')
     with self.assertOutputMatches('%s\n' % expected):
-      __main__.main(['__main__.py', 'os.path', 'join', 'part1', 'part2',
-                     'part3'])
+      __main__.main(
+          ['__main__.py', 'os.path', 'join', 'part1', 'part2', 'part3'])
     with self.assertOutputMatches('%s\n' % expected):
-      __main__.main(['__main__.py', 'os', 'path', '-', 'join', 'part1',
-                     'part2', 'part3'])
+      __main__.main(
+          ['__main__.py', 'os', 'path', '-', 'join', 'part1', 'part2', 'part3'])
+
+
+class MainModuleFileTest(testutils.BaseTestCase):
+  """Tests to verify correct import behavior for file executables."""
+
+  def setUp(self):
+    super(MainModuleFileTest, self).setUp()
+    self.file = tempfile.NamedTemporaryFile(suffix='.py')
+    self.file.write(b'class Foo:\n  def double(self, n):\n    return 2 * n\n')
+    self.file.flush()
+
+    self.file2 = tempfile.NamedTemporaryFile()
+
+  def testFileNameFire(self):
+    # Confirm that the file is correctly imported and doubles the number.
+    with self.assertOutputMatches('4'):
+      __main__.main(
+          ['__main__.py', self.file.name, 'Foo', 'double', '--n', '2'])
+
+  def testFileNameFailure(self):
+    # Confirm that an existing file without a .py suffix raises a ValueError.
+    with self.assertRaises(ValueError):
+      __main__.main(
+          ['__main__.py', self.file2.name, 'Foo', 'double', '--n', '2'])
+
+  def testFileNameModuleDuplication(self):
+    # Confirm that a file that masks a module still loads the module.
+    with self.assertOutputMatches('gettempdir'):
+      file = self.create_tempfile('tempfile')
+
+      with testutils.ChangeDirectory(os.path.dirname(file.full_path)):
+        __main__.main([
+            '__main__.py',
+            'tempfile',
+        ])
+
+  def testFileNameModuleFileFailure(self):
+    # Confirm that an invalid file that masks a non-existent module fails.
+    with self.assertRaisesWithLiteralMatch(
+        ValueError, 'Fire can only be called on .py files.'):
+      file = self.create_tempfile('foobar')
+
+      with testutils.ChangeDirectory(os.path.dirname(file.full_path)):
+        assert os.path.exists('foobar')
+
+        __main__.main([
+            '__main__.py',
+            'foobar',
+        ])
 
 
 if __name__ == '__main__':
diff --git a/fire/testutils.py b/fire/testutils.py
index 3463fd50..831ce78a 100644
--- a/fire/testutils.py
+++ b/fire/testutils.py
@@ -19,6 +19,7 @@
 from __future__ import print_function
 
 import contextlib
+import os
 import re
 import sys
 import unittest
@@ -44,6 +45,7 @@ def assertOutputMatches(self, stdout='.*', stderr='.*', capture=True):
       stdout: (str) regexp to match against stdout (None will check no stdout)
       stderr: (str) regexp to match against stderr (None will check no stderr)
       capture: (bool, default True) do not bubble up stdout or stderr
+
     Yields:
       Yields to the wrapped context.
     """
@@ -80,6 +82,7 @@ def assertRaisesFireExit(self, code, regexp='.*'):
     Args:
       code: The status code that the FireExit should contain.
       regexp: stdout must match this regex.
+
     Yields:
       Yields to the wrapped context.
     """
@@ -89,12 +92,23 @@ def assertRaisesFireExit(self, code, regexp='.*'):
           yield
         except core.FireExit as exc:
           if exc.code != code:
-            raise AssertionError('Incorrect exit code: %r != %r' % (exc.code,
-                                                                    code))
+            raise AssertionError('Incorrect exit code: %r != %r' %
+                                 (exc.code, code))
           self.assertIsInstance(exc.trace, trace.FireTrace)
           raise
 
 
+@contextlib.contextmanager
+def ChangeDirectory(directory):
+  cwdir = os.getcwd()
+  os.chdir(directory)
+
+  try:
+    yield directory
+  finally:
+    os.chdir(cwdir)
+
+
 # pylint: disable=invalid-name
 main = unittest.main
 skip = unittest.skip

From c1a0450c66e0c3d9b177d5618d9864f04c565091 Mon Sep 17 00:00:00 2001
From: Jacob Austin <jaaustin@google.com>
Date: Thu, 29 Oct 2020 11:44:38 -0700
Subject: [PATCH 209/324] Fixed compatibility issues with unittest module and
 Python versions below 3.5.

PiperOrigin-RevId: 339713431
Change-Id: I80cb92f74a3958cde56ece04e393d1f2c079d86e
---
 fire/__main__.py  | 13 ++++++++++++-
 fire/main_test.py | 36 +++++++++++++++++++-----------------
 2 files changed, 31 insertions(+), 18 deletions(-)

diff --git a/fire/__main__.py b/fire/__main__.py
index f23c7d9a..2ea8ae10 100644
--- a/fire/__main__.py
+++ b/fire/__main__.py
@@ -42,7 +42,17 @@ def import_from_file_path(path):
   """Performs a module import given the filename."""
   module_name = os.path.basename(path)
 
-  if sys.version_info.major == 3:
+  if sys.version_info.major == 3 and sys.version_info.minor < 5:
+    loader = importlib.machinery.SourceFileLoader(
+        fullname=module_name,
+        path=path,
+    )
+    spec = importlib.util.spec_from_loader(loader.name, loader, origin=path)
+    module = importlib.util.module_from_spec(spec)
+    sys.modules[loader.name] = module
+    loader.exec_module(module)
+
+  elif sys.version_info.major == 3:
     from importlib import util  # pylint: disable=g-import-not-at-top
     spec = util.spec_from_file_location(module_name, path)
 
@@ -51,6 +61,7 @@ def import_from_file_path(path):
 
     module = util.module_from_spec(spec)
     spec.loader.exec_module(module)  # pytype: disable=attribute-error
+
   else:
     import imp  # pylint: disable=g-import-not-at-top
     module = imp.load_source(module_name, path)
diff --git a/fire/main_test.py b/fire/main_test.py
index 478e370a..75707dc4 100644
--- a/fire/main_test.py
+++ b/fire/main_test.py
@@ -65,27 +65,29 @@ def testFileNameFailure(self):
   def testFileNameModuleDuplication(self):
     # Confirm that a file that masks a module still loads the module.
     with self.assertOutputMatches('gettempdir'):
-      file = self.create_tempfile('tempfile')
+      dirname = os.path.dirname(self.file.name)
+      with testutils.ChangeDirectory(dirname):
+        with open('tempfile', 'w'):
+          __main__.main([
+              '__main__.py',
+              'tempfile',
+          ])
 
-      with testutils.ChangeDirectory(os.path.dirname(file.full_path)):
-        __main__.main([
-            '__main__.py',
-            'tempfile',
-        ])
+        os.remove('tempfile')
 
   def testFileNameModuleFileFailure(self):
     # Confirm that an invalid file that masks a non-existent module fails.
-    with self.assertRaisesWithLiteralMatch(
-        ValueError, 'Fire can only be called on .py files.'):
-      file = self.create_tempfile('foobar')
-
-      with testutils.ChangeDirectory(os.path.dirname(file.full_path)):
-        assert os.path.exists('foobar')
-
-        __main__.main([
-            '__main__.py',
-            'foobar',
-        ])
+    with self.assertRaisesRegex(ValueError,
+                                r'Fire can only be called on \.py files\.'):
+      dirname = os.path.dirname(self.file.name)
+      with testutils.ChangeDirectory(dirname):
+        with open('foobar', 'w'):
+          __main__.main([
+              '__main__.py',
+              'foobar',
+          ])
+
+        os.remove('foobar')
 
 
 if __name__ == '__main__':

From 6ef90c0155c7e092163cac9413ea22dd2a31bfeb Mon Sep 17 00:00:00 2001
From: Jacob Austin <jaaustin@google.com>
Date: Thu, 29 Oct 2020 20:13:20 -0700
Subject: [PATCH 210/324] Updated to fix unittest compatibility and Python 3.4
 backwards compatibility.

PiperOrigin-RevId: 339798577
Change-Id: I94655e65e19dc56d6ee608e84ec02056558154d3
---
 fire/__main__.py  | 50 +++++++++++++++++++++++++++++++++++++----------
 fire/testutils.py |  8 ++++++++
 2 files changed, 48 insertions(+), 10 deletions(-)

diff --git a/fire/__main__.py b/fire/__main__.py
index 2ea8ae10..3a26ade1 100644
--- a/fire/__main__.py
+++ b/fire/__main__.py
@@ -12,8 +12,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+# Lint as: python2, python3
 # pylint: disable=invalid-name
-"""Enables use of Python Fire as a "main" function (i.e. `python -m fire`).
+"""Enables use of Python Fire as a "main" function (i.e. "python -m fire").
 
 This allows using Fire with third-party libraries without modifying their code.
 """
@@ -39,7 +40,22 @@
 
 
 def import_from_file_path(path):
-  """Performs a module import given the filename."""
+  """Performs a module import given the filename.
+
+  Args:
+    path (str): the path to the file to be imported.
+
+  Raises:
+    IOError: if the given file does not exist or importlib fails to load it.
+
+  Returns:
+    Tuple[ModuleType, str]: returns the imported module and the module name,
+      usually extracted from the path itself.
+  """
+
+  if not os.path.exists(path):
+    raise IOError('Given file path does not exist.')
+
   module_name = os.path.basename(path)
 
   if sys.version_info.major == 3 and sys.version_info.minor < 5:
@@ -47,13 +63,11 @@ def import_from_file_path(path):
         fullname=module_name,
         path=path,
     )
-    spec = importlib.util.spec_from_loader(loader.name, loader, origin=path)
-    module = importlib.util.module_from_spec(spec)
-    sys.modules[loader.name] = module
-    loader.exec_module(module)
+
+    module = loader.load_module(module_name)  # pylint: disable=deprecated-method
 
   elif sys.version_info.major == 3:
-    from importlib import util  # pylint: disable=g-import-not-at-top
+    from importlib import util  # pylint: disable=g-import-not-at-top,import-outside-toplevel
     spec = util.spec_from_file_location(module_name, path)
 
     if spec is None:
@@ -63,19 +77,35 @@ def import_from_file_path(path):
     spec.loader.exec_module(module)  # pytype: disable=attribute-error
 
   else:
-    import imp  # pylint: disable=g-import-not-at-top
+    import imp  # pylint: disable=g-import-not-at-top,import-outside-toplevel
     module = imp.load_source(module_name, path)
 
   return module, module_name
 
 
 def import_from_module_name(module_name):
+  """Imports a module and returns it and its name."""
   module = importlib.import_module(module_name)
   return module, module_name
 
 
 def import_module(module_or_filename):
-  """Imports a given module or filename."""
+  """Imports a given module or filename.
+
+  If the module_or_filename exists in the file system and ends with .py, we
+  attempt to import it. If that import fails, try to import it as a module.
+
+  Args:
+    module_or_filename (str): string name of path or module.
+
+  Raises:
+    ValueError: if the given file is invalid.
+    IOError: if the file or module can not be found or imported.
+
+  Returns:
+    Tuple[ModuleType, str]: returns the imported module and the module name,
+      usually extracted from the path itself.
+  """
 
   if os.path.exists(module_or_filename):
     # importlib.util.spec_from_file_location requires .py
@@ -98,7 +128,7 @@ def main(args):
 
   if len(args) < 2:
     print(cli_string)
-    exit(1)
+    sys.exit(1)
 
   module_or_filename = args[1]
   module, module_name = import_module(module_or_filename)
diff --git a/fire/testutils.py b/fire/testutils.py
index 831ce78a..b66b51e4 100644
--- a/fire/testutils.py
+++ b/fire/testutils.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+# Lint as: python2, python3
 """Utilities for Python Fire's tests."""
 
 from __future__ import absolute_import
@@ -72,6 +73,12 @@ def assertOutputMatches(self, stdout='.*', stderr='.*', capture=True):
           raise AssertionError('%s: Expected %r to match %r' %
                                (name, value, regexp))
 
+  def assertRaisesRegex(self, *args, **kwargs):
+    if sys.version_info.major == 2:
+      return super(BaseTestCase, self).assertRaisesRegexp(*args, **kwargs)  # pylint: disable=deprecated-method
+    else:
+      return super(BaseTestCase, self).assertRaisesRegex(*args, **kwargs)
+
   @contextlib.contextmanager
   def assertRaisesFireExit(self, code, regexp='.*'):
     """Asserts that a FireExit error is raised in the context.
@@ -100,6 +107,7 @@ def assertRaisesFireExit(self, code, regexp='.*'):
 
 @contextlib.contextmanager
 def ChangeDirectory(directory):
+  """Context manager to mock a directory change and revert on exit."""
   cwdir = os.getcwd()
   os.chdir(directory)
 

From 5aa754a1e15f095294d2aad17ff78a1cad7bfa8b Mon Sep 17 00:00:00 2001
From: Jacob Austin <jaaustin@google.com>
Date: Fri, 30 Oct 2020 07:12:06 -0700
Subject: [PATCH 211/324] Fixed linting issues with import compatibility across
 multiple Python versions.

PiperOrigin-RevId: 339865504
Change-Id: I9ee3cf4e6324c67c34665b20116109e17acf9e93
---
 fire/__main__.py  | 10 +++++++---
 fire/testutils.py |  2 +-
 2 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/fire/__main__.py b/fire/__main__.py
index 3a26ade1..30d556e4 100644
--- a/fire/__main__.py
+++ b/fire/__main__.py
@@ -19,6 +19,10 @@
 This allows using Fire with third-party libraries without modifying their code.
 """
 
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
 import importlib
 import os
 import sys
@@ -59,7 +63,7 @@ def import_from_file_path(path):
   module_name = os.path.basename(path)
 
   if sys.version_info.major == 3 and sys.version_info.minor < 5:
-    loader = importlib.machinery.SourceFileLoader(
+    loader = importlib.machinery.SourceFileLoader(  # pylint: disable=no-member
         fullname=module_name,
         path=path,
     )
@@ -67,13 +71,13 @@ def import_from_file_path(path):
     module = loader.load_module(module_name)  # pylint: disable=deprecated-method
 
   elif sys.version_info.major == 3:
-    from importlib import util  # pylint: disable=g-import-not-at-top,import-outside-toplevel
+    from importlib import util  # pylint: disable=g-import-not-at-top,import-outside-toplevel,no-name-in-module
     spec = util.spec_from_file_location(module_name, path)
 
     if spec is None:
       raise IOError('Unable to load module from specified path.')
 
-    module = util.module_from_spec(spec)
+    module = util.module_from_spec(spec)  # pylint: disable=no-member
     spec.loader.exec_module(module)  # pytype: disable=attribute-error
 
   else:
diff --git a/fire/testutils.py b/fire/testutils.py
index b66b51e4..dfcb2d74 100644
--- a/fire/testutils.py
+++ b/fire/testutils.py
@@ -77,7 +77,7 @@ def assertRaisesRegex(self, *args, **kwargs):
     if sys.version_info.major == 2:
       return super(BaseTestCase, self).assertRaisesRegexp(*args, **kwargs)  # pylint: disable=deprecated-method
     else:
-      return super(BaseTestCase, self).assertRaisesRegex(*args, **kwargs)
+      return super(BaseTestCase, self).assertRaisesRegex(*args, **kwargs)  # pylint: disable=no-member,arguments-differ
 
   @contextlib.contextmanager
   def assertRaisesFireExit(self, code, regexp='.*'):

From fea0be79ccfa130853aca0dd2b298ae817289e09 Mon Sep 17 00:00:00 2001
From: Jacob Austin <jaaustin@google.com>
Date: Fri, 30 Oct 2020 10:14:16 -0700
Subject: [PATCH 212/324] CL fixes for pytype and pylint.

PiperOrigin-RevId: 339893046
Change-Id: I9df74fefee03eb801262bccdf1f92999453bfb85
---
 fire/main_test.py | 2 +-
 fire/testutils.py | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/fire/main_test.py b/fire/main_test.py
index 75707dc4..20eaa26f 100644
--- a/fire/main_test.py
+++ b/fire/main_test.py
@@ -78,7 +78,7 @@ def testFileNameModuleDuplication(self):
   def testFileNameModuleFileFailure(self):
     # Confirm that an invalid file that masks a non-existent module fails.
     with self.assertRaisesRegex(ValueError,
-                                r'Fire can only be called on \.py files\.'):
+                                r'Fire can only be called on \.py files\.'):  # pylint: disable=line-too-long  # pytype: disable=attribute-error
       dirname = os.path.dirname(self.file.name)
       with testutils.ChangeDirectory(dirname):
         with open('foobar', 'w'):
diff --git a/fire/testutils.py b/fire/testutils.py
index dfcb2d74..31a2badb 100644
--- a/fire/testutils.py
+++ b/fire/testutils.py
@@ -73,11 +73,11 @@ def assertOutputMatches(self, stdout='.*', stderr='.*', capture=True):
           raise AssertionError('%s: Expected %r to match %r' %
                                (name, value, regexp))
 
-  def assertRaisesRegex(self, *args, **kwargs):
+  def assertRaisesRegex(self, *args, **kwargs):  # pylint: disable=arguments-differ
     if sys.version_info.major == 2:
       return super(BaseTestCase, self).assertRaisesRegexp(*args, **kwargs)  # pylint: disable=deprecated-method
     else:
-      return super(BaseTestCase, self).assertRaisesRegex(*args, **kwargs)  # pylint: disable=no-member,arguments-differ
+      return super(BaseTestCase, self).assertRaisesRegex(*args, **kwargs)  # pylint: disable=no-member
 
   @contextlib.contextmanager
   def assertRaisesFireExit(self, code, regexp='.*'):

From 3be260e65a0c25d1dbbe1b15eeb0bf13ac7ec38f Mon Sep 17 00:00:00 2001
From: Jacob Austin <jaaustin@google.com>
Date: Fri, 30 Oct 2020 10:52:24 -0700
Subject: [PATCH 213/324] Fixed line-too-long linting issue in Python 2.7.

PiperOrigin-RevId: 339900581
Change-Id: Ibedaed45ffca68f8b12344f8905b4ff379c6e589
---
 fire/main_test.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fire/main_test.py b/fire/main_test.py
index 20eaa26f..41699ac6 100644
--- a/fire/main_test.py
+++ b/fire/main_test.py
@@ -78,7 +78,7 @@ def testFileNameModuleDuplication(self):
   def testFileNameModuleFileFailure(self):
     # Confirm that an invalid file that masks a non-existent module fails.
     with self.assertRaisesRegex(ValueError,
-                                r'Fire can only be called on \.py files\.'):  # pylint: disable=line-too-long  # pytype: disable=attribute-error
+                                r'Fire can only be called on \.py files\.'):  # pylint: disable=line-too-long,  # pytype: disable=attribute-error
       dirname = os.path.dirname(self.file.name)
       with testutils.ChangeDirectory(dirname):
         with open('foobar', 'w'):

From e4cb72a1350012cf2651f4a233ae2d0ceacb7265 Mon Sep 17 00:00:00 2001
From: Robert Weiss <weiss@formlabs.com>
Date: Wed, 16 Dec 2020 12:19:42 -0800
Subject: [PATCH 214/324] Support asyncio coroutines

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/127 from robweiss:run_coroutines 56bea93ea0340bea962a111bbd20122c1ad4beff
PiperOrigin-RevId: 347872801
Change-Id: I17407ac8c3cb7b29026bd7d78729747a3a9875cf
---
 fire/core.py                | 11 ++++++++++-
 fire/fire_test.py           |  5 +++++
 fire/inspectutils.py        | 12 +++++++++++-
 fire/test_components_py3.py |  8 ++++++++
 4 files changed, 34 insertions(+), 2 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index 763b3d13..43f13e64 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -74,6 +74,9 @@ def main(argv):
 from fire.console import console_io
 import six
 
+if six.PY34:
+  import asyncio  # pylint: disable=g-import-not-at-top,import-error  # pytype: disable=import-error
+
 
 def Fire(component=None, command=None, name=None):
   """This function, Fire, is the main entrypoint for Python Fire.
@@ -669,7 +672,13 @@ def _CallAndUpdateTrace(component, args, component_trace, treatment='class',
   fn = component.__call__ if treatment == 'callable' else component
   parse = _MakeParseFn(fn, metadata)
   (varargs, kwargs), consumed_args, remaining_args, capacity = parse(args)
-  component = fn(*varargs, **kwargs)
+
+  # Call the function.
+  if inspectutils.IsCoroutineFunction(fn):
+    loop = asyncio.get_event_loop()
+    component = loop.run_until_complete(fn(*varargs, **kwargs))
+  else:
+    component = fn(*varargs, **kwargs)
 
   if treatment == 'class':
     action = trace.INSTANTIATED_CLASS
diff --git a/fire/fire_test.py b/fire/fire_test.py
index 4f2ffb9b..8b904c29 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -721,6 +721,11 @@ def testHelpKwargsDecorator(self):
     with self.assertRaisesFireExit(0):
       fire.Fire(tc.decorated_method, command=['--help'])
 
+  @testutils.skipIf(six.PY2, 'Asyncio not available in Python 2.')
+  def testFireAsyncio(self):
+    self.assertEqual(fire.Fire(tc.py3.WithAsyncio,
+                               command=['double', '--count', '10']), 20)
+
 
 if __name__ == '__main__':
   testutils.main()
diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index c7559ae6..80cc43f2 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -26,6 +26,9 @@
 
 import six
 
+if six.PY34:
+  import asyncio  # pylint: disable=g-import-not-at-top,import-error  # pytype: disable=import-error
+
 
 class FullArgSpec(object):
   """The arguments of a function, as in Python 3's inspect.FullArgSpec."""
@@ -250,7 +253,7 @@ def GetFileAndLine(component):
   try:
     unused_code, lineindex = inspect.findsource(component)
     lineno = lineindex + 1
-  except IOError:
+  except (IOError, IndexError):
     lineno = None
 
   return filename, lineno
@@ -360,3 +363,10 @@ def GetClassAttrsDict(component):
       class_attr.name: class_attr
       for class_attr in class_attrs_list
   }
+
+
+def IsCoroutineFunction(fn):
+  try:
+    return six.PY34 and asyncio.iscoroutinefunction(fn)
+  except:  # pylint: disable=bare-except
+    return False
diff --git a/fire/test_components_py3.py b/fire/test_components_py3.py
index 56f64356..3c21f4ba 100644
--- a/fire/test_components_py3.py
+++ b/fire/test_components_py3.py
@@ -15,6 +15,7 @@
 # Lint as: python3
 """This module has components that use Python 3 specific syntax."""
 
+import asyncio
 import functools
 from typing import Tuple
 
@@ -49,6 +50,13 @@ def lru_cache_decorated(arg1):
   return arg1
 
 
+class WithAsyncio(object):
+
+  @asyncio.coroutine
+  def double(self, count=0):
+    return 2 * count
+
+
 class WithTypes(object):
   """Class with functions that have default arguments and types."""
 

From aa18534de3e1f44ed3dd525162568605a13e839a Mon Sep 17 00:00:00 2001
From: Amy Lei <42757189+amy-lei@users.noreply.github.com>
Date: Wed, 16 Dec 2020 12:26:35 -0800
Subject: [PATCH 215/324] Add :key type name: parameters to help text

a22cb7dd08c49357cdc5bf62d59d6c650fb9548b by Amy <leiamy12@gmail.com>
26aba559cef2ad07ea0516e92db029ee04956a16 by Amy <leiamy12@gmail.com>
9bcccf992b15d1eebb05a3e2267bc6228269dd90 by Amy <leiamy12@gmail.com>

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/275 from amy-lei:key-param 9bcccf992b15d1eebb05a3e2267bc6228269dd90
PiperOrigin-RevId: 347874060
Change-Id: Id7d59472a73bf1fa33d37334e7e72702a07c72f6
---
 fire/docstrings.py      | 32 ++++++++++++++++++++++++--------
 fire/docstrings_test.py | 32 +++++++++++++++++++++++++++++---
 fire/helptext.py        | 41 +++++++++++++++++++++++++++++++++--------
 fire/helptext_test.py   | 28 ++++++++++++++++++++++++++++
 fire/test_components.py | 24 ++++++++++++++++++++++++
 5 files changed, 138 insertions(+), 19 deletions(-)

diff --git a/fire/docstrings.py b/fire/docstrings.py
index e173d192..de18ed9b 100644
--- a/fire/docstrings.py
+++ b/fire/docstrings.py
@@ -53,13 +53,11 @@
 from __future__ import division
 from __future__ import print_function
 
-
 import collections
+import enum
 import re
 import textwrap
 
-import enum
-
 
 class DocstringInfo(
     collections.namedtuple(
@@ -77,6 +75,11 @@ class ArgInfo(
 ArgInfo.__new__.__defaults__ = (None,) * len(ArgInfo._fields)
 
 
+class KwargInfo(ArgInfo):
+  pass
+KwargInfo.__new__.__defaults__ = (None,) * len(KwargInfo._fields)
+
+
 class Namespace(dict):
   """A dict with attribute (dot-notation) access enabled."""
 
@@ -108,7 +111,7 @@ class Formats(enum.Enum):
 
 
 SECTION_TITLES = {
-    Sections.ARGS: ('argument', 'arg', 'parameter', 'param'),
+    Sections.ARGS: ('argument', 'arg', 'parameter', 'param', 'key'),
     Sections.RETURNS: ('return',),
     Sections.YIELDS: ('yield',),
     Sections.RAISES: ('raise', 'except', 'exception', 'throw', 'error', 'warn'),
@@ -169,6 +172,7 @@ def parse(docstring):
   state.summary.lines = []
   state.description.lines = []
   state.args = []
+  state.kwargs = []
   state.current_arg = None
   state.returns.lines = []
   state.yields.lines = []
@@ -194,6 +198,10 @@ def parse(docstring):
       name=arg.name, type=_cast_to_known_type(_join_lines(arg.type.lines)),
       description=_join_lines(arg.description.lines)) for arg in state.args]
 
+  args.extend([KwargInfo(
+      name=arg.name, type=_cast_to_known_type(_join_lines(arg.type.lines)),
+      description=_join_lines(arg.description.lines)) for arg in state.kwargs])
+
   return DocstringInfo(
       summary=summary,
       description=description,
@@ -267,7 +275,7 @@ def _join_lines(lines):
   return '\n\n'.join(group_texts)
 
 
-def _get_or_create_arg_by_name(state, name):
+def _get_or_create_arg_by_name(state, name, is_kwarg=False):
   """Gets or creates a new Arg.
 
   These Arg objects (Namespaces) are turned into the ArgInfo namedtuples
@@ -277,17 +285,21 @@ def _get_or_create_arg_by_name(state, name):
   Args:
     state: The state of the parser.
     name: The name of the arg to create.
+    is_kwarg: A boolean representing whether the argument is a keyword arg.
   Returns:
     The new Arg.
   """
-  for arg in state.args:
+  for arg in state.args + state.kwargs:
     if arg.name == name:
       return arg
   arg = Namespace()  # TODO(dbieber): Switch to an explicit class.
   arg.name = name
   arg.type.lines = []
   arg.description.lines = []
-  state.args.append(arg)
+  if is_kwarg:
+    state.kwargs.append(arg)
+  else:
+    state.args.append(arg)
   return arg
 
 
@@ -429,7 +441,11 @@ def _consume_line(line_info, state):
     directive_tokens = directive.split()  # pytype: disable=attribute-error
     if state.section.title == Sections.ARGS:
       name = directive_tokens[-1]
-      arg = _get_or_create_arg_by_name(state, name)
+      arg = _get_or_create_arg_by_name(
+          state,
+          name,
+          is_kwarg=directive_tokens[0] == 'key'
+      )
       if len(directive_tokens) == 3:
         # A param directive of the form ":param type arg:".
         arg.type.lines.append(directive_tokens[1])
diff --git a/fire/docstrings_test.py b/fire/docstrings_test.py
index 8b1d7685..63aa5e78 100644
--- a/fire/docstrings_test.py
+++ b/fire/docstrings_test.py
@@ -21,9 +21,11 @@
 from fire import docstrings
 from fire import testutils
 
-
-DocstringInfo = docstrings.DocstringInfo  # pylint: disable=invalid-name
-ArgInfo = docstrings.ArgInfo  # pylint: disable=invalid-name
+# pylint: disable=invalid-name
+DocstringInfo = docstrings.DocstringInfo
+ArgInfo = docstrings.ArgInfo
+KwargInfo = docstrings.KwargInfo
+# pylint: enable=invalid-name
 
 
 class DocstringsTest(testutils.BaseTestCase):
@@ -279,6 +281,30 @@ def test_numpy_colon_in_description(self):
     )
     self.assertEqual(expected_docstring_info, docstring_info)
 
+  def test_rst_format_typed_args_and_kwargs(self):
+    docstring = """Docstring summary.
+
+    :param arg1: Description of arg1.
+    :type arg1: str.
+    :key arg2: Description of arg2.
+    :type arg2: bool.
+    :key arg3: Description of arg3.
+    :type arg3: str.
+    """
+    docstring_info = docstrings.parse(docstring)
+    expected_docstring_info = DocstringInfo(
+        summary='Docstring summary.',
+        args=[
+            ArgInfo(name='arg1', type='str',
+                    description='Description of arg1.'),
+            KwargInfo(name='arg2', type='bool',
+                      description='Description of arg2.'),
+            KwargInfo(name='arg3', type='str',
+                      description='Description of arg3.'),
+        ],
+    )
+    self.assertEqual(expected_docstring_info, docstring_info)
+
 
 if __name__ == '__main__':
   testutils.main()
diff --git a/fire/helptext.py b/fire/helptext.py
index ecb6bbb4..b1d10b44 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -39,6 +39,7 @@
 from fire import completion
 from fire import custom_descriptions
 from fire import decorators
+from fire import docstrings
 from fire import formatting
 from fire import inspectutils
 from fire import value_types
@@ -219,10 +220,30 @@ def _ArgsAndFlagsSections(info, spec, metadata):
   flag_items = positional_flag_items + kwonly_flag_items
 
   if spec.varkw:
+    # Include kwargs documented via :key param:
+    flag_string = '--{name}'
+    documented_kwargs = []
+    for flag in docstring_info.args or []:
+      if isinstance(flag, docstrings.KwargInfo):
+        flag_item = _CreateFlagItem(
+            flag.name, docstring_info, spec,
+            flag_string=flag_string.format(name=flag.name))
+        documented_kwargs.append(flag_item)
+    if documented_kwargs:
+      # Separate documented kwargs from other flags using a message
+      if flag_items:
+        message = 'The following flags are also accepted.'
+        item = _CreateItem(message, None, indent=4)
+        flag_items.append(item)
+      flag_items.extend(documented_kwargs)
+
     description = _GetArgDescription(spec.varkw, docstring_info)
-    message = ('Additional flags are accepted.'
-               if flag_items else
-               'Flags are accepted.')
+    if documented_kwargs:
+      message = 'Additional undocumented flags may also be accepted.'
+    elif flag_items:
+      message = 'Additional flags are accepted.'
+    else:
+      message = 'Flags are accepted.'
     item = _CreateItem(message, description, indent=4)
     flag_items.append(item)
 
@@ -400,7 +421,8 @@ def _CreateArgItem(arg, docstring_info, spec):
   return _CreateItem(arg_string, description, indent=SUBSECTION_INDENTATION)
 
 
-def _CreateFlagItem(flag, docstring_info, spec, required=False):
+def _CreateFlagItem(flag, docstring_info, spec, required=False,
+                    flag_string=None):
   """Returns a string describing a flag using docstring and FullArgSpec info.
 
   Args:
@@ -410,6 +432,8 @@ def _CreateFlagItem(flag, docstring_info, spec, required=False):
     spec: An instance of fire.inspectutils.FullArgSpec, containing type and
      default information about the arguments to a callable.
     required: Whether the flag is required.
+    flag_string: If provided, use this string for the flag, rather than
+      constructing one from the flag name.
   Returns:
     A string to be used in constructing the help screen for the function.
   """
@@ -424,10 +448,11 @@ def _CreateFlagItem(flag, docstring_info, spec, required=False):
 
   description = _GetArgDescription(flag, docstring_info)
 
-  flag_string_template = '--{flag_name}={flag_name_upper}'
-  flag_string = flag_string_template.format(
-      flag_name=flag,
-      flag_name_upper=formatting.Underline(flag.upper()))
+  if not flag_string:
+    flag_string_template = '--{flag_name}={flag_name_upper}'
+    flag_string = flag_string_template.format(
+        flag_name=flag,
+        flag_name_upper=formatting.Underline(flag.upper()))
   if required:
     flag_string += ' (required)'
 
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 1ec16295..3cb40fbe 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -99,6 +99,34 @@ def testHelpTextFunctionWithLongDefaults(self):
         help_screen)
     self.assertNotIn('NOTES', help_screen)
 
+  def testHelpTextFunctionWithKwargs(self):
+    component = tc.fn_with_kwarg
+    help_screen = helptext.HelpText(
+        component=component,
+        trace=trace.FireTrace(component, name='text'))
+    self.assertIn('NAME\n    text', help_screen)
+    self.assertIn('SYNOPSIS\n    text ARG1 ARG2 <flags>', help_screen)
+    self.assertIn('DESCRIPTION\n    Function with kwarg', help_screen)
+    self.assertIn(
+        'FLAGS\n    --arg3\n        Description of arg3.\n    '
+        'Additional undocumented flags may also be accepted.',
+        help_screen)
+
+  def testHelpTextFunctionWithKwargsAndDefaults(self):
+    component = tc.fn_with_kwarg_and_defaults
+    help_screen = helptext.HelpText(
+        component=component,
+        trace=trace.FireTrace(component, name='text'))
+    self.assertIn('NAME\n    text', help_screen)
+    self.assertIn('SYNOPSIS\n    text ARG1 ARG2 <flags>', help_screen)
+    self.assertIn('DESCRIPTION\n    Function with kwarg', help_screen)
+    self.assertIn(
+        'FLAGS\n    --opt=OPT\n        Default: True\n'
+        '    The following flags are also accepted.'
+        '\n    --arg3\n        Description of arg3.\n    '
+        'Additional undocumented flags may also be accepted.',
+        help_screen)
+
   @testutils.skipIf(
       sys.version_info[0:2] < (3, 5),
       'Python < 3.5 does not support type hints.')
diff --git a/fire/test_components.py b/fire/test_components.py
index 824e73fc..eee9a07c 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -539,3 +539,27 @@ def wrapper(*args, **kwargs):
 @simple_decorator
 def decorated_method(name='World'):
   return 'Hello %s' % name
+
+
+# pylint: disable=g-doc-args,g-doc-return-or-yield
+def fn_with_kwarg(arg1, arg2, **kwargs):
+  """Function with kwarg.
+
+  :param arg1: Description of arg1.
+  :param arg2: Description of arg2.
+  :key arg3: Description of arg3.
+  """
+  del arg1, arg2
+  return kwargs.get('arg3')
+
+
+def fn_with_kwarg_and_defaults(arg1, arg2, opt=True, **kwargs):
+  """Function with kwarg and defaults.
+
+  :param arg1: Description of arg1.
+  :param arg2: Description of arg2.
+  :key arg3: Description of arg3.
+  """
+  del arg1, arg2, opt
+  return kwargs.get('arg3')
+# pylint: enable=g-doc-args,g-doc-return-or-yield

From a938ef905674bb8e6c6eeb62f61425e0765cd70f Mon Sep 17 00:00:00 2001
From: Rebecca Chen <rechen@google.com>
Date: Wed, 16 Dec 2020 12:42:03 -0800
Subject: [PATCH 216/324] Don't run pytype in Python 3.5.

Copybara import of the project:

--
eeef066e30e7fbdee0420c5624d8fd4c3d59d5ea by Rebecca Chen <rechen@google.com>:

Don't run pytype in Python 3.5.

The next release of pytype drops support for running under Python 3.5:
https://github.com/google/pytype/issues/677.

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/296 from rchen152:drop35 eeef066e30e7fbdee0420c5624d8fd4c3d59d5ea
PiperOrigin-RevId: 347877059
Change-Id: I8b2e8acba3d3ce3ebc47612d7f136d7a643650d3
---
 .travis.yml | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 30f0cad6..c6529bce 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -18,7 +18,8 @@ script:
   - pip install ipython
   - python -m pytest  # Now run the tests with IPython.
   - pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console
-  - if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then
+  - if [[ $TRAVIS_PYTHON_VERSION != 3.4 && \
+          $TRAVIS_PYTHON_VERSION != 3.5 ]]; then
       pip install pytype;
     fi
   # Run type-checking, excluding files that define or use py3 features in py2.
@@ -27,6 +28,7 @@ script:
         fire/fire_test.py
         fire/inspectutils_test.py
         fire/test_components_py3.py;
-    elif [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then
+    elif [[ $TRAVIS_PYTHON_VERSION != 3.4 && \
+            $TRAVIS_PYTHON_VERSION != 3.5 ]]; then
       pytype;
     fi

From bc43dca6a52ef6914f0e5d3ec76b316d8fdd648b Mon Sep 17 00:00:00 2001
From: Roopesh V S <txtmeroopesh@gmail.com>
Date: Wed, 16 Dec 2020 13:58:14 -0800
Subject: [PATCH 217/324] Support functions even when they override getattr in
 non-standard ways.

This resolves an issue with Beautiful Soup.

Copybara import of the project:
--
f4cd8e946a9b2a874deb1024c6b2e62358f6e537 by Roopesh V S <txtmeroopesh@gmail.com>:

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/281 from roopeshvs:master f4cd8e946a9b2a874deb1024c6b2e62358f6e537
PiperOrigin-RevId: 347892296
Change-Id: Ifbbfc9397ce9623cd1a354599252826ac7ddcc92
---
 fire/core.py         |  2 +-
 fire/decorators.py   | 20 ++++++++++++++++++--
 fire/inspectutils.py |  2 +-
 3 files changed, 20 insertions(+), 4 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index 43f13e64..8ca142c7 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -75,7 +75,7 @@ def main(argv):
 import six
 
 if six.PY34:
-  import asyncio  # pylint: disable=g-import-not-at-top,import-error  # pytype: disable=import-error
+  import asyncio  # pylint: disable=import-error,g-import-not-at-top  # pytype: disable=import-error
 
 
 def Fire(component=None, command=None, name=None):
diff --git a/fire/decorators.py b/fire/decorators.py
index b7b3c660..9e56d6df 100644
--- a/fire/decorators.py
+++ b/fire/decorators.py
@@ -71,7 +71,7 @@ def SetParseFns(*positional, **named):
   def _Decorator(fn):
     parse_fns = GetParseFns(fn)
     parse_fns['positional'] = positional
-    parse_fns['named'].update(named)
+    parse_fns['named'].update(named)  # pytype: disable=attribute-error
     _SetMetadata(fn, FIRE_PARSE_FNS, parse_fns)
     return fn
 
@@ -85,15 +85,31 @@ def _SetMetadata(fn, attribute, value):
 
 
 def GetMetadata(fn):
+  # type: (...) -> dict
+  """Gets metadata attached to the function `fn` as an attribute.
+
+  Args:
+    fn: The function from which to retrieve the function metadata.
+  Returns:
+    A dictionary mapping property strings to their value.
+  """
   # Class __init__ functions and object __call__ functions require flag style
   # arguments. Other methods and functions may accept positional args.
   default = {
       ACCEPTS_POSITIONAL_ARGS: inspect.isroutine(fn),
   }
-  return getattr(fn, FIRE_METADATA, default)
+  try:
+    metadata = getattr(fn, FIRE_METADATA, default)
+    if ACCEPTS_POSITIONAL_ARGS in metadata:
+      return metadata
+    else:
+      return default
+  except:  # pylint: disable=bare-except
+    return default
 
 
 def GetParseFns(fn):
+  # type: (...) -> dict
   metadata = GetMetadata(fn)
   default = dict(default=None, positional=[], named={})
   return metadata.get(FIRE_PARSE_FNS, default)
diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index 80cc43f2..0fa8e7d3 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -27,7 +27,7 @@
 import six
 
 if six.PY34:
-  import asyncio  # pylint: disable=g-import-not-at-top,import-error  # pytype: disable=import-error
+  import asyncio  # pylint: disable=import-error,g-import-not-at-top  # pytype: disable=import-error
 
 
 class FullArgSpec(object):

From 629d91c3f3656049f61b44b734044e51a7895619 Mon Sep 17 00:00:00 2001
From: MichaelCG8 <42502192+MichaelCG8@users.noreply.github.com>
Date: Thu, 17 Dec 2020 10:54:46 -0800
Subject: [PATCH 218/324] Added detail to CONTRIBUTING.md

Copybara import of the project:
--
7a2eeb93bda700749a0edf45cffdf87b22b88e64 by Michael Garbutt <michael.c.garbutt@gmail.com>:

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/253 from MichaelCG8:issue-252-adding-detail-to-CONTRIBUTING.md 7a2eeb93bda700749a0edf45cffdf87b22b88e64
PiperOrigin-RevId: 348052636
Change-Id: I75d88223a0fbb7aada074f97b4491272ee2f295e
---
 CONTRIBUTING.md | 44 +++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 41 insertions(+), 3 deletions(-)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0786fdf4..08c2aef1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -3,6 +3,14 @@
 We'd love to accept your patches and contributions to this project. There are
 just a few small guidelines you need to follow.
 
+First, read these guidelines.
+Before you begin making changes, state your intent to do so in an Issue.
+Then, fork the project. Make changes in your copy of the repository.
+Then open a pull request once your changes are ready.
+If this is your first contribution, sign the Contributor License Agreement.
+A discussion about your change will follow, and if accepted your contribution
+will be incorporated into the Python Fire codebase.
+
 ## Contributor License Agreement
 
 Contributions to this project must be accompanied by a Contributor License
@@ -17,8 +25,38 @@ again.
 
 ## Code reviews
 
-All submissions, including submissions by project members, require review. We
-use GitHub pull requests for this purpose. Consult [GitHub Help] for more
-information on using pull requests.
+All submissions, including submissions by project members, require review.
+For changes introduced by non-Googlers, we use GitHub pull requests for this
+purpose. Consult [GitHub Help] for more information on using pull requests.
 
 [GitHub Help]: https://help.github.com/articles/about-pull-requests/
+
+## Code style
+
+In general, Python Fire follows the guidelines in the
+[Google Python Style Guide].
+
+In addition, the project follows a convention of:
+- Maximum line length: 80 characters
+- Indentation: 2 spaces (4 for line continuation)
+- PascalCase for function and method names.
+- No type hints, as described in [PEP 484], to maintain compatibility with
+Python versions < 3.5.
+- Single quotes around strings, three double quotes around docstrings.
+
+[Google Python Style Guide]: http://google.github.io/styleguide/pyguide.html
+[PEP 484]: https://www.python.org/dev/peps/pep-0484
+
+## Testing
+
+Python Fire uses Travis CI to run tests on each pull request. You can run
+these tests yourself as well. To do this, first install the test dependencies
+listed in setup.py (e.g. pytest, mock, termcolor, and hypothesis).
+Then run the tests by running `pytest` in the root directory of the repository.
+
+## Linting
+
+Please run lint on your pull requests to make accepting the requests easier.
+To do this, run `pylint fire` in the root directory of the repository.
+Note that even if lint is passing, additional style changes to your submission
+may be made during merging.

From c39de6ae333fdaf7d3371ab7cb213cb28703120d Mon Sep 17 00:00:00 2001
From: MichaelCG8 <42502192+MichaelCG8@users.noreply.github.com>
Date: Fri, 22 Jan 2021 08:40:05 -0800
Subject: [PATCH 219/324] issue-261 Fixed missing parts of argument
 descriptions for Google and Numpy style docstrings.
 482bb9da4bc09522984d982716a79caeafabf3e2 by Michael Garbutt
 <michael.c.garbutt@gmail.com>:

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/262 from MichaelCG8:issue-261-bugfix-multi-line-docstring-parameter-descriptions 482bb9da4bc09522984d982716a79caeafabf3e2
PiperOrigin-RevId: 353248737
Change-Id: I3033200c69c5aba0ce9bb819574f37248e32c1cb
---
 fire/docstrings.py      | 14 ++++++-----
 fire/docstrings_test.py | 55 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 63 insertions(+), 6 deletions(-)

diff --git a/fire/docstrings.py b/fire/docstrings.py
index de18ed9b..1cfadea9 100644
--- a/fire/docstrings.py
+++ b/fire/docstrings.py
@@ -307,9 +307,8 @@ def _is_arg_name(name):
   """Returns whether name is a valid arg name.
 
   This is used to prevent multiple words (plaintext) from being misinterpreted
-  as an argument name. So if ":" appears in the middle of a line in a docstring,
-  we don't accidentally interpret the first half of that line as a single arg
-  name.
+  as an argument name. Any line that doesn't match the pattern for a valid
+  argument is treated as not being an argument.
 
   Args:
     name: The name of the potential arg.
@@ -317,9 +316,11 @@ def _is_arg_name(name):
     True if name looks like an arg name, False otherwise.
   """
   name = name.strip()
-  return (name
-          and ' ' not in name
-          and ':' not in name)
+  # arg_pattern is a letter or underscore followed by
+  # zero or more letters, numbers, or underscores.
+  arg_pattern = r'^[a-zA-Z_]\w*$'
+  re.match(arg_pattern, name)
+  return re.match(arg_pattern, name) is not None
 
 
 def _as_arg_name_and_type(text):
@@ -402,6 +403,7 @@ def _consume_google_args_line(line_info, state):
         arg = _get_or_create_arg_by_name(state, arg_name)
         arg.type.lines.append(type_str)
         arg.description.lines.append(second.strip())
+        state.current_arg = arg
       else:
         if state.current_arg:
           state.current_arg.description.lines.append(split_line[0])
diff --git a/fire/docstrings_test.py b/fire/docstrings_test.py
index 63aa5e78..2328ef16 100644
--- a/fire/docstrings_test.py
+++ b/fire/docstrings_test.py
@@ -144,6 +144,32 @@ def test_google_format_typed_args_and_returns(self):
     )
     self.assertEqual(expected_docstring_info, docstring_info)
 
+  def test_google_format_multiline_arg_description(self):
+    docstring = """Docstring summary.
+
+    This is a longer description of the docstring. It spans multiple lines, as
+    is allowed.
+
+    Args:
+        param1 (int): The first parameter.
+        param2 (str): The second parameter. This has a lot of text, enough to
+        cover two lines.
+    """
+    docstring_info = docstrings.parse(docstring)
+    expected_docstring_info = DocstringInfo(
+        summary='Docstring summary.',
+        description='This is a longer description of the docstring. It spans '
+        'multiple lines, as\nis allowed.',
+        args=[
+            ArgInfo(name='param1', type='int',
+                    description='The first parameter.'),
+            ArgInfo(name='param2', type='str',
+                    description='The second parameter. This has a lot of text, '
+                                'enough to cover two lines.'),
+        ],
+    )
+    self.assertEqual(expected_docstring_info, docstring_info)
+
   def test_rst_format_typed_args_and_returns(self):
     docstring = """Docstring summary.
 
@@ -207,6 +233,35 @@ def test_numpy_format_typed_args_and_returns(self):
     )
     self.assertEqual(expected_docstring_info, docstring_info)
 
+  def test_numpy_format_multiline_arg_description(self):
+    docstring = """Docstring summary.
+
+    This is a longer description of the docstring. It spans across multiple
+    lines.
+
+    Parameters
+    ----------
+    param1 : int
+        The first parameter.
+    param2 : str
+        The second parameter. This has a lot of text, enough to cover two
+        lines.
+    """
+    docstring_info = docstrings.parse(docstring)
+    expected_docstring_info = DocstringInfo(
+        summary='Docstring summary.',
+        description='This is a longer description of the docstring. It spans '
+        'across multiple\nlines.',
+        args=[
+            ArgInfo(name='param1', type='int',
+                    description='The first parameter.'),
+            ArgInfo(name='param2', type='str',
+                    description='The second parameter. This has a lot of text, '
+                                'enough to cover two lines.'),
+        ],
+    )
+    self.assertEqual(expected_docstring_info, docstring_info)
+
   def test_multisection_docstring(self):
     docstring = """Docstring summary.
 

From 56d28ebbb41e0feb50510aa96c88a78e5b6c9e4e Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 22 Jan 2021 08:53:33 -0800
Subject: [PATCH 220/324] Bump Python Fire version number from 0.3.1 to 0.4.0

PiperOrigin-RevId: 353250692
Change-Id: I85231dd561a90f9f7224683b82b7dda0a3632347
---
 fire/__init__.py | 2 +-
 setup.py         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/fire/__init__.py b/fire/__init__.py
index 2bacc8fe..9c34a5af 100644
--- a/fire/__init__.py
+++ b/fire/__init__.py
@@ -21,4 +21,4 @@
 from fire.core import Fire
 
 __all__ = ['Fire']
-__version__ = '0.3.1'
+__version__ = '0.4.0'
diff --git a/setup.py b/setup.py
index 572fb3c6..dd1d7e1c 100644
--- a/setup.py
+++ b/setup.py
@@ -40,7 +40,7 @@
     'python-Levenshtein',
 ]
 
-VERSION = '0.3.1'
+VERSION = '0.4.0'
 URL = 'https://github.com/google/python-fire'
 
 setup(

From f2ab298e436f12d6a3fe36744b7eab6afcd04d80 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 22 Jan 2021 08:57:47 -0800
Subject: [PATCH 221/324] Test Python Fire on Python 3.9

PiperOrigin-RevId: 353251383
Change-Id: Iaa74341995640e59619f922997bbc451003a0f1c
---
 .travis.yml | 1 +
 setup.py    | 1 +
 2 files changed, 2 insertions(+)

diff --git a/.travis.yml b/.travis.yml
index c6529bce..2999ad29 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,6 +5,7 @@ python:
   - "3.5"
   - "3.7"
   - "3.8"
+  - "3.9"
 
 before_install:
   - pip install --upgrade setuptools pip
diff --git a/setup.py b/setup.py
index dd1d7e1c..1cc64a07 100644
--- a/setup.py
+++ b/setup.py
@@ -71,6 +71,7 @@
         'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
+        'Programming Language :: Python :: 3.9',
 
         'Operating System :: OS Independent',
         'Operating System :: POSIX',

From b61b68c7086e3af9fbccbdcf7244928b4258221f Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 22 Jan 2021 09:16:24 -0800
Subject: [PATCH 222/324] Don't run pytype on Python 3.9, not supported yet.

PiperOrigin-RevId: 353254829
Change-Id: I4709add1f381cd3a83f83d713834782c2f4dd3a8
---
 .travis.yml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.travis.yml b/.travis.yml
index 2999ad29..559eb749 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -30,6 +30,7 @@ script:
         fire/inspectutils_test.py
         fire/test_components_py3.py;
     elif [[ $TRAVIS_PYTHON_VERSION != 3.4 && \
-            $TRAVIS_PYTHON_VERSION != 3.5 ]]; then
+            $TRAVIS_PYTHON_VERSION != 3.5 && \
+            $TRAVIS_PYTHON_VERSION != 3.9 ]]; then
       pytype;
     fi

From fac1ea00430757fd3aa3d8e2b5f8226d25afe9b0 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 22 Jan 2021 09:18:27 -0800
Subject: [PATCH 223/324] Documentation updates for 0.4.0. (python -m fire
 path/to/file.py)

PiperOrigin-RevId: 353255172
Change-Id: I3c88d5e308d236e7a14d0a3639fd8752a60e6cd1
---
 docs/api.md   | 10 +++++++++-
 docs/guide.md |  8 ++++++++
 2 files changed, 17 insertions(+), 1 deletion(-)

diff --git a/docs/api.md b/docs/api.md
index aa918160..66451071 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -60,6 +60,14 @@ Help is an exception; the isolated `--` is optional for getting help._
 
 ## Using a Fire CLI without modifying any code
 
+You can use Python Fire on a module without modifying the code of the module.
+The syntax for this is:
+
 `python -m fire <module> <arguments>`
 
-For example, `python -m fire calendar -h`.
+or
+
+`python -m fire <filepath> <arguments>`
+
+For example, `python -m fire calendar -h` will treat the built in `calendar`
+module as a CLI and provide its help.
diff --git a/docs/guide.md b/docs/guide.md
index 5f693a40..909ec439 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -115,6 +115,14 @@ $ python -m fire example hello --name=World
 Hello World!
 ```
 
+You can also specify the filepath of example.py rather than its module path,
+like so:
+
+```bash
+$ python -m fire example.py hello --name=World
+Hello World!
+```
+
 ### Exposing Multiple Commands
 
 In the previous example, we exposed a single function to the command line. Now

From 4c51b892e26d0b558bb9d67f42f60754b8cb10b9 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 22 Jan 2021 09:39:51 -0800
Subject: [PATCH 224/324] Only run pytype on 2.7 and 3.7

PiperOrigin-RevId: 353259081
Change-Id: I410e5af3f280db6cb2780e2837977c725b21bb0f
---
 .travis.yml | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 559eb749..de9d1c59 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -19,8 +19,8 @@ script:
   - pip install ipython
   - python -m pytest  # Now run the tests with IPython.
   - pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console
-  - if [[ $TRAVIS_PYTHON_VERSION != 3.4 && \
-          $TRAVIS_PYTHON_VERSION != 3.5 ]]; then
+  - if [[ $TRAVIS_PYTHON_VERSION == 2.7 || \
+          $TRAVIS_PYTHON_VERSION == 3.7 ]]; then
       pip install pytype;
     fi
   # Run type-checking, excluding files that define or use py3 features in py2.
@@ -29,8 +29,6 @@ script:
         fire/fire_test.py
         fire/inspectutils_test.py
         fire/test_components_py3.py;
-    elif [[ $TRAVIS_PYTHON_VERSION != 3.4 && \
-            $TRAVIS_PYTHON_VERSION != 3.5 && \
-            $TRAVIS_PYTHON_VERSION != 3.9 ]]; then
+    elif [[ $TRAVIS_PYTHON_VERSION == 3.7 ]]; then
       pytype;
     fi

From 2b5902a15857287066108fdd204a790b3cca8887 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 22 Jan 2021 10:01:09 -0800
Subject: [PATCH 225/324] Travis pytype bug fix

PiperOrigin-RevId: 353263304
Change-Id: Ic4d40ba1aa60eb1c4433cc36e0fdb22af561efdf
---
 .travis.yml | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index de9d1c59..a7193ca1 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -19,8 +19,7 @@ script:
   - pip install ipython
   - python -m pytest  # Now run the tests with IPython.
   - pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console
-  - if [[ $TRAVIS_PYTHON_VERSION == 2.7 || \
-          $TRAVIS_PYTHON_VERSION == 3.7 ]]; then
+  - if [[ $TRAVIS_PYTHON_VERSION == 2.7 || $TRAVIS_PYTHON_VERSION == 3.7 ]]; then
       pip install pytype;
     fi
   # Run type-checking, excluding files that define or use py3 features in py2.

From 59eb4daeeef9e655610d786302f3d69080bbb15c Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Fri, 22 Jan 2021 11:43:57 -0800
Subject: [PATCH 226/324] Migrate Python Fire CI from Travis to Github Actions.

PiperOrigin-RevId: 353286136
Change-Id: I030d43b4c911137c2859b1d5dcd91e5831dc6b2d
---
 .github/scripts/build.sh     | 40 ++++++++++++++++++++++++++++++++++++
 .github/workflows/build.yaml | 28 +++++++++++++++++++++++++
 2 files changed, 68 insertions(+)
 create mode 100644 .github/scripts/build.sh
 create mode 100644 .github/workflows/build.yaml

diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh
new file mode 100644
index 00000000..ee768eb0
--- /dev/null
+++ b/.github/scripts/build.sh
@@ -0,0 +1,40 @@
+# 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.
+
+#!/usr/bin/env bash
+
+PYTHON_VERSION=${PYTHON_VERSION:-2.7}
+
+pip install --upgrade setuptools pip
+pip install --upgrade pylint pytest pytest-pylint pytest-runner
+pip install termcolor
+pip install hypothesis python-Levenshtein
+python setup.py develop
+python -m pytest  # Run the tests without IPython.
+pip install ipython
+python -m pytest  # Now run the tests with IPython.
+pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console
+if [[ ${PYTHON_VERSION} == 2.7 || ${PYTHON_VERSION} == 3.7 ]]; then
+  pip install pytype;
+fi
+# Run type-checking, excluding files that define or use py3 features in py2.
+if [[ ${PYTHON_VERSION} == 2.7 ]]; then
+  pytype -x \
+    fire/fire_test.py \
+    fire/inspectutils_test.py \
+    fire/test_components_py3.py;
+elif [[ ${PYTHON_VERSION} == 3.7 ]]; then
+  pytype;
+fi
+
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
new file mode 100644
index 00000000..b87d1f8a
--- /dev/null
+++ b/.github/workflows/build.yaml
@@ -0,0 +1,28 @@
+name: Python Fire
+
+on: [push]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: [2.7, 3.4, 3.5, 3.7, 3.8, 3.9]
+
+    steps:
+     # Checkout the repo.
+     - name: Checkout Python Fire repository
+       uses: actions/checkout@v2
+
+     # Set up Python environment.
+     - name: Set up Python ${{ matrix.python-version }}
+       uses: actions/setup-python@v2
+       with:
+         python-version: ${{ matrix.python-version }}
+
+     # Build Python Fire using the build.sh script.
+     - name: Run build script
+       shell: bash
+       run: ./.github/scripts/build.sh
+       env:
+         PYTHON_VERSION: ${{ matrix.python-version }}

From fbe1bf858f71cd3c38c730e16f865ac55d5b719e Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Fri, 22 Jan 2021 12:02:50 -0800
Subject: [PATCH 227/324] chmod +x .github/scripts/build.sh

PiperOrigin-RevId: 353290438
Change-Id: Ia5cf4c00333f8e7439edebce3fb2e366eef88462
---
 .github/scripts/build.sh | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 mode change 100644 => 100755 .github/scripts/build.sh

diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh
old mode 100644
new mode 100755

From c97ac311eeb478d5e0d723985f73f133891e7f00 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 22 Jan 2021 12:25:46 -0800
Subject: [PATCH 228/324] Trigger copybara Renames yaml to yml.

PiperOrigin-RevId: 353295857
Change-Id: Ifdd0b0dca8819257ae51bd1dc62690a05ae7fdcc
---
 .github/workflows/{build.yaml => build.yml} | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename .github/workflows/{build.yaml => build.yml} (100%)

diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yml
similarity index 100%
rename from .github/workflows/build.yaml
rename to .github/workflows/build.yml

From 58fd850862a10808460b654fb8b57074a324dde6 Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Fri, 22 Jan 2021 13:43:12 -0800
Subject: [PATCH 229/324] - Installed mock, required by the tests. - Exits the
 build shell script if any of the commands fails.

PiperOrigin-RevId: 353310695
Change-Id: I7e4df5de32ec4b3c8ca03ab6f5cf7a03f8f61856
---
 .github/scripts/build.sh | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh
index ee768eb0..2d5fcd10 100755
--- a/.github/scripts/build.sh
+++ b/.github/scripts/build.sh
@@ -14,12 +14,16 @@
 
 #!/usr/bin/env bash
 
+# Exit when any command fails.
+set -e
+
 PYTHON_VERSION=${PYTHON_VERSION:-2.7}
 
 pip install --upgrade setuptools pip
 pip install --upgrade pylint pytest pytest-pylint pytest-runner
 pip install termcolor
 pip install hypothesis python-Levenshtein
+pip install mock
 python setup.py develop
 python -m pytest  # Run the tests without IPython.
 pip install ipython

From 8e9b1d52132cfab5f789435a9d88bdf881bbbe22 Mon Sep 17 00:00:00 2001
From: Joe Chen <zuhaochen@google.com>
Date: Mon, 25 Jan 2021 10:21:24 -0800
Subject: [PATCH 230/324] - Removed travis config file. - Updated reference of
 travis to Github Actions.

PiperOrigin-RevId: 353675111
Change-Id: Ib1d357252e47acfa6cf03e82109fca6d6bf407da
---
 .travis.yml           | 33 ---------------------------------
 CONTRIBUTING.md       |  2 +-
 fire/helptext_test.py |  2 +-
 3 files changed, 2 insertions(+), 35 deletions(-)
 delete mode 100644 .travis.yml

diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index a7193ca1..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-language: python
-python:
-  - "2.7"
-  - "3.4"
-  - "3.5"
-  - "3.7"
-  - "3.8"
-  - "3.9"
-
-before_install:
-  - pip install --upgrade setuptools pip
-  - pip install --upgrade pylint pytest pytest-pylint pytest-runner
-install:
-  - pip install termcolor
-  - pip install hypothesis python-Levenshtein
-  - python setup.py develop
-script:
-  - python -m pytest  # Run the tests without IPython.
-  - pip install ipython
-  - python -m pytest  # Now run the tests with IPython.
-  - pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console
-  - if [[ $TRAVIS_PYTHON_VERSION == 2.7 || $TRAVIS_PYTHON_VERSION == 3.7 ]]; then
-      pip install pytype;
-    fi
-  # Run type-checking, excluding files that define or use py3 features in py2.
-  - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then
-      pytype -x
-        fire/fire_test.py
-        fire/inspectutils_test.py
-        fire/test_components_py3.py;
-    elif [[ $TRAVIS_PYTHON_VERSION == 3.7 ]]; then
-      pytype;
-    fi
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 08c2aef1..b3c758e1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -49,7 +49,7 @@ Python versions < 3.5.
 
 ## Testing
 
-Python Fire uses Travis CI to run tests on each pull request. You can run
+Python Fire uses [Github Actions](https://github.com/google/python-fire/actions) to run tests on each pull request. You can run
 these tests yourself as well. To do this, first install the test dependencies
 listed in setup.py (e.g. pytest, mock, termcolor, and hypothesis).
 Then run the tests by running `pytest` in the root directory of the repository.
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 3cb40fbe..29250cec 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -191,7 +191,7 @@ def testHelpTextFunctionWithLongTypes(self):
     self.assertIn('NAME\n    long_type', help_screen)
     self.assertIn('SYNOPSIS\n    long_type LONG_OBJ', help_screen)
     self.assertNotIn('DESCRIPTION', help_screen)
-    # TODO(dbieber): Assert type is displayed correctly. Type displays
+    # TODO(dbieber): Assert type is displayed correctly. Type displayed
     # differently in Travis vs in Google.
     # self.assertIn(
     #     'POSITIONAL ARGUMENTS\n    LONG_OBJ\n'

From c1266d0dbb2114514fcf8be62044344b5a51c733 Mon Sep 17 00:00:00 2001
From: Pratyush Raj <pratyushraj2fastandfurious@gmail.com>
Date: Mon, 12 Apr 2021 14:50:21 +0000
Subject: [PATCH 231/324] Clarify docs by giving the grouping example some
 output.

PiperOrigin-RevId: 367994276
---
 docs/guide.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/docs/guide.md b/docs/guide.md
index 909ec439..d5da3212 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -320,6 +320,7 @@ class Pipeline(object):
   def run(self):
     self.ingestion.run()
     self.digestion.run()
+    return 'Pipeline complete'
 
 if __name__ == '__main__':
   fire.Fire(Pipeline)

From ed44d8b801fc24e40729abef11b2dcbf6588d361 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 17 Jun 2021 11:02:53 -0700
Subject: [PATCH 232/324] Fix typos in test component docstrings.

PiperOrigin-RevId: 379999792
Change-Id: I2594db4540cfe20820951bae479d16c167ba391a
---
 fire/test_components.py     | 2 +-
 fire/test_components_py3.py | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/fire/test_components.py b/fire/test_components.py
index eee9a07c..027a6b19 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -98,7 +98,7 @@ def double(self, count=0):
       count: Input number that you want to double.
 
     Returns:
-      A number that is the double of count.s
+      A number that is the double of count.
     """
     return 2 * count
 
diff --git a/fire/test_components_py3.py b/fire/test_components_py3.py
index 3c21f4ba..b6c78c84 100644
--- a/fire/test_components_py3.py
+++ b/fire/test_components_py3.py
@@ -67,7 +67,7 @@ def double(self, count: float) -> float:
       count: Input number that you want to double.
 
     Returns:
-      A number that is the double of count.s
+      A number that is the double of count.
     """
     return 2 * count
 
@@ -89,7 +89,7 @@ def double(self, count: float = 0) -> float:
       count: Input number that you want to double.
 
     Returns:
-      A number that is the double of count.s
+      A number that is the double of count.
     """
     return 2 * count
 

From 703f8a2d59cfea11c5f816b785ae7b2ced31876c Mon Sep 17 00:00:00 2001
From: Yilei Yang <yileiyang@google.com>
Date: Thu, 14 Apr 2022 07:52:52 -0700
Subject: [PATCH 233/324] Remove unused comments related to Python 2
 compatibility.

PiperOrigin-RevId: 441760209
Change-Id: I32e87cdbd00d99ba2eb015c5709c2acceb38e53d
---
 fire/__main__.py            | 1 -
 fire/test_components_py3.py | 1 -
 fire/testutils.py           | 1 -
 3 files changed, 3 deletions(-)

diff --git a/fire/__main__.py b/fire/__main__.py
index 30d556e4..c7248e29 100644
--- a/fire/__main__.py
+++ b/fire/__main__.py
@@ -12,7 +12,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Lint as: python2, python3
 # pylint: disable=invalid-name
 """Enables use of Python Fire as a "main" function (i.e. "python -m fire").
 
diff --git a/fire/test_components_py3.py b/fire/test_components_py3.py
index b6c78c84..9f7590a0 100644
--- a/fire/test_components_py3.py
+++ b/fire/test_components_py3.py
@@ -12,7 +12,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Lint as: python3
 """This module has components that use Python 3 specific syntax."""
 
 import asyncio
diff --git a/fire/testutils.py b/fire/testutils.py
index 31a2badb..ea410e82 100644
--- a/fire/testutils.py
+++ b/fire/testutils.py
@@ -12,7 +12,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Lint as: python2, python3
 """Utilities for Python Fire's tests."""
 
 from __future__ import absolute_import

From 21ae57c38d00d8ba3594b94944bcd955977939df Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Sat, 16 Apr 2022 15:58:07 -0400
Subject: [PATCH 234/324] Remove testing for Python 3.4 (#388)

* Removes testing from CI for Python 3.4
* Adds lint disabling for new linter checks that have been added.
---
 .github/scripts/build.sh    | 2 +-
 .github/workflows/build.yml | 2 +-
 fire/__main__.py            | 2 +-
 fire/custom_descriptions.py | 4 ++--
 fire/main_test.py           | 4 ++--
 pylintrc                    | 2 +-
 setup.py                    | 1 -
 7 files changed, 8 insertions(+), 9 deletions(-)

diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh
index 2d5fcd10..ed75b37b 100755
--- a/.github/scripts/build.sh
+++ b/.github/scripts/build.sh
@@ -39,6 +39,6 @@ if [[ ${PYTHON_VERSION} == 2.7 ]]; then
     fire/inspectutils_test.py \
     fire/test_components_py3.py;
 elif [[ ${PYTHON_VERSION} == 3.7 ]]; then
-  pytype;
+  pytype -x fire/test_components_py3.py;
 fi
 
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index b87d1f8a..b934b759 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -7,7 +7,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [2.7, 3.4, 3.5, 3.7, 3.8, 3.9]
+        python-version: [2.7, 3.5, 3.7, 3.8, 3.9]
 
     steps:
      # Checkout the repo.
diff --git a/fire/__main__.py b/fire/__main__.py
index c7248e29..2ad217d6 100644
--- a/fire/__main__.py
+++ b/fire/__main__.py
@@ -80,7 +80,7 @@ def import_from_file_path(path):
     spec.loader.exec_module(module)  # pytype: disable=attribute-error
 
   else:
-    import imp  # pylint: disable=g-import-not-at-top,import-outside-toplevel
+    import imp  # pylint: disable=g-import-not-at-top,import-outside-toplevel,deprecated-module
     module = imp.load_source(module_name, path)
 
   return module, module_name
diff --git a/fire/custom_descriptions.py b/fire/custom_descriptions.py
index 191e8b29..865a528e 100644
--- a/fire/custom_descriptions.py
+++ b/fire/custom_descriptions.py
@@ -137,7 +137,7 @@ def GetStringTypeDescription(obj, available_space, line_length):
 
 def GetSummary(obj, available_space, line_length):
   obj_type_name = type(obj).__name__
-  if obj_type_name in CUSTOM_DESC_SUM_FN_DICT.keys():
+  if obj_type_name in CUSTOM_DESC_SUM_FN_DICT:
     return CUSTOM_DESC_SUM_FN_DICT.get(obj_type_name)[0](obj, available_space,
                                                          line_length)
   return None
@@ -145,7 +145,7 @@ def GetSummary(obj, available_space, line_length):
 
 def GetDescription(obj, available_space, line_length):
   obj_type_name = type(obj).__name__
-  if obj_type_name in CUSTOM_DESC_SUM_FN_DICT.keys():
+  if obj_type_name in CUSTOM_DESC_SUM_FN_DICT:
     return CUSTOM_DESC_SUM_FN_DICT.get(obj_type_name)[1](obj, available_space,
                                                          line_length)
   return None
diff --git a/fire/main_test.py b/fire/main_test.py
index 41699ac6..a0184620 100644
--- a/fire/main_test.py
+++ b/fire/main_test.py
@@ -44,11 +44,11 @@ class MainModuleFileTest(testutils.BaseTestCase):
 
   def setUp(self):
     super(MainModuleFileTest, self).setUp()
-    self.file = tempfile.NamedTemporaryFile(suffix='.py')
+    self.file = tempfile.NamedTemporaryFile(suffix='.py')  # pylint: disable=consider-using-with
     self.file.write(b'class Foo:\n  def double(self, n):\n    return 2 * n\n')
     self.file.flush()
 
-    self.file2 = tempfile.NamedTemporaryFile()
+    self.file2 = tempfile.NamedTemporaryFile()  # pylint: disable=consider-using-with
 
   def testFileNameFire(self):
     # Confirm that the file is correctly imported and doubles the number.
diff --git a/pylintrc b/pylintrc
index 1b1c5cc2..fa054fb5 100644
--- a/pylintrc
+++ b/pylintrc
@@ -32,7 +32,7 @@ enable=indexing-exception,old-raise-syntax
 # Disable the message, report, category or checker with the given id(s). You
 # can either give multiple identifier separated by comma (,) or put this option
 # multiple time.
-disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return,super-with-arguments,raise-missing-from
+disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return,super-with-arguments,raise-missing-from,consider-using-f-string,unspecified-encoding
 
 
 [REPORTS]
diff --git a/setup.py b/setup.py
index 1cc64a07..e1efe1ab 100644
--- a/setup.py
+++ b/setup.py
@@ -66,7 +66,6 @@
         'Programming Language :: Python :: 2',
         'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: 3.7',

From 09f836e8db7147472192b6c5468f0b7bc997cfad Mon Sep 17 00:00:00 2001
From: Xavier Figueroa <xavierfigueroav@gmail.com>
Date: Sat, 16 Apr 2022 15:20:32 -0500
Subject: [PATCH 235/324] Fix #315 Change the _KeywordOnlyArguments filtering
 condition (#316)

* Change the _KeywordOnlyArguments filtering condition
* Add test case for usage output of function with mixed defaults

Signed-off-by: Xavier Figueroa <xavierfigueroav@gmail.com>
---
 fire/helptext.py        |  2 +-
 fire/helptext_test.py   | 14 ++++++++++++++
 fire/test_components.py |  3 +++
 3 files changed, 18 insertions(+), 1 deletion(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index b1d10b44..5098b26b 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -700,7 +700,7 @@ def _GetCallableUsageItems(spec, metadata):
 
 def _KeywordOnlyArguments(spec, required=True):
   return (flag for flag in spec.kwonlyargs
-          if required == (flag in spec.kwonlydefaults))
+          if required != (flag in spec.kwonlydefaults))
 
 
 def _GetCallableAvailabilityLines(spec):
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 29250cec..81600965 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -497,6 +497,20 @@ def testUsageOutputFunctionWithDocstring(self):
         textwrap.dedent(expected_output).lstrip('\n'),
         usage_output)
 
+  def testUsageOutputFunctionMixedDefaults(self):
+    component = tc.MixedDefaults().identity2
+    t = trace.FireTrace(component, name='FunctionMixedDefaults')
+    usage_output = helptext.UsageText(component, trace=t, verbose=False)
+    expected_output = """
+    Usage: FunctionMixedDefaults <flags>
+      optional flags:        --beta
+      required flags:        --alpha
+
+    For detailed information on this command, run:
+      FunctionMixedDefaults --help"""
+    expected_output = textwrap.dedent(expected_output).lstrip('\n')
+    self.assertEqual(expected_output, usage_output)
+
   def testUsageOutputCallable(self):
     # This is both a group and a command.
     component = tc.CallableWithKeywordArgument()
diff --git a/fire/test_components.py b/fire/test_components.py
index 027a6b19..f29021a9 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -133,6 +133,9 @@ def sum(self, alpha=0, beta=0):
   def identity(self, alpha, beta='0'):
     return alpha, beta
 
+  def identity2(self, *, alpha, beta='0'):
+    return alpha, beta
+
 
 class SimilarArgNames(object):
 

From 8469e487dcb9392856a513f6f861eaf86da80595 Mon Sep 17 00:00:00 2001
From: David Bieber <david810@gmail.com>
Date: Sat, 16 Apr 2022 16:42:02 -0400
Subject: [PATCH 236/324] Move python 3 only component to
 test_components_py3.py (#389)

* #316 adds a test using Python 3 only features; this CL makes that test only run for Python 3 versions.
---
 .github/scripts/build.sh    | 12 +++---------
 fire/helptext_test.py       |  5 ++++-
 fire/test_components.py     |  3 ---
 fire/test_components_py3.py |  6 ++++++
 4 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh
index ed75b37b..c05ea1c3 100755
--- a/.github/scripts/build.sh
+++ b/.github/scripts/build.sh
@@ -29,16 +29,10 @@ python -m pytest  # Run the tests without IPython.
 pip install ipython
 python -m pytest  # Now run the tests with IPython.
 pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console
-if [[ ${PYTHON_VERSION} == 2.7 || ${PYTHON_VERSION} == 3.7 ]]; then
+if [[ ${PYTHON_VERSION} == 3.7 ]]; then
   pip install pytype;
 fi
-# Run type-checking, excluding files that define or use py3 features in py2.
-if [[ ${PYTHON_VERSION} == 2.7 ]]; then
-  pytype -x \
-    fire/fire_test.py \
-    fire/inspectutils_test.py \
-    fire/test_components_py3.py;
-elif [[ ${PYTHON_VERSION} == 3.7 ]]; then
+# Run type-checking.
+if [[ ${PYTHON_VERSION} == 3.7 ]]; then
   pytype -x fire/test_components_py3.py;
 fi
-
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 81600965..03ade4a5 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -497,8 +497,11 @@ def testUsageOutputFunctionWithDocstring(self):
         textwrap.dedent(expected_output).lstrip('\n'),
         usage_output)
 
+  @testutils.skipIf(
+      six.PY2,
+      'Python 2 does not support required name-only arguments.')
   def testUsageOutputFunctionMixedDefaults(self):
-    component = tc.MixedDefaults().identity2
+    component = tc.py3.HelpTextComponent().identity
     t = trace.FireTrace(component, name='FunctionMixedDefaults')
     usage_output = helptext.UsageText(component, trace=t, verbose=False)
     expected_output = """
diff --git a/fire/test_components.py b/fire/test_components.py
index f29021a9..027a6b19 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -133,9 +133,6 @@ def sum(self, alpha=0, beta=0):
   def identity(self, alpha, beta='0'):
     return alpha, beta
 
-  def identity2(self, *, alpha, beta='0'):
-    return alpha, beta
-
 
 class SimilarArgNames(object):
 
diff --git a/fire/test_components_py3.py b/fire/test_components_py3.py
index 9f7590a0..5140921d 100644
--- a/fire/test_components_py3.py
+++ b/fire/test_components_py3.py
@@ -25,6 +25,12 @@ def identity(arg1, arg2: int, arg3=10, arg4: int = 20, *arg5,
   return arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10
 
 
+class HelpTextComponent:
+
+  def identity(self, *, alpha, beta='0'):
+    return alpha, beta
+
+
 class KeywordOnly(object):
 
   def double(self, *, count):

From 8bddeec6bd9c78b9b5ad20e9a58625cbd3096fe9 Mon Sep 17 00:00:00 2001
From: Bea Steers <bea.steers@gmail.com>
Date: Sat, 16 Apr 2022 17:21:05 -0400
Subject: [PATCH 237/324] Add custom formatter for Fire result (#345)

Fixes #344 (see issue for more details)

This lets you define a function that will take the result from the Fire component and allows the user to alter it before fire looks at it to render it.
---
 fire/core.py      | 13 ++++++++++---
 fire/core_test.py | 24 ++++++++++++++++++++++++
 2 files changed, 34 insertions(+), 3 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index 8ca142c7..6fd1bf7a 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -78,7 +78,7 @@ def main(argv):
   import asyncio  # pylint: disable=import-error,g-import-not-at-top  # pytype: disable=import-error
 
 
-def Fire(component=None, command=None, name=None):
+def Fire(component=None, command=None, name=None, serialize=None):
   """This function, Fire, is the main entrypoint for Python Fire.
 
   Executes a command either from the `command` argument or from sys.argv by
@@ -164,7 +164,7 @@ def Fire(component=None, command=None, name=None):
     raise FireExit(0, component_trace)
 
   # The command succeeded normally; print the result.
-  _PrintResult(component_trace, verbose=component_trace.verbose)
+  _PrintResult(component_trace, verbose=component_trace.verbose, serialize=serialize)
   result = component_trace.GetResult()
   return result
 
@@ -241,12 +241,19 @@ def _IsHelpShortcut(component_trace, remaining_args):
   return show_help
 
 
-def _PrintResult(component_trace, verbose=False):
+def _PrintResult(component_trace, verbose=False, serialize=None):
   """Prints the result of the Fire call to stdout in a human readable way."""
   # TODO(dbieber): Design human readable deserializable serialization method
   # and move serialization to its own module.
   result = component_trace.GetResult()
 
+  # Allow users to modify the return value of the component and provide 
+  # custom formatting.
+  if serialize:
+    if not callable(serialize):
+      raise FireError("serialize argument {} must be empty or callable.".format(serialize))
+    result = serialize(result)
+
   if value_types.HasCustomStr(result):
     # If the object has a custom __str__ method, rather than one inherited from
     # object, then we use that to serialize the object.
diff --git a/fire/core_test.py b/fire/core_test.py
index 27c9f418..a0576ee9 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -194,6 +194,30 @@ def testClassMethod(self):
         7,
     )
 
+  def testCustomSerialize(self):
+    def serialize(x):
+      if isinstance(x, list):
+        return ', '.join(str(xi) for xi in x)
+      if isinstance(x, dict):
+        return ', '.join('{}={!r}'.format(k, v) for k, v in x.items())
+      if x == 'special':
+        return ['SURPRISE!!', "I'm a list!"]
+      return x
+
+    ident = lambda x: x
+    
+    with self.assertOutputMatches(stdout='a, b', stderr=None):
+      result = core.Fire(ident, command=['[a,b]'], serialize=serialize)
+    with self.assertOutputMatches(stdout='a=5, b=6', stderr=None):
+      result = core.Fire(ident, command=['{a:5,b:6}'], serialize=serialize)
+    with self.assertOutputMatches(stdout='asdf', stderr=None):
+      result = core.Fire(ident, command=['asdf'], serialize=serialize)
+    with self.assertOutputMatches(stdout="SURPRISE!!\nI'm a list!\n", stderr=None):
+      result = core.Fire(ident, command=['special'], serialize=serialize)
+    with self.assertRaises(core.FireError):
+      core.Fire(ident, command=['asdf'], serialize=55)
+
+
   @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.')
   def testLruCacheDecoratorBoundArg(self):
     self.assertEqual(

From 37c4305194ff3d0a63f435f08a40e14b1978bd4e Mon Sep 17 00:00:00 2001
From: David Bieber <david810@gmail.com>
Date: Sat, 16 Apr 2022 18:20:08 -0400
Subject: [PATCH 238/324] Lint error cleanup following #345 (#390)

* Lint error cleanup following #345
* Makes new serialize= test deterministic
---
 fire/core.py      |  8 +++++---
 fire/core_test.py | 15 ++++++++-------
 2 files changed, 13 insertions(+), 10 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index 6fd1bf7a..4546b273 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -164,7 +164,8 @@ def Fire(component=None, command=None, name=None, serialize=None):
     raise FireExit(0, component_trace)
 
   # The command succeeded normally; print the result.
-  _PrintResult(component_trace, verbose=component_trace.verbose, serialize=serialize)
+  _PrintResult(
+      component_trace, verbose=component_trace.verbose, serialize=serialize)
   result = component_trace.GetResult()
   return result
 
@@ -247,11 +248,12 @@ def _PrintResult(component_trace, verbose=False, serialize=None):
   # and move serialization to its own module.
   result = component_trace.GetResult()
 
-  # Allow users to modify the return value of the component and provide 
+  # Allow users to modify the return value of the component and provide
   # custom formatting.
   if serialize:
     if not callable(serialize):
-      raise FireError("serialize argument {} must be empty or callable.".format(serialize))
+      raise FireError(
+          'The argument `serialize` must be empty or callable:', serialize)
     result = serialize(result)
 
   if value_types.HasCustomStr(result):
diff --git a/fire/core_test.py b/fire/core_test.py
index a0576ee9..0d11467e 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -199,21 +199,22 @@ def serialize(x):
       if isinstance(x, list):
         return ', '.join(str(xi) for xi in x)
       if isinstance(x, dict):
-        return ', '.join('{}={!r}'.format(k, v) for k, v in x.items())
+        return ', '.join('{}={!r}'.format(k, v) for k, v in sorted(x.items()))
       if x == 'special':
         return ['SURPRISE!!', "I'm a list!"]
       return x
 
     ident = lambda x: x
-    
+
     with self.assertOutputMatches(stdout='a, b', stderr=None):
-      result = core.Fire(ident, command=['[a,b]'], serialize=serialize)
+      _ = core.Fire(ident, command=['[a,b]'], serialize=serialize)
     with self.assertOutputMatches(stdout='a=5, b=6', stderr=None):
-      result = core.Fire(ident, command=['{a:5,b:6}'], serialize=serialize)
+      _ = core.Fire(ident, command=['{a:5,b:6}'], serialize=serialize)
     with self.assertOutputMatches(stdout='asdf', stderr=None):
-      result = core.Fire(ident, command=['asdf'], serialize=serialize)
-    with self.assertOutputMatches(stdout="SURPRISE!!\nI'm a list!\n", stderr=None):
-      result = core.Fire(ident, command=['special'], serialize=serialize)
+      _ = core.Fire(ident, command=['asdf'], serialize=serialize)
+    with self.assertOutputMatches(
+        stdout="SURPRISE!!\nI'm a list!\n", stderr=None):
+      _ = core.Fire(ident, command=['special'], serialize=serialize)
     with self.assertRaises(core.FireError):
       core.Fire(ident, command=['asdf'], serialize=55)
 

From c367ce99aa9b8e69abb48d3296cac45b33638bb8 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 28 Nov 2022 16:09:56 -0500
Subject: [PATCH 239/324] Bring continuous integration to working state (#415)

Contained in this PR:

* Use latest versions of checkout and python setup GitHub actions
* Fix Ubuntu version since not all Python versions are available on ubuntu-latest
* Lint fixes: use setattr directly instead of via dunder-method, and ignore unnecessary lambda lint warnings.
* Loosen formatting tests
---
 .github/workflows/build.yml | 6 +++---
 fire/core.py                | 3 ++-
 fire/formatting_test.py     | 4 ++--
 fire/parser.py              | 2 +-
 pylintrc                    | 2 +-
 5 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index b934b759..bc5e0405 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -4,7 +4,7 @@ on: [push]
 
 jobs:
   build:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     strategy:
       matrix:
         python-version: [2.7, 3.5, 3.7, 3.8, 3.9]
@@ -12,11 +12,11 @@ jobs:
     steps:
      # Checkout the repo.
      - name: Checkout Python Fire repository
-       uses: actions/checkout@v2
+       uses: actions/checkout@v3
 
      # Set up Python environment.
      - name: Set up Python ${{ matrix.python-version }}
-       uses: actions/setup-python@v2
+       uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}
 
diff --git a/fire/core.py b/fire/core.py
index 4546b273..c1e97367 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -525,7 +525,8 @@ def _Fire(component, args, parsed_flag_args, context, name=None):
         # The target isn't present in the dict as a string key, but maybe it is
         # a key as another type.
         # TODO(dbieber): Consider alternatives for accessing non-string keys.
-        for key, value in component_dict.items():
+        for key, value in (
+            component_dict.items()):  # pytype: disable=attribute-error
           if target == str(key):
             component = value
             handled = True
diff --git a/fire/formatting_test.py b/fire/formatting_test.py
index 61cce0e8..05a88c49 100644
--- a/fire/formatting_test.py
+++ b/fire/formatting_test.py
@@ -28,11 +28,11 @@ class FormattingTest(testutils.BaseTestCase):
 
   def test_bold(self):
     text = formatting.Bold('hello')
-    self.assertEqual('\x1b[1mhello\x1b[0m', text)
+    self.assertIn(text, ['hello', '\x1b[1mhello\x1b[0m'])
 
   def test_underline(self):
     text = formatting.Underline('hello')
-    self.assertEqual('\x1b[4mhello\x1b[0m', text)
+    self.assertIn(text, ['hello', '\x1b[4mhello\x1b[0m'])
 
   def test_indent(self):
     text = formatting.Indent('hello', spaces=2)
diff --git a/fire/parser.py b/fire/parser.py
index 404e18e7..2aff8bd7 100644
--- a/fire/parser.py
+++ b/fire/parser.py
@@ -106,7 +106,7 @@ def _LiteralEval(value):
 
       elif isinstance(child, ast.Name):
         replacement = _Replacement(child)
-        node.__setattr__(field, replacement)
+        setattr(node, field, replacement)
 
   # ast.literal_eval supports the following types:
   # strings, bytes, numbers, tuples, lists, dicts, sets, booleans, and None
diff --git a/pylintrc b/pylintrc
index fa054fb5..b89b16d1 100644
--- a/pylintrc
+++ b/pylintrc
@@ -32,7 +32,7 @@ enable=indexing-exception,old-raise-syntax
 # Disable the message, report, category or checker with the given id(s). You
 # can either give multiple identifier separated by comma (,) or put this option
 # multiple time.
-disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return,super-with-arguments,raise-missing-from,consider-using-f-string,unspecified-encoding
+disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return,super-with-arguments,raise-missing-from,consider-using-f-string,unspecified-encoding,unnecessary-lambda-assignment
 
 
 [REPORTS]

From c4bd14b45a3a68574cd30ce6699312ebf66ae0b3 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 28 Nov 2022 16:13:34 -0500
Subject: [PATCH 240/324] Python Fire Version Bump (#416)

Bumps version to 0.5.0 in preparation for next release
---
 .github/scripts/build.sh | 4 +---
 fire/__init__.py         | 2 +-
 setup.py                 | 2 +-
 3 files changed, 3 insertions(+), 5 deletions(-)

diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh
index c05ea1c3..6fd8f73b 100755
--- a/.github/scripts/build.sh
+++ b/.github/scripts/build.sh
@@ -30,9 +30,7 @@ pip install ipython
 python -m pytest  # Now run the tests with IPython.
 pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console
 if [[ ${PYTHON_VERSION} == 3.7 ]]; then
+  # Run type-checking.
   pip install pytype;
-fi
-# Run type-checking.
-if [[ ${PYTHON_VERSION} == 3.7 ]]; then
   pytype -x fire/test_components_py3.py;
 fi
diff --git a/fire/__init__.py b/fire/__init__.py
index 9c34a5af..4cc76210 100644
--- a/fire/__init__.py
+++ b/fire/__init__.py
@@ -21,4 +21,4 @@
 from fire.core import Fire
 
 __all__ = ['Fire']
-__version__ = '0.4.0'
+__version__ = '0.5.0'
diff --git a/setup.py b/setup.py
index e1efe1ab..f1f91103 100644
--- a/setup.py
+++ b/setup.py
@@ -40,7 +40,7 @@
     'python-Levenshtein',
 ]
 
-VERSION = '0.4.0'
+VERSION = '0.5.0'
 URL = 'https://github.com/google/python-fire'
 
 setup(

From a49184782c37746278e42e8a175dfab4fe89db8a Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 28 Nov 2022 16:46:26 -0500
Subject: [PATCH 241/324] Show default values for kwonly arguments in help text
 (#414)

Fixes #410

* Shows default values for kwonly arguments in help text
* Adds test verifying that default values are shown for kwonly args
---
 fire/helptext.py      |  2 ++
 fire/helptext_test.py | 12 ++++++++++++
 2 files changed, 14 insertions(+)

diff --git a/fire/helptext.py b/fire/helptext.py
index 5098b26b..331b6649 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -524,6 +524,8 @@ def _GetArgDefault(flag, spec):
   for arg, default in zip(args_with_defaults, spec.defaults):
     if arg == flag:
       return repr(default)
+  if flag in spec.kwonlydefaults:
+    return repr(spec.kwonlydefaults[flag])
   return ''
 
 
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 03ade4a5..14e0874a 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -296,6 +296,18 @@ def testHelpTextKeywordOnlyArgumentsWithoutDefault(self):
     self.assertIn('NAME\n    double', output)
     self.assertIn('FLAGS\n    --count=COUNT (required)', output)
 
+  @testutils.skipIf(
+      six.PY2,
+      'Python 2 does not support required name-only arguments.')
+  def testHelpTextFunctionMixedDefaults(self):
+    component = tc.py3.HelpTextComponent().identity
+    t = trace.FireTrace(component, name='FunctionMixedDefaults')
+    output = helptext.HelpText(component, trace=t)
+    self.assertIn('NAME\n    FunctionMixedDefaults', output)
+    self.assertIn('FunctionMixedDefaults <flags>', output)
+    self.assertIn('--alpha=ALPHA (required)', output)
+    self.assertIn('--beta=BETA\n        Default: \'0\'', output)
+
   def testHelpScreen(self):
     component = tc.ClassWithDocstring()
     t = trace.FireTrace(component, name='ClassWithDocstring')

From 82e2d87f0e2a01e9c01e958dd2d2ec510a77fc6d Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 28 Nov 2022 16:49:17 -0500
Subject: [PATCH 242/324] Remove extra newline (#417)

Remove extra newline in core_test.py #417
---
 fire/core_test.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/fire/core_test.py b/fire/core_test.py
index 0d11467e..75b76998 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -218,7 +218,6 @@ def serialize(x):
     with self.assertRaises(core.FireError):
       core.Fire(ident, command=['asdf'], serialize=55)
 
-
   @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.')
   def testLruCacheDecoratorBoundArg(self):
     self.assertEqual(

From e5b33f6a15b3a2ded83f924079b9c2645983bd05 Mon Sep 17 00:00:00 2001
From: kpakda <5531544+khodadadp@users.noreply.github.com>
Date: Fri, 9 Dec 2022 15:16:49 -0500
Subject: [PATCH 243/324] Update guide.md (#387)

Should either print the returned value or join and return. Otherwise, the output in the example won't get generated.
---
 docs/guide.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/guide.md b/docs/guide.md
index d5da3212..8e3f03c9 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -318,8 +318,8 @@ class Pipeline(object):
     self.digestion = DigestionStage()
 
   def run(self):
-    self.ingestion.run()
-    self.digestion.run()
+    print(self.ingestion.run())
+    print(self.digestion.run())
     return 'Pipeline complete'
 
 if __name__ == '__main__':

From 1eec95480568e4f67a0ce287b10c977d269a5c49 Mon Sep 17 00:00:00 2001
From: Martin Gruber <martin.gruber1@web.de>
Date: Fri, 9 Dec 2022 21:18:54 +0100
Subject: [PATCH 244/324] Interactive mode: Mention IPython REPL requirement
 (#383)

Mention that the ipython package is an optional requirement for using the IPython REPL, as described in the [source file](https://github.com/google/python-fire/blob/master/fire/interact.py#L17).
---
 docs/using-cli.md | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/docs/using-cli.md b/docs/using-cli.md
index 0f369a9a..ba1c35dc 100644
--- a/docs/using-cli.md
+++ b/docs/using-cli.md
@@ -137,6 +137,9 @@ will put you in an IPython REPL, with the variable `widget` already defined.
 You can then explore the Python object that `widget` corresponds to
 interactively using Python.
 
+Note: if you want fire to start the IPython REPL instead of the regular Python one,
+the `ipython` package needs to be installed in your environment.
+
 
 ### `--completion`: Generating a completion script <a name="completion-flag"></a>
 

From c3703f443c7ae5603538eb4fa4354787b9fb7c88 Mon Sep 17 00:00:00 2001
From: Martin Gruber <martin.gruber1@web.de>
Date: Fri, 9 Dec 2022 21:19:49 +0100
Subject: [PATCH 245/324] Interactive mode: Mention IPython REPL requirement
 (#383)

Mention that the ipython package is an optional requirement for using the IPython REPL, as described in the [source file](https://github.com/google/python-fire/blob/master/fire/interact.py#L17).

From 0ad057143b326f78398fbc9a3840d9d6b378e0be Mon Sep 17 00:00:00 2001
From: Conor Sheehan <conor.sheehan.dev@gmail.com>
Date: Fri, 9 Dec 2022 20:31:12 +0000
Subject: [PATCH 246/324] Fix #317 Help text short args (#318)

* Add short args to help text for all types of kwargs.
* Updates tests to reflect new help text.
---
 fire/helptext.py        | 58 +++++++++++++++++++++++++++++++++++------
 fire/helptext_test.py   | 36 ++++++++++++++++++-------
 fire/test_components.py | 10 +++++++
 3 files changed, 87 insertions(+), 17 deletions(-)

diff --git a/fire/helptext.py b/fire/helptext.py
index 331b6649..6e7fbb07 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -33,6 +33,7 @@
 from __future__ import division
 from __future__ import print_function
 
+import collections
 import itertools
 import sys
 
@@ -172,9 +173,25 @@ def _DescriptionSection(component, info):
     return None
 
 
-def _CreateKeywordOnlyFlagItem(flag, docstring_info, spec):
+def _CreateKeywordOnlyFlagItem(flag, docstring_info, spec, short_arg):
   return _CreateFlagItem(
-      flag, docstring_info, spec, required=flag not in spec.kwonlydefaults)
+      flag, docstring_info, spec, required=flag not in spec.kwonlydefaults,
+      short_arg=short_arg)
+
+
+def _GetShortFlags(flags):
+  """Gets a list of single-character flags that uniquely identify a flag.
+
+  Args:
+    flags: list of strings representing flags
+
+  Returns:
+    List of single character short flags,
+    where the character occurred at the start of a flag once.
+  """
+  short_flags = [f[0] for f in flags]
+  short_flag_counts = collections.Counter(short_flags)
+  return [v for v in short_flags if short_flag_counts[v] == 1]
 
 
 def _ArgsAndFlagsSections(info, spec, metadata):
@@ -209,25 +226,47 @@ def _ArgsAndFlagsSections(info, spec, metadata):
           ('NOTES', 'You can also use flags syntax for POSITIONAL ARGUMENTS')
       )
 
+  unique_short_args = _GetShortFlags(args_with_defaults)
   positional_flag_items = [
-      _CreateFlagItem(flag, docstring_info, spec, required=False)
+      _CreateFlagItem(
+          flag, docstring_info, spec, required=False,
+          short_arg=flag[0] in unique_short_args
+      )
       for flag in args_with_defaults
   ]
+
+  unique_short_kwonly_flags = _GetShortFlags(spec.kwonlyargs)
   kwonly_flag_items = [
-      _CreateKeywordOnlyFlagItem(flag, docstring_info, spec)
+      _CreateKeywordOnlyFlagItem(
+          flag, docstring_info, spec,
+          short_arg=flag[0] in unique_short_kwonly_flags
+      )
       for flag in spec.kwonlyargs
   ]
   flag_items = positional_flag_items + kwonly_flag_items
 
   if spec.varkw:
     # Include kwargs documented via :key param:
-    flag_string = '--{name}'
     documented_kwargs = []
-    for flag in docstring_info.args or []:
+    flag_string = '--{name}'
+    short_flag_string = '-{short_name}, --{name}'
+
+    # add short flags if possible
+    flags = docstring_info.args or []
+    flag_names = [f.name for f in flags]
+    unique_short_flags = _GetShortFlags(flag_names)
+    for flag in flags:
       if isinstance(flag, docstrings.KwargInfo):
+        if flag.name[0] in unique_short_flags:
+          flag_string = short_flag_string.format(
+              name=flag.name, short_name=flag.name[0]
+          )
+        else:
+          flag_string = flag_string.format(name=flag.name)
+
         flag_item = _CreateFlagItem(
             flag.name, docstring_info, spec,
-            flag_string=flag_string.format(name=flag.name))
+            flag_string=flag_string)
         documented_kwargs.append(flag_item)
     if documented_kwargs:
       # Separate documented kwargs from other flags using a message
@@ -422,7 +461,7 @@ def _CreateArgItem(arg, docstring_info, spec):
 
 
 def _CreateFlagItem(flag, docstring_info, spec, required=False,
-                    flag_string=None):
+                    flag_string=None, short_arg=False):
   """Returns a string describing a flag using docstring and FullArgSpec info.
 
   Args:
@@ -434,6 +473,7 @@ def _CreateFlagItem(flag, docstring_info, spec, required=False,
     required: Whether the flag is required.
     flag_string: If provided, use this string for the flag, rather than
       constructing one from the flag name.
+    short_arg: Whether the flag has a short variation or not.
   Returns:
     A string to be used in constructing the help screen for the function.
   """
@@ -455,6 +495,8 @@ def _CreateFlagItem(flag, docstring_info, spec, required=False,
         flag_name_upper=formatting.Underline(flag.upper()))
   if required:
     flag_string += ' (required)'
+  if short_arg:
+    flag_string = '-{short_flag}, '.format(short_flag=flag[0]) + flag_string
 
   arg_type = _GetArgType(flag, spec)
   arg_default = _GetArgDefault(flag, spec)
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 14e0874a..404d9812 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -81,7 +81,9 @@ 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\n        Default: 0', help_screen)
+    self.assertIn(
+        'FLAGS\n    -c, --count=COUNT\n        Default: 0',
+        help_screen)
     self.assertNotIn('NOTES', help_screen)
 
   def testHelpTextFunctionWithLongDefaults(self):
@@ -93,7 +95,7 @@ def testHelpTextFunctionWithLongDefaults(self):
     self.assertIn('SYNOPSIS\n    text <flags>', help_screen)
     self.assertNotIn('DESCRIPTION', help_screen)
     self.assertIn(
-        'FLAGS\n    --string=STRING\n'
+        'FLAGS\n    -s, --string=STRING\n'
         '        Default: \'0001020304050607080910'
         '1112131415161718192021222324252627282...',
         help_screen)
@@ -121,7 +123,7 @@ def testHelpTextFunctionWithKwargsAndDefaults(self):
     self.assertIn('SYNOPSIS\n    text ARG1 ARG2 <flags>', help_screen)
     self.assertIn('DESCRIPTION\n    Function with kwarg', help_screen)
     self.assertIn(
-        'FLAGS\n    --opt=OPT\n        Default: True\n'
+        'FLAGS\n    -o, --opt=OPT\n        Default: True\n'
         '    The following flags are also accepted.'
         '\n    --arg3\n        Description of arg3.\n    '
         'Additional undocumented flags may also be accepted.',
@@ -140,7 +142,7 @@ def testHelpTextFunctionWithDefaultsAndTypes(self):
     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',
+        'FLAGS\n    -c, --count=COUNT\n        Type: float\n        Default: 0',
         help_screen)
     self.assertNotIn('NOTES', help_screen)
 
@@ -157,7 +159,7 @@ def testHelpTextFunctionWithTypesAndDefaultNone(self):
     self.assertIn('SYNOPSIS\n    get_int <flags>', help_screen)
     self.assertNotIn('DESCRIPTION', help_screen)
     self.assertIn(
-        'FLAGS\n    --value=VALUE\n'
+        'FLAGS\n    -v, --value=VALUE\n'
         '        Type: Optional[int]\n        Default: None',
         help_screen)
     self.assertNotIn('NOTES', help_screen)
@@ -285,7 +287,7 @@ def testHelpTextKeywordOnlyArgumentsWithDefault(self):
     output = helptext.HelpText(
         component=component, trace=trace.FireTrace(component, 'with_default'))
     self.assertIn('NAME\n    with_default', output)
-    self.assertIn('FLAGS\n    --x=X', output)
+    self.assertIn('FLAGS\n    -x, --x=X', output)
 
   @testutils.skipIf(
       six.PY2, 'Python 2 does not support keyword-only arguments.')
@@ -294,7 +296,7 @@ def testHelpTextKeywordOnlyArgumentsWithoutDefault(self):
     output = helptext.HelpText(
         component=component, trace=trace.FireTrace(component, 'double'))
     self.assertIn('NAME\n    double', output)
-    self.assertIn('FLAGS\n    --count=COUNT (required)', output)
+    self.assertIn('FLAGS\n    -c, --count=COUNT (required)', output)
 
   @testutils.skipIf(
       six.PY2,
@@ -374,7 +376,7 @@ def testHelpScreenForFunctionFunctionWithDefaultArgs(self):
         Returns the input multiplied by 2.
 
     FLAGS
-        --count=COUNT
+        -c, --count=COUNT
             Default: 0
             Input number that you want to double."""
     self.assertEqual(textwrap.dedent(expected_output).strip(),
@@ -389,7 +391,8 @@ def testHelpTextUnderlineFlag(self):
         formatting.Bold('SYNOPSIS') + '\n    triple <flags>',
         help_screen)
     self.assertIn(
-        formatting.Bold('FLAGS') + '\n    --' + formatting.Underline('count'),
+        formatting.Bold('FLAGS') + '\n    -c, --' +
+        formatting.Underline('count'),
         help_screen)
 
   def testHelpTextBoldCommandName(self):
@@ -435,6 +438,21 @@ def testHelpTextNameSectionCommandWithSeparatorVerbose(self):
     self.assertIn('double -', help_screen)
     self.assertIn('double - -', help_screen)
 
+  def testHelpTextMultipleKeywoardArgumentsWithShortArgs(self):
+    component = tc.fn_with_multiple_defaults
+    t = trace.FireTrace(component, name='shortargs')
+    help_screen = helptext.HelpText(component, t)
+    self.assertIn(formatting.Bold('NAME') + '\n    shortargs', help_screen)
+    self.assertIn(
+        formatting.Bold('SYNOPSIS') + '\n    shortargs <flags>',
+        help_screen)
+    self.assertIn(
+        formatting.Bold('FLAGS') + '\n    -f, --first',
+        help_screen)
+    self.assertIn('\n    --last', help_screen)
+    self.assertIn('\n    --late', help_screen)
+
+
 
 class UsageTest(testutils.BaseTestCase):
 
diff --git a/fire/test_components.py b/fire/test_components.py
index 027a6b19..5fcb056e 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -563,3 +563,13 @@ def fn_with_kwarg_and_defaults(arg1, arg2, opt=True, **kwargs):
   del arg1, arg2, opt
   return kwargs.get('arg3')
 # pylint: enable=g-doc-args,g-doc-return-or-yield
+
+def fn_with_multiple_defaults(first='first', last='last', late='late'):
+  """Function with kwarg and defaults.
+
+  :key first: Description of first.
+  :key last: Description of last.
+  :key late: Description of late.
+  """
+  del last, late
+  return first

From eda25f878900146d929c32d9ac2a87656c6e974a Mon Sep 17 00:00:00 2001
From: Martial Himanshu <66832784+mhimanshu0101@users.noreply.github.com>
Date: Sat, 10 Dec 2022 02:15:14 +0530
Subject: [PATCH 247/324] Small docs improvement for using CLI (#372)

* Updated the formatting of the text to improve the documentation.
---
 docs/using-cli.md | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/docs/using-cli.md b/docs/using-cli.md
index ba1c35dc..bdfcb7db 100644
--- a/docs/using-cli.md
+++ b/docs/using-cli.md
@@ -57,7 +57,7 @@ If your command corresponds to a list or tuple, you can extend your command by
 adding the index of an element of the component to your command as an argument.
 
 For example, `widget function-that-returns-list 2` will correspond to item 2 of
-the result of function_that_returns_list.
+the result of `function_that_returns_list`.
 
 
 ### Calling a function
@@ -90,7 +90,7 @@ See also the section on [Changing the Separator](#separator-flag).
 ### Instantiating a class
 
 If your command corresponds to a class, you can extend your command by adding
-the arguments of the class's \_\_init\_\_ function. Arguments must be specified
+the arguments of the class's `__init__` function. Arguments must be specified
 by name, using the flags syntax. See the section on
 [calling a function](#calling-a-function) for more details.
 
@@ -105,8 +105,8 @@ after the final standalone `--` argument. (If there is no `--` argument, then no
 arguments are used for flags.)
 
 For example, to set the alsologtostderr flag, you could run the command:
-`widget bang --noise=boom -- --alsologtostderr`. The --noise argument is
-consumed by Fire, but the --alsologtostderr argument is treated as a normal
+`widget bang --noise=boom -- --alsologtostderr`. The `--noise` argument is
+consumed by Fire, but the `--alsologtostderr` argument is treated as a normal
 Flag.
 
 All CLIs built with Python Fire share some flags, as described in the next
@@ -146,7 +146,7 @@ the `ipython` package needs to be installed in your environment.
 Call `widget -- --completion` to generate a completion script for the Fire CLI
 `widget`. To save the completion script to your home directory, you could e.g.
 run `widget -- --completion > ~/.widget-completion`. You should then source this
-file; to get permanent completion, source this file from your .bashrc file.
+file; to get permanent completion, source this file from your `.bashrc` file.
 
 Call `widget -- --completion fish` to generate a completion script for the Fish
 shell. Source this file from your fish.config.
@@ -177,7 +177,7 @@ corresponds to, as well as usage information for how to extend that command.
 ### `--trace`: Getting a Fire trace <a name="trace-flag"></a>
 
 In order to understand what is happening when you call Python Fire, it can be
-useful to request a trace. This is done via the --trace flag, e.g.
+useful to request a trace. This is done via the `--trace` flag, e.g.
 `widget whack 5 -- --trace`.
 
 A trace provides step by step information about how the Fire command was

From 7b5d4f713ba4c252cc34f523811a49a6bb15f681 Mon Sep 17 00:00:00 2001
From: Tim Gates <tim.gates@iress.com>
Date: Sat, 10 Dec 2022 07:53:52 +1100
Subject: [PATCH 248/324] docs: Fix a few typos (#399)

There are small typos in:
- fire/console/console_attr.py
- fire/custom_descriptions.py

Fixes:
- Should read `support` rather than `suport`.
- Should read `pertinent` rather than `pertinant`.
- Should read `environment` rather than `envrionment`.
---
 fire/console/console_attr.py | 4 ++--
 fire/custom_descriptions.py  | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/fire/console/console_attr.py b/fire/console/console_attr.py
index 35c10fba..f88d5788 100644
--- a/fire/console/console_attr.py
+++ b/fire/console/console_attr.py
@@ -288,7 +288,7 @@ def __init__(self, encoding=None, suppress_output=False):
     elif self._encoding == 'cp437' and not is_screen_reader:
       self._box_line_characters = BoxLineCharactersUnicode()
       self._bullets = self._BULLETS_WINDOWS
-      # Windows does not suport the unicode characters used for the spinner.
+      # Windows does not support the unicode characters used for the spinner.
       self._progress_tracker_symbols = ProgressTrackerSymbolsAscii()
     else:
       self._box_line_characters = BoxLineCharactersAscii()
@@ -456,7 +456,7 @@ def GetRawKey(self):
     return self._get_raw_key[0]()
 
   def GetTermIdentifier(self):
-    """Returns the TERM envrionment variable for the console.
+    """Returns the TERM environment variable for the console.
 
     Returns:
       str: A str that describes the console's text capabilities
diff --git a/fire/custom_descriptions.py b/fire/custom_descriptions.py
index 865a528e..266671f1 100644
--- a/fire/custom_descriptions.py
+++ b/fire/custom_descriptions.py
@@ -28,7 +28,7 @@
 dict(**kwargs) -> new dictionary initialized with the name=value pairs
     in the keyword argument list.  For example:  dict(one=1, two=2)
 
-As you can see, this docstring is more pertinant to the function `dict` and
+As you can see, this docstring is more pertinent to the function `dict` and
 would be suitable as the result of `dict.__doc__`, but is wholely unsuitable
 as a description for the dict `{'key': 'value'}`.
 

From b2415b9331268050fbfe4da7ea377333aa4cf9cd Mon Sep 17 00:00:00 2001
From: Wei-Chung Liao <wchngliao@gmail.com>
Date: Fri, 9 Dec 2022 16:16:15 -0500
Subject: [PATCH 249/324] Fix completion for Fire(fn) (#336)

---
 fire/completion.py | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/fire/completion.py b/fire/completion.py
index ed7a1b61..9659ec6a 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -156,7 +156,11 @@ def _GetOptsAssignmentTemplate(command):
       return opts_assignment_subcommand_template
 
   lines = []
-  for command in set(subcommands_map.keys()).union(set(options_map.keys())):
+  commands_set = set()
+  commands_set.add(name)
+  commands_set = commands_set.union(set(subcommands_map.keys()))
+  commands_set = commands_set.union(set(options_map.keys()))
+  for command in commands_set:
     opts_assignment = _GetOptsAssignmentTemplate(command).format(
         options=' '.join(
             sorted(options_map[command].union(subcommands_map[command]))

From 2cb16f66c52562dd8f4ad7ae8513d78ae6c0a659 Mon Sep 17 00:00:00 2001
From: Wei-Chung Liao <wchngliao@gmail.com>
Date: Fri, 9 Dec 2022 16:20:13 -0500
Subject: [PATCH 250/324] Modify the grouping example to fit the output (#334)

Co-authored-by: David Bieber <dbieber@google.com>
---
 docs/guide.md | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/docs/guide.md b/docs/guide.md
index 8e3f03c9..cb2c07db 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -318,9 +318,9 @@ class Pipeline(object):
     self.digestion = DigestionStage()
 
   def run(self):
-    print(self.ingestion.run())
-    print(self.digestion.run())
-    return 'Pipeline complete'
+    ingestion_output = self.ingestion.run()
+    digestion_output = self.digestion.run()
+    return [ingestion_output, digestion_output]
 
 if __name__ == '__main__':
   fire.Fire(Pipeline)

From 51afa538b9dc3df50cb15970b19aece54480f851 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 9 Dec 2022 17:11:58 -0500
Subject: [PATCH 251/324] Update documentation to show new serialize kwarg and
 to clean up misformatted tables (#419)

* Shows new serialize kwarg
* Cleans up misformatted tables
---
 docs/api.md          | 59 ++++++++++++--------------------------------
 docs/guide.md        | 13 +++++++++-
 docs/index.md        |  3 ++-
 docs/installation.md |  4 +--
 4 files changed, 32 insertions(+), 47 deletions(-)

diff --git a/docs/api.md b/docs/api.md
index 66451071..aae92cd6 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -1,37 +1,23 @@
 ## Python Fire Quick Reference
 
 | Setup   | Command             | Notes
-| :------ | :------------------ | :---------
-| install | `pip install fire`  |
+| ------- | ------------------- | ----------
+| install | `pip install fire`  | Installs fire from pypi
 
 | Creating a CLI | Command                | Notes
-| :--------------| :--------------------- | :---------
+| ---------------| ---------------------- | ----------
 | import         | `import fire`          |
 | Call           | `fire.Fire()`          | Turns the current module into a Fire CLI.
 | Call           | `fire.Fire(component)` | Turns `component` into a Fire CLI.
 
-| Using a CLI                                | Command        | Notes          |
-| :----------------------------------------- | :------------- | :------------- |
-| [Help](using-cli.md#help-flag)             | `command       | Show the help  |
-:                                            : --help`        : screen.        :
-| [REPL](using-cli.md#interactive-flag)      | `command --    | Enters         |
-:                                            : --interactive` : interactive    :
-:                                            :                : mode.          :
-| [Separator](using-cli.md#separator-flag)   | `command --    | This sets the  |
-:                                            : --separator=X` : separator to   :
-:                                            :                : `X`. The       :
-:                                            :                : default        :
-:                                            :                : separator is   :
-:                                            :                : `-`.           :
-| [Completion](using-cli.md#completion-flag) | `command --    | Generate a     |
-:                                            : --completion   : completion     :
-:                                            : [shell]`       : script for the :
-:                                            :                : CLI.           :
-| [Trace](using-cli.md#trace-flag)           | `command --    | Gets a Fire    |
-:                                            : --trace`       : trace for the  :
-:                                            :                : command.       :
-| [Verbose](using-cli.md#verbose-flag)       | `command --    |                |
-:                                            : --verbose`     :                :
+| Using a CLI                                | Command                           | Notes          |
+| ------------------------------------------ | -----------------                 | -------------- |
+| [Help](using-cli.md#help-flag)             | `command --help`                  | Show the help screen. |
+| [REPL](using-cli.md#interactive-flag)      | `command -- --interactive`        | Enters interactive mode. |
+| [Separator](using-cli.md#separator-flag)   | `command -- --separator=X`        | This sets the separator to `X`. The default separator is `-`. |
+| [Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI. |
+| [Trace](using-cli.md#trace-flag)           | `command -- --trace`              | Gets a Fire trace for the command. |
+| [Verbose](using-cli.md#verbose-flag)       | `command -- --verbose`            |                |
 
 _Note that flags are separated from the Fire command by an isolated `--` arg.
 Help is an exception; the isolated `--` is optional for getting help._
@@ -39,24 +25,11 @@ Help is an exception; the isolated `--` is optional for getting help._
 ## Arguments for Calling fire.Fire()
 
 | Argument  | Usage                     | Notes                                |
-| :-------- | :------------------------ | :----------------------------------- |
-| component | `fire.Fire(component)`    | If omitted, defaults to a dict of    |
-:           :                           : all locals and globals.              :
-| command   | `fire.Fire(command='hello | Either a string or a list of         |
-:           : --name=5')`               : arguments. If a string is provided,  :
-:           :                           : it is split to determine the         :
-:           :                           : arguments. If a list or tuple is     :
-:           :                           : provided, they are the arguments. If :
-:           :                           : `command` is omitted, then           :
-:           :                           : `sys.argv[1\:]` (the arguments from  :
-:           :                           : the command line) are used by        :
-:           :                           : default.                             :
-| name      | `fire.Fire(name='tool')`  | The name of the CLI, ideally the     |
-:           :                           : name users will enter to run the     :
-:           :                           : CLI. This name will be used in the   :
-:           :                           : CLI's help screens. If the argument  :
-:           :                           : is omitted, it will be inferred      :
-:           :                           : automatically.                       :
+| --------- | ------------------------- | ------------------------------------ |
+| component | `fire.Fire(component)`    | If omitted, defaults to a dict of all locals and globals. |
+| command   | `fire.Fire(command='hello --name=5')` | Either a string or a list of arguments. If a string is provided, it is split to determine the arguments. If a list or tuple is provided, they are the arguments. If `command` is omitted, then `sys.argv[1:]` (the arguments from the command line) are used by default. |
+| name      | `fire.Fire(name='tool')`  | The name of the CLI, ideally the name users will enter to run the CLI. This name will be used in the CLI's help screens. If the argument is omitted, it will be inferred automatically.|
+| serialize | `fire.Fire(serialize=custom_serializer)` | If omitted, simple types are serialized via their builtin str method, and any objects that define a custom `__str__` method are serialized with that. If specified, all objects are serialized to text via the provided method. |
 
 ## Using a Fire CLI without modifying any code
 
diff --git a/docs/guide.md b/docs/guide.md
index cb2c07db..44d8a46d 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -742,7 +742,18 @@ The complete set of flags available is shown below, in the reference section.
 | [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
 | [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` | Include private members in the output.
 
-_Note that flags are separated from the Fire command by an isolated `--` arg._
+_Note that flags are separated from the Fire command by an isolated `--` arg.
+Help is an exception; the isolated `--` is optional for getting help._
+
+
+##### Arguments for Calling fire.Fire()
+
+| Argument  | Usage                     | Notes                                |
+| --------- | ------------------------- | ------------------------------------ |
+| component | `fire.Fire(component)`    | If omitted, defaults to a dict of all locals and globals. |
+| command   | `fire.Fire(command='hello --name=5')` | Either a string or a list of arguments. If a string is provided, it is split to determine the arguments. If a list or tuple is provided, they are the arguments. If `command` is omitted, then `sys.argv[1:]` (the arguments from the command line) are used by default. |
+| name      | `fire.Fire(name='tool')`  | The name of the CLI, ideally the name users will enter to run the CLI. This name will be used in the CLI's help screens. If the argument is omitted, it will be inferred automatically.|
+| serialize | `fire.Fire(serialize=custom_serializer)` | If omitted, simple types are serialized via their builtin str method, and any objects that define a custom `__str__` method are serialized with that. If specified, all objects are serialized to text via the provided method. |
 
 
 ### Disclaimer
diff --git a/docs/index.md b/docs/index.md
index 4eb114b1..8dcc5db6 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -106,7 +106,8 @@ Please see [The Python Fire Guide](guide.md).
 | [Trace](using-cli.md#trace-flag)           | `command -- --trace`                    | Gets a Fire trace for the command.
 | [Verbose](using-cli.md#verbose-flag)       | `command -- --verbose`                  |
 
-_Note that these flags are separated from the Fire command by an isolated `--`._
+_Note that flags are separated from the Fire command by an isolated `--` arg.
+Help is an exception; the isolated `--` is optional for getting help._
 
 ## License
 
diff --git a/docs/installation.md b/docs/installation.md
index 614243af..7e4cccb8 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -4,5 +4,5 @@ To install Python Fire with pip, run: `pip install fire`
 
 To install Python Fire with conda, run: `conda install fire -c conda-forge`
 
-To install Python Fire from source, first clone the repository and then run:
-`python setup.py install`
+To install Python Fire from source, first clone the repository and then run
+`python setup.py install`. To install from source for development, instead run `python setup.py develop`.

From e1e95ffe2454f7e9887640883e35c01543f54139 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 12 Dec 2022 15:43:49 -0500
Subject: [PATCH 252/324] Change mkdocs from pages to nav (#421)

"pages" has been deprecated in favor of "nav"
---
 mkdocs.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/mkdocs.yml b/mkdocs.yml
index bb815e37..bbe1e848 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,7 +1,7 @@
 site_name: Python Fire
 theme: readthedocs
 markdown_extensions: [fenced_code]
-pages:
+nav:
     - Overview: index.md
     - Installation: installation.md
     - Benefits: benefits.md

From 2d2fa6196e5a8ce4ddcbabc565bacad163323b0f Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 1 Feb 2023 10:53:09 -0800
Subject: [PATCH 253/324] Use literal dict (#430)

Use literal dict to satisfy linter
---
 fire/decorators.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fire/decorators.py b/fire/decorators.py
index 9e56d6df..b2e9b322 100644
--- a/fire/decorators.py
+++ b/fire/decorators.py
@@ -111,5 +111,5 @@ def GetMetadata(fn):
 def GetParseFns(fn):
   # type: (...) -> dict
   metadata = GetMetadata(fn)
-  default = dict(default=None, positional=[], named={})
+  default = {"default": None, "positional": [], "named": {}}
   return metadata.get(FIRE_PARSE_FNS, default)

From 396ef1c4efa1d977217e52267855b8acdc8d5a62 Mon Sep 17 00:00:00 2001
From: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Date: Thu, 2 Feb 2023 04:47:53 +0900
Subject: [PATCH 254/324] freeze CI requirements (#431)

* freeze CI requirements, including pylint
---
 .github/scripts/build.sh         |  6 +-----
 .github/scripts/requirements.txt | 10 ++++++++++
 2 files changed, 11 insertions(+), 5 deletions(-)
 create mode 100644 .github/scripts/requirements.txt

diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh
index 6fd8f73b..958bc677 100755
--- a/.github/scripts/build.sh
+++ b/.github/scripts/build.sh
@@ -19,11 +19,7 @@ set -e
 
 PYTHON_VERSION=${PYTHON_VERSION:-2.7}
 
-pip install --upgrade setuptools pip
-pip install --upgrade pylint pytest pytest-pylint pytest-runner
-pip install termcolor
-pip install hypothesis python-Levenshtein
-pip install mock
+pip install -U -r requirements.txt
 python setup.py develop
 python -m pytest  # Run the tests without IPython.
 pip install ipython
diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
new file mode 100644
index 00000000..9e48e20d
--- /dev/null
+++ b/.github/scripts/requirements.txt
@@ -0,0 +1,10 @@
+setuptools <65.7.0
+pip <23.0
+pylint <2.15.10
+pytest <=7.2.1
+pytest-pylint <=1.1.2
+pytest-runner <6.0.0
+termcolor <2.2.0
+hypothesis <6.62.0
+python-Levenshtein <0.20.9
+mock <5.0.0
\ No newline at end of file

From 910b1f8a9448d305f3a4107a294c1963de18bddf Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <hugovk@users.noreply.github.com>
Date: Wed, 1 Feb 2023 22:34:49 +0200
Subject: [PATCH 255/324] Fix path to requirements.txt (#433)

---
 .github/scripts/build.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh
index 958bc677..1f9ed766 100755
--- a/.github/scripts/build.sh
+++ b/.github/scripts/build.sh
@@ -19,7 +19,7 @@ set -e
 
 PYTHON_VERSION=${PYTHON_VERSION:-2.7}
 
-pip install -U -r requirements.txt
+pip install -U -r .github/scripts/requirements.txt
 python setup.py develop
 python -m pytest  # Run the tests without IPython.
 pip install ipython

From 0438367c157c3ff9b46c68fad0b7d66903e7b3a1 Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <hugovk@users.noreply.github.com>
Date: Wed, 1 Feb 2023 22:36:20 +0200
Subject: [PATCH 256/324] Fix deprecation warning: LICENSE is autodetected
 (#434)

---
 setup.cfg | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/setup.cfg b/setup.cfg
index 7a980136..977056b0 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,3 @@
-[metadata]
-license-file = LICENSE
-
 [wheel]
 universal = 1
 

From c29b923133668a86b802d7451e83aa92ed648de1 Mon Sep 17 00:00:00 2001
From: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Date: Tue, 7 Feb 2023 00:41:38 +0900
Subject: [PATCH 257/324] adding python 3.10 [tag & CI] (#428)

* adding python 3.10
---
 .github/workflows/build.yml | 2 +-
 setup.py                    | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index bc5e0405..9bf78e8b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -7,7 +7,7 @@ jobs:
     runs-on: ubuntu-20.04
     strategy:
       matrix:
-        python-version: [2.7, 3.5, 3.7, 3.8, 3.9]
+        python-version: ["2.7", "3.5", "3.7", "3.8", "3.9", "3.10"]
 
     steps:
      # Checkout the repo.
diff --git a/setup.py b/setup.py
index f1f91103..8e95f414 100644
--- a/setup.py
+++ b/setup.py
@@ -71,6 +71,7 @@
         'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
         'Programming Language :: Python :: 3.9',
+        'Programming Language :: Python :: 3.10',
 
         'Operating System :: OS Independent',
         'Operating System :: POSIX',

From 71743b6fbaa2c1ca35a229ad3cc3e4638606bb35 Mon Sep 17 00:00:00 2001
From: John Bampton <jbampton@users.noreply.github.com>
Date: Tue, 7 Feb 2023 01:43:43 +1000
Subject: [PATCH 258/324] docs: fix brand name `Github` -> `GitHub` (#425)

---
 CONTRIBUTING.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b3c758e1..baae1a6e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -49,7 +49,7 @@ Python versions < 3.5.
 
 ## Testing
 
-Python Fire uses [Github Actions](https://github.com/google/python-fire/actions) to run tests on each pull request. You can run
+Python Fire uses [GitHub Actions](https://github.com/google/python-fire/actions) to run tests on each pull request. You can run
 these tests yourself as well. To do this, first install the test dependencies
 listed in setup.py (e.g. pytest, mock, termcolor, and hypothesis).
 Then run the tests by running `pytest` in the root directory of the repository.

From 527e3d056ecc134531f1ef3ed45d46581b5844b9 Mon Sep 17 00:00:00 2001
From: Yaroslav Halchenko <debian@onerussian.com>
Date: Mon, 13 Feb 2023 14:20:46 -0500
Subject: [PATCH 259/324] Fix typos in console and tests (#436)

---
 fire/console/console_attr.py    |  6 +++---
 fire/console/console_attr_os.py |  2 +-
 fire/console/console_pager.py   |  2 +-
 fire/docstrings_test.py         | 12 ++++++------
 fire/parser_test.py             |  4 ++--
 5 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/fire/console/console_attr.py b/fire/console/console_attr.py
index f88d5788..815e16b8 100644
--- a/fire/console/console_attr.py
+++ b/fire/console/console_attr.py
@@ -268,7 +268,7 @@ def __init__(self, encoding=None, suppress_output=False):
 
     # ANSI "standard" attributes.
     if self.SupportsAnsi():
-      # Select Graphic Rendition paramaters from
+      # Select Graphic Rendition parameters from
       # http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
       # Italic '3' would be nice here but its not widely supported.
       self._csi = '\x1b['
@@ -394,7 +394,7 @@ def GetControlSequenceIndicator(self):
     """Returns the control sequence indicator string.
 
     Returns:
-      The conrol sequence indicator string or None if control sequences are not
+      The control sequence indicator string or None if control sequences are not
       supported.
     """
     return self._csi
@@ -408,7 +408,7 @@ def GetControlSequenceLen(self, buf):
       buf: The string to check for a control sequence.
 
     Returns:
-      The conrol sequence length at the beginning of buf or 0 if buf does not
+      The control sequence length at the beginning of buf or 0 if buf does not
       start with a control sequence.
     """
     if not self._csi or not buf.startswith(self._csi):
diff --git a/fire/console/console_attr_os.py b/fire/console/console_attr_os.py
index 8482c7bc..869c5949 100644
--- a/fire/console/console_attr_os.py
+++ b/fire/console/console_attr_os.py
@@ -123,7 +123,7 @@ def _GetTermSizeEnvironment():
 
 
 def _GetTermSizeTput():
-  """Returns the terminal x and y dimemsions from tput(1)."""
+  """Returns the terminal x and y dimensions from tput(1)."""
   import subprocess  # pylint: disable=g-import-not-at-top
   output = encoding.Decode(subprocess.check_output(['tput', 'cols'],
                                                    stderr=subprocess.STDOUT))
diff --git a/fire/console/console_pager.py b/fire/console/console_pager.py
index 044fcb37..565c7e1e 100644
--- a/fire/console/console_pager.py
+++ b/fire/console/console_pager.py
@@ -94,7 +94,7 @@ def __init__(self, contents, out=None, prompt=None):
     Args:
       contents: The entire contents of the text lines to page.
       out: The output stream, log.out (effectively) if None.
-      prompt: The page break prompt, a defalt prompt is used if None..
+      prompt: The page break prompt, a default prompt is used if None..
     """
     self._contents = contents
     self._out = out or sys.stdout
diff --git a/fire/docstrings_test.py b/fire/docstrings_test.py
index 2328ef16..0d6e5d18 100644
--- a/fire/docstrings_test.py
+++ b/fire/docstrings_test.py
@@ -50,12 +50,12 @@ def test_one_line_simple_whitespace(self):
 
   def test_one_line_too_long(self):
     # pylint: disable=line-too-long
-    docstring = """A one line docstring thats both a little too verbose and a little too long so it keeps going well beyond a reasonable length for a one-liner.
+    docstring = """A one line docstring that is both a little too verbose and a little too long so it keeps going well beyond a reasonable length for a one-liner.
     """
     # pylint: enable=line-too-long
     docstring_info = docstrings.parse(docstring)
     expected_docstring_info = DocstringInfo(
-        summary='A one line docstring thats both a little too verbose and '
+        summary='A one line docstring that is both a little too verbose and '
         'a little too long so it keeps going well beyond a reasonable length '
         'for a one-liner.',
     )
@@ -63,25 +63,25 @@ def test_one_line_too_long(self):
 
   def test_one_line_runs_over(self):
     # pylint: disable=line-too-long
-    docstring = """A one line docstring thats both a little too verbose and a little too long
+    docstring = """A one line docstring that is both a little too verbose and a little too long
     so it runs onto a second line.
     """
     # pylint: enable=line-too-long
     docstring_info = docstrings.parse(docstring)
     expected_docstring_info = DocstringInfo(
-        summary='A one line docstring thats both a little too verbose and '
+        summary='A one line docstring that is both a little too verbose and '
         'a little too long so it runs onto a second line.',
     )
     self.assertEqual(expected_docstring_info, docstring_info)
 
   def test_one_line_runs_over_whitespace(self):
     docstring = """
-      A one line docstring thats both a little too verbose and a little too long
+      A one line docstring that is both a little too verbose and a little too long
       so it runs onto a second line.
     """
     docstring_info = docstrings.parse(docstring)
     expected_docstring_info = DocstringInfo(
-        summary='A one line docstring thats both a little too verbose and '
+        summary='A one line docstring that is both a little too verbose and '
         'a little too long so it runs onto a second line.',
     )
     self.assertEqual(expected_docstring_info, docstring_info)
diff --git a/fire/parser_test.py b/fire/parser_test.py
index 0257be28..6b6b79b1 100644
--- a/fire/parser_test.py
+++ b/fire/parser_test.py
@@ -117,8 +117,8 @@ def testDefaultParseValueBareWordsTuple(self):
 
   def testDefaultParseValueNestedContainers(self):
     self.assertEqual(
-        parser.DefaultParseValue('[(A, 2, "3"), 5, {alph: 10.2, beta: "cat"}]'),
-        [('A', 2, '3'), 5, {'alph': 10.2, 'beta': 'cat'}])
+        parser.DefaultParseValue('[(A, 2, "3"), 5, {alpha: 10.2, beta: "cat"}]'),
+        [('A', 2, '3'), 5, {'alpha': 10.2, 'beta': 'cat'}])
 
   def testDefaultParseValueComments(self):
     self.assertEqual(parser.DefaultParseValue('"0#comments"'), '0#comments')

From 6e485c971f4998175b841c775edcd980f2fb55ea Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 13 Feb 2023 15:24:46 -0500
Subject: [PATCH 260/324] Split too long line, fixing lint #437

---
 fire/parser_test.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/fire/parser_test.py b/fire/parser_test.py
index 6b6b79b1..8aeabc61 100644
--- a/fire/parser_test.py
+++ b/fire/parser_test.py
@@ -117,7 +117,8 @@ def testDefaultParseValueBareWordsTuple(self):
 
   def testDefaultParseValueNestedContainers(self):
     self.assertEqual(
-        parser.DefaultParseValue('[(A, 2, "3"), 5, {alpha: 10.2, beta: "cat"}]'),
+        parser.DefaultParseValue(
+            '[(A, 2, "3"), 5, {alpha: 10.2, beta: "cat"}]'),
         [('A', 2, '3'), 5, {'alpha': 10.2, 'beta': 'cat'}])
 
   def testDefaultParseValueComments(self):

From 1bcb5d1a265ed78e02b57174171aae9d813dbcf5 Mon Sep 17 00:00:00 2001
From: Panagiotis Simakis <sp1thas@autistici.org>
Date: Thu, 5 Oct 2023 18:39:58 +0300
Subject: [PATCH 261/324] Add missing parameter description (#462)

---
 fire/core.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/fire/core.py b/fire/core.py
index c1e97367..6367262d 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -94,6 +94,8 @@ def Fire(component=None, command=None, name=None, serialize=None):
         a string or a list of strings; a list of strings is preferred.
     name: Optional. The name of the command as entered at the command line.
         Used in interactive mode and for generating the completion script.
+    serialize: Optional. If supplied, all objects are serialized to text via
+        the provided callable.
   Returns:
     The result of executing the Fire command. Execution begins with the initial
     target component. The component is updated by using the command arguments

From d44d33d4ac9389854b046ca0270c112693b309e6 Mon Sep 17 00:00:00 2001
From: Max <4649120+maximehk@users.noreply.github.com>
Date: Mon, 13 Nov 2023 19:49:58 +0100
Subject: [PATCH 262/324] Fix missing `$` sign in bash completion (#472)

Related issue  https://github.com/google/python-fire/issues/64

Co-authored-by: Max Hacker <maximeh@google.com>
---
 fire/completion.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fire/completion.py b/fire/completion.py
index 9659ec6a..4393880d 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -104,7 +104,7 @@ def _BashScript(name, commands, default_options=None):
 option_already_entered()
 {{
   local opt
-  for opt in ${{COMP_WORDS[@]:0:COMP_CWORD}}
+  for opt in ${{COMP_WORDS[@]:0:$COMP_CWORD}}
   do
     if [ $1 == $opt ]; then
       return 0

From 1d8a137893222b977fc23764ad6e56293cf83a32 Mon Sep 17 00:00:00 2001
From: Hai Zhu <35182391+cocolato@users.noreply.github.com>
Date: Wed, 3 Jan 2024 04:25:30 +0800
Subject: [PATCH 263/324] remove asyncio.coroutine (#440)

---
 fire/test_components_py3.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/fire/test_components_py3.py b/fire/test_components_py3.py
index 5140921d..17fb932c 100644
--- a/fire/test_components_py3.py
+++ b/fire/test_components_py3.py
@@ -57,8 +57,7 @@ def lru_cache_decorated(arg1):
 
 class WithAsyncio(object):
 
-  @asyncio.coroutine
-  def double(self, count=0):
+  async def double(self, count=0):
     return 2 * count
 
 

From ffb8121ab8d342bad2e613df41fe406b2ed5e133 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Tue, 2 Jan 2024 15:45:49 -0500
Subject: [PATCH 264/324] Update build.yml dropping Python 2.7 (#479)

We'll want to reenable later.
---
 .github/workflows/build.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9bf78e8b..9864ae98 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -7,7 +7,7 @@ jobs:
     runs-on: ubuntu-20.04
     strategy:
       matrix:
-        python-version: ["2.7", "3.5", "3.7", "3.8", "3.9", "3.10"]
+        python-version: ["3.5", "3.7", "3.8", "3.9", "3.10"]
 
     steps:
      # Checkout the repo.

From 8a41c91a474209b38d58839437f5711190fd3cee Mon Sep 17 00:00:00 2001
From: eXcript <excriptbrasil@gmail.com>
Date: Tue, 9 Jan 2024 15:22:03 -0300
Subject: [PATCH 265/324] Update formatting_windows.py (#477)

Add a check if 'isatty' member exists in the object, required for packaging with PyInstaller.
---
 fire/formatting_windows.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fire/formatting_windows.py b/fire/formatting_windows.py
index 2b85820d..4bcf82e0 100644
--- a/fire/formatting_windows.py
+++ b/fire/formatting_windows.py
@@ -35,7 +35,7 @@ def initialize_or_disable():
   """Enables ANSI processing on Windows or disables it as needed."""
   if HAS_COLORAMA:
     wrap = True
-    if sys.stdout.isatty() and platform.release() == '10':
+    if hasattr(sys.stdout, "isatty") and sys.stdout.isatty() and platform.release() == '10':
       # Enables native ANSI sequences in console.
       # Windows 10, 2016, and 2019 only.
 

From 343e6b6cec2d174d511e99dec7e5a24849121c2e Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Sat, 24 Feb 2024 13:07:41 -0500
Subject: [PATCH 266/324] Add Python 3.11 and Python 3.12 to build workflow
 (#485)

This change brings github actions back into the green.
---
 .github/scripts/requirements.txt | 11 +++++++----
 .github/workflows/build.yml      |  2 +-
 fire/__init__.py                 |  2 +-
 fire/__main__.py                 |  2 +-
 fire/console/encoding.py         | 12 ++++++------
 fire/formatting_windows.py       |  4 +++-
 fire/inspectutils.py             |  4 ++--
 fire/testutils.py                |  2 +-
 pylintrc                         |  2 +-
 setup.py                         |  4 +++-
 10 files changed, 26 insertions(+), 19 deletions(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 9e48e20d..13880c9c 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -1,10 +1,13 @@
-setuptools <65.7.0
-pip <23.0
+setuptools <65.7.0 ; python_version == '2.7'
+setuptools <=69.1.1 ; python_version >= '3.8'
+pip <23.0 ; python_version == '2.7'
+pip ; python_version >= '3.5'
 pylint <2.15.10
 pytest <=7.2.1
 pytest-pylint <=1.1.2
 pytest-runner <6.0.0
 termcolor <2.2.0
 hypothesis <6.62.0
-python-Levenshtein <0.20.9
-mock <5.0.0
\ No newline at end of file
+python-Levenshtein <0.20.9 ; python_version == '2.7'
+levenshtein <=0.25.0 ; python_version >= '3.5'
+mock <5.0.0
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9864ae98..eb510f03 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -7,7 +7,7 @@ jobs:
     runs-on: ubuntu-20.04
     strategy:
       matrix:
-        python-version: ["3.5", "3.7", "3.8", "3.9", "3.10"]
+        python-version: ["3.5", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
 
     steps:
      # Checkout the repo.
diff --git a/fire/__init__.py b/fire/__init__.py
index 4cc76210..fae18489 100644
--- a/fire/__init__.py
+++ b/fire/__init__.py
@@ -21,4 +21,4 @@
 from fire.core import Fire
 
 __all__ = ['Fire']
-__version__ = '0.5.0'
+__version__ = '0.6.0'
diff --git a/fire/__main__.py b/fire/__main__.py
index 2ad217d6..9d8227ad 100644
--- a/fire/__main__.py
+++ b/fire/__main__.py
@@ -80,7 +80,7 @@ def import_from_file_path(path):
     spec.loader.exec_module(module)  # pytype: disable=attribute-error
 
   else:
-    import imp  # pylint: disable=g-import-not-at-top,import-outside-toplevel,deprecated-module
+    import imp  # pylint: disable=g-import-not-at-top,import-outside-toplevel,deprecated-module,import-error
     module = imp.load_source(module_name, path)
 
   return module, module_name
diff --git a/fire/console/encoding.py b/fire/console/encoding.py
index 780e5a28..41bda634 100644
--- a/fire/console/encoding.py
+++ b/fire/console/encoding.py
@@ -86,7 +86,7 @@ def Decode(data, encoding=None):
 
   try:
     # Just return the string if its pure ASCII.
-    return string.decode('ascii')
+    return string.decode('ascii')  # pytype: disable=attribute-error
   except UnicodeError:
     # The string is not ASCII encoded.
     pass
@@ -94,7 +94,7 @@ def Decode(data, encoding=None):
   # Try the suggested encoding if specified.
   if encoding:
     try:
-      return string.decode(encoding)
+      return string.decode(encoding)  # pytype: disable=attribute-error
     except UnicodeError:
       # Bad suggestion.
       pass
@@ -103,21 +103,21 @@ def Decode(data, encoding=None):
   # be exceptional if a valid extended ascii encoding with extended chars
   # were also a valid UITF-8 encoding.
   try:
-    return string.decode('utf8')
+    return string.decode('utf8')  # pytype: disable=attribute-error
   except UnicodeError:
     # Not a UTF-8 encoding.
     pass
 
   # Try the filesystem encoding.
   try:
-    return string.decode(sys.getfilesystemencoding())
+    return string.decode(sys.getfilesystemencoding())  # pytype: disable=attribute-error
   except UnicodeError:
     # string is not encoded for filesystem paths.
     pass
 
   # Try the system default encoding.
   try:
-    return string.decode(sys.getdefaultencoding())
+    return string.decode(sys.getdefaultencoding())  # pytype: disable=attribute-error
   except UnicodeError:
     # string is not encoded using the default encoding.
     pass
@@ -137,7 +137,7 @@ def Decode(data, encoding=None):
   #   string = '\xdc'
   #   string = string.decode('iso-8859-1')
   #   string = string.encode('ascii', 'backslashreplace')
-  return string.decode('iso-8859-1')
+  return string.decode('iso-8859-1')  # pytype: disable=attribute-error
 
 
 def GetEncodedValue(env, name, default=None):
diff --git a/fire/formatting_windows.py b/fire/formatting_windows.py
index 4bcf82e0..ce0f677d 100644
--- a/fire/formatting_windows.py
+++ b/fire/formatting_windows.py
@@ -35,7 +35,9 @@ def initialize_or_disable():
   """Enables ANSI processing on Windows or disables it as needed."""
   if HAS_COLORAMA:
     wrap = True
-    if hasattr(sys.stdout, "isatty") and sys.stdout.isatty() and platform.release() == '10':
+    if (hasattr(sys.stdout, "isatty")
+        and sys.stdout.isatty()
+        and platform.release() == '10'):
       # Enables native ANSI sequences in console.
       # Windows 10, 2016, and 2019 only.
 
diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index 0fa8e7d3..15f32f91 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -98,10 +98,10 @@ class with an __init__ method.
 def Py2GetArgSpec(fn):
   """A wrapper around getargspec that tries both fn and fn.__call__."""
   try:
-    return inspect.getargspec(fn)  # pylint: disable=deprecated-method
+    return inspect.getargspec(fn)  # pylint: disable=deprecated-method,no-member
   except TypeError:
     if hasattr(fn, '__call__'):
-      return inspect.getargspec(fn.__call__)  # pylint: disable=deprecated-method
+      return inspect.getargspec(fn.__call__)  # pylint: disable=deprecated-method,no-member
     raise
 
 
diff --git a/fire/testutils.py b/fire/testutils.py
index ea410e82..5f875147 100644
--- a/fire/testutils.py
+++ b/fire/testutils.py
@@ -74,7 +74,7 @@ def assertOutputMatches(self, stdout='.*', stderr='.*', capture=True):
 
   def assertRaisesRegex(self, *args, **kwargs):  # pylint: disable=arguments-differ
     if sys.version_info.major == 2:
-      return super(BaseTestCase, self).assertRaisesRegexp(*args, **kwargs)  # pylint: disable=deprecated-method
+      return super(BaseTestCase, self).assertRaisesRegexp(*args, **kwargs)  # pylint: disable=deprecated-method,no-member
     else:
       return super(BaseTestCase, self).assertRaisesRegex(*args, **kwargs)  # pylint: disable=no-member
 
diff --git a/pylintrc b/pylintrc
index b89b16d1..558d3ba2 100644
--- a/pylintrc
+++ b/pylintrc
@@ -32,7 +32,7 @@ enable=indexing-exception,old-raise-syntax
 # Disable the message, report, category or checker with the given id(s). You
 # can either give multiple identifier separated by comma (,) or put this option
 # multiple time.
-disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return,super-with-arguments,raise-missing-from,consider-using-f-string,unspecified-encoding,unnecessary-lambda-assignment
+disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return,super-with-arguments,raise-missing-from,consider-using-f-string,unspecified-encoding,unnecessary-lambda-assignment,wrong-import-position,ungrouped-imports,deprecated-module
 
 
 [REPORTS]
diff --git a/setup.py b/setup.py
index 8e95f414..24e0e325 100644
--- a/setup.py
+++ b/setup.py
@@ -40,7 +40,7 @@
     'python-Levenshtein',
 ]
 
-VERSION = '0.5.0'
+VERSION = '0.6.0'
 URL = 'https://github.com/google/python-fire'
 
 setup(
@@ -72,6 +72,8 @@
         'Programming Language :: Python :: 3.8',
         'Programming Language :: Python :: 3.9',
         'Programming Language :: Python :: 3.10',
+        'Programming Language :: Python :: 3.11',
+        'Programming Language :: Python :: 3.12',
 
         'Operating System :: OS Independent',
         'Operating System :: POSIX',

From 3c230d8c347225cb4430a823464c5336a01b36a6 Mon Sep 17 00:00:00 2001
From: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Date: Mon, 11 Mar 2024 21:34:50 +0100
Subject: [PATCH 267/324] Adding GitHub dependabot (#432)

Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>
---
 .github/dependabot.yml | 31 +++++++++++++++++++++++++++++++
 1 file changed, 31 insertions(+)
 create mode 100644 .github/dependabot.yml

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..17c20d04
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,31 @@
+# Basic dependabot.yml file with minimum configuration for two package managers
+
+version: 2
+updates:
+  # Enable version updates for python
+  - package-ecosystem: "pip"
+    directory: ".github/scripts/"
+    schedule:
+      interval: "monthly"
+    labels: ["ci"]
+    pull-request-branch-name:
+      separator: "-"
+    open-pull-requests-limit: 5
+    reviewers:
+      - "dbieber"
+
+  # Enable version updates for GitHub Actions
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "monthly"
+    groups:
+      pip:
+        patterns:
+          - "*"  # Check all dependencies
+    labels: ["ci"]
+    pull-request-branch-name:
+      separator: "-"
+    open-pull-requests-limit: 5
+    reviewers:
+      - "dbieber"

From db3fcaf737f917d61015f3b408a6fac0898b2030 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 11 Mar 2024 16:43:24 -0400
Subject: [PATCH 268/324] 2 dependabot updates (#491)

2 updates: [actions/checkout](https://github.com/actions/checkout) and [actions/setup-python](https://github.com/actions/setup-python).

Updates `actions/checkout` from 3 to 4
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

Updates `actions/setup-python` from 4 to 5
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: pip
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/workflows/build.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index eb510f03..f5562820 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -12,11 +12,11 @@ jobs:
     steps:
      # Checkout the repo.
      - name: Checkout Python Fire repository
-       uses: actions/checkout@v3
+       uses: actions/checkout@v4
 
      # Set up Python environment.
      - name: Set up Python ${{ matrix.python-version }}
-       uses: actions/setup-python@v4
+       uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
 

From c417aec195cee982d24f85223e7831f71fa5adc6 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 11 Mar 2024 16:44:16 -0400
Subject: [PATCH 269/324] Update hypothesis requirement in /.github/scripts
 (#492)

Updates the requirements on [hypothesis](https://github.com/HypothesisWorks/hypothesis) to permit the latest version.
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-ruby-0.0.1...hypothesis-python-6.99.4)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 13880c9c..493ad0f0 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -7,7 +7,7 @@ pytest <=7.2.1
 pytest-pylint <=1.1.2
 pytest-runner <6.0.0
 termcolor <2.2.0
-hypothesis <6.62.0
+hypothesis <6.100.0
 python-Levenshtein <0.20.9 ; python_version == '2.7'
 levenshtein <=0.25.0 ; python_version >= '3.5'
 mock <5.0.0

From 027c50272911e92f02be7ae93c60b4559c98a1a1 Mon Sep 17 00:00:00 2001
From: Vladimir Pestov <92364726+BasedDepartment1@users.noreply.github.com>
Date: Tue, 12 Mar 2024 01:47:19 +0500
Subject: [PATCH 270/324] #444: Removed pipes dependency (#447)

Co-authored-by: Svayp11 <bayakl227@gmail.com>
---
 fire/core.py  | 5 ++---
 fire/trace.py | 6 +++---
 2 files changed, 5 insertions(+), 6 deletions(-)

diff --git a/fire/core.py b/fire/core.py
index 6367262d..fada01b1 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -56,7 +56,6 @@ def main(argv):
 import inspect
 import json
 import os
-import pipes
 import re
 import shlex
 import sys
@@ -240,7 +239,7 @@ def _IsHelpShortcut(component_trace, remaining_args):
     component_trace.show_help = True
     command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
     print('INFO: Showing help with the command {cmd}.\n'.format(
-        cmd=pipes.quote(command)), file=sys.stderr)
+        cmd=shlex.quote(command)), file=sys.stderr)
   return show_help
 
 
@@ -296,7 +295,7 @@ def _DisplayError(component_trace):
   if show_help:
     command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
     print('INFO: Showing help with the command {cmd}.\n'.format(
-        cmd=pipes.quote(command)), file=sys.stderr)
+        cmd=shlex.quote(command)), file=sys.stderr)
     help_text = helptext.HelpText(result, trace=component_trace,
                                   verbose=component_trace.verbose)
     output.append(help_text)
diff --git a/fire/trace.py b/fire/trace.py
index 7174f994..4c9674e3 100644
--- a/fire/trace.py
+++ b/fire/trace.py
@@ -29,7 +29,7 @@
 from __future__ import division
 from __future__ import print_function
 
-import pipes
+import shlex
 
 from fire import inspectutils
 
@@ -166,8 +166,8 @@ def display(arg1, arg2='!'):
   def _Quote(self, arg):
     if arg.startswith('--') and '=' in arg:
       prefix, value = arg.split('=', 1)
-      return pipes.quote(prefix) + '=' + pipes.quote(value)
-    return pipes.quote(arg)
+      return shlex.quote(prefix) + '=' + shlex.quote(value)
+    return shlex.quote(arg)
 
   def GetCommand(self, include_separators=True):
     """Returns the command representing the trace up to this point.

From 8beb85e4e3192d1ec0c5614ce2f8a10450670e82 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 11 Mar 2024 16:51:50 -0400
Subject: [PATCH 271/324] Update termcolor requirement from <2.2.0 to <2.5.0 in
 /.github/scripts (#493)

Updates the requirements on [termcolor](https://github.com/termcolor/termcolor) to permit the latest version.
- [Release notes](https://github.com/termcolor/termcolor/releases)
- [Changelog](https://github.com/termcolor/termcolor/blob/main/CHANGES.md)
- [Commits](https://github.com/termcolor/termcolor/compare/0.1...2.4.0)

---
updated-dependencies:
- dependency-name: termcolor
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 493ad0f0..31238e4d 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -6,7 +6,7 @@ pylint <2.15.10
 pytest <=7.2.1
 pytest-pylint <=1.1.2
 pytest-runner <6.0.0
-termcolor <2.2.0
+termcolor <2.5.0
 hypothesis <6.100.0
 python-Levenshtein <0.20.9 ; python_version == '2.7'
 levenshtein <=0.25.0 ; python_version >= '3.5'

From 595239ec7c096d8d95822153ee61190e8985f7bf Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 11 Mar 2024 16:54:31 -0400
Subject: [PATCH 272/324] Update mock requirement from <5.0.0 to <6.0.0 in
 /.github/scripts (#495)

Updates the requirements on [mock](https://github.com/testing-cabal/mock) to permit the latest version.
- [Changelog](https://github.com/testing-cabal/mock/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/testing-cabal/mock/compare/release-0.5.0...5.1.0)

---
updated-dependencies:
- dependency-name: mock
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 31238e4d..b4efe7a9 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -10,4 +10,4 @@ termcolor <2.5.0
 hypothesis <6.100.0
 python-Levenshtein <0.20.9 ; python_version == '2.7'
 levenshtein <=0.25.0 ; python_version >= '3.5'
-mock <5.0.0
+mock <6.0.0

From ab310cf8847b0c47216b37525cd54a6d358f9fc9 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 11 Mar 2024 17:02:02 -0400
Subject: [PATCH 273/324] Update pytest requirement from <=7.2.1 to <=8.1.1 in
 /.github/scripts (#494)

Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/1.0.0b3...8.1.1)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index b4efe7a9..8cb6ad15 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -3,7 +3,7 @@ setuptools <=69.1.1 ; python_version >= '3.8'
 pip <23.0 ; python_version == '2.7'
 pip ; python_version >= '3.5'
 pylint <2.15.10
-pytest <=7.2.1
+pytest <=8.1.1
 pytest-pylint <=1.1.2
 pytest-runner <6.0.0
 termcolor <2.5.0

From 014a637f668db5fcfb81c1d426995b08e2b2ade7 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 11 Mar 2024 17:02:46 -0400
Subject: [PATCH 274/324] Update pytest-runner requirement in /.github/scripts
 (#496)

Updates the requirements on [pytest-runner](https://github.com/pytest-dev/pytest-runner) to permit the latest version.
- [Release notes](https://github.com/pytest-dev/pytest-runner/releases)
- [Changelog](https://github.com/pytest-dev/pytest-runner/blob/main/CHANGES.rst)
- [Commits](https://github.com/pytest-dev/pytest-runner/compare/1.0a1...v6.0.1)

---
updated-dependencies:
- dependency-name: pytest-runner
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 8cb6ad15..654f6079 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -5,7 +5,7 @@ pip ; python_version >= '3.5'
 pylint <2.15.10
 pytest <=8.1.1
 pytest-pylint <=1.1.2
-pytest-runner <6.0.0
+pytest-runner <7.0.0
 termcolor <2.5.0
 hypothesis <6.100.0
 python-Levenshtein <0.20.9 ; python_version == '2.7'

From f332cb1fe60c3a381d1ef5dc23c6e6d2142117df Mon Sep 17 00:00:00 2001
From: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Date: Thu, 14 Mar 2024 18:02:23 +0100
Subject: [PATCH 275/324] Fix typo in dependabot github actions group (#497)

---
 .github/dependabot.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 17c20d04..d31b409b 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -20,7 +20,7 @@ updates:
     schedule:
       interval: "monthly"
     groups:
-      pip:
+      gh-actions:
         patterns:
           - "*"  # Check all dependencies
     labels: ["ci"]

From de2852a41746538e9077d8ab2586875cffd3dc57 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 14 Mar 2024 13:11:36 -0400
Subject: [PATCH 276/324] Update setuptools requirement in /.github/scripts
 (#500)

Updates the requirements on [setuptools](https://github.com/pypa/setuptools) to permit the latest version.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/0.6...v69.2.0)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 654f6079..98111196 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -1,5 +1,5 @@
 setuptools <65.7.0 ; python_version == '2.7'
-setuptools <=69.1.1 ; python_version >= '3.8'
+setuptools <=69.2.0 ; python_version >= '3.8'
 pip <23.0 ; python_version == '2.7'
 pip ; python_version >= '3.5'
 pylint <2.15.10

From 6902939a317dca9d41446168d8e88f108e0c0f11 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 5 Apr 2024 16:21:26 -0400
Subject: [PATCH 277/324] Update hypothesis requirement in /.github/scripts
 (#506)

Updates the requirements on [hypothesis](https://github.com/HypothesisWorks/hypothesis) to permit the latest version.
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-ruby-0.0.1...hypothesis-python-6.100.0)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 98111196..15d2b017 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -7,7 +7,7 @@ pytest <=8.1.1
 pytest-pylint <=1.1.2
 pytest-runner <7.0.0
 termcolor <2.5.0
-hypothesis <6.100.0
+hypothesis <6.101.0
 python-Levenshtein <0.20.9 ; python_version == '2.7'
 levenshtein <=0.25.0 ; python_version >= '3.5'
 mock <6.0.0

From e9f49b0fa9d5ee627e80a15e74624fcd41a34add Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 9 Aug 2024 16:15:45 -0400
Subject: [PATCH 278/324] Update levenshtein requirement in /.github/scripts
 (#510)

Updates the requirements on [levenshtein](https://github.com/rapidfuzz/Levenshtein) to permit the latest version.
- [Release notes](https://github.com/rapidfuzz/Levenshtein/releases)
- [Changelog](https://github.com/rapidfuzz/Levenshtein/blob/main/HISTORY.md)
- [Commits](https://github.com/rapidfuzz/Levenshtein/compare/v0.13.0...v0.25.1)

---
updated-dependencies:
- dependency-name: levenshtein
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 15d2b017..f7d3cacd 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -9,5 +9,5 @@ pytest-runner <7.0.0
 termcolor <2.5.0
 hypothesis <6.101.0
 python-Levenshtein <0.20.9 ; python_version == '2.7'
-levenshtein <=0.25.0 ; python_version >= '3.5'
+levenshtein <=0.25.1 ; python_version >= '3.5'
 mock <6.0.0

From a59f6bad3f72ae6035b076504744d3e9f619afea Mon Sep 17 00:00:00 2001
From: Weida Hong <wdhongtw@gmail.com>
Date: Sat, 10 Aug 2024 04:16:48 +0800
Subject: [PATCH 279/324] Use ast.Constant for recent Python versions (#526)

ast.Str is planned to removed in Python 3.14, use ast.Constant instead
whenever the later is available.
---
 fire/parser.py | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/fire/parser.py b/fire/parser.py
index 2aff8bd7..bdf3cdbf 100644
--- a/fire/parser.py
+++ b/fire/parser.py
@@ -20,7 +20,12 @@
 
 import argparse
 import ast
+import sys
 
+if sys.version_info[0:2] < (3, 8):
+  _StrNode = ast.Str
+else:
+  _StrNode = ast.Constant
 
 def CreateParser():
   parser = argparse.ArgumentParser(add_help=False)
@@ -127,4 +132,4 @@ def _Replacement(node):
   # These are the only builtin constants supported by literal_eval.
   if value in ('True', 'False', 'None'):
     return node
-  return ast.Str(value)
+  return _StrNode(value)

From 8b063b952fba6dec79dfbc8688a9edf047de3b6c Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 19 Sep 2024 20:20:44 -0400
Subject: [PATCH 280/324] Remove future imports now that we've dropped support
 for Python 2 (#539)

* Remove future imports now that we've dropped support for Python 2
  * Keep future imports for use in MemberVisible
* Drop support for Python 3.5
* Remove indications of support for Python <3.7 and bump version number.
---
 .github/scripts/build.sh         | 2 +-
 .github/workflows/build.yml      | 2 +-
 fire/__init__.py                 | 4 ----
 fire/__main__.py                 | 4 ----
 fire/completion_test.py          | 4 ----
 fire/console/console_io.py       | 4 ----
 fire/core.py                     | 4 ----
 fire/core_test.py                | 4 ----
 fire/custom_descriptions.py      | 4 ----
 fire/custom_descriptions_test.py | 4 ----
 fire/decorators.py               | 4 ----
 fire/decorators_test.py          | 4 ----
 fire/docstrings.py               | 4 ----
 fire/docstrings_fuzz_test.py     | 4 ----
 fire/docstrings_test.py          | 4 ----
 fire/fire_test.py                | 4 ----
 fire/formatting.py               | 4 ----
 fire/formatting_test.py          | 4 ----
 fire/formatting_windows.py       | 4 ----
 fire/helptext.py                 | 4 ----
 fire/helptext_test.py            | 4 ----
 fire/inspectutils.py             | 4 ----
 fire/inspectutils_test.py        | 4 ----
 fire/interact.py                 | 4 ----
 fire/interact_test.py            | 4 ----
 fire/parser.py                   | 4 ----
 fire/parser_fuzz_test.py         | 4 ----
 fire/parser_test.py              | 4 ----
 fire/test_components.py          | 4 ----
 fire/test_components_bin.py      | 4 ----
 fire/test_components_test.py     | 4 ----
 fire/testutils.py                | 4 ----
 fire/testutils_test.py           | 4 ----
 fire/trace.py                    | 4 ----
 fire/trace_test.py               | 4 ----
 fire/value_types.py              | 4 ----
 setup.py                         | 6 +-----
 37 files changed, 3 insertions(+), 143 deletions(-)

diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh
index 1f9ed766..111257ae 100755
--- a/.github/scripts/build.sh
+++ b/.github/scripts/build.sh
@@ -17,7 +17,7 @@
 # Exit when any command fails.
 set -e
 
-PYTHON_VERSION=${PYTHON_VERSION:-2.7}
+PYTHON_VERSION=${PYTHON_VERSION:-3.7}
 
 pip install -U -r .github/scripts/requirements.txt
 python setup.py develop
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f5562820..7f5225c5 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -7,7 +7,7 @@ jobs:
     runs-on: ubuntu-20.04
     strategy:
       matrix:
-        python-version: ["3.5", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
+        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
 
     steps:
      # Checkout the repo.
diff --git a/fire/__init__.py b/fire/__init__.py
index fae18489..742b03ac 100644
--- a/fire/__init__.py
+++ b/fire/__init__.py
@@ -14,10 +14,6 @@
 
 """The Python Fire module."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from fire.core import Fire
 
 __all__ = ['Fire']
diff --git a/fire/__main__.py b/fire/__main__.py
index 9d8227ad..15a9d6c8 100644
--- a/fire/__main__.py
+++ b/fire/__main__.py
@@ -18,10 +18,6 @@
 This allows using Fire with third-party libraries without modifying their code.
 """
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import importlib
 import os
 import sys
diff --git a/fire/completion_test.py b/fire/completion_test.py
index 582e5bbc..5bafc279 100644
--- a/fire/completion_test.py
+++ b/fire/completion_test.py
@@ -14,10 +14,6 @@
 
 """Tests for the completion module."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from fire import completion
 from fire import test_components as tc
 from fire import testutils
diff --git a/fire/console/console_io.py b/fire/console/console_io.py
index 3d3b9f81..ec0858d9 100644
--- a/fire/console/console_io.py
+++ b/fire/console/console_io.py
@@ -15,10 +15,6 @@
 
 """General console printing utilities used by the Cloud SDK."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import os
 import signal
 import subprocess
diff --git a/fire/core.py b/fire/core.py
index fada01b1..0a6dae7d 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -49,10 +49,6 @@ def main(argv):
   --trace: Get the Fire Trace for the command.
 """
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import inspect
 import json
 import os
diff --git a/fire/core_test.py b/fire/core_test.py
index 75b76998..b9033c22 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -14,10 +14,6 @@
 
 """Tests for the core module."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from fire import core
 from fire import test_components as tc
 from fire import testutils
diff --git a/fire/custom_descriptions.py b/fire/custom_descriptions.py
index 266671f1..f7df90b0 100644
--- a/fire/custom_descriptions.py
+++ b/fire/custom_descriptions.py
@@ -36,10 +36,6 @@
 descriptions for primitive typed values.
 """
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from fire import formatting
 import six
 
diff --git a/fire/custom_descriptions_test.py b/fire/custom_descriptions_test.py
index 79d7c7a1..6cff2d5d 100644
--- a/fire/custom_descriptions_test.py
+++ b/fire/custom_descriptions_test.py
@@ -14,10 +14,6 @@
 
 """Tests for custom description module."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from fire import custom_descriptions
 from fire import testutils
 
diff --git a/fire/decorators.py b/fire/decorators.py
index b2e9b322..eb5b0d20 100644
--- a/fire/decorators.py
+++ b/fire/decorators.py
@@ -18,10 +18,6 @@
 command line arguments to client code.
 """
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import inspect
 
 FIRE_METADATA = 'FIRE_METADATA'
diff --git a/fire/decorators_test.py b/fire/decorators_test.py
index cc7d6203..a316b79f 100644
--- a/fire/decorators_test.py
+++ b/fire/decorators_test.py
@@ -14,10 +14,6 @@
 
 """Tests for the decorators module."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from fire import core
 from fire import decorators
 from fire import testutils
diff --git a/fire/docstrings.py b/fire/docstrings.py
index 1cfadea9..2d7c7e63 100644
--- a/fire/docstrings.py
+++ b/fire/docstrings.py
@@ -49,10 +49,6 @@
 - "True | False" indicates bool type.
 """
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import collections
 import enum
 import re
diff --git a/fire/docstrings_fuzz_test.py b/fire/docstrings_fuzz_test.py
index 7609f4f8..66be8006 100644
--- a/fire/docstrings_fuzz_test.py
+++ b/fire/docstrings_fuzz_test.py
@@ -14,10 +14,6 @@
 
 """Fuzz tests for the docstring parser module."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from fire import docstrings
 from fire import testutils
 
diff --git a/fire/docstrings_test.py b/fire/docstrings_test.py
index 0d6e5d18..ce516944 100644
--- a/fire/docstrings_test.py
+++ b/fire/docstrings_test.py
@@ -14,10 +14,6 @@
 
 """Tests for fire docstrings module."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from fire import docstrings
 from fire import testutils
 
diff --git a/fire/fire_test.py b/fire/fire_test.py
index 8b904c29..6b9a2fa2 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -14,10 +14,6 @@
 
 """Tests for the fire module."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import os
 import sys
 
diff --git a/fire/formatting.py b/fire/formatting.py
index faef8047..68484c27 100644
--- a/fire/formatting.py
+++ b/fire/formatting.py
@@ -14,10 +14,6 @@
 
 """Formatting utilities for use in creating help text."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from fire import formatting_windows  # pylint: disable=unused-import
 import termcolor
 
diff --git a/fire/formatting_test.py b/fire/formatting_test.py
index 05a88c49..e0f6699d 100644
--- a/fire/formatting_test.py
+++ b/fire/formatting_test.py
@@ -14,10 +14,6 @@
 
 """Tests for formatting.py."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from fire import formatting
 from fire import testutils
 
diff --git a/fire/formatting_windows.py b/fire/formatting_windows.py
index ce0f677d..f8241eaa 100644
--- a/fire/formatting_windows.py
+++ b/fire/formatting_windows.py
@@ -14,10 +14,6 @@
 
 """This module is used for enabling formatting on Windows."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import ctypes
 import os
 import platform
diff --git a/fire/helptext.py b/fire/helptext.py
index 6e7fbb07..93072897 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -29,10 +29,6 @@
 information.
 """
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import collections
 import itertools
 import sys
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 404d9812..9a0f4f6d 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -14,10 +14,6 @@
 
 """Tests for the helptext module."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import os
 import sys
 import textwrap
diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index 15f32f91..ca51a9a5 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -14,10 +14,6 @@
 
 """Inspection utility functions for Python Fire."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import inspect
 import sys
 import types
diff --git a/fire/inspectutils_test.py b/fire/inspectutils_test.py
index ea8eb0e2..bb62f402 100644
--- a/fire/inspectutils_test.py
+++ b/fire/inspectutils_test.py
@@ -14,10 +14,6 @@
 
 """Tests for the inspectutils module."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import os
 import unittest
 
diff --git a/fire/interact.py b/fire/interact.py
index 7df32841..7bdeb9a7 100644
--- a/fire/interact.py
+++ b/fire/interact.py
@@ -20,10 +20,6 @@
 InteractiveConsole class.
 """
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import inspect
 
 
diff --git a/fire/interact_test.py b/fire/interact_test.py
index 29fa7597..99cde285 100644
--- a/fire/interact_test.py
+++ b/fire/interact_test.py
@@ -14,10 +14,6 @@
 
 """Tests for the interact module."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from fire import interact
 from fire import testutils
 
diff --git a/fire/parser.py b/fire/parser.py
index bdf3cdbf..c4708455 100644
--- a/fire/parser.py
+++ b/fire/parser.py
@@ -14,10 +14,6 @@
 
 """Provides parsing functionality used by Python Fire."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import argparse
 import ast
 import sys
diff --git a/fire/parser_fuzz_test.py b/fire/parser_fuzz_test.py
index af0be038..38e17725 100644
--- a/fire/parser_fuzz_test.py
+++ b/fire/parser_fuzz_test.py
@@ -14,10 +14,6 @@
 
 """Fuzz tests for the parser module."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from fire import parser
 from fire import testutils
 from hypothesis import example
diff --git a/fire/parser_test.py b/fire/parser_test.py
index 8aeabc61..a404eea2 100644
--- a/fire/parser_test.py
+++ b/fire/parser_test.py
@@ -14,10 +14,6 @@
 
 """Tests for the parser module."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from fire import parser
 from fire import testutils
 
diff --git a/fire/test_components.py b/fire/test_components.py
index 5fcb056e..e50f647c 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -14,10 +14,6 @@
 
 """This module has components that are used for testing Python Fire."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import collections
 import enum
 import functools
diff --git a/fire/test_components_bin.py b/fire/test_components_bin.py
index fbb41952..62afdf11 100644
--- a/fire/test_components_bin.py
+++ b/fire/test_components_bin.py
@@ -17,10 +17,6 @@
 This file is useful for replicating test results manually.
 """
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import fire
 from fire import test_components
 
diff --git a/fire/test_components_test.py b/fire/test_components_test.py
index f35d7ab5..531f882c 100644
--- a/fire/test_components_test.py
+++ b/fire/test_components_test.py
@@ -14,10 +14,6 @@
 
 """Tests for the test_components module."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from fire import test_components as tc
 from fire import testutils
 
diff --git a/fire/testutils.py b/fire/testutils.py
index 5f875147..76faa3f4 100644
--- a/fire/testutils.py
+++ b/fire/testutils.py
@@ -14,10 +14,6 @@
 
 """Utilities for Python Fire's tests."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import contextlib
 import os
 import re
diff --git a/fire/testutils_test.py b/fire/testutils_test.py
index ad604193..0999a4c8 100644
--- a/fire/testutils_test.py
+++ b/fire/testutils_test.py
@@ -14,10 +14,6 @@
 
 """Test the test utilities for Fire's tests."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import sys
 
 from fire import testutils
diff --git a/fire/trace.py b/fire/trace.py
index 4c9674e3..2145186e 100644
--- a/fire/trace.py
+++ b/fire/trace.py
@@ -25,10 +25,6 @@
 component will be None.
 """
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import shlex
 
 from fire import inspectutils
diff --git a/fire/trace_test.py b/fire/trace_test.py
index 1621a593..1f858f5e 100644
--- a/fire/trace_test.py
+++ b/fire/trace_test.py
@@ -14,10 +14,6 @@
 
 """Tests for the trace module."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 from fire import testutils
 from fire import trace
 
diff --git a/fire/value_types.py b/fire/value_types.py
index c0a137fd..b2d0a0b3 100644
--- a/fire/value_types.py
+++ b/fire/value_types.py
@@ -14,10 +14,6 @@
 
 """Types of values."""
 
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
 import inspect
 
 from fire import inspectutils
diff --git a/setup.py b/setup.py
index 24e0e325..f861f9a5 100644
--- a/setup.py
+++ b/setup.py
@@ -40,7 +40,7 @@
     'python-Levenshtein',
 ]
 
-VERSION = '0.6.0'
+VERSION = '0.7.0'
 URL = 'https://github.com/google/python-fire'
 
 setup(
@@ -63,11 +63,7 @@
         'License :: OSI Approved :: Apache Software License',
 
         'Programming Language :: Python',
-        'Programming Language :: Python :: 2',
-        'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.5',
-        'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
         'Programming Language :: Python :: 3.9',

From 5d0706d814e8c9297f078fabc0a1638c34c1ef30 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 19 Sep 2024 20:57:15 -0400
Subject: [PATCH 281/324] remove six: Replace six.string_types and
 six.integer_types, etc. (#541)

This commit includes:

* assuming Python 3 for test skipping
* assuming Python 3 for fire's various type checks
* assuming Python 3 for imports (like asyncio)
* assuming Python 3 for getting function signatures
* six is no longer considered a hidden module (and so if a user of fire has six in their globals when they call Fire(), six will now show up where it used to be hidden)

This commit does not remove six from console/ code.
---
 fire/completion.py          | 21 +++++++--------------
 fire/core.py                |  8 +++-----
 fire/core_test.py           |  4 ----
 fire/custom_descriptions.py |  9 +++------
 fire/fire_test.py           |  3 ---
 fire/helptext_test.py       | 11 -----------
 fire/inspectutils.py        | 27 ++++-----------------------
 fire/inspectutils_test.py   |  9 +--------
 fire/parser_fuzz_test.py    |  7 +++----
 fire/test_components.py     |  5 +----
 fire/testutils.py           | 11 ++++-------
 fire/testutils_test.py      |  8 +++-----
 fire/value_types.py         |  3 +--
 13 files changed, 30 insertions(+), 96 deletions(-)

diff --git a/fire/completion.py b/fire/completion.py
index 4393880d..3aa8ab11 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -23,7 +23,6 @@
 import inspect
 
 from fire import inspectutils
-import six
 
 
 def Script(name, component, default_options=None, shell='bash'):
@@ -308,7 +307,7 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False):
   Returns
     A boolean value indicating whether the member should be included.
   """
-  if isinstance(name, six.string_types) and name.startswith('__'):
+  if isinstance(name, str) and name.startswith('__'):
     return False
   if verbose:
     return True
@@ -316,10 +315,11 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False):
       or member is division
       or member is print_function):
     return False
-  if isinstance(member, type(absolute_import)) and six.PY34:
+  if isinstance(member, type(absolute_import)):
     return False
-  if inspect.ismodule(member) and member is six:
-    # TODO(dbieber): Determine more generally which modules to hide.
+  # TODO(dbieber): Determine more generally which modules to hide.
+  modules_to_hide = []
+  if inspect.ismodule(member) and member in modules_to_hide:
     return False
   if inspect.isclass(component):
     # If class_attrs has not been provided, compute it.
@@ -336,14 +336,7 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False):
       tuplegetter = getattr(collections, '_tuplegetter', type(None))
       if isinstance(class_attr.object, tuplegetter):
         return False
-  if (six.PY2 and inspect.isfunction(component)
-      and name in ('func_closure', 'func_code', 'func_defaults',
-                   'func_dict', 'func_doc', 'func_globals', 'func_name')):
-    return False
-  if (six.PY2 and inspect.ismethod(component)
-      and name in ('im_class', 'im_func', 'im_self')):
-    return False
-  if isinstance(name, six.string_types):
+  if isinstance(name, str):
     return not name.startswith('_')
   return True  # Default to including the member
 
@@ -438,7 +431,7 @@ def _FormatForCommand(token):
   Returns:
     The transformed token.
   """
-  if not isinstance(token, six.string_types):
+  if not isinstance(token, str):
     token = str(token)
 
   if token.startswith('_'):
diff --git a/fire/core.py b/fire/core.py
index 0a6dae7d..c61a8b57 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -67,10 +67,8 @@ def main(argv):
 from fire import trace
 from fire import value_types
 from fire.console import console_io
-import six
 
-if six.PY34:
-  import asyncio  # pylint: disable=import-error,g-import-not-at-top  # pytype: disable=import-error
+import asyncio  # pylint: disable=import-error,g-import-not-at-top  # pytype: disable=import-error
 
 
 def Fire(component=None, command=None, name=None, serialize=None):
@@ -109,7 +107,7 @@ def Fire(component=None, command=None, name=None, serialize=None):
   name = name or os.path.basename(sys.argv[0])
 
   # Get args as a list.
-  if isinstance(command, six.string_types):
+  if isinstance(command, str):
     args = shlex.split(command)
   elif isinstance(command, (list, tuple)):
     args = command
@@ -344,7 +342,7 @@ def _DictAsString(result, verbose=False):
 def _OneLineResult(result):
   """Returns result serialized to a single line string."""
   # TODO(dbieber): Ensure line is fewer than eg 120 characters.
-  if isinstance(result, six.string_types):
+  if isinstance(result, str):
     return str(result).replace('\n', ' ')
 
   # TODO(dbieber): Show a small amount of usage information about the function
diff --git a/fire/core_test.py b/fire/core_test.py
index b9033c22..9e1f7dba 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -20,8 +20,6 @@
 from fire import trace
 import mock
 
-import six
-
 
 class CoreTest(testutils.BaseTestCase):
 
@@ -214,13 +212,11 @@ def serialize(x):
     with self.assertRaises(core.FireError):
       core.Fire(ident, command=['asdf'], serialize=55)
 
-  @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.')
   def testLruCacheDecoratorBoundArg(self):
     self.assertEqual(
         core.Fire(tc.py3.LruCacheDecoratedMethod,  # pytype: disable=module-attr
                   command=['lru_cache_in_class', 'foo']), 'foo')
 
-  @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.')
   def testLruCacheDecorator(self):
     self.assertEqual(
         core.Fire(tc.py3.lru_cache_decorated,  # pytype: disable=module-attr
diff --git a/fire/custom_descriptions.py b/fire/custom_descriptions.py
index f7df90b0..768f0e23 100644
--- a/fire/custom_descriptions.py
+++ b/fire/custom_descriptions.py
@@ -37,7 +37,6 @@
 """
 
 from fire import formatting
-import six
 
 TWO_DOUBLE_QUOTES = '""'
 STRING_DESC_PREFIX = 'The string '
@@ -60,13 +59,11 @@ def NeedsCustomDescription(component):
     Whether the component should use a custom description and summary.
   """
   type_ = type(component)
-  if (type_ in six.string_types
-      or type_ in six.integer_types
-      or type_ is six.text_type
-      or type_ is six.binary_type
+  if (
+      type_ in (str, int, bytes)
       or type_ in (float, complex, bool)
       or type_ in (dict, tuple, list, set, frozenset)
-     ):
+  ):
     return True
   return False
 
diff --git a/fire/fire_test.py b/fire/fire_test.py
index 6b9a2fa2..74b3bb25 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -22,7 +22,6 @@
 from fire import testutils
 
 import mock
-import six
 
 
 class FireTest(testutils.BaseTestCase):
@@ -180,7 +179,6 @@ def testFireAnnotatedArgs(self):
     self.assertEqual(fire.Fire(tc.Annotations, command=['double', '5']), 10)
     self.assertEqual(fire.Fire(tc.Annotations, command=['triple', '5']), 15)
 
-  @testutils.skipIf(six.PY2, 'Keyword-only arguments not in Python 2.')
   def testFireKeywordOnlyArgs(self):
     with self.assertRaisesFireExit(2):
       # Keyword arguments must be passed with flag syntax.
@@ -717,7 +715,6 @@ def testHelpKwargsDecorator(self):
     with self.assertRaisesFireExit(0):
       fire.Fire(tc.decorated_method, command=['--help'])
 
-  @testutils.skipIf(six.PY2, 'Asyncio not available in Python 2.')
   def testFireAsyncio(self):
     self.assertEqual(fire.Fire(tc.py3.WithAsyncio,
                                command=['double', '--count', '10']), 20)
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 9a0f4f6d..2250f199 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -23,7 +23,6 @@
 from fire import test_components as tc
 from fire import testutils
 from fire import trace
-import six
 
 
 class HelpTest(testutils.BaseTestCase):
@@ -276,8 +275,6 @@ def testHelpTextNoInit(self):
     self.assertIn('NAME\n    OldStyleEmpty', help_screen)
     self.assertIn('SYNOPSIS\n    OldStyleEmpty', help_screen)
 
-  @testutils.skipIf(
-      six.PY2, 'Python 2 does not support keyword-only arguments.')
   def testHelpTextKeywordOnlyArgumentsWithDefault(self):
     component = tc.py3.KeywordOnly.with_default  # pytype: disable=module-attr
     output = helptext.HelpText(
@@ -285,8 +282,6 @@ def testHelpTextKeywordOnlyArgumentsWithDefault(self):
     self.assertIn('NAME\n    with_default', output)
     self.assertIn('FLAGS\n    -x, --x=X', output)
 
-  @testutils.skipIf(
-      six.PY2, 'Python 2 does not support keyword-only arguments.')
   def testHelpTextKeywordOnlyArgumentsWithoutDefault(self):
     component = tc.py3.KeywordOnly.double  # pytype: disable=module-attr
     output = helptext.HelpText(
@@ -294,9 +289,6 @@ def testHelpTextKeywordOnlyArgumentsWithoutDefault(self):
     self.assertIn('NAME\n    double', output)
     self.assertIn('FLAGS\n    -c, --count=COUNT (required)', output)
 
-  @testutils.skipIf(
-      six.PY2,
-      'Python 2 does not support required name-only arguments.')
   def testHelpTextFunctionMixedDefaults(self):
     component = tc.py3.HelpTextComponent().identity
     t = trace.FireTrace(component, name='FunctionMixedDefaults')
@@ -523,9 +515,6 @@ def testUsageOutputFunctionWithDocstring(self):
         textwrap.dedent(expected_output).lstrip('\n'),
         usage_output)
 
-  @testutils.skipIf(
-      six.PY2,
-      'Python 2 does not support required name-only arguments.')
   def testUsageOutputFunctionMixedDefaults(self):
     component = tc.py3.HelpTextComponent().identity
     t = trace.FireTrace(component, name='FunctionMixedDefaults')
diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index ca51a9a5..0d0b048d 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -20,10 +20,7 @@
 
 from fire import docstrings
 
-import six
-
-if six.PY34:
-  import asyncio  # pylint: disable=import-error,g-import-not-at-top  # pytype: disable=import-error
+import asyncio  # pylint: disable=import-error,g-import-not-at-top  # pytype: disable=import-error
 
 
 class FullArgSpec(object):
@@ -74,8 +71,6 @@ class with an __init__ method.
   if inspect.isclass(fn):
     # If the function is a class, we try to use its init method.
     skip_arg = True
-    if six.PY2 and hasattr(fn, '__init__'):
-      fn = fn.__init__
   elif inspect.ismethod(fn):
     # If the function is a bound method, we skip the `self` argument.
     skip_arg = fn.__self__ is not None
@@ -91,16 +86,6 @@ class with an __init__ method.
   return fn, skip_arg
 
 
-def Py2GetArgSpec(fn):
-  """A wrapper around getargspec that tries both fn and fn.__call__."""
-  try:
-    return inspect.getargspec(fn)  # pylint: disable=deprecated-method,no-member
-  except TypeError:
-    if hasattr(fn, '__call__'):
-      return inspect.getargspec(fn.__call__)  # pylint: disable=deprecated-method,no-member
-    raise
-
-
 def Py3GetFullArgSpec(fn):
   """A alternative to the builtin getfullargspec.
 
@@ -185,13 +170,9 @@ def GetFullArgSpec(fn):
     if sys.version_info[0:2] >= (3, 5):
       (args, varargs, varkw, defaults,
        kwonlyargs, kwonlydefaults, annotations) = Py3GetFullArgSpec(fn)
-    elif six.PY3:  # Specifically Python 3.4.
+    else:  # Specifically Python 3.4.
       (args, varargs, varkw, defaults,
        kwonlyargs, kwonlydefaults, annotations) = inspect.getfullargspec(fn)  # pylint: disable=deprecated-method,no-member
-    else:  # six.PY2
-      args, varargs, varkw, defaults = Py2GetArgSpec(fn)
-      kwonlyargs = kwonlydefaults = None
-      annotations = getattr(fn, '__annotations__', None)
 
   except TypeError:
     # If we can't get the argspec, how do we know if the fn should take args?
@@ -221,7 +202,7 @@ def GetFullArgSpec(fn):
     return FullArgSpec()
 
   # In Python 3.5+ Py3GetFullArgSpec uses skip_bound_arg=True already.
-  skip_arg_required = six.PY2 or sys.version_info[0:2] == (3, 4)
+  skip_arg_required = sys.version_info[0:2] == (3, 4)
   if skip_arg_required and skip_arg and args:
     args.pop(0)  # Remove 'self' or 'cls' from the list of arguments.
   return FullArgSpec(args, varargs, varkw, defaults,
@@ -363,6 +344,6 @@ def GetClassAttrsDict(component):
 
 def IsCoroutineFunction(fn):
   try:
-    return six.PY34 and asyncio.iscoroutinefunction(fn)
+    return asyncio.iscoroutinefunction(fn)
   except:  # pylint: disable=bare-except
     return False
diff --git a/fire/inspectutils_test.py b/fire/inspectutils_test.py
index bb62f402..47de7e72 100644
--- a/fire/inspectutils_test.py
+++ b/fire/inspectutils_test.py
@@ -15,14 +15,11 @@
 """Tests for the inspectutils module."""
 
 import os
-import unittest
 
 from fire import inspectutils
 from fire import test_components as tc
 from fire import testutils
 
-import six
-
 
 class InspectUtilsTest(testutils.BaseTestCase):
 
@@ -36,7 +33,6 @@ def testGetFullArgSpec(self):
     self.assertEqual(spec.kwonlydefaults, {})
     self.assertEqual(spec.annotations, {'arg2': int, 'arg4': int})
 
-  @unittest.skipIf(six.PY2, 'No keyword arguments in python 2')
   def testGetFullArgSpecPy3(self):
     spec = inspectutils.GetFullArgSpec(tc.py3.identity)
     self.assertEqual(spec.args, ['arg1', 'arg2', 'arg3', 'arg4'])
@@ -121,10 +117,7 @@ def testInfoClass(self):
 
   def testInfoClassNoInit(self):
     info = inspectutils.Info(tc.OldStyleEmpty)
-    if six.PY2:
-      self.assertEqual(info.get('type_name'), 'classobj')
-    else:
-      self.assertEqual(info.get('type_name'), 'type')
+    self.assertEqual(info.get('type_name'), 'type')
     self.assertIn(os.path.join('fire', 'test_components.py'), info.get('file'))
     self.assertGreater(info.get('line'), 0)
 
diff --git a/fire/parser_fuzz_test.py b/fire/parser_fuzz_test.py
index 38e17725..9739ec4e 100644
--- a/fire/parser_fuzz_test.py
+++ b/fire/parser_fuzz_test.py
@@ -21,7 +21,6 @@
 from hypothesis import settings
 from hypothesis import strategies as st
 import Levenshtein
-import six
 
 
 class ParserFuzzTest(testutils.BaseTestCase):
@@ -64,8 +63,8 @@ def testDefaultParseValueFuzz(self, value):
       raise
 
     try:
-      uvalue = six.text_type(value)
-      uresult = six.text_type(result)
+      uvalue = str(value)
+      uresult = str(result)
     except UnicodeDecodeError:
       # This is not what we're testing.
       return
@@ -82,7 +81,7 @@ def testDefaultParseValueFuzz(self, value):
     if '#' in value:
       max_distance += len(value) - value.index('#')
 
-    if not isinstance(result, six.string_types):
+    if not isinstance(result, str):
       max_distance += value.count('0')  # Leading 0s are stripped.
 
     # Note: We don't check distance for dicts since item order can be changed.
diff --git a/fire/test_components.py b/fire/test_components.py
index e50f647c..540a9e16 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -18,10 +18,7 @@
 import enum
 import functools
 
-import six
-
-if six.PY3:
-  from fire import test_components_py3 as py3  # pylint: disable=unused-import,no-name-in-module,g-import-not-at-top
+from fire import test_components_py3 as py3  # pylint: disable=unused-import,no-name-in-module,g-import-not-at-top
 
 
 def identity(arg1, arg2, arg3=10, arg4=20, *arg5, **arg6):  # pylint: disable=keyword-arg-before-vararg
diff --git a/fire/testutils.py b/fire/testutils.py
index 76faa3f4..fa1ca86d 100644
--- a/fire/testutils.py
+++ b/fire/testutils.py
@@ -15,6 +15,7 @@
 """Utilities for Python Fire's tests."""
 
 import contextlib
+import io
 import os
 import re
 import sys
@@ -24,7 +25,6 @@
 from fire import trace
 
 import mock
-import six
 
 
 class BaseTestCase(unittest.TestCase):
@@ -45,8 +45,8 @@ def assertOutputMatches(self, stdout='.*', stderr='.*', capture=True):
     Yields:
       Yields to the wrapped context.
     """
-    stdout_fp = six.StringIO()
-    stderr_fp = six.StringIO()
+    stdout_fp = io.StringIO()
+    stderr_fp = io.StringIO()
     try:
       with mock.patch.object(sys, 'stdout', stdout_fp):
         with mock.patch.object(sys, 'stderr', stderr_fp):
@@ -69,10 +69,7 @@ def assertOutputMatches(self, stdout='.*', stderr='.*', capture=True):
                                (name, value, regexp))
 
   def assertRaisesRegex(self, *args, **kwargs):  # pylint: disable=arguments-differ
-    if sys.version_info.major == 2:
-      return super(BaseTestCase, self).assertRaisesRegexp(*args, **kwargs)  # pylint: disable=deprecated-method,no-member
-    else:
-      return super(BaseTestCase, self).assertRaisesRegex(*args, **kwargs)  # pylint: disable=no-member
+    return super(BaseTestCase, self).assertRaisesRegex(*args, **kwargs)  # pylint: disable=no-member
 
   @contextlib.contextmanager
   def assertRaisesFireExit(self, code, regexp='.*'):
diff --git a/fire/testutils_test.py b/fire/testutils_test.py
index 0999a4c8..4cfc0937 100644
--- a/fire/testutils_test.py
+++ b/fire/testutils_test.py
@@ -18,8 +18,6 @@
 
 from fire import testutils
 
-import six
-
 
 class TestTestUtils(testutils.BaseTestCase):
   """Let's get meta."""
@@ -30,15 +28,15 @@ def testNoCheckOnException(self):
         raise ValueError()
 
   def testCheckStdoutOrStderrNone(self):
-    with six.assertRaisesRegex(self, AssertionError, 'stdout:'):
+    with self.assertRaisesRegex(AssertionError, 'stdout:'):
       with self.assertOutputMatches(stdout=None):
         print('blah')
 
-    with six.assertRaisesRegex(self, AssertionError, 'stderr:'):
+    with self.assertRaisesRegex(AssertionError, 'stderr:'):
       with self.assertOutputMatches(stderr=None):
         print('blah', file=sys.stderr)
 
-    with six.assertRaisesRegex(self, AssertionError, 'stderr:'):
+    with self.assertRaisesRegex(AssertionError, 'stderr:'):
       with self.assertOutputMatches(stdout='apple', stderr=None):
         print('apple')
         print('blah', file=sys.stderr)
diff --git a/fire/value_types.py b/fire/value_types.py
index b2d0a0b3..81308973 100644
--- a/fire/value_types.py
+++ b/fire/value_types.py
@@ -17,10 +17,9 @@
 import inspect
 
 from fire import inspectutils
-import six
 
 
-VALUE_TYPES = (bool, six.string_types, six.integer_types, float, complex,
+VALUE_TYPES = (bool, str, bytes, int, float, complex,
                type(Ellipsis), type(None), type(NotImplemented))
 
 

From b13c13bf1767caf80bf38349dd5fbf0dd08e18b8 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 19 Sep 2024 21:08:42 -0400
Subject: [PATCH 282/324] Remove sys.version_info checks (#542)

* Remove sys.version_info checks where no longer needed
---
 fire/__main__.py      | 25 ++++++-------------------
 fire/fire_test.py     |  2 --
 fire/helptext.py      |  5 +----
 fire/helptext_test.py | 13 -------------
 fire/parser.py        |  1 +
 5 files changed, 8 insertions(+), 38 deletions(-)

diff --git a/fire/__main__.py b/fire/__main__.py
index 15a9d6c8..11fb1b42 100644
--- a/fire/__main__.py
+++ b/fire/__main__.py
@@ -19,6 +19,7 @@
 """
 
 import importlib
+from importlib import util
 import os
 import sys
 
@@ -57,27 +58,13 @@ def import_from_file_path(path):
 
   module_name = os.path.basename(path)
 
-  if sys.version_info.major == 3 and sys.version_info.minor < 5:
-    loader = importlib.machinery.SourceFileLoader(  # pylint: disable=no-member
-        fullname=module_name,
-        path=path,
-    )
+  spec = util.spec_from_file_location(module_name, path)
 
-    module = loader.load_module(module_name)  # pylint: disable=deprecated-method
+  if spec is None:
+    raise IOError('Unable to load module from specified path.')
 
-  elif sys.version_info.major == 3:
-    from importlib import util  # pylint: disable=g-import-not-at-top,import-outside-toplevel,no-name-in-module
-    spec = util.spec_from_file_location(module_name, path)
-
-    if spec is None:
-      raise IOError('Unable to load module from specified path.')
-
-    module = util.module_from_spec(spec)  # pylint: disable=no-member
-    spec.loader.exec_module(module)  # pytype: disable=attribute-error
-
-  else:
-    import imp  # pylint: disable=g-import-not-at-top,import-outside-toplevel,deprecated-module,import-error
-    module = imp.load_source(module_name, path)
+  module = util.module_from_spec(spec)  # pylint: disable=no-member
+  spec.loader.exec_module(module)  # pytype: disable=attribute-error
 
   return module, module_name
 
diff --git a/fire/fire_test.py b/fire/fire_test.py
index 74b3bb25..74f1f6e6 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -706,8 +706,6 @@ def testClassWithInvalidProperty(self):
         fire.Fire(tc.InvalidProperty, command=['double', '10']), 20
     )
 
-  @testutils.skipIf(sys.version_info[0:2] <= (3, 4),
-                    'Cannot inspect wrapped signatures in Python 2 or 3.4.')
   def testHelpKwargsDecorator(self):
     # Issue #190, follow the wrapped method instead of crashing.
     with self.assertRaisesFireExit(0):
diff --git a/fire/helptext.py b/fire/helptext.py
index 93072897..1c0cb626 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -31,7 +31,6 @@
 
 import collections
 import itertools
-import sys
 
 from fire import completion
 from fire import custom_descriptions
@@ -533,9 +532,7 @@ def _GetArgType(arg, spec):
   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__
+      return arg_type.__qualname__
     except AttributeError:
       # Some typing objects, such as typing.Union do not have either a __name__
       # or __qualname__ attribute.
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 2250f199..4d35dc0a 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -15,7 +15,6 @@
 """Tests for the helptext module."""
 
 import os
-import sys
 import textwrap
 
 from fire import formatting
@@ -124,9 +123,6 @@ def testHelpTextFunctionWithKwargsAndDefaults(self):
         'Additional undocumented flags may also be accepted.',
         help_screen)
 
-  @testutils.skipIf(
-      sys.version_info[0:2] < (3, 5),
-      'Python < 3.5 does not support type hints.')
   def testHelpTextFunctionWithDefaultsAndTypes(self):
     component = (
         tc.py3.WithDefaultsAndTypes().double)  # pytype: disable=module-attr
@@ -141,9 +137,6 @@ def testHelpTextFunctionWithDefaultsAndTypes(self):
         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.py3.WithDefaultsAndTypes().get_int)  # pytype: disable=module-attr
@@ -159,9 +152,6 @@ def testHelpTextFunctionWithTypesAndDefaultNone(self):
         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.py3.WithTypes().double  # pytype: disable=module-attr
     help_screen = helptext.HelpText(
@@ -177,9 +167,6 @@ def testHelpTextFunctionWithTypes(self):
         '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.py3.WithTypes().long_type  # pytype: disable=module-attr
     help_screen = helptext.HelpText(
diff --git a/fire/parser.py b/fire/parser.py
index c4708455..d945b8ce 100644
--- a/fire/parser.py
+++ b/fire/parser.py
@@ -23,6 +23,7 @@
 else:
   _StrNode = ast.Constant
 
+
 def CreateParser():
   parser = argparse.ArgumentParser(add_help=False)
   parser.add_argument('--verbose', '-v', action='store_true')

From 2d950337499156ccb9a17dbf5f389d0c7d10ec24 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 19 Sep 2024 21:09:02 -0400
Subject: [PATCH 283/324] Update setuptools requirement in /.github/scripts
 (#540)

Updates the requirements on [setuptools](https://github.com/pypa/setuptools) to permit the latest version.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/0.6...v75.1.0)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index f7d3cacd..1932f53d 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -1,5 +1,5 @@
 setuptools <65.7.0 ; python_version == '2.7'
-setuptools <=69.2.0 ; python_version >= '3.8'
+setuptools <=75.1.0 ; python_version >= '3.8'
 pip <23.0 ; python_version == '2.7'
 pip ; python_version >= '3.5'
 pylint <2.15.10

From f012df240f16bd7e1c3eb90e96472ef5051ac5e2 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 19 Sep 2024 21:20:37 -0400
Subject: [PATCH 284/324] Simplify requirements.txt by assuming Python 3 (#543)

* Remove universal=1 line
* Update setup.py
* Update requirements.txt
---
 .github/scripts/requirements.txt | 9 +++------
 setup.cfg                        | 3 ---
 setup.py                         | 3 +--
 3 files changed, 4 insertions(+), 11 deletions(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 1932f53d..d0344221 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -1,13 +1,10 @@
-setuptools <65.7.0 ; python_version == '2.7'
-setuptools <=75.1.0 ; python_version >= '3.8'
-pip <23.0 ; python_version == '2.7'
-pip ; python_version >= '3.5'
+setuptools <=75.1.0
+pip
 pylint <2.15.10
 pytest <=8.1.1
 pytest-pylint <=1.1.2
 pytest-runner <7.0.0
 termcolor <2.5.0
 hypothesis <6.101.0
-python-Levenshtein <0.20.9 ; python_version == '2.7'
-levenshtein <=0.25.1 ; python_version >= '3.5'
+levenshtein <=0.25.1
 mock <6.0.0
diff --git a/setup.cfg b/setup.cfg
index 977056b0..ed53d83b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,3 @@
-[wheel]
-universal = 1
-
 [aliases]
 test = pytest
 
diff --git a/setup.py b/setup.py
index f861f9a5..6adbef46 100644
--- a/setup.py
+++ b/setup.py
@@ -31,13 +31,12 @@
 DEPENDENCIES = [
     'six',
     'termcolor',
-    'enum34; python_version < "3.4"'
 ]
 
 TEST_DEPENDENCIES = [
     'hypothesis',
     'mock',
-    'python-Levenshtein',
+    'levenshtein',
 ]
 
 VERSION = '0.7.0'

From 9825623bd5c66692be16126087d3492eed9a0161 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 19 Sep 2024 21:26:51 -0400
Subject: [PATCH 285/324] Update hypothesis requirement in /.github/scripts
 (#544)

Updates the requirements on [hypothesis](https://github.com/HypothesisWorks/hypothesis) to permit the latest version.
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-ruby-0.0.1...hypothesis-python-6.112.1)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index d0344221..669f09e1 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -5,6 +5,6 @@ pytest <=8.1.1
 pytest-pylint <=1.1.2
 pytest-runner <7.0.0
 termcolor <2.5.0
-hypothesis <6.101.0
+hypothesis <6.113.0
 levenshtein <=0.25.1
 mock <6.0.0

From b83fa05b72d8b225182043740e35cd0c28a29293 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 19 Sep 2024 21:30:54 -0400
Subject: [PATCH 286/324] Remove mock in favor of unittest.mock (#545)

* Remove mock in favor of unittest.mock
---
 .github/scripts/requirements.txt | 1 -
 fire/core_test.py                | 3 ++-
 fire/fire_import_test.py         | 2 +-
 fire/fire_test.py                | 3 +--
 fire/interact_test.py            | 4 ++--
 fire/testutils.py                | 3 +--
 setup.py                         | 1 -
 7 files changed, 7 insertions(+), 10 deletions(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 669f09e1..8c0a5dcc 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -7,4 +7,3 @@ pytest-runner <7.0.0
 termcolor <2.5.0
 hypothesis <6.113.0
 levenshtein <=0.25.1
-mock <6.0.0
diff --git a/fire/core_test.py b/fire/core_test.py
index 9e1f7dba..90b7f466 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -14,11 +14,12 @@
 
 """Tests for the core module."""
 
+from unittest import mock
+
 from fire import core
 from fire import test_components as tc
 from fire import testutils
 from fire import trace
-import mock
 
 
 class CoreTest(testutils.BaseTestCase):
diff --git a/fire/fire_import_test.py b/fire/fire_import_test.py
index c5975681..a6b4acc3 100644
--- a/fire/fire_import_test.py
+++ b/fire/fire_import_test.py
@@ -15,10 +15,10 @@
 """Tests importing the fire module."""
 
 import sys
+from unittest import mock
 
 import fire
 from fire import testutils
-import mock
 
 
 class FireImportTest(testutils.BaseTestCase):
diff --git a/fire/fire_test.py b/fire/fire_test.py
index 74f1f6e6..99b4a7c6 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -16,13 +16,12 @@
 
 import os
 import sys
+from unittest import mock
 
 import fire
 from fire import test_components as tc
 from fire import testutils
 
-import mock
-
 
 class FireTest(testutils.BaseTestCase):
 
diff --git a/fire/interact_test.py b/fire/interact_test.py
index 99cde285..2f286824 100644
--- a/fire/interact_test.py
+++ b/fire/interact_test.py
@@ -14,11 +14,11 @@
 
 """Tests for the interact module."""
 
+from unittest import mock
+
 from fire import interact
 from fire import testutils
 
-import mock
-
 
 try:
   import IPython  # pylint: disable=unused-import, g-import-not-at-top
diff --git a/fire/testutils.py b/fire/testutils.py
index fa1ca86d..816551b5 100644
--- a/fire/testutils.py
+++ b/fire/testutils.py
@@ -20,12 +20,11 @@
 import re
 import sys
 import unittest
+from unittest import mock
 
 from fire import core
 from fire import trace
 
-import mock
-
 
 class BaseTestCase(unittest.TestCase):
   """Shared test case for Python Fire tests."""
diff --git a/setup.py b/setup.py
index 6adbef46..53f3381a 100644
--- a/setup.py
+++ b/setup.py
@@ -35,7 +35,6 @@
 
 TEST_DEPENDENCIES = [
     'hypothesis',
-    'mock',
     'levenshtein',
 ]
 

From 374d8c60787f66b803aab7590fc7ab5e4307d84a Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 19 Sep 2024 21:36:29 -0400
Subject: [PATCH 287/324] Update pytest requirement from <=8.1.1 to <=8.3.3 in
 /.github/scripts (#546)

Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/1.0.0b3...8.3.3)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 8c0a5dcc..b9cd377e 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -1,7 +1,7 @@
 setuptools <=75.1.0
 pip
 pylint <2.15.10
-pytest <=8.1.1
+pytest <=8.3.3
 pytest-pylint <=1.1.2
 pytest-runner <7.0.0
 termcolor <2.5.0

From 32b5142151789d5c64618db1ea5b0a47eb27a6c0 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 19 Sep 2024 21:41:22 -0400
Subject: [PATCH 288/324] Update label used by dependabot (#547)

---
 .github/dependabot.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index d31b409b..ba1b7f19 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -7,7 +7,7 @@ updates:
     directory: ".github/scripts/"
     schedule:
       interval: "monthly"
-    labels: ["ci"]
+    labels: ["dependabot"]
     pull-request-branch-name:
       separator: "-"
     open-pull-requests-limit: 5
@@ -23,7 +23,7 @@ updates:
       gh-actions:
         patterns:
           - "*"  # Check all dependencies
-    labels: ["ci"]
+    labels: ["dependabot"]
     pull-request-branch-name:
       separator: "-"
     open-pull-requests-limit: 5

From 9c9e8c63f745da0aeb6332a1f027b05972f579d7 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 19 Sep 2024 18:59:27 -0700
Subject: [PATCH 289/324] Update levenshtein requirement in /.github/scripts
 (#548)

Updates the requirements on [levenshtein](https://github.com/rapidfuzz/Levenshtein) to permit the latest version.
- [Release notes](https://github.com/rapidfuzz/Levenshtein/releases)
- [Changelog](https://github.com/rapidfuzz/Levenshtein/blob/main/HISTORY.md)
- [Commits](https://github.com/rapidfuzz/Levenshtein/compare/v0.13.0...v0.26.0)

---
updated-dependencies:
- dependency-name: levenshtein
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index b9cd377e..157c77b2 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -6,4 +6,4 @@ pytest-pylint <=1.1.2
 pytest-runner <7.0.0
 termcolor <2.5.0
 hypothesis <6.113.0
-levenshtein <=0.25.1
+levenshtein <=0.26.0

From f9293c9da7fe6645a25448537d40b563d970b2f4 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 19 Sep 2024 22:06:38 -0400
Subject: [PATCH 290/324] Upgrade pylint version (#549)

* Upgrade pylint
* Fixing lint errors like removing a useless super call
* Merging bad-names lists and other modernization in pylintrc
---
 .github/scripts/requirements.txt |  2 +-
 fire/core.py                     |  1 +
 fire/test_components.py          |  3 +--
 fire/testutils.py                |  3 ---
 pylintrc                         | 22 ++--------------------
 5 files changed, 5 insertions(+), 26 deletions(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 157c77b2..a5648989 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -1,6 +1,6 @@
 setuptools <=75.1.0
 pip
-pylint <2.15.10
+pylint <3.2.8
 pytest <=8.3.3
 pytest-pylint <=1.1.2
 pytest-runner <7.0.0
diff --git a/fire/core.py b/fire/core.py
index c61a8b57..bce9b641 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -872,6 +872,7 @@ def _ParseKeywordArgs(args, fn_spec):
         key, value = stripped_argument.split('=', 1)
       else:
         key = stripped_argument
+        value = None  # value will be set later on.
 
       key = key.replace('-', '_')
       is_bool_syntax = (not contains_equals and
diff --git a/fire/test_components.py b/fire/test_components.py
index 540a9e16..eb3a9e24 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -388,8 +388,7 @@ def example_generator(n):
         [0, 1, 2, 3]
 
     """
-    for i in range(n):
-      yield i
+    yield from range(n)
 
 
 def simple_set():
diff --git a/fire/testutils.py b/fire/testutils.py
index 816551b5..eca37f43 100644
--- a/fire/testutils.py
+++ b/fire/testutils.py
@@ -67,9 +67,6 @@ def assertOutputMatches(self, stdout='.*', stderr='.*', capture=True):
           raise AssertionError('%s: Expected %r to match %r' %
                                (name, value, regexp))
 
-  def assertRaisesRegex(self, *args, **kwargs):  # pylint: disable=arguments-differ
-    return super(BaseTestCase, self).assertRaisesRegex(*args, **kwargs)  # pylint: disable=no-member
-
   @contextlib.contextmanager
   def assertRaisesFireExit(self, code, regexp='.*'):
     """Asserts that a FireExit error is raised in the context.
diff --git a/pylintrc b/pylintrc
index 558d3ba2..8896bb5b 100644
--- a/pylintrc
+++ b/pylintrc
@@ -7,9 +7,6 @@
 # pygtk.require().
 #init-hook=
 
-# Profiled execution.
-profile=no
-
 # Add <file or directory> to the black list. It should be a base name, not a
 # path. You may set this option multiple times.
 ignore=
@@ -41,14 +38,6 @@ disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-d
 # (visual studio) and html
 output-format=text
 
-# Include message's id in output
-include-ids=no
-
-# Put messages in a separate file for each module / package specified on the
-# command line instead of printing them on stdout. Reports (if any) will be
-# written in a file name "pylint_global.[txt|html]".
-files-output=no
-
 # Tells whether to display a full report or only the messages
 reports=yes
 
@@ -59,10 +48,6 @@ reports=yes
 # (R0004).
 evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
 
-# Add a comment according to your evaluation note. This is used by the global
-# evaluation report (R0004).
-comment=no
-
 
 [VARIABLES]
 
@@ -79,9 +64,6 @@ additional-builtins=
 
 [BASIC]
 
-# List of builtins function names that should not be used, separated by a comma
-bad-functions=map,filter,apply,input,reduce
-
 # Regular expression which should only match correct module names
 module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
 
@@ -114,7 +96,7 @@ inlinevar-rgx=^[a-z][a-z0-9_]*$
 good-names=i,j,k,ex,main,Run,_
 
 # Bad variable names which should always be refused, separated by a comma
-bad-names=foo,bar,baz,toto,tutu,tata
+bad-names=map,filter,apply,input,reduce,foo,bar,baz,toto,tutu,tata
 
 # Regular expression which should only match functions or classes name which do
 # not require a docstring
@@ -186,7 +168,7 @@ max-locals=15
 max-returns=6
 
 # Maximum number of branch for function / method body
-max-branchs=12
+max-branches=12
 
 # Maximum number of statements in function / method body
 max-statements=50

From c5f5f9008303a661558339ea3c298d247248fdbe Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 19 Sep 2024 22:18:32 -0400
Subject: [PATCH 291/324] Run github action on pull_request (#550)

* Run github action on pull_request
---
 .github/workflows/build.yml | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 7f5225c5..a6649201 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,6 +1,12 @@
 name: Python Fire
 
-on: [push]
+on:
+  push:
+    branches:
+      - master
+  pull_request:
+    branches:
+      - master
 
 jobs:
   build:

From ca4e80b9072397ec1c03bdf83a0bd62524a601aa Mon Sep 17 00:00:00 2001
From: Kai Chen <foreverbonfy@163.com>
Date: Fri, 20 Sep 2024 10:22:06 +0800
Subject: [PATCH 292/324] Add current system MSYS check (#278)

---
 fire/console/platforms.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/fire/console/platforms.py b/fire/console/platforms.py
index 018eb89e..13fd8204 100644
--- a/fire/console/platforms.py
+++ b/fire/console/platforms.py
@@ -153,6 +153,8 @@ def Current():
       return OperatingSystem.MACOSX
     elif 'cygwin' in sys.platform:
       return OperatingSystem.CYGWIN
+    elif 'msys' in sys.platform:
+      return OperatingSystem.MSYS
     return None
 
   @staticmethod

From 4efd44dbb14ba2bf044f2fae701f787da0bfbe1e Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 19 Sep 2024 23:06:02 -0400
Subject: [PATCH 293/324] Remove .format in favor of f-strings (#551)

* Remove .format in favor of f-strings
---
 docs/guide.md             | 10 ++---
 examples/widget/widget.py |  2 +-
 fire/completion.py        |  7 +--
 fire/completion_test.py   |  3 +-
 fire/core.py              | 32 +++++++-------
 fire/helptext.py          | 92 +++++++++++++++++----------------------
 fire/interact.py          | 11 ++---
 fire/trace.py             | 13 +++---
 8 files changed, 75 insertions(+), 95 deletions(-)

diff --git a/docs/guide.md b/docs/guide.md
index 44d8a46d..cdc3b2d0 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -30,7 +30,7 @@ the program to the command line.
 import fire
 
 def hello(name):
-  return 'Hello {name}!'.format(name=name)
+  return f'Hello {name}!'
 
 if __name__ == '__main__':
   fire.Fire()
@@ -52,7 +52,7 @@ command line.
 import fire
 
 def hello(name):
-  return 'Hello {name}!'.format(name=name)
+  return f'Hello {name}!'
 
 if __name__ == '__main__':
   fire.Fire(hello)
@@ -76,7 +76,7 @@ We can alternatively write this program like this:
 import fire
 
 def hello(name):
-  return 'Hello {name}!'.format(name=name)
+  return f'Hello {name}!'
 
 def main():
   fire.Fire(hello)
@@ -93,7 +93,7 @@ then simply this:
 import fire
 
 def hello(name):
-  return 'Hello {name}!'.format(name=name)
+  return f'Hello {name}!'
 
 def main():
   fire.Fire(hello)
@@ -105,7 +105,7 @@ If you have a file `example.py` that doesn't even import fire:
 
 ```python
 def hello(name):
-  return 'Hello {name}!'.format(name=name)
+  return f'Hello {name}!'
 ```
 
 Then you can use it with Fire like this:
diff --git a/examples/widget/widget.py b/examples/widget/widget.py
index bf1cbeb2..9092ad75 100644
--- a/examples/widget/widget.py
+++ b/examples/widget/widget.py
@@ -25,7 +25,7 @@ def whack(self, n=1):
 
   def bang(self, noise='bang'):
     """Makes a loud noise."""
-    return '{noise} bang!'.format(noise=noise)
+    return f'{noise} bang!'
 
 
 def main():
diff --git a/fire/completion.py b/fire/completion.py
index 3aa8ab11..625e9d86 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -277,10 +277,7 @@ def _FishScript(name, commands, default_options=None):
       )
 
   return fish_source.format(
-      global_options=' '.join(
-          '"{option}"'.format(option=option)
-          for option in global_options
-      )
+      global_options=' '.join(f'"{option}"' for option in global_options)
   )
 
 
@@ -385,7 +382,7 @@ def _CompletionsFromArgs(fn_args):
   completions = []
   for arg in fn_args:
     arg = arg.replace('_', '-')
-    completions.append('--{arg}'.format(arg=arg))
+    completions.append(f'--{arg}')
   return completions
 
 
diff --git a/fire/completion_test.py b/fire/completion_test.py
index 5bafc279..c0d5d24f 100644
--- a/fire/completion_test.py
+++ b/fire/completion_test.py
@@ -33,9 +33,8 @@ def testCompletionBashScript(self):
     self.assertIn('command', script)
     self.assertIn('halt', script)
 
-    assert_template = '{command})'
     for last_command in ['command', 'halt']:
-      self.assertIn(assert_template.format(command=last_command), script)
+      self.assertIn(f'{last_command})', script)
 
   def testCompletionFishScript(self):
     # A sanity check test to make sure the fish completion script satisfies
diff --git a/fire/core.py b/fire/core.py
index bce9b641..e4156760 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -139,7 +139,7 @@ def Fire(component=None, command=None, name=None, serialize=None):
     _DisplayError(component_trace)
     raise FireExit(2, component_trace)
   if component_trace.show_trace and component_trace.show_help:
-    output = ['Fire trace:\n{trace}\n'.format(trace=component_trace)]
+    output = [f'Fire trace:\n{component_trace}\n']
     result = component_trace.GetResult()
     help_text = helptext.HelpText(
         result, trace=component_trace, verbose=component_trace.verbose)
@@ -147,7 +147,7 @@ def Fire(component=None, command=None, name=None, serialize=None):
     Display(output, out=sys.stderr)
     raise FireExit(0, component_trace)
   if component_trace.show_trace:
-    output = ['Fire trace:\n{trace}'.format(trace=component_trace)]
+    output = [f'Fire trace:\n{component_trace}']
     Display(output, out=sys.stderr)
     raise FireExit(0, component_trace)
   if component_trace.show_help:
@@ -231,9 +231,9 @@ def _IsHelpShortcut(component_trace, remaining_args):
 
   if show_help:
     component_trace.show_help = True
-    command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
-    print('INFO: Showing help with the command {cmd}.\n'.format(
-        cmd=shlex.quote(command)), file=sys.stderr)
+    command = f'{component_trace.GetCommand()} -- --help'
+    print(f'INFO: Showing help with the command {shlex.quote(command)}.\n',
+          file=sys.stderr)
   return show_help
 
 
@@ -287,9 +287,9 @@ def _DisplayError(component_trace):
       show_help = True
 
   if show_help:
-    command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
-    print('INFO: Showing help with the command {cmd}.\n'.format(
-        cmd=shlex.quote(command)), file=sys.stderr)
+    command = f'{component_trace.GetCommand()} -- --help'
+    print(f'INFO: Showing help with the command {shlex.quote(command)}.\n',
+          file=sys.stderr)
     help_text = helptext.HelpText(result, trace=component_trace,
                                   verbose=component_trace.verbose)
     output.append(help_text)
@@ -327,14 +327,13 @@ def _DictAsString(result, verbose=False):
     return '{}'
 
   longest_key = max(len(str(key)) for key in result_visible.keys())
-  format_string = '{{key:{padding}s}} {{value}}'.format(padding=longest_key + 1)
+  format_string = f'{{key:{longest_key + 1}s}} {{value}}'
 
   lines = []
   for key, value in result.items():
     if completion.MemberVisible(result, key, value, class_attrs=class_attrs,
                                 verbose=verbose):
-      line = format_string.format(key=str(key) + ':',
-                                  value=_OneLineResult(value))
+      line = format_string.format(key=f'{key}:', value=_OneLineResult(value))
       lines.append(line)
   return '\n'.join(lines)
 
@@ -348,10 +347,10 @@ def _OneLineResult(result):
   # TODO(dbieber): Show a small amount of usage information about the function
   # or module if it fits cleanly on the line.
   if inspect.isfunction(result):
-    return '<function {name}>'.format(name=result.__name__)
+    return f'<function {result.__name__}>'
 
   if inspect.ismodule(result):
-    return '<module {name}>'.format(name=result.__name__)
+    return f'<module {result.__name__}>'
 
   try:
     # Don't force conversion to ascii.
@@ -890,9 +889,10 @@ def _ParseKeywordArgs(args, fn_spec):
         if len(matching_fn_args) == 1:
           keyword = matching_fn_args[0]
         elif len(matching_fn_args) > 1:
-          raise FireError("The argument '{}' is ambiguous as it could "
-                          "refer to any of the following arguments: {}".format(
-                              argument, matching_fn_args))
+          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:
diff --git a/fire/helptext.py b/fire/helptext.py
index 1c0cb626..e57eb7d8 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -106,7 +106,7 @@ def _NameSection(component, info, trace=None, verbose=False):
                                              LINE_LENGTH)
 
   if summary:
-    text = current_command + ' - ' + summary
+    text = f'{current_command} - {summary}'
   else:
     text = current_command
   return ('NAME', text)
@@ -132,11 +132,7 @@ def _SynopsisSection(component, actions_grouped_by_kind, spec, metadata,
       continuations.append(trace.separator)
   continuation = ' | '.join(continuations)
 
-  synopsis_template = '{current_command} {continuation}'
-  text = synopsis_template.format(
-      current_command=current_command,
-      continuation=continuation)
-
+  text = f'{current_command} {continuation}'
   return ('SYNOPSIS', text)
 
 
@@ -243,8 +239,6 @@ def _ArgsAndFlagsSections(info, spec, metadata):
   if spec.varkw:
     # Include kwargs documented via :key param:
     documented_kwargs = []
-    flag_string = '--{name}'
-    short_flag_string = '-{short_name}, --{name}'
 
     # add short flags if possible
     flags = docstring_info.args or []
@@ -253,11 +247,10 @@ def _ArgsAndFlagsSections(info, spec, metadata):
     for flag in flags:
       if isinstance(flag, docstrings.KwargInfo):
         if flag.name[0] in unique_short_flags:
-          flag_string = short_flag_string.format(
-              name=flag.name, short_name=flag.name[0]
-          )
+          short_name = flag.name[0]
+          flag_string = f'-{short_name}, --{flag.name}'
         else:
-          flag_string = flag_string.format(name=flag.name)
+          flag_string = f'--{flag.name}'
 
         flag_item = _CreateFlagItem(
             flag.name, docstring_info, spec,
@@ -347,9 +340,9 @@ def _GetArgsAndFlagsString(spec, metadata):
                      for arg in args_with_no_defaults]
     else:
       arg_strings = [
-          '--{arg}={arg_upper}'.format(
-              arg=arg, arg_upper=formatting.Underline(arg.upper()))
-          for arg in args_with_no_defaults]
+          f'--{arg}={formatting.Underline(arg.upper())}'
+          for arg in args_with_no_defaults
+      ]
     arg_and_flag_strings.extend(arg_strings)
 
   # If there are any arguments that are treated as flags:
@@ -357,8 +350,8 @@ def _GetArgsAndFlagsString(spec, metadata):
     arg_and_flag_strings.append('<flags>')
 
   if spec.varargs:
-    varargs_string = '[{varargs}]...'.format(
-        varargs=formatting.Underline(spec.varargs.upper()))
+    varargs_underlined = formatting.Underline(spec.varargs.upper())
+    varargs_string = f'[{varargs_underlined}]...'
     arg_and_flag_strings.append(varargs_string)
 
   return ' '.join(arg_and_flag_strings)
@@ -401,7 +394,7 @@ def _GetActionsGroupedByKind(component, verbose=False):
     if component_len < 10:
       indexes.Add(name=', '.join(str(x) for x in range(component_len)))
     else:
-      indexes.Add(name='0..{max}'.format(max=component_len-1))
+      indexes.Add(name=f'0..{component_len-1}')
 
   return [groups, commands, values, indexes]
 
@@ -416,10 +409,8 @@ def _GetCurrentCommand(trace=None, include_separators=True):
 
 
 def _CreateOutputSection(name, content):
-  return """{name}
-{content}""".format(
-    name=formatting.Bold(name),
-    content=formatting.Indent(content, SECTION_INDENTATION))
+  return f"""{formatting.Bold(name)}
+{formatting.Indent(content, SECTION_INDENTATION)}"""
 
 
 def _CreateArgItem(arg, docstring_info, spec):
@@ -430,7 +421,7 @@ def _CreateArgItem(arg, docstring_info, spec):
     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.
+      default information about the arguments to a callable.
 
   Returns:
     A string to be used in constructing the help screen for the function.
@@ -445,7 +436,7 @@ def _CreateArgItem(arg, docstring_info, spec):
   arg_string = formatting.BoldUnderline(arg.upper())
 
   arg_type = _GetArgType(arg, spec)
-  arg_type = 'Type: {}'.format(arg_type) if arg_type else ''
+  arg_type = f'Type: {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))
@@ -484,14 +475,13 @@ def _CreateFlagItem(flag, docstring_info, spec, required=False,
   description = _GetArgDescription(flag, docstring_info)
 
   if not flag_string:
-    flag_string_template = '--{flag_name}={flag_name_upper}'
-    flag_string = flag_string_template.format(
-        flag_name=flag,
-        flag_name_upper=formatting.Underline(flag.upper()))
+    flag_name_upper=formatting.Underline(flag.upper())
+    flag_string = f'--{flag}={flag_name_upper}'
   if required:
     flag_string += ' (required)'
   if short_arg:
-    flag_string = '-{short_flag}, '.format(short_flag=flag[0]) + flag_string
+    short_flag = flag[0]
+    flag_string = f'-{short_flag}, {flag_string}'
 
   arg_type = _GetArgType(flag, spec)
   arg_default = _GetArgDefault(flag, spec)
@@ -499,14 +489,14 @@ def _CreateFlagItem(flag, docstring_info, spec, required=False,
   # 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 = f'Optional[{arg_type}]'
 
-  arg_type = 'Type: {}'.format(arg_type) if arg_type else ''
+  arg_type = f'Type: {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 ''
+  arg_default = f'Default: {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))
@@ -567,15 +557,15 @@ def _GetArgDefault(flag, spec):
 def _CreateItem(name, description, indent=2):
   if not description:
     return name
-  return """{name}
-{description}""".format(name=name,
-                        description=formatting.Indent(description, indent))
+  description = formatting.Indent(description, indent)
+  return f"""{name}
+{description}"""
 
 
 def _GetArgDescription(name, docstring_info):
   if docstring_info.args:
     for arg_in_docstring in docstring_info.args:
-      if arg_in_docstring.name in (name, '*' + name, '**' + name):
+      if arg_in_docstring.name in (name, f'*{name}', f'**{name}'):
         return arg_in_docstring.description
   return None
 
@@ -621,9 +611,9 @@ def _ValuesUsageDetailsSection(component, values):
 
 
 def _NewChoicesSection(name, choices):
+  name_formatted = formatting.Bold(formatting.Underline(name))
   return _CreateItem(
-      '{name} is one of the following:'.format(
-          name=formatting.Bold(formatting.Underline(name))),
+      f'{name_formatted} is one of the following:',
       '\n' + '\n\n'.join(choices),
       indent=1)
 
@@ -639,11 +629,6 @@ def UsageText(component, trace=None, verbose=False):
   Returns:
     String suitable for display in an error screen.
   """
-  output_template = """Usage: {continued_command}
-{availability_lines}
-For detailed information on this command, run:
-  {help_command}"""
-
   # Get the command so far:
   if trace:
     command = trace.GetCommand()
@@ -687,15 +672,16 @@ def UsageText(component, trace=None, verbose=False):
       + '--help'
   )
 
-  return output_template.format(
-      continued_command=continued_command,
-      availability_lines=''.join(availability_lines),
-      help_command=help_command)
+  return f"""Usage: {continued_command}
+{''.join(availability_lines)}
+For detailed information on this command, run:
+  {help_command}"""
 
 
 def _GetPossibleActionsUsageString(possible_actions):
   if possible_actions:
-    return '<{actions}>'.format(actions='|'.join(possible_actions))
+    actions_str = '|'.join(possible_actions)
+    return f'<{actions_str}>'
   return None
 
 
@@ -704,7 +690,7 @@ def _UsageAvailabilityLines(actions_grouped_by_kind):
   for action_group in actions_grouped_by_kind:
     if action_group.members:
       availability_line = _CreateAvailabilityLine(
-          header='available {plural}:'.format(plural=action_group.plural),
+          header=f'available {action_group.plural}:',
           items=action_group.names
       )
       availability_lines.append(availability_line)
@@ -720,7 +706,7 @@ def _GetCallableUsageItems(spec, metadata):
   accepts_positional_args = metadata.get(decorators.ACCEPTS_POSITIONAL_ARGS)
 
   if not accepts_positional_args:
-    items = ['--{arg}={upper}'.format(arg=arg, upper=arg.upper())
+    items = [f'--{arg}={arg.upper()}'
              for arg in args_with_no_defaults]
   else:
     items = [arg.upper() for arg in args_with_no_defaults]
@@ -730,7 +716,7 @@ def _GetCallableUsageItems(spec, metadata):
     items.append('<flags>')
 
   if spec.varargs:
-    items.append('[{varargs}]...'.format(varargs=spec.varargs.upper()))
+    items.append(f'[{spec.varargs.upper()}]...')
 
   return items
 
@@ -745,10 +731,10 @@ def _GetCallableAvailabilityLines(spec):
   args_with_defaults = spec.args[len(spec.args) - len(spec.defaults):]
 
   # TODO(dbieber): Handle args_with_no_defaults if not accepts_positional_args.
-  optional_flags = [('--' + flag) for flag in itertools.chain(
+  optional_flags = [f'--{flag}' for flag in itertools.chain(
       args_with_defaults, _KeywordOnlyArguments(spec, required=False))]
   required_flags = [
-      ('--' + flag) for flag in _KeywordOnlyArguments(spec, required=True)
+      f'--{flag}' for flag in _KeywordOnlyArguments(spec, required=True)
   ]
 
   # Flags section:
diff --git a/fire/interact.py b/fire/interact.py
index 7bdeb9a7..eccd3990 100644
--- a/fire/interact.py
+++ b/fire/interact.py
@@ -65,16 +65,17 @@ def _AvailableString(variables, verbose=False):
   lists = [
       ('Modules', modules),
       ('Objects', other)]
-  liststrs = []
+  list_strs = []
   for name, varlist in lists:
     if varlist:
-      liststrs.append(
-          '{name}: {items}'.format(name=name, items=', '.join(sorted(varlist))))
+      items_str = ', '.join(sorted(varlist))
+      list_strs.append(f'{name}: {items_str}')
 
+  lists_str = '\n'.join(list_strs)
   return (
       'Fire is starting a Python REPL with the following objects:\n'
-      '{liststrs}\n'
-  ).format(liststrs='\n'.join(liststrs))
+      f'{lists_str}\n'
+  )
 
 
 def _EmbedIPython(variables, argv=None):
diff --git a/fire/trace.py b/fire/trace.py
index 2145186e..68b48ce5 100644
--- a/fire/trace.py
+++ b/fire/trace.py
@@ -212,10 +212,7 @@ def NeedsSeparator(self):
   def __str__(self):
     lines = []
     for index, element in enumerate(self.elements):
-      line = '{index}. {trace_string}'.format(
-          index=index + 1,
-          trace_string=element,
-      )
+      line = f'{index + 1}. {element}'
       lines.append(line)
     return '\n'.join(lines)
 
@@ -261,7 +258,7 @@ def __init__(self,
 
     Args:
       component: The result of this element of the trace.
-      action: The type of action (eg instantiating a class) taking place.
+      action: The type of action (e.g. instantiating a class) taking place.
       target: (string) The name of the component being acted upon.
       args: The args consumed by the represented action.
       filename: The file in which the action is defined, or None if N/A.
@@ -301,11 +298,11 @@ def __str__(self):
       # Format is: {action} "{target}" ({filename}:{lineno})
       string = self._action
       if self._target is not None:
-        string += ' "{target}"'.format(target=self._target)
+        string += f' "{self._target}"'
       if self._filename is not None:
         path = self._filename
         if self._lineno is not None:
-          path += ':{lineno}'.format(lineno=self._lineno)
+          path += f':{self._lineno}'
 
-        string += ' ({path})'.format(path=path)
+        string += f' ({path})'
       return string

From 93b0e3243b522ae9ab6ebbdc6dcf9a5bb68cc30d Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 19 Sep 2024 23:24:46 -0400
Subject: [PATCH 294/324] Remove six from console, eliminates six entirely
 (#552)

---
 fire/console/console_attr.py | 18 ++++++++----------
 fire/console/encoding.py     | 31 ++++++-------------------------
 fire/console/files.py        |  4 +---
 setup.py                     |  1 -
 4 files changed, 15 insertions(+), 39 deletions(-)

diff --git a/fire/console/console_attr.py b/fire/console/console_attr.py
index 815e16b8..c0a3d784 100644
--- a/fire/console/console_attr.py
+++ b/fire/console/console_attr.py
@@ -100,8 +100,6 @@
 from fire.console import encoding as encoding_util
 from fire.console import text
 
-import six
-
 
 # TODO: Unify this logic with console.style.mappings
 class BoxLineCharacters(object):
@@ -355,9 +353,9 @@ def ConvertOutputToUnicode(self, buf):
     Returns:
       The console output string buf converted to unicode.
     """
-    if isinstance(buf, six.text_type):
+    if isinstance(buf, str):
       buf = buf.encode(self._encoding)
-    return six.text_type(buf, self._encoding, 'replace')
+    return str(buf, self._encoding, 'replace')
 
   def GetBoxLineCharacters(self):
     """Returns the box/line drawing characters object.
@@ -480,7 +478,7 @@ def DisplayWidth(self, buf):
     Returns:
       The display width of buf, handling unicode and ANSI controls.
     """
-    if not isinstance(buf, six.string_types):
+    if not isinstance(buf, str):
       # Handle non-string objects like Colorizer().
       return len(buf)
 
@@ -595,16 +593,16 @@ def __init__(self, string, color, justify=None):
     self._justify = justify
 
   def __eq__(self, other):
-    return self._string == six.text_type(other)
+    return self._string == str(other)
 
   def __ne__(self, other):
     return not self == other
 
   def __gt__(self, other):
-    return self._string > six.text_type(other)
+    return self._string > str(other)
 
   def __lt__(self, other):
-    return self._string < six.text_type(other)
+    return self._string < str(other)
 
   def __ge__(self, other):
     return not self < other
@@ -692,7 +690,7 @@ def GetCharacterDisplayWidth(char):
   Returns:
     The monospaced terminal display width of char: either 0, 1, or 2.
   """
-  if not isinstance(char, six.text_type):
+  if not isinstance(char, str):
     # Non-unicode chars have width 1. Don't use this function on control chars.
     return 1
 
@@ -779,7 +777,7 @@ def EncodeToBytes(data):
     return data
 
   # Coerce to text that will be converted to bytes.
-  s = six.text_type(data)
+  s = str(data)
 
   try:
     # Assume the text can be directly converted to bytes (8-bit ascii).
diff --git a/fire/console/encoding.py b/fire/console/encoding.py
index 41bda634..0a7fedfc 100644
--- a/fire/console/encoding.py
+++ b/fire/console/encoding.py
@@ -22,8 +22,6 @@
 
 import sys
 
-import six
-
 
 def Encode(string, encoding=None):
   """Encode the text string to a byte string.
@@ -35,18 +33,7 @@ def Encode(string, encoding=None):
   Returns:
     str, The binary string.
   """
-  if string is None:
-    return None
-  if not six.PY2:
-    # In Python 3, the environment sets and gets accept and return text strings
-    # only, and it handles the encoding itself so this is not necessary.
-    return string
-  if isinstance(string, six.binary_type):
-    # Already an encoded byte string, we are done
-    return string
-
-  encoding = encoding or _GetEncoding()
-  return string.encode(encoding)
+  return string
 
 
 def Decode(data, encoding=None):
@@ -67,20 +54,13 @@ def Decode(data, encoding=None):
     return None
 
   # First we are going to get the data object to be a text string.
-  # Don't use six.string_types here because on Python 3 bytes is not considered
-  # a string type and we want to include that.
-  if isinstance(data, six.text_type) or isinstance(data, six.binary_type):
+  if isinstance(data, str) or isinstance(data, bytes):
     string = data
   else:
     # Some non-string type of object.
-    try:
-      string = six.text_type(data)
-    except (TypeError, UnicodeError):
-      # The string cannot be converted to unicode -- default to str() which will
-      # catch objects with special __str__ methods.
-      string = str(data)
+    string = str(data)
 
-  if isinstance(string, six.text_type):
+  if isinstance(string, str):
     # Our work is done here.
     return string
 
@@ -199,7 +179,8 @@ def EncodeEnv(env, encoding=None):
   encoding = encoding or _GetEncoding()
   return {
       Encode(k, encoding=encoding): Encode(v, encoding=encoding)
-      for k, v in six.iteritems(env)}
+      for k, v in env.items()
+  }
 
 
 def _GetEncoding():
diff --git a/fire/console/files.py b/fire/console/files.py
index 69970f43..97222c3d 100644
--- a/fire/console/files.py
+++ b/fire/console/files.py
@@ -24,8 +24,6 @@
 from fire.console import encoding as encoding_util
 from fire.console import platforms
 
-import six
-
 
 def _GetSystemPath():
   """Returns properly encoded system PATH variable string."""
@@ -48,7 +46,7 @@ def _FindExecutableOnPath(executable, path, pathext):
     ValueError: invalid input.
   """
 
-  if isinstance(pathext, six.string_types):
+  if isinstance(pathext, str):
     raise ValueError('_FindExecutableOnPath(..., pathext=\'{0}\') failed '
                      'because pathext must be an iterable of strings, but got '
                      'a string.'.format(pathext))
diff --git a/setup.py b/setup.py
index 53f3381a..82073be4 100644
--- a/setup.py
+++ b/setup.py
@@ -29,7 +29,6 @@
 A library for automatically generating command line interfaces.""".strip()
 
 DEPENDENCIES = [
-    'six',
     'termcolor',
 ]
 

From 5b2dadd7f3912bf8f05e7f1c381631ef5c14cada Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Thu, 19 Sep 2024 23:47:19 -0400
Subject: [PATCH 295/324] Move asyncio imports and update docs (#553)

---
 docs/guide.md        | 19 +++++++++++++++++++
 fire/core.py         |  3 +--
 fire/inspectutils.py |  3 +--
 3 files changed, 21 insertions(+), 4 deletions(-)

diff --git a/docs/guide.md b/docs/guide.md
index cdc3b2d0..444a76ff 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -589,6 +589,25 @@ default values that you don't want to specify. It is also important to remember
 to change the separator if you want to pass `-` as an argument.
 
 
+##### Async Functions
+
+Fire supports calling async functions too. Here's a simple example.
+
+```python
+import asyncio
+
+async def count_to_ten():
+  for i in range(1, 11):
+    await asyncio.sleep(1)
+    print(i)
+
+if __name__ == '__main__':
+  fire.Fire(count_to_ten)
+```
+
+Whenever fire encounters a coroutine function, it runs it, blocking until it completes.
+
+
 ### Argument Parsing
 
 The types of the arguments are determined by their values, rather than by the
diff --git a/fire/core.py b/fire/core.py
index e4156760..6cd1907e 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -49,6 +49,7 @@ def main(argv):
   --trace: Get the Fire Trace for the command.
 """
 
+import asyncio
 import inspect
 import json
 import os
@@ -68,8 +69,6 @@ def main(argv):
 from fire import value_types
 from fire.console import console_io
 
-import asyncio  # pylint: disable=import-error,g-import-not-at-top  # pytype: disable=import-error
-
 
 def Fire(component=None, command=None, name=None, serialize=None):
   """This function, Fire, is the main entrypoint for Python Fire.
diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index 0d0b048d..a3ae7c27 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -14,14 +14,13 @@
 
 """Inspection utility functions for Python Fire."""
 
+import asyncio
 import inspect
 import sys
 import types
 
 from fire import docstrings
 
-import asyncio  # pylint: disable=import-error,g-import-not-at-top  # pytype: disable=import-error
-
 
 class FullArgSpec(object):
   """The arguments of a function, as in Python 3's inspect.FullArgSpec."""

From d3204373c4bba38a09db92f910d048222b8d6f0f Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 20 Sep 2024 07:20:04 -0700
Subject: [PATCH 296/324] Include Python 3.13 in github actions (#554)

* Include Python 3.13 in github actions list
* Include version in supported versions list
---
 .github/workflows/build.yml | 2 +-
 setup.py                    | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a6649201..63c87edf 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -13,7 +13,7 @@ jobs:
     runs-on: ubuntu-20.04
     strategy:
       matrix:
-        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
+        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-rc.2"]
 
     steps:
      # Checkout the repo.
diff --git a/setup.py b/setup.py
index 82073be4..beb367cf 100644
--- a/setup.py
+++ b/setup.py
@@ -67,6 +67,7 @@
         'Programming Language :: Python :: 3.10',
         'Programming Language :: Python :: 3.11',
         'Programming Language :: Python :: 3.12',
+        'Programming Language :: Python :: 3.13',
 
         'Operating System :: OS Independent',
         'Operating System :: POSIX',

From 36a56c0a777d874f30e39412b2877ab171118d54 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Sat, 21 Sep 2024 08:04:39 -0700
Subject: [PATCH 297/324] Continue upgrade of codebase to Python 3 (#556)

Obtained with pyupgrade, reverting any changes that remove a module we still depend on.
`uv run pyupgrade **/**.py --py3-plus`
---
 fire/__main__.py            |  6 ++--
 fire/core.py                |  2 +-
 fire/decorators_test.py     | 12 +++----
 fire/helptext.py            |  2 +-
 fire/helptext_test.py       |  2 +-
 fire/inspectutils.py        |  6 ++--
 fire/main_test.py           |  2 +-
 fire/parser_fuzz_test.py    |  2 +-
 fire/test_components.py     | 66 ++++++++++++++++++-------------------
 fire/test_components_py3.py | 10 +++---
 fire/trace.py               |  4 +--
 11 files changed, 57 insertions(+), 57 deletions(-)

diff --git a/fire/__main__.py b/fire/__main__.py
index 11fb1b42..140b4a76 100644
--- a/fire/__main__.py
+++ b/fire/__main__.py
@@ -54,14 +54,14 @@ def import_from_file_path(path):
   """
 
   if not os.path.exists(path):
-    raise IOError('Given file path does not exist.')
+    raise OSError('Given file path does not exist.')
 
   module_name = os.path.basename(path)
 
   spec = util.spec_from_file_location(module_name, path)
 
   if spec is None:
-    raise IOError('Unable to load module from specified path.')
+    raise OSError('Unable to load module from specified path.')
 
   module = util.module_from_spec(spec)  # pylint: disable=no-member
   spec.loader.exec_module(module)  # pytype: disable=attribute-error
@@ -104,7 +104,7 @@ def import_module(module_or_filename):
     return import_from_file_path(module_or_filename)
 
   if os.path.sep in module_or_filename:  # Use / to detect if it was a filename.
-    raise IOError('Fire was passed a filename which could not be found.')
+    raise OSError('Fire was passed a filename which could not be found.')
 
   return import_from_module_name(module_or_filename)  # Assume it's a module.
 
diff --git a/fire/core.py b/fire/core.py
index 6cd1907e..26a25753 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -199,7 +199,7 @@ def __init__(self, code, component_trace):
       code: (int) Exit code for the Fire CLI.
       component_trace: (FireTrace) The trace for the Fire command.
     """
-    super(FireExit, self).__init__(code)
+    super().__init__(code)
     self.trace = component_trace
 
 
diff --git a/fire/decorators_test.py b/fire/decorators_test.py
index a316b79f..9988743c 100644
--- a/fire/decorators_test.py
+++ b/fire/decorators_test.py
@@ -19,7 +19,7 @@
 from fire import testutils
 
 
-class NoDefaults(object):
+class NoDefaults:
   """A class for testing decorated functions without default values."""
 
   @decorators.SetParseFns(count=int)
@@ -40,7 +40,7 @@ def double(count):
   return 2 * count
 
 
-class WithDefaults(object):
+class WithDefaults:
 
   @decorators.SetParseFns(float)
   def example1(self, arg1=10):
@@ -51,14 +51,14 @@ def example2(self, arg1=10):
     return arg1, type(arg1)
 
 
-class MixedArguments(object):
+class MixedArguments:
 
   @decorators.SetParseFns(float, arg2=str)
   def example3(self, arg1, arg2):
     return arg1, arg2
 
 
-class PartialParseFn(object):
+class PartialParseFn:
 
   @decorators.SetParseFns(arg1=str)
   def example4(self, arg1, arg2):
@@ -69,7 +69,7 @@ def example5(self, arg1, arg2):
     return arg1, arg2
 
 
-class WithKwargs(object):
+class WithKwargs:
 
   @decorators.SetParseFns(mode=str, count=int)
   def example6(self, **kwargs):
@@ -79,7 +79,7 @@ def example6(self, **kwargs):
     )
 
 
-class WithVarArgs(object):
+class WithVarArgs:
 
   @decorators.SetParseFn(str)
   def example7(self, arg1, arg2=None, *varargs, **kwargs):  # pylint: disable=keyword-arg-before-vararg
diff --git a/fire/helptext.py b/fire/helptext.py
index e57eb7d8..9b578fac 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -767,7 +767,7 @@ def _CreateAvailabilityLine(header, items,
   return indented_header + indented_items_text[len(indented_header):] + '\n'
 
 
-class ActionGroup(object):
+class ActionGroup:
   """A group of actions of the same kind."""
 
   def __init__(self, name, plural):
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index 4d35dc0a..d1a3f368 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -27,7 +27,7 @@
 class HelpTest(testutils.BaseTestCase):
 
   def setUp(self):
-    super(HelpTest, self).setUp()
+    super().setUp()
     os.environ['ANSI_COLORS_DISABLED'] = '1'
 
   def testHelpTextNoDefaults(self):
diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index a3ae7c27..d1438972 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -22,7 +22,7 @@
 from fire import docstrings
 
 
-class FullArgSpec(object):
+class FullArgSpec:
   """The arguments of a function, as in Python 3's inspect.FullArgSpec."""
 
   def __init__(self, args=None, varargs=None, varkw=None, defaults=None,
@@ -229,7 +229,7 @@ def GetFileAndLine(component):
   try:
     unused_code, lineindex = inspect.findsource(component)
     lineno = lineindex + 1
-  except (IOError, IndexError):
+  except (OSError, IndexError):
     lineno = None
 
   return filename, lineno
@@ -268,7 +268,7 @@ def Info(component):
   try:
     unused_code, lineindex = inspect.findsource(component)
     info['line'] = lineindex + 1
-  except (TypeError, IOError):
+  except (TypeError, OSError):
     info['line'] = None
 
   if 'docstring' in info:
diff --git a/fire/main_test.py b/fire/main_test.py
index a0184620..a2723347 100644
--- a/fire/main_test.py
+++ b/fire/main_test.py
@@ -43,7 +43,7 @@ class MainModuleFileTest(testutils.BaseTestCase):
   """Tests to verify correct import behavior for file executables."""
 
   def setUp(self):
-    super(MainModuleFileTest, self).setUp()
+    super().setUp()
     self.file = tempfile.NamedTemporaryFile(suffix='.py')  # pylint: disable=consider-using-with
     self.file.write(b'class Foo:\n  def double(self, n):\n    return 2 * n\n')
     self.file.flush()
diff --git a/fire/parser_fuzz_test.py b/fire/parser_fuzz_test.py
index 9739ec4e..10f497cf 100644
--- a/fire/parser_fuzz_test.py
+++ b/fire/parser_fuzz_test.py
@@ -53,7 +53,7 @@ def testDefaultParseValueFuzz(self, value):
       result = parser.DefaultParseValue(value)
     except TypeError:
       # It's OK to get a TypeError if the string has the null character.
-      if u'\x00' in value:
+      if '\x00' in value:
         return
       raise
     except MemoryError:
diff --git a/fire/test_components.py b/fire/test_components.py
index eb3a9e24..2dc4e0cc 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -43,7 +43,7 @@ def function_with_help(help=True):  # pylint: disable=redefined-builtin
   return help
 
 
-class Empty(object):
+class Empty:
   pass
 
 
@@ -51,20 +51,20 @@ class OldStyleEmpty:  # pylint: disable=old-style-class,no-init
   pass
 
 
-class WithInit(object):
+class WithInit:
 
   def __init__(self):
     pass
 
 
-class ErrorInConstructor(object):
+class ErrorInConstructor:
 
   def __init__(self, value='value'):
     self.value = value
     raise ValueError('Error in constructor')
 
 
-class WithHelpArg(object):
+class WithHelpArg:
   """Test class for testing when class has a help= arg."""
 
   def __init__(self, help=True):  # pylint: disable=redefined-builtin
@@ -72,7 +72,7 @@ def __init__(self, help=True):  # pylint: disable=redefined-builtin
     self.dictionary = {'__help': 'help in a dict'}
 
 
-class NoDefaults(object):
+class NoDefaults:
 
   def double(self, count):
     return 2 * count
@@ -81,7 +81,7 @@ def triple(self, count):
     return 3 * count
 
 
-class WithDefaults(object):
+class WithDefaults:
   """Class with functions that have default arguments."""
 
   def double(self, count=0):
@@ -115,7 +115,7 @@ def triple(self, count=0):
     return 3 * count
 
 
-class MixedDefaults(object):
+class MixedDefaults:
 
   def ten(self):
     return 10
@@ -127,7 +127,7 @@ def identity(self, alpha, beta='0'):
     return alpha, beta
 
 
-class SimilarArgNames(object):
+class SimilarArgNames:
 
   def identity(self, bool_one=False, bool_two=False):
     return bool_one, bool_two
@@ -136,13 +136,13 @@ def identity2(self, a=None, alpha=None):
     return a, alpha
 
 
-class CapitalizedArgNames(object):
+class CapitalizedArgNames:
 
   def sum(self, Delta=1.0, Gamma=2.0):  # pylint: disable=invalid-name
     return Delta + Gamma
 
 
-class Annotations(object):
+class Annotations:
 
   def double(self, count=0):
     return 2 * count
@@ -154,7 +154,7 @@ def triple(self, count=0):
   triple.__annotations__ = {'count': float}
 
 
-class TypedProperties(object):
+class TypedProperties:
   """Test class for testing Python Fire with properties of various types."""
 
   def __init__(self):
@@ -173,7 +173,7 @@ def __init__(self):
     self.gamma = 'myexcitingstring'
 
 
-class VarArgs(object):
+class VarArgs:
   """Test class for testing Python Fire with a property with varargs."""
 
   def cumsums(self, *items):
@@ -191,7 +191,7 @@ def varchars(self, alpha=0, beta=0, *chars):  # pylint: disable=keyword-arg-befo
     return alpha, beta, ''.join(chars)
 
 
-class Underscores(object):
+class Underscores:
 
   def __init__(self):
     self.underscore_example = 'fish fingers'
@@ -200,20 +200,20 @@ def underscore_function(self, underscore_arg):
     return underscore_arg
 
 
-class BoolConverter(object):
+class BoolConverter:
 
   def as_bool(self, arg=False):
     return bool(arg)
 
 
-class ReturnsObj(object):
+class ReturnsObj:
 
   def get_obj(self, *items):
     del items  # Unused
     return BoolConverter()
 
 
-class NumberDefaults(object):
+class NumberDefaults:
 
   def reciprocal(self, divisor=10.0):
     return 1.0 / divisor
@@ -222,7 +222,7 @@ def integer_reciprocal(self, divisor=10):
     return 1.0 / divisor
 
 
-class InstanceVars(object):
+class InstanceVars:
 
   def __init__(self, arg1, arg2):
     self.arg1 = arg1
@@ -232,7 +232,7 @@ def run(self, arg1, arg2):
     return (self.arg1, self.arg2, arg1, arg2)
 
 
-class Kwargs(object):
+class Kwargs:
 
   def props(self, **kwargs):
     return kwargs
@@ -244,13 +244,13 @@ def run(self, positional, named=None, **kwargs):
     return (positional, named, kwargs)
 
 
-class ErrorRaiser(object):
+class ErrorRaiser:
 
   def fail(self):
     raise ValueError('This error is part of a test.')
 
 
-class NonComparable(object):
+class NonComparable:
 
   def __eq__(self, other):
     raise ValueError('Instances of this class cannot be compared.')
@@ -259,7 +259,7 @@ def __ne__(self, other):
     raise ValueError('Instances of this class cannot be compared.')
 
 
-class EmptyDictOutput(object):
+class EmptyDictOutput:
 
   def totally_empty(self):
     return {}
@@ -268,7 +268,7 @@ def nothing_printable(self):
     return {'__do_not_print_me': 1}
 
 
-class CircularReference(object):
+class CircularReference:
 
   def create(self):
     x = {}
@@ -276,7 +276,7 @@ def create(self):
     return x
 
 
-class OrderedDictionary(object):
+class OrderedDictionary:
 
   def empty(self):
     return collections.OrderedDict()
@@ -288,7 +288,7 @@ def non_empty(self):
     return ordered_dict
 
 
-class NamedTuple(object):
+class NamedTuple:
   """Functions returning named tuples used for testing."""
 
   def point(self):
@@ -304,7 +304,7 @@ def matching_names(self):
     return Point(x='x', y='y')
 
 
-class CallableWithPositionalArgs(object):
+class CallableWithPositionalArgs:
   """Test class for supporting callable."""
 
   TEST = 1
@@ -326,12 +326,12 @@ def coordinate_sum(self):
     return self.x + self.y
 
 
-class CallableWithKeywordArgument(object):
+class CallableWithKeywordArgument:
   """Test class for supporting callable."""
 
   def __call__(self, **kwargs):
     for key, value in kwargs.items():
-      print('%s: %s' % (key, value))
+      print('{}: {}'.format(key, value))
 
   def print_msg(self, msg):
     print(msg)
@@ -340,7 +340,7 @@ def print_msg(self, msg):
 CALLABLE_WITH_KEYWORD_ARGUMENT = CallableWithKeywordArgument()
 
 
-class ClassWithDocstring(object):
+class ClassWithDocstring:
   """Test class for testing help text output.
 
   This is some detail description of this test class.
@@ -363,7 +363,7 @@ def print_msg(self, msg=None):
     print(msg)
 
 
-class ClassWithMultilineDocstring(object):
+class ClassWithMultilineDocstring:
   """Test class for testing help text output with multiline docstring.
 
   This is a test class that has a long docstring description that spans across
@@ -413,7 +413,7 @@ class Color(enum.Enum):
   BLUE = 3
 
 
-class HasStaticAndClassMethods(object):
+class HasStaticAndClassMethods:
   """A class with a static method and a class method."""
 
   CLASS_STATE = 1
@@ -467,7 +467,7 @@ def fn_with_code_in_docstring():
   return True
 
 
-class BinaryCanvas(object):
+class BinaryCanvas:
   """A canvas with which to make binary art, one bit at a time."""
 
   def __init__(self, size=10):
@@ -500,7 +500,7 @@ def set(self, value):
     return self
 
 
-class DefaultMethod(object):
+class DefaultMethod:
 
   def double(self, number):
     return 2 * number
@@ -511,7 +511,7 @@ def _missing():
     return _missing
 
 
-class InvalidProperty(object):
+class InvalidProperty:
 
   def double(self, number):
     return 2 * number
diff --git a/fire/test_components_py3.py b/fire/test_components_py3.py
index 17fb932c..192302d3 100644
--- a/fire/test_components_py3.py
+++ b/fire/test_components_py3.py
@@ -31,7 +31,7 @@ def identity(self, *, alpha, beta='0'):
     return alpha, beta
 
 
-class KeywordOnly(object):
+class KeywordOnly:
 
   def double(self, *, count):
     return count * 2
@@ -43,7 +43,7 @@ def with_default(self, *, x="x"):
     print("x: " + x)
 
 
-class LruCacheDecoratedMethod(object):
+class LruCacheDecoratedMethod:
 
   @functools.lru_cache()
   def lru_cache_in_class(self, arg1):
@@ -55,13 +55,13 @@ def lru_cache_decorated(arg1):
   return arg1
 
 
-class WithAsyncio(object):
+class WithAsyncio:
 
   async def double(self, count=0):
     return 2 * count
 
 
-class WithTypes(object):
+class WithTypes:
   """Class with functions that have default arguments and types."""
 
   def double(self, count: float) -> float:
@@ -83,7 +83,7 @@ def long_type(
     return long_obj
 
 
-class WithDefaultsAndTypes(object):
+class WithDefaultsAndTypes:
   """Class with functions that have default arguments and types."""
 
   def double(self, count: float = 0) -> float:
diff --git a/fire/trace.py b/fire/trace.py
index 68b48ce5..3a75cc9c 100644
--- a/fire/trace.py
+++ b/fire/trace.py
@@ -38,7 +38,7 @@
 INTERACTIVE_MODE = 'Entered interactive mode'
 
 
-class FireTrace(object):
+class FireTrace:
   """A FireTrace represents the steps taken during a single Fire execution.
 
   A FireTrace consists of a sequence of FireTraceElement objects. Each element
@@ -238,7 +238,7 @@ def NeedsSeparatingHyphenHyphen(self, flag='help'):
             or flag in spec.kwonlyargs)
 
 
-class FireTraceElement(object):
+class FireTraceElement:
   """A FireTraceElement represents a single step taken by a Fire execution.
 
   Examples of a FireTraceElement are the instantiation of a class or the

From 8227364f113fcaf8661290fa3bb0c79741ff3be7 Mon Sep 17 00:00:00 2001
From: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Date: Sat, 21 Sep 2024 17:09:24 +0200
Subject: [PATCH 298/324] Update required Python 3.7 in `setup.py` (#555)

---
 setup.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/setup.py b/setup.py
index beb367cf..8d4a381b 100644
--- a/setup.py
+++ b/setup.py
@@ -77,6 +77,7 @@
 
     keywords='command line interface cli python fire interactive bash tool',
 
+    requires_python='>=3.7',
     packages=['fire', 'fire.console'],
 
     install_requires=DEPENDENCIES,

From 1c43c36174feb7020e7c5a5c53f3a47bc8e368c8 Mon Sep 17 00:00:00 2001
From: David Bieber <david810@gmail.com>
Date: Sat, 21 Sep 2024 08:12:09 -0700
Subject: [PATCH 299/324] Bump version number in __init__ to setup.py

---
 fire/__init__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fire/__init__.py b/fire/__init__.py
index 742b03ac..9ff696d3 100644
--- a/fire/__init__.py
+++ b/fire/__init__.py
@@ -17,4 +17,4 @@
 from fire.core import Fire
 
 __all__ = ['Fire']
-__version__ = '0.6.0'
+__version__ = '0.7.0'

From efcf60f7f8202d9887b9da16e4ff81a554b9d023 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Sat, 21 Sep 2024 08:45:04 -0700
Subject: [PATCH 300/324] Lint improvements and type safety (#558)

* Clean up of lint errors
* Always return an element when requested
---
 fire/console/encoding.py   | 1 +
 fire/decorators.py         | 2 +-
 fire/formatting_windows.py | 2 +-
 fire/helptext.py           | 2 +-
 fire/helptext_test.py      | 1 -
 fire/test_components.py    | 3 ++-
 fire/trace.py              | 2 +-
 7 files changed, 7 insertions(+), 6 deletions(-)

diff --git a/fire/console/encoding.py b/fire/console/encoding.py
index 0a7fedfc..3ce30cb5 100644
--- a/fire/console/encoding.py
+++ b/fire/console/encoding.py
@@ -33,6 +33,7 @@ def Encode(string, encoding=None):
   Returns:
     str, The binary string.
   """
+  del encoding  # Unused.
   return string
 
 
diff --git a/fire/decorators.py b/fire/decorators.py
index eb5b0d20..2758b0aa 100644
--- a/fire/decorators.py
+++ b/fire/decorators.py
@@ -107,5 +107,5 @@ def GetMetadata(fn):
 def GetParseFns(fn):
   # type: (...) -> dict
   metadata = GetMetadata(fn)
-  default = {"default": None, "positional": [], "named": {}}
+  default = {'default': None, 'positional': [], 'named': {}}
   return metadata.get(FIRE_PARSE_FNS, default)
diff --git a/fire/formatting_windows.py b/fire/formatting_windows.py
index f8241eaa..cee6f393 100644
--- a/fire/formatting_windows.py
+++ b/fire/formatting_windows.py
@@ -31,7 +31,7 @@ def initialize_or_disable():
   """Enables ANSI processing on Windows or disables it as needed."""
   if HAS_COLORAMA:
     wrap = True
-    if (hasattr(sys.stdout, "isatty")
+    if (hasattr(sys.stdout, 'isatty')
         and sys.stdout.isatty()
         and platform.release() == '10'):
       # Enables native ANSI sequences in console.
diff --git a/fire/helptext.py b/fire/helptext.py
index 9b578fac..318d6276 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -475,7 +475,7 @@ def _CreateFlagItem(flag, docstring_info, spec, required=False,
   description = _GetArgDescription(flag, docstring_info)
 
   if not flag_string:
-    flag_name_upper=formatting.Underline(flag.upper())
+    flag_name_upper = formatting.Underline(flag.upper())
     flag_string = f'--{flag}={flag_name_upper}'
   if required:
     flag_string += ' (required)'
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index d1a3f368..aeff5240 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -428,7 +428,6 @@ def testHelpTextMultipleKeywoardArgumentsWithShortArgs(self):
     self.assertIn('\n    --late', help_screen)
 
 
-
 class UsageTest(testutils.BaseTestCase):
 
   def testUsageOutput(self):
diff --git a/fire/test_components.py b/fire/test_components.py
index 2dc4e0cc..887a0dc6 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -554,7 +554,7 @@ def fn_with_kwarg_and_defaults(arg1, arg2, opt=True, **kwargs):
   """
   del arg1, arg2, opt
   return kwargs.get('arg3')
-# pylint: enable=g-doc-args,g-doc-return-or-yield
+
 
 def fn_with_multiple_defaults(first='first', last='last', late='late'):
   """Function with kwarg and defaults.
@@ -565,3 +565,4 @@ def fn_with_multiple_defaults(first='first', last='last', late='late'):
   """
   del last, late
   return first
+# pylint: enable=g-doc-args,g-doc-return-or-yield
diff --git a/fire/trace.py b/fire/trace.py
index 3a75cc9c..4a6d4776 100644
--- a/fire/trace.py
+++ b/fire/trace.py
@@ -77,7 +77,7 @@ def GetLastHealthyElement(self):
     for element in reversed(self.elements):
       if not element.HasError():
         return element
-    return None
+    return self.elements[0]  # The initial element is always healthy.
 
   def HasError(self):
     """Returns whether the Fire execution encountered a Fire usage error."""

From a0cb1ca3c2697ea7f5f1f7314353f1c7e920088f Mon Sep 17 00:00:00 2001
From: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Date: Sun, 22 Sep 2024 18:00:15 +0200
Subject: [PATCH 301/324] Expand build matrix to include mac (#490)

* Expand build matrix to include mac (#490)

---------

Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Co-authored-by: David Bieber <dbieber@google.com>
---
 .github/workflows/build.yml | 18 +++++++++++-------
 1 file changed, 11 insertions(+), 7 deletions(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 63c87edf..59b0a4ba 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -2,18 +2,23 @@ name: Python Fire
 
 on:
   push:
-    branches:
-      - master
+    branches: ["master"]
   pull_request:
-    branches:
-      - master
+    branches: ["master"]
+
+defaults:
+  run:
+    shell: bash
 
 jobs:
   build:
-    runs-on: ubuntu-20.04
+    runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-rc.2"]
+        os: ["macos-latest", "ubuntu-latest"]
+        python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-rc.2"]
+        include:
+          - {os: "ubuntu-20.04", python-version: "3.7"}
 
     steps:
      # Checkout the repo.
@@ -28,7 +33,6 @@ jobs:
 
      # Build Python Fire using the build.sh script.
      - name: Run build script
-       shell: bash
        run: ./.github/scripts/build.sh
        env:
          PYTHON_VERSION: ${{ matrix.python-version }}

From 90b7f824f2e760e6363b0d10c52b1940346a0fa6 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Sun, 22 Sep 2024 09:14:50 -0700
Subject: [PATCH 302/324] Replace Python 2 type hints with real type
 annotations (#559)

* Replace Python 2 type hints with real type annotations
---
 CONTRIBUTING.md    | 3 ---
 fire/decorators.py | 7 +++----
 2 files changed, 3 insertions(+), 7 deletions(-)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index baae1a6e..b5d67c96 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -40,12 +40,9 @@ In addition, the project follows a convention of:
 - Maximum line length: 80 characters
 - Indentation: 2 spaces (4 for line continuation)
 - PascalCase for function and method names.
-- No type hints, as described in [PEP 484], to maintain compatibility with
-Python versions < 3.5.
 - Single quotes around strings, three double quotes around docstrings.
 
 [Google Python Style Guide]: http://google.github.io/styleguide/pyguide.html
-[PEP 484]: https://www.python.org/dev/peps/pep-0484
 
 ## Testing
 
diff --git a/fire/decorators.py b/fire/decorators.py
index 2758b0aa..914b1de6 100644
--- a/fire/decorators.py
+++ b/fire/decorators.py
@@ -18,6 +18,7 @@
 command line arguments to client code.
 """
 
+from typing import Any, Dict
 import inspect
 
 FIRE_METADATA = 'FIRE_METADATA'
@@ -80,8 +81,7 @@ def _SetMetadata(fn, attribute, value):
   setattr(fn, FIRE_METADATA, metadata)
 
 
-def GetMetadata(fn):
-  # type: (...) -> dict
+def GetMetadata(fn) -> Dict[str, Any]:
   """Gets metadata attached to the function `fn` as an attribute.
 
   Args:
@@ -104,8 +104,7 @@ def GetMetadata(fn):
     return default
 
 
-def GetParseFns(fn):
-  # type: (...) -> dict
+def GetParseFns(fn) -> Dict[str, Any]:
   metadata = GetMetadata(fn)
   default = {'default': None, 'positional': [], 'named': {}}
   return metadata.get(FIRE_PARSE_FNS, default)

From 8feb04a5936ba7b94e96430cc04f6f4b9ba8170c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 1 Oct 2024 10:34:19 -0400
Subject: [PATCH 303/324] Update pylint requirement from <3.2.8 to <3.3.2 in
 /.github/scripts (#562)

Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/pylint-version-0.18.1...v3.3.1)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index a5648989..0b32ac05 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -1,6 +1,6 @@
 setuptools <=75.1.0
 pip
-pylint <3.2.8
+pylint <3.3.2
 pytest <=8.3.3
 pytest-pylint <=1.1.2
 pytest-runner <7.0.0

From 0aff6b9452080a1fe53bf8537b5e71bd04986c24 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 2 Nov 2024 13:56:00 -0400
Subject: [PATCH 304/324] Update hypothesis requirement in /.github/scripts
 (#566)

Updates the requirements on [hypothesis](https://github.com/HypothesisWorks/hypothesis) to permit the latest version.
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-ruby-0.0.1...hypothesis-python-6.116.0)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 0b32ac05..5c947b3e 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -5,5 +5,5 @@ pytest <=8.3.3
 pytest-pylint <=1.1.2
 pytest-runner <7.0.0
 termcolor <2.5.0
-hypothesis <6.113.0
+hypothesis <6.117.0
 levenshtein <=0.26.0

From dfa1071b7a9aee0ca1538dc4e08236c0c5dedda4 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 2 Nov 2024 14:09:07 -0400
Subject: [PATCH 305/324] Update termcolor requirement from <2.5.0 to <2.6.0 in
 /.github/scripts (#569)

Updates the requirements on [termcolor](https://github.com/termcolor/termcolor) to permit the latest version.
- [Release notes](https://github.com/termcolor/termcolor/releases)
- [Changelog](https://github.com/termcolor/termcolor/blob/main/CHANGES.md)
- [Commits](https://github.com/termcolor/termcolor/compare/0.1...2.5.0)

---
updated-dependencies:
- dependency-name: termcolor
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 5c947b3e..6147fcbb 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -4,6 +4,6 @@ pylint <3.3.2
 pytest <=8.3.3
 pytest-pylint <=1.1.2
 pytest-runner <7.0.0
-termcolor <2.5.0
+termcolor <2.6.0
 hypothesis <6.117.0
 levenshtein <=0.26.0

From 98d7fbce9c93a6b0c3b209a8ed1144cd233decc0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 2 Nov 2024 14:14:29 -0400
Subject: [PATCH 306/324] Update setuptools requirement in /.github/scripts
 (#567)

Updates the requirements on [setuptools](https://github.com/pypa/setuptools) to permit the latest version.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/0.6...v75.3.0)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 6147fcbb..6df54e28 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -1,4 +1,4 @@
-setuptools <=75.1.0
+setuptools <=75.3.0
 pip
 pylint <3.3.2
 pytest <=8.3.3

From c3b4474ea42c2f5330ee28fea7a9fc31e4c59451 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 2 Nov 2024 18:49:46 -0400
Subject: [PATCH 307/324] Update levenshtein requirement in /.github/scripts
 (#568)

Updates the requirements on [levenshtein](https://github.com/rapidfuzz/Levenshtein) to permit the latest version.
- [Release notes](https://github.com/rapidfuzz/Levenshtein/releases)
- [Changelog](https://github.com/rapidfuzz/Levenshtein/blob/main/HISTORY.md)
- [Commits](https://github.com/rapidfuzz/Levenshtein/compare/v0.13.0...v0.26.1)

---
updated-dependencies:
- dependency-name: levenshtein
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 6df54e28..cf8a3420 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -6,4 +6,4 @@ pytest-pylint <=1.1.2
 pytest-runner <7.0.0
 termcolor <2.6.0
 hypothesis <6.117.0
-levenshtein <=0.26.0
+levenshtein <=0.26.1

From deb25efee883191f9dac1cec579d3f96f1e32226 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 11 Dec 2024 11:52:02 -0500
Subject: [PATCH 308/324] Update setuptools requirement in /.github/scripts
 (#573)

Updates the requirements on [setuptools](https://github.com/pypa/setuptools) to permit the latest version.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/0.6...v75.6.0)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index cf8a3420..0480a001 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -1,4 +1,4 @@
-setuptools <=75.3.0
+setuptools <=75.6.0
 pip
 pylint <3.3.2
 pytest <=8.3.3

From 6cf45c663075c96b20dd0dfa733c2374545a4ad6 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 11 Dec 2024 11:53:58 -0500
Subject: [PATCH 309/324] Update hypothesis requirement in /.github/scripts
 (#574)

Updates the requirements on [hypothesis](https://github.com/HypothesisWorks/hypothesis) to permit the latest version.
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-ruby-0.0.1...hypothesis-python-6.122.0)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 0480a001..958c6248 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -5,5 +5,5 @@ pytest <=8.3.3
 pytest-pylint <=1.1.2
 pytest-runner <7.0.0
 termcolor <2.6.0
-hypothesis <6.117.0
+hypothesis <6.123.0
 levenshtein <=0.26.1

From 2e0867d3371db9db6e95fad7f82d58ccb894d94c Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Sun, 23 Mar 2025 14:43:44 -0400
Subject: [PATCH 310/324] Use Neutral theme for Inspector (#588)

* Use Neutral theme for Inspector
* Catch when theme_name not available
---
 fire/inspectutils.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index d1438972..06c30ef1 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -256,7 +256,10 @@ def Info(component):
   """
   try:
     from IPython.core import oinspect  # pylint: disable=import-outside-toplevel,g-import-not-at-top
-    inspector = oinspect.Inspector()
+    try:
+      inspector = oinspect.Inspector(theme_name="Neutral")
+    except TypeError:  # Only recent versions of IPython support theme_name.
+      inspector = oinspect.Inspector()
     info = inspector.info(component)
 
     # IPython's oinspect.Inspector.info may return '<no docstring>'

From 45152e18255e5c5803f3805604eb738c50befeff Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 27 Mar 2025 19:34:56 -0400
Subject: [PATCH 311/324] Update pylint requirement from <3.3.2 to <3.3.5 in
 /.github/scripts (#581)

Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/pylint-version-0.18.1...v3.3.4)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 958c6248..5810abf5 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -1,6 +1,6 @@
 setuptools <=75.6.0
 pip
-pylint <3.3.2
+pylint <3.3.5
 pytest <=8.3.3
 pytest-pylint <=1.1.2
 pytest-runner <7.0.0

From 525708c3d7bcfc36a71e23694f09d0b587a7bf72 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Mon, 14 Apr 2025 14:40:30 -0400
Subject: [PATCH 312/324] Use lowercase neutral instead of upper (#596)

---
 fire/inspectutils.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index 06c30ef1..d9c62ca7 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -257,7 +257,7 @@ def Info(component):
   try:
     from IPython.core import oinspect  # pylint: disable=import-outside-toplevel,g-import-not-at-top
     try:
-      inspector = oinspect.Inspector(theme_name="Neutral")
+      inspector = oinspect.Inspector(theme_name="neutral")
     except TypeError:  # Only recent versions of IPython support theme_name.
       inspector = oinspect.Inspector()
     info = inspector.info(component)

From c5ab602240a160902986e48db8980d59338be944 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 14 Apr 2025 14:40:45 -0400
Subject: [PATCH 313/324] Update hypothesis requirement in /.github/scripts
 (#594)

Updates the requirements on [hypothesis](https://github.com/HypothesisWorks/hypothesis) to permit the latest version.
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-ruby-0.0.1...hypothesis-python-6.130.6)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-version: 6.130.6
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 5810abf5..b922f04a 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -5,5 +5,5 @@ pytest <=8.3.3
 pytest-pylint <=1.1.2
 pytest-runner <7.0.0
 termcolor <2.6.0
-hypothesis <6.123.0
+hypothesis <6.131.0
 levenshtein <=0.26.1

From 8527235d18835223dad5055e29d50664ab5bfb2d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 14 Apr 2025 14:41:02 -0400
Subject: [PATCH 314/324] Update setuptools requirement in /.github/scripts
 (#593)

Updates the requirements on [setuptools](https://github.com/pypa/setuptools) to permit the latest version.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/0.6...v78.1.0)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-version: 78.1.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index b922f04a..8db34c71 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -1,4 +1,4 @@
-setuptools <=75.6.0
+setuptools <=78.1.0
 pip
 pylint <3.3.5
 pytest <=8.3.3

From fb01c7c619eda3107c7e32c42370573f6f63f33c Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Wed, 21 May 2025 12:23:08 -0700
Subject: [PATCH 315/324] Call inspectutils.GetClassAttrsDict on component, not
 None (#606)

* inspectutils.GetClassAttrsDict on component, not None
* Remove ubuntu-20.04 in favor of ubuntu-22.04 for Python 3.7
---
 .github/workflows/build.yml | 2 +-
 fire/completion.py          | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 59b0a4ba..75a687f3 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -18,7 +18,7 @@ jobs:
         os: ["macos-latest", "ubuntu-latest"]
         python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-rc.2"]
         include:
-          - {os: "ubuntu-20.04", python-version: "3.7"}
+          - {os: "ubuntu-22.04", python-version: "3.7"}
 
     steps:
      # Checkout the repo.
diff --git a/fire/completion.py b/fire/completion.py
index 625e9d86..1597d464 100644
--- a/fire/completion.py
+++ b/fire/completion.py
@@ -321,7 +321,7 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False):
   if inspect.isclass(component):
     # If class_attrs has not been provided, compute it.
     if class_attrs is None:
-      class_attrs = inspectutils.GetClassAttrsDict(class_attrs) or {}
+      class_attrs = inspectutils.GetClassAttrsDict(component) or {}
     class_attr = class_attrs.get(name)
     if class_attr:
       # Methods and properties should only be accessible on instantiated

From 51974c67bf72ac649ed28015d960884712bcbc0f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 21 May 2025 12:23:37 -0700
Subject: [PATCH 316/324] Update pylint requirement from <3.3.5 to <3.3.7 in
 /.github/scripts (#591)

Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/pylint-version-0.18.1...v3.3.6)

---
updated-dependencies:
- dependency-name: pylint
  dependency-version: 3.3.6
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 8db34c71..82b1be4a 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -1,6 +1,6 @@
 setuptools <=78.1.0
 pip
-pylint <3.3.5
+pylint <3.3.7
 pytest <=8.3.3
 pytest-pylint <=1.1.2
 pytest-runner <7.0.0

From dba7e1d0da014e555d174225fdf5ab4c4574b18b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 1 Jun 2025 10:55:56 -0400
Subject: [PATCH 317/324] Update hypothesis requirement in /.github/scripts
 (#608)

Updates the requirements on [hypothesis](https://github.com/HypothesisWorks/hypothesis) to permit the latest version.
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-ruby-0.0.1...hypothesis-python-6.132.0)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-version: 6.132.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/scripts/requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 82b1be4a..613c4da0 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -5,5 +5,5 @@ pytest <=8.3.3
 pytest-pylint <=1.1.2
 pytest-runner <7.0.0
 termcolor <2.6.0
-hypothesis <6.131.0
+hypothesis <6.133.0
 levenshtein <=0.26.1

From 2e6f8d2b248411fb4bbfb7fbf3701ee96c0e9a61 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 18 Jul 2025 21:23:40 -0400
Subject: [PATCH 318/324] Bump version to 0.7.1 (#609)

* Bump version to 0.7.1

* Bump dependency versions
---
 .github/scripts/requirements.txt | 8 ++++----
 fire/__init__.py                 | 2 +-
 setup.py                         | 2 +-
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
index 613c4da0..882dd440 100644
--- a/.github/scripts/requirements.txt
+++ b/.github/scripts/requirements.txt
@@ -1,9 +1,9 @@
-setuptools <=78.1.0
+setuptools <=80.9.0
 pip
 pylint <3.3.7
-pytest <=8.3.3
+pytest <=8.3.5
 pytest-pylint <=1.1.2
 pytest-runner <7.0.0
-termcolor <2.6.0
+termcolor <3.2.0
 hypothesis <6.133.0
-levenshtein <=0.26.1
+levenshtein <=0.27.1
diff --git a/fire/__init__.py b/fire/__init__.py
index 9ff696d3..b1470692 100644
--- a/fire/__init__.py
+++ b/fire/__init__.py
@@ -17,4 +17,4 @@
 from fire.core import Fire
 
 __all__ = ['Fire']
-__version__ = '0.7.0'
+__version__ = '0.7.1'
diff --git a/setup.py b/setup.py
index 8d4a381b..23b7b472 100644
--- a/setup.py
+++ b/setup.py
@@ -37,7 +37,7 @@
     'levenshtein',
 ]
 
-VERSION = '0.7.0'
+VERSION = '0.7.1'
 URL = 'https://github.com/google/python-fire'
 
 setup(

From d33056cb32f217c57b432040484901f34b9f5411 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 18 Jul 2025 22:27:20 -0400
Subject: [PATCH 319/324] Move to pyproject.toml (#613)

* Migrate from setup.py and setup.cfg to pyproject.toml
* Point dependabot at the pyproject file
---
 .github/dependabot.yml           |  2 +-
 .github/scripts/build.sh         |  3 +-
 .github/scripts/requirements.txt |  9 ----
 pyproject.toml                   | 69 ++++++++++++++++++++++++++
 requirements.txt                 |  1 -
 setup.cfg                        | 10 ----
 setup.py                         | 85 --------------------------------
 7 files changed, 71 insertions(+), 108 deletions(-)
 delete mode 100644 .github/scripts/requirements.txt
 create mode 100644 pyproject.toml
 delete mode 100644 requirements.txt
 delete mode 100644 setup.cfg
 delete mode 100644 setup.py

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index ba1b7f19..8be46672 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -4,7 +4,7 @@ version: 2
 updates:
   # Enable version updates for python
   - package-ecosystem: "pip"
-    directory: ".github/scripts/"
+    directory: "/"
     schedule:
       interval: "monthly"
     labels: ["dependabot"]
diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh
index 111257ae..7f5cf491 100755
--- a/.github/scripts/build.sh
+++ b/.github/scripts/build.sh
@@ -19,8 +19,7 @@ set -e
 
 PYTHON_VERSION=${PYTHON_VERSION:-3.7}
 
-pip install -U -r .github/scripts/requirements.txt
-python setup.py develop
+pip install -e .[test]
 python -m pytest  # Run the tests without IPython.
 pip install ipython
 python -m pytest  # Now run the tests with IPython.
diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt
deleted file mode 100644
index 882dd440..00000000
--- a/.github/scripts/requirements.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-setuptools <=80.9.0
-pip
-pylint <3.3.7
-pytest <=8.3.5
-pytest-pylint <=1.1.2
-pytest-runner <7.0.0
-termcolor <3.2.0
-hypothesis <6.133.0
-levenshtein <=0.27.1
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..6a6ba63e
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,69 @@
+[build-system]
+requires = ["setuptools>=45", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "fire"
+version = "0.7.1"
+description = "A library for automatically generating command line interfaces."
+readme = "README.md"
+license = {text = "Apache-2.0"}
+authors = [
+    {name = "David Bieber", email = "dbieber@google.com"}
+]
+classifiers = [
+    "Development Status :: 4 - Beta",
+    "Intended Audience :: Developers",
+    "Topic :: Software Development :: Libraries :: Python Modules",
+    "Programming Language :: Python",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.7",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Programming Language :: Python :: 3.13",
+    "Operating System :: OS Independent",
+    "Operating System :: POSIX",
+    "Operating System :: MacOS",
+    "Operating System :: Unix",
+]
+keywords = ["command", "line", "interface", "cli", "python", "fire", "interactive", "bash", "tool"]
+requires-python = ">=3.7"
+dependencies = [
+    "termcolor",
+]
+
+[project.urls]
+Homepage = "https://github.com/google/python-fire"
+Repository = "https://github.com/google/python-fire"
+
+[project.optional-dependencies]
+test = [
+    "setuptools<=80.9.0",
+    "pip",
+    "pylint<3.3.7",
+    "pytest<=8.3.5",
+    "pytest-pylint<=1.1.2",
+    "pytest-runner<7.0.0",
+    "termcolor<3.2.0",
+    "hypothesis<6.133.0",
+    "levenshtein<=0.27.1",
+]
+
+[tool.setuptools.packages.find]
+include = ["fire*"]
+
+[tool.setuptools.package-data]
+fire = ["console/*"]
+
+[tool.pytest.ini_options]
+addopts = [
+    "--ignore=fire/test_components_py3.py",
+    "--ignore=fire/parser_fuzz_test.py"
+]
+
+[tool.pytype]
+inputs = "."
+output = ".pytype"
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 9c558e35..00000000
--- a/requirements.txt
+++ /dev/null
@@ -1 +0,0 @@
-.
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index ed53d83b..00000000
--- a/setup.cfg
+++ /dev/null
@@ -1,10 +0,0 @@
-[aliases]
-test = pytest
-
-[tool:pytest]
-addopts = --ignore=fire/test_components_py3.py
-          --ignore=fire/parser_fuzz_test.py
-
-[pytype]
-inputs = .
-output = .pytype
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 23b7b472..00000000
--- a/setup.py
+++ /dev/null
@@ -1,85 +0,0 @@
-# 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.
-
-"""The setup.py file for Python Fire."""
-
-from setuptools import setup
-
-LONG_DESCRIPTION = """
-Python Fire is a library for automatically generating command line interfaces
-(CLIs) with a single line of code.
-
-It will turn any Python module, class, object, function, etc. (any Python
-component will work!) into a CLI. It's called Fire because when you call Fire(),
-it fires off your command.
-""".strip()
-
-SHORT_DESCRIPTION = """
-A library for automatically generating command line interfaces.""".strip()
-
-DEPENDENCIES = [
-    'termcolor',
-]
-
-TEST_DEPENDENCIES = [
-    'hypothesis',
-    'levenshtein',
-]
-
-VERSION = '0.7.1'
-URL = 'https://github.com/google/python-fire'
-
-setup(
-    name='fire',
-    version=VERSION,
-    description=SHORT_DESCRIPTION,
-    long_description=LONG_DESCRIPTION,
-    url=URL,
-
-    author='David Bieber',
-    author_email='dbieber@google.com',
-    license='Apache Software License',
-
-    classifiers=[
-        'Development Status :: 4 - Beta',
-
-        'Intended Audience :: Developers',
-        'Topic :: Software Development :: Libraries :: Python Modules',
-
-        'License :: OSI Approved :: Apache Software License',
-
-        'Programming Language :: Python',
-        'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.7',
-        'Programming Language :: Python :: 3.8',
-        'Programming Language :: Python :: 3.9',
-        'Programming Language :: Python :: 3.10',
-        'Programming Language :: Python :: 3.11',
-        'Programming Language :: Python :: 3.12',
-        'Programming Language :: Python :: 3.13',
-
-        'Operating System :: OS Independent',
-        'Operating System :: POSIX',
-        'Operating System :: MacOS',
-        'Operating System :: Unix',
-    ],
-
-    keywords='command line interface cli python fire interactive bash tool',
-
-    requires_python='>=3.7',
-    packages=['fire', 'fire.console'],
-
-    install_requires=DEPENDENCIES,
-    tests_require=TEST_DEPENDENCIES,
-)

From 84496196045c96ade6ef7c42ebd374f9b6bddee0 Mon Sep 17 00:00:00 2001
From: David Bieber <dbieber@google.com>
Date: Fri, 18 Jul 2025 23:21:02 -0400
Subject: [PATCH 320/324] Use ty in place of pytype (#617)

* Use ty in place of pytype
---
 .github/scripts/build.sh        |  8 ++++----
 fire/__main__.py                |  4 ++--
 fire/console/console_attr_os.py |  5 +----
 fire/console/encoding.py        | 12 ++++++------
 fire/core.py                    |  4 ++--
 fire/core_test.py               |  4 ++--
 fire/custom_descriptions.py     |  8 ++++----
 fire/decorators.py              |  2 +-
 fire/docstrings.py              |  2 +-
 fire/formatting_windows.py      |  6 +++---
 fire/helptext.py                | 15 +++++++++------
 fire/helptext_test.py           | 12 ++++++------
 fire/inspectutils.py            | 17 ++++++++---------
 fire/main_test.py               |  2 +-
 fire/parser.py                  |  2 +-
 fire/trace.py                   |  2 --
 pyproject.toml                  |  4 ----
 17 files changed, 51 insertions(+), 58 deletions(-)

diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh
index 7f5cf491..d9207dfe 100755
--- a/.github/scripts/build.sh
+++ b/.github/scripts/build.sh
@@ -24,8 +24,8 @@ python -m pytest  # Run the tests without IPython.
 pip install ipython
 python -m pytest  # Now run the tests with IPython.
 pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console
-if [[ ${PYTHON_VERSION} == 3.7 ]]; then
-  # Run type-checking.
-  pip install pytype;
-  pytype -x fire/test_components_py3.py;
+if [[ ${PYTHON_VERSION} == 3.12 ]]; then
+  # Run type-checking
+  pip install ty
+  python -m ty check --python $(which python) --exclude fire/test_components_py3.py --exclude fire/console/ --exclude fire/formatting_windows.py
 fi
diff --git a/fire/__main__.py b/fire/__main__.py
index 140b4a76..eb98b1a4 100644
--- a/fire/__main__.py
+++ b/fire/__main__.py
@@ -60,11 +60,11 @@ def import_from_file_path(path):
 
   spec = util.spec_from_file_location(module_name, path)
 
-  if spec is None:
+  if spec is None or spec.loader is None:
     raise OSError('Unable to load module from specified path.')
 
   module = util.module_from_spec(spec)  # pylint: disable=no-member
-  spec.loader.exec_module(module)  # pytype: disable=attribute-error
+  spec.loader.exec_module(module)
 
   return module, module_name
 
diff --git a/fire/console/console_attr_os.py b/fire/console/console_attr_os.py
index 869c5949..a7f38d4f 100644
--- a/fire/console/console_attr_os.py
+++ b/fire/console/console_attr_os.py
@@ -14,9 +14,6 @@
 # limitations under the License.
 
 """OS specific console_attr helper functions."""
-# This file contains platform specific code which is not currently handled
-# by pytype.
-# pytype: skip-file
 
 from __future__ import absolute_import
 from __future__ import division
@@ -73,7 +70,7 @@ def _GetXY(fd):
     try:
       # This magic incantation converts a struct from ioctl(2) containing two
       # binary shorts to a (rows, columns) int tuple.
-      rc = struct.unpack(b'hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, 'junk'))
+      rc = struct.unpack(b'hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, b'junk'))
       return (rc[1], rc[0]) if rc else None
     except:  # pylint: disable=bare-except
       return None
diff --git a/fire/console/encoding.py b/fire/console/encoding.py
index 3ce30cb5..662342c6 100644
--- a/fire/console/encoding.py
+++ b/fire/console/encoding.py
@@ -67,7 +67,7 @@ def Decode(data, encoding=None):
 
   try:
     # Just return the string if its pure ASCII.
-    return string.decode('ascii')  # pytype: disable=attribute-error
+    return string.decode('ascii')
   except UnicodeError:
     # The string is not ASCII encoded.
     pass
@@ -75,7 +75,7 @@ def Decode(data, encoding=None):
   # Try the suggested encoding if specified.
   if encoding:
     try:
-      return string.decode(encoding)  # pytype: disable=attribute-error
+      return string.decode(encoding)
     except UnicodeError:
       # Bad suggestion.
       pass
@@ -84,21 +84,21 @@ def Decode(data, encoding=None):
   # be exceptional if a valid extended ascii encoding with extended chars
   # were also a valid UITF-8 encoding.
   try:
-    return string.decode('utf8')  # pytype: disable=attribute-error
+    return string.decode('utf8')
   except UnicodeError:
     # Not a UTF-8 encoding.
     pass
 
   # Try the filesystem encoding.
   try:
-    return string.decode(sys.getfilesystemencoding())  # pytype: disable=attribute-error
+    return string.decode(sys.getfilesystemencoding())
   except UnicodeError:
     # string is not encoded for filesystem paths.
     pass
 
   # Try the system default encoding.
   try:
-    return string.decode(sys.getdefaultencoding())  # pytype: disable=attribute-error
+    return string.decode(sys.getdefaultencoding())
   except UnicodeError:
     # string is not encoded using the default encoding.
     pass
@@ -118,7 +118,7 @@ def Decode(data, encoding=None):
   #   string = '\xdc'
   #   string = string.decode('iso-8859-1')
   #   string = string.encode('ascii', 'backslashreplace')
-  return string.decode('iso-8859-1')  # pytype: disable=attribute-error
+  return string.decode('iso-8859-1')
 
 
 def GetEncodedValue(env, name, default=None):
diff --git a/fire/core.py b/fire/core.py
index 26a25753..32e0e9cc 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -504,7 +504,7 @@ def _Fire(component, args, parsed_flag_args, context, name=None):
 
       # Treat namedtuples as dicts when handling them as a map.
       if inspectutils.IsNamedTuple(component):
-        component_dict = component._asdict()  # pytype: disable=attribute-error
+        component_dict = component._asdict()
       else:
         component_dict = component
 
@@ -519,7 +519,7 @@ def _Fire(component, args, parsed_flag_args, context, name=None):
         # a key as another type.
         # TODO(dbieber): Consider alternatives for accessing non-string keys.
         for key, value in (
-            component_dict.items()):  # pytype: disable=attribute-error
+            component_dict.items()):
           if target == str(key):
             component = value
             handled = True
diff --git a/fire/core_test.py b/fire/core_test.py
index 90b7f466..f48d6e2d 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -215,12 +215,12 @@ def serialize(x):
 
   def testLruCacheDecoratorBoundArg(self):
     self.assertEqual(
-        core.Fire(tc.py3.LruCacheDecoratedMethod,  # pytype: disable=module-attr
+        core.Fire(tc.py3.LruCacheDecoratedMethod,
                   command=['lru_cache_in_class', 'foo']), 'foo')
 
   def testLruCacheDecorator(self):
     self.assertEqual(
-        core.Fire(tc.py3.lru_cache_decorated,  # pytype: disable=module-attr
+        core.Fire(tc.py3.lru_cache_decorated,
                   command=['foo']), 'foo')
 
 
diff --git a/fire/custom_descriptions.py b/fire/custom_descriptions.py
index 768f0e23..ef1130a3 100644
--- a/fire/custom_descriptions.py
+++ b/fire/custom_descriptions.py
@@ -131,14 +131,14 @@ def GetStringTypeDescription(obj, available_space, line_length):
 def GetSummary(obj, available_space, line_length):
   obj_type_name = type(obj).__name__
   if obj_type_name in CUSTOM_DESC_SUM_FN_DICT:
-    return CUSTOM_DESC_SUM_FN_DICT.get(obj_type_name)[0](obj, available_space,
-                                                         line_length)
+    return CUSTOM_DESC_SUM_FN_DICT[obj_type_name][0](obj, available_space,
+                                                     line_length)
   return None
 
 
 def GetDescription(obj, available_space, line_length):
   obj_type_name = type(obj).__name__
   if obj_type_name in CUSTOM_DESC_SUM_FN_DICT:
-    return CUSTOM_DESC_SUM_FN_DICT.get(obj_type_name)[1](obj, available_space,
-                                                         line_length)
+    return CUSTOM_DESC_SUM_FN_DICT[obj_type_name][1](obj, available_space,
+                                                     line_length)
   return None
diff --git a/fire/decorators.py b/fire/decorators.py
index 914b1de6..547153c6 100644
--- a/fire/decorators.py
+++ b/fire/decorators.py
@@ -68,7 +68,7 @@ def SetParseFns(*positional, **named):
   def _Decorator(fn):
     parse_fns = GetParseFns(fn)
     parse_fns['positional'] = positional
-    parse_fns['named'].update(named)  # pytype: disable=attribute-error
+    parse_fns['named'].update(named)
     _SetMetadata(fn, FIRE_PARSE_FNS, parse_fns)
     return fn
 
diff --git a/fire/docstrings.py b/fire/docstrings.py
index 2d7c7e63..2adfe5ec 100644
--- a/fire/docstrings.py
+++ b/fire/docstrings.py
@@ -436,7 +436,7 @@ def _consume_line(line_info, state):
   if state.section.new and state.section.format == Formats.RST:
     # The current line starts with an RST directive, e.g. ":param arg:".
     directive = _get_directive(line_info)
-    directive_tokens = directive.split()  # pytype: disable=attribute-error
+    directive_tokens = directive.split()
     if state.section.title == Sections.ARGS:
       name = directive_tokens[-1]
       arg = _get_or_create_arg_by_name(
diff --git a/fire/formatting_windows.py b/fire/formatting_windows.py
index cee6f393..749ab6d0 100644
--- a/fire/formatting_windows.py
+++ b/fire/formatting_windows.py
@@ -21,7 +21,7 @@
 import sys
 
 try:
-  import colorama  # pylint: disable=g-import-not-at-top,  # pytype: disable=import-error
+  import colorama  # pylint: disable=g-import-not-at-top
   HAS_COLORAMA = True
 except ImportError:
   HAS_COLORAMA = False
@@ -38,9 +38,9 @@ def initialize_or_disable():
       # Windows 10, 2016, and 2019 only.
 
       wrap = False
-      kernel32 = ctypes.windll.kernel32  # pytype: disable=module-attr
+      kernel32 = ctypes.windll.kernel32
       enable_virtual_terminal_processing = 0x04
-      out_handle = kernel32.GetStdHandle(subprocess.STD_OUTPUT_HANDLE)  # pylint: disable=line-too-long,  # pytype: disable=module-attr
+      out_handle = kernel32.GetStdHandle(subprocess.STD_OUTPUT_HANDLE)  # pylint: disable=line-too-long,
       # GetConsoleMode fails if the terminal isn't native.
       mode = ctypes.wintypes.DWORD()
       if kernel32.GetConsoleMode(out_handle, ctypes.byref(mode)) == 0:
diff --git a/fire/helptext.py b/fire/helptext.py
index 318d6276..347278da 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -29,6 +29,8 @@
 information.
 """
 
+from __future__ import annotations
+
 import collections
 import itertools
 
@@ -85,13 +87,14 @@ def HelpText(component, trace=None, verbose=False):
       + usage_details_sections
       + notes_sections
   )
+  valid_sections = [section for section in sections if section is not None]
   return '\n\n'.join(
-      _CreateOutputSection(*section)
-      for section in sections if section is not None
+      _CreateOutputSection(name, content)
+      for name, content in valid_sections
   )
 
 
-def _NameSection(component, info, trace=None, verbose=False):
+def _NameSection(component, info, trace=None, verbose=False) -> tuple[str, str]:
   """The "Name" section of the help string."""
 
   # Only include separators in the name in verbose mode.
@@ -113,7 +116,7 @@ def _NameSection(component, info, trace=None, verbose=False):
 
 
 def _SynopsisSection(component, actions_grouped_by_kind, spec, metadata,
-                     trace=None):
+                     trace=None) -> tuple[str, str]:
   """The "Synopsis" section of the help string."""
   current_command = _GetCurrentCommand(trace=trace, include_separators=True)
 
@@ -136,7 +139,7 @@ def _SynopsisSection(component, actions_grouped_by_kind, spec, metadata,
   return ('SYNOPSIS', text)
 
 
-def _DescriptionSection(component, info):
+def _DescriptionSection(component, info) -> tuple[str, str] | None:
   """The "Description" sections of the help string.
 
   Args:
@@ -408,7 +411,7 @@ def _GetCurrentCommand(trace=None, include_separators=True):
   return current_command
 
 
-def _CreateOutputSection(name, content):
+def _CreateOutputSection(name: str, content: str) -> str:
   return f"""{formatting.Bold(name)}
 {formatting.Indent(content, SECTION_INDENTATION)}"""
 
diff --git a/fire/helptext_test.py b/fire/helptext_test.py
index aeff5240..c7098fc4 100644
--- a/fire/helptext_test.py
+++ b/fire/helptext_test.py
@@ -125,7 +125,7 @@ def testHelpTextFunctionWithKwargsAndDefaults(self):
 
   def testHelpTextFunctionWithDefaultsAndTypes(self):
     component = (
-        tc.py3.WithDefaultsAndTypes().double)  # pytype: disable=module-attr
+        tc.py3.WithDefaultsAndTypes().double)
     help_screen = helptext.HelpText(
         component=component,
         trace=trace.FireTrace(component, name='double'))
@@ -139,7 +139,7 @@ def testHelpTextFunctionWithDefaultsAndTypes(self):
 
   def testHelpTextFunctionWithTypesAndDefaultNone(self):
     component = (
-        tc.py3.WithDefaultsAndTypes().get_int)  # pytype: disable=module-attr
+        tc.py3.WithDefaultsAndTypes().get_int)
     help_screen = helptext.HelpText(
         component=component,
         trace=trace.FireTrace(component, name='get_int'))
@@ -153,7 +153,7 @@ def testHelpTextFunctionWithTypesAndDefaultNone(self):
     self.assertNotIn('NOTES', help_screen)
 
   def testHelpTextFunctionWithTypes(self):
-    component = tc.py3.WithTypes().double  # pytype: disable=module-attr
+    component = tc.py3.WithTypes().double
     help_screen = helptext.HelpText(
         component=component,
         trace=trace.FireTrace(component, name='double'))
@@ -168,7 +168,7 @@ def testHelpTextFunctionWithTypes(self):
         help_screen)
 
   def testHelpTextFunctionWithLongTypes(self):
-    component = tc.py3.WithTypes().long_type  # pytype: disable=module-attr
+    component = tc.py3.WithTypes().long_type
     help_screen = helptext.HelpText(
         component=component,
         trace=trace.FireTrace(component, name='long_type'))
@@ -263,14 +263,14 @@ def testHelpTextNoInit(self):
     self.assertIn('SYNOPSIS\n    OldStyleEmpty', help_screen)
 
   def testHelpTextKeywordOnlyArgumentsWithDefault(self):
-    component = tc.py3.KeywordOnly.with_default  # pytype: disable=module-attr
+    component = tc.py3.KeywordOnly.with_default
     output = helptext.HelpText(
         component=component, trace=trace.FireTrace(component, 'with_default'))
     self.assertIn('NAME\n    with_default', output)
     self.assertIn('FLAGS\n    -x, --x=X', output)
 
   def testHelpTextKeywordOnlyArgumentsWithoutDefault(self):
-    component = tc.py3.KeywordOnly.double  # pytype: disable=module-attr
+    component = tc.py3.KeywordOnly.double
     output = helptext.HelpText(
         component=component, trace=trace.FireTrace(component, 'double'))
     self.assertIn('NAME\n    double', output)
diff --git a/fire/inspectutils.py b/fire/inspectutils.py
index d9c62ca7..6dd8fd67 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -100,9 +100,9 @@ def Py3GetFullArgSpec(fn):
     An inspect.FullArgSpec namedtuple with the full arg spec of the function.
   """
   # pylint: disable=no-member
-  # pytype: disable=module-attr
+
   try:
-    sig = inspect._signature_from_callable(  # pylint: disable=protected-access
+    sig = inspect._signature_from_callable(  # pylint: disable=protected-access  # type: ignore
         fn,
         skip_bound_arg=True,
         follow_wrapper_chains=True,
@@ -129,19 +129,19 @@ def Py3GetFullArgSpec(fn):
     name = param.name
 
     # pylint: disable=protected-access
-    if kind is inspect._POSITIONAL_ONLY:
+    if kind is inspect._POSITIONAL_ONLY:  # type: ignore
       args.append(name)
-    elif kind is  inspect._POSITIONAL_OR_KEYWORD:
+    elif kind is inspect._POSITIONAL_OR_KEYWORD:  # type: ignore
       args.append(name)
       if param.default is not param.empty:
         defaults += (param.default,)
-    elif kind is  inspect._VAR_POSITIONAL:
+    elif kind is inspect._VAR_POSITIONAL:  # type: ignore
       varargs = name
-    elif kind is  inspect._KEYWORD_ONLY:
+    elif kind is inspect._KEYWORD_ONLY:  # type: ignore
       kwonlyargs.append(name)
       if param.default is not param.empty:
         kwdefaults[name] = param.default
-    elif kind is  inspect._VAR_KEYWORD:
+    elif kind is inspect._VAR_KEYWORD:  # type: ignore
       varkw = name
     if param.annotation is not param.empty:
       annotations[name] = param.annotation
@@ -157,7 +157,6 @@ def Py3GetFullArgSpec(fn):
   return inspect.FullArgSpec(args, varargs, varkw, defaults,
                              kwonlyargs, kwdefaults, annotations)
   # pylint: enable=no-member
-  # pytype: enable=module-attr
 
 
 def GetFullArgSpec(fn):
@@ -259,7 +258,7 @@ def Info(component):
     try:
       inspector = oinspect.Inspector(theme_name="neutral")
     except TypeError:  # Only recent versions of IPython support theme_name.
-      inspector = oinspect.Inspector()
+      inspector = oinspect.Inspector()  # type: ignore
     info = inspector.info(component)
 
     # IPython's oinspect.Inspector.info may return '<no docstring>'
diff --git a/fire/main_test.py b/fire/main_test.py
index a2723347..9e1c382b 100644
--- a/fire/main_test.py
+++ b/fire/main_test.py
@@ -78,7 +78,7 @@ def testFileNameModuleDuplication(self):
   def testFileNameModuleFileFailure(self):
     # Confirm that an invalid file that masks a non-existent module fails.
     with self.assertRaisesRegex(ValueError,
-                                r'Fire can only be called on \.py files\.'):  # pylint: disable=line-too-long,  # pytype: disable=attribute-error
+                                r'Fire can only be called on \.py files\.'):  # pylint: disable=line-too-long,
       dirname = os.path.dirname(self.file.name)
       with testutils.ChangeDirectory(dirname):
         with open('foobar', 'w'):
diff --git a/fire/parser.py b/fire/parser.py
index d945b8ce..b8e7f19c 100644
--- a/fire/parser.py
+++ b/fire/parser.py
@@ -96,7 +96,7 @@ def _LiteralEval(value):
     SyntaxError: If the value string has a syntax error.
   """
   root = ast.parse(value, mode='eval')
-  if isinstance(root.body, ast.BinOp):  # pytype: disable=attribute-error
+  if isinstance(root.body, ast.BinOp):
     raise ValueError(value)
 
   for node in ast.walk(root):
diff --git a/fire/trace.py b/fire/trace.py
index 4a6d4776..601026fd 100644
--- a/fire/trace.py
+++ b/fire/trace.py
@@ -62,9 +62,7 @@ def __init__(self, initial_component, name=None, separator='-', verbose=False,
 
   def GetResult(self):
     """Returns the component from the last element of the trace."""
-    # pytype: disable=attribute-error
     return self.GetLastHealthyElement().component
-    # pytype: enable=attribute-error
 
   def GetLastHealthyElement(self):
     """Returns the last element of the trace that is not an error.
diff --git a/pyproject.toml b/pyproject.toml
index 6a6ba63e..dfb1eeba 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -63,7 +63,3 @@ addopts = [
     "--ignore=fire/test_components_py3.py",
     "--ignore=fire/parser_fuzz_test.py"
 ]
-
-[tool.pytype]
-inputs = "."
-output = ".pytype"

From cec0119b10d2007e9de7c58ea4d7eac22682dc04 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 18 Jul 2025 23:22:05 -0400
Subject: [PATCH 321/324] Update hypothesis requirement from <6.133.0 to
 <6.136.0 (#616)

Updates the requirements on [hypothesis](https://github.com/HypothesisWorks/hypothesis) to permit the latest version.
- [Release notes](https://github.com/HypothesisWorks/hypothesis/releases)
- [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-ruby-0.0.1...hypothesis-python-6.135.33)

---
updated-dependencies:
- dependency-name: hypothesis
  dependency-version: 6.135.33
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 pyproject.toml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pyproject.toml b/pyproject.toml
index dfb1eeba..b42c0aec 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -48,7 +48,7 @@ test = [
     "pytest-pylint<=1.1.2",
     "pytest-runner<7.0.0",
     "termcolor<3.2.0",
-    "hypothesis<6.133.0",
+    "hypothesis<6.136.0",
     "levenshtein<=0.27.1",
 ]
 

From 8c62e05569cd7111731e2f704cbba5e3e4157b01 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 18 Jul 2025 23:25:48 -0400
Subject: [PATCH 322/324] Update pytest requirement from <=8.3.5 to <=8.4.1
 (#615)

Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/1.0.0b3...8.4.1)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 8.4.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 pyproject.toml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pyproject.toml b/pyproject.toml
index b42c0aec..34ad8426 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -44,7 +44,7 @@ test = [
     "setuptools<=80.9.0",
     "pip",
     "pylint<3.3.7",
-    "pytest<=8.3.5",
+    "pytest<=8.4.1",
     "pytest-pylint<=1.1.2",
     "pytest-runner<7.0.0",
     "termcolor<3.2.0",

From 86bf4ca693106a85827d9419ae36ff2c7ac29a9a Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 18 Jul 2025 23:30:59 -0400
Subject: [PATCH 323/324] Update pylint requirement from <3.3.7 to <3.3.8
 (#614)

Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/pylint-version-0.18.1...v3.3.7)

---
updated-dependencies:
- dependency-name: pylint
  dependency-version: 3.3.7
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 pyproject.toml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pyproject.toml b/pyproject.toml
index 34ad8426..b9a7217b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -43,7 +43,7 @@ Repository = "https://github.com/google/python-fire"
 test = [
     "setuptools<=80.9.0",
     "pip",
-    "pylint<3.3.7",
+    "pylint<3.3.8",
     "pytest<=8.4.1",
     "pytest-pylint<=1.1.2",
     "pytest-runner<7.0.0",

From ea8c7f5e74157c9f6bf2e251fce8ddcac81ef3d5 Mon Sep 17 00:00:00 2001
From: David Bieber <david810@gmail.com>
Date: Sat, 19 Jul 2025 08:20:55 -0400
Subject: [PATCH 324/324] Remove unused MANIFEST

---
 MANIFEST.in | 1 -
 1 file changed, 1 deletion(-)
 delete mode 100644 MANIFEST.in

diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 1aba38f6..00000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1 +0,0 @@
-include LICENSE