diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..8be46672
--- /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: "/"
+ schedule:
+ interval: "monthly"
+ labels: ["dependabot"]
+ 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:
+ gh-actions:
+ patterns:
+ - "*" # Check all dependencies
+ labels: ["dependabot"]
+ pull-request-branch-name:
+ separator: "-"
+ open-pull-requests-limit: 5
+ reviewers:
+ - "dbieber"
diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh
new file mode 100755
index 00000000..d9207dfe
--- /dev/null
+++ b/.github/scripts/build.sh
@@ -0,0 +1,31 @@
+# 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
+
+# Exit when any command fails.
+set -e
+
+PYTHON_VERSION=${PYTHON_VERSION:-3.7}
+
+pip install -e .[test]
+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.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/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 00000000..6b9d1eae
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,38 @@
+name: Python Fire
+
+on:
+ push:
+ branches: ["master"]
+ pull_request:
+ branches: ["master"]
+
+defaults:
+ run:
+ shell: bash
+
+jobs:
+ build:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: ["macos-latest", "ubuntu-latest"]
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14.0-rc.2"]
+ include:
+ - {os: "ubuntu-22.04", python-version: "3.7"}
+
+ steps:
+ # Checkout the repo.
+ - name: Checkout Python Fire repository
+ uses: actions/checkout@v4
+
+ # Set up Python environment.
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ # Build Python Fire using the build.sh script.
+ - name: Run build script
+ run: ./.github/scripts/build.sh
+ env:
+ PYTHON_VERSION: ${{ matrix.python-version }}
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 44edde6e..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-language: python
-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
-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
- - 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/CONTRIBUTING.md b/CONTRIBUTING.md
index 0786fdf4..b5d67c96 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,35 @@ 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.
+- Single quotes around strings, three double quotes around docstrings.
+
+[Google Python Style Guide]: http://google.github.io/styleguide/pyguide.html
+
+## Testing
+
+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.
+
+## 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.
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
diff --git a/README.md b/README.md
index 862f5f23..1482d56d 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,19 @@
# Python Fire [](https://github.com/google/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]](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
@@ -20,7 +24,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:
@@ -74,17 +77,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 +97,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/api.md b/docs/api.md
index c3bb2ef6..aae92cd6 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -1,20 +1,46 @@
+## 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 -- --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` | 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._
+
+## 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. |
+
+## 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 `
+
+or
+
+`python -m fire `
-_Note that flags are separated from the Fire command by an isolated `--` arg._
+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 2e59ce8a..444a76ff 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,12 +93,36 @@ then simply this:
import fire
def hello(name):
- return 'Hello {name}!'.format(name=name)
+ return f'Hello {name}!'
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 f'Hello {name}!'
+```
+
+Then you can use it with Fire like this:
+
+```bash
+$ 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
@@ -294,8 +318,9 @@ class Pipeline(object):
self.digestion = DigestionStage()
def run(self):
- self.ingestion.run()
- self.digestion.run()
+ ingestion_output = self.ingestion.run()
+ digestion_output = self.digestion.run()
+ return [ingestion_output, digestion_output]
if __name__ == '__main__':
fire.Fire(Pipeline)
@@ -564,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
@@ -595,7 +639,7 @@ $ python example.py [1,2]
list
$ python example.py True
bool
-$ python example.py {name: David}
+$ python example.py {name:David}
dict
```
@@ -685,8 +729,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.
@@ -716,7 +761,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 171ae26e..8dcc5db6 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,15 +1,19 @@
# Python Fire [](https://github.com/google/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)
-
+- 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
@@ -20,7 +24,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:
@@ -74,17 +77,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,16 +97,17 @@ 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` |
-
-_Note that these flags are separated from the Fire command by an isolated `--`._
+| 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.
+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`.
diff --git a/docs/using-cli.md b/docs/using-cli.md
index 4cd95e14..bdfcb7db 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
@@ -54,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
@@ -87,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.
@@ -102,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
@@ -134,13 +137,16 @@ 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
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.
@@ -171,7 +177,7 @@ corresponds to, as well as usage information for how to extend that command.
### `--trace`: Getting a Fire trace
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
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/__init__.py b/fire/__init__.py
index 9eff51c4..b1470692 100644
--- a/fire/__init__.py
+++ b/fire/__init__.py
@@ -14,11 +14,7 @@
"""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']
-__version__ = '0.2.2'
+__version__ = '0.7.1'
diff --git a/fire/__main__.py b/fire/__main__.py
new file mode 100644
index 00000000..eb98b1a4
--- /dev/null
+++ b/fire/__main__.py
@@ -0,0 +1,126 @@
+# 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
+from importlib import util
+import os
+import sys
+
+import fire
+
+cli_string = """usage: python -m fire [module] [arg] ..."
+
+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.
+
+ 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 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 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)
+
+ 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.
+
+ 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
+ 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 OSError('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)
+ sys.exit(1)
+
+ module_or_filename = args[1]
+ module, module_name = import_module(module_or_filename)
+
+ fire.Fire(module, name=module_name, command=args[2:])
+
+
+if __name__ == '__main__':
+ main(sys.argv)
diff --git a/fire/completion.py b/fire/completion.py
index 19d068fd..1597d464 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'):
@@ -104,7 +103,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
@@ -156,7 +155,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]))
@@ -274,24 +277,10 @@ 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)
)
-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.
@@ -309,37 +298,42 @@ 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.
"""
- if isinstance(name, six.string_types) and name.startswith('__'):
+ if isinstance(name, str) and name.startswith('__'):
return False
if verbose:
return True
+ if (member is absolute_import
+ or member is division
+ or member is print_function):
+ return False
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.
if class_attrs is None:
- class_attrs = GetClassAttrsDict(class_attrs)
+ class_attrs = inspectutils.GetClassAttrsDict(component) 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 (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 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 isinstance(name, str):
return not name.startswith('_')
return True # Default to including the member
@@ -353,9 +347,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.
@@ -369,7 +363,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,
@@ -388,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
@@ -434,7 +428,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/completion_test.py b/fire/completion_test.py
index 582e5bbc..c0d5d24f 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
@@ -37,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/console/console_attr.py b/fire/console/console_attr.py
index 35c10fba..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):
@@ -268,7 +266,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['
@@ -288,7 +286,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()
@@ -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.
@@ -394,7 +392,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 +406,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):
@@ -456,7 +454,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
@@ -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/console_attr_os.py b/fire/console/console_attr_os.py
index 8482c7bc..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
@@ -123,7 +120,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_io.py b/fire/console/console_io.py
index 777f1f48..ec0858d9 100644
--- a/fire/console/console_io.py
+++ b/fire/console/console_io.py
@@ -15,11 +15,8 @@
"""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
import sys
@@ -97,10 +94,15 @@ 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)
+ # 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)
p = subprocess.Popen(pager, stdin=subprocess.PIPE, shell=True)
enc = console_attr.GetConsoleAttr().GetEncoding()
p.communicate(input=contents.encode(enc))
p.wait()
+ # 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)
return
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/console/encoding.py b/fire/console/encoding.py
index 780e5a28..662342c6 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,8 @@ 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)
+ del encoding # Unused.
+ return string
def Decode(data, encoding=None):
@@ -67,20 +55,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 +180,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/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
diff --git a/fire/core.py b/fire/core.py
index 07993de9..8e23e76b 100644
--- a/fire/core.py
+++ b/fire/core.py
@@ -49,14 +49,10 @@ 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 asyncio
import inspect
import json
import os
-import pipes
import re
import shlex
import sys
@@ -72,10 +68,9 @@ def main(argv):
from fire import trace
from fire import value_types
from fire.console import console_io
-import six
-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
@@ -91,6 +86,8 @@ def Fire(component=None, command=None, name=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
@@ -109,7 +106,7 @@ def Fire(component=None, command=None, name=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
@@ -141,7 +138,7 @@ def Fire(component=None, command=None, name=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)
@@ -149,7 +146,7 @@ def Fire(component=None, command=None, name=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:
@@ -161,7 +158,8 @@ 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
@@ -201,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
@@ -232,26 +230,31 @@ 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=pipes.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
-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()
- if hasattr(result, '__str__'):
+ # Allow users to modify the return value of the component and provide
+ # custom formatting.
+ if serialize:
+ if not callable(serialize):
+ raise FireError(
+ 'The argument `serialize` must be empty or callable:', 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.
- 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:
@@ -283,9 +286,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=pipes.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)
@@ -312,7 +315,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,
@@ -323,14 +326,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)
@@ -338,16 +340,16 @@ 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
# or module if it fits cleanly on the line.
if inspect.isfunction(result):
- return ''.format(name=result.__name__)
+ return f''
if inspect.ismodule(result):
- return ''.format(name=result.__name__)
+ return f''
try:
# Don't force conversion to ascii.
@@ -502,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
@@ -516,7 +518,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()):
if target == str(key):
component = value
handled = True
@@ -632,7 +635,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 +644,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)
@@ -672,7 +675,19 @@ 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):
+ try:
+ loop = asyncio.get_running_loop()
+ except RuntimeError:
+ # No event loop running, create a new one
+ component = asyncio.run(fn(*varargs, **kwargs))
+ else:
+ # Event loop is already running
+ component = loop.run_until_complete(fn(*varargs, **kwargs))
+ else:
+ component = fn(*varargs, **kwargs)
if treatment == 'class':
action = trace.INSTANTIATED_CLASS
@@ -861,6 +876,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
@@ -878,9 +894,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/core_test.py b/fire/core_test.py
index 2f2df3fd..f48d6e2d 100644
--- a/fire/core_test.py
+++ b/fire/core_test.py
@@ -14,15 +14,12 @@
"""Tests for the core module."""
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
+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):
@@ -192,5 +189,40 @@ 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 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):
+ _ = core.Fire(ident, command=['[a,b]'], serialize=serialize)
+ with self.assertOutputMatches(stdout='a=5, b=6', stderr=None):
+ _ = core.Fire(ident, command=['{a:5,b:6}'], serialize=serialize)
+ with self.assertOutputMatches(stdout='asdf', stderr=None):
+ _ = 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)
+
+ def testLruCacheDecoratorBoundArg(self):
+ self.assertEqual(
+ core.Fire(tc.py3.LruCacheDecoratedMethod,
+ command=['lru_cache_in_class', 'foo']), 'foo')
+
+ def testLruCacheDecorator(self):
+ self.assertEqual(
+ core.Fire(tc.py3.lru_cache_decorated,
+ command=['foo']), 'foo')
+
+
if __name__ == '__main__':
testutils.main()
diff --git a/fire/custom_descriptions.py b/fire/custom_descriptions.py
index 695b01b3..ef1130a3 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'}`.
@@ -36,11 +36,10 @@
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
+TWO_DOUBLE_QUOTES = '""'
+STRING_DESC_PREFIX = 'The string '
def NeedsCustomDescription(component):
@@ -60,12 +59,86 @@ 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
+
+
+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 ""'. 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:
+ 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[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..6cff2d5d
--- /dev/null
+++ b/fire/custom_descriptions_test.py
@@ -0,0 +1,69 @@
+# 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 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/decorators.py b/fire/decorators.py
index b7b3c660..547153c6 100644
--- a/fire/decorators.py
+++ b/fire/decorators.py
@@ -18,10 +18,7 @@
command line arguments to client code.
"""
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
+from typing import Any, Dict
import inspect
FIRE_METADATA = 'FIRE_METADATA'
@@ -84,16 +81,30 @@ def _SetMetadata(fn, attribute, value):
setattr(fn, FIRE_METADATA, metadata)
-def GetMetadata(fn):
+def GetMetadata(fn) -> Dict[str, Any]:
+ """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):
+def GetParseFns(fn) -> Dict[str, Any]:
metadata = GetMetadata(fn)
- default = dict(default=None, positional=[], named={})
+ default = {'default': None, 'positional': [], 'named': {}}
return metadata.get(FIRE_PARSE_FNS, default)
diff --git a/fire/decorators_test.py b/fire/decorators_test.py
index cc7d6203..9988743c 100644
--- a/fire/decorators_test.py
+++ b/fire/decorators_test.py
@@ -14,16 +14,12 @@
"""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
-class NoDefaults(object):
+class NoDefaults:
"""A class for testing decorated functions without default values."""
@decorators.SetParseFns(count=int)
@@ -44,7 +40,7 @@ def double(count):
return 2 * count
-class WithDefaults(object):
+class WithDefaults:
@decorators.SetParseFns(float)
def example1(self, arg1=10):
@@ -55,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):
@@ -73,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):
@@ -83,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/docstrings.py b/fire/docstrings.py
index a58a42fe..2adfe5ec 100644
--- a/fire/docstrings.py
+++ b/fire/docstrings.py
@@ -49,17 +49,11 @@
- "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
import textwrap
-import enum
-
class DocstringInfo(
collections.namedtuple(
@@ -77,6 +71,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 +107,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'),
@@ -149,6 +148,7 @@ def parse(docstring):
Args:
docstring: The docstring to parse.
+
Returns:
A DocstringInfo containing information about the docstring.
"""
@@ -168,6 +168,7 @@ def parse(docstring):
state.summary.lines = []
state.description.lines = []
state.args = []
+ state.kwargs = []
state.current_arg = None
state.returns.lines = []
state.yields.lines = []
@@ -175,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
@@ -192,6 +194,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,
@@ -265,7 +271,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
@@ -275,17 +281,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
@@ -293,9 +303,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.
@@ -303,9 +312,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):
@@ -388,6 +399,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])
@@ -424,10 +436,14 @@ 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(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])
@@ -454,7 +470,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:
@@ -491,8 +507,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()
@@ -505,6 +521,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
@@ -725,3 +746,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_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 adb89492..ce516944 100644
--- a/fire/docstrings_test.py
+++ b/fire/docstrings_test.py
@@ -14,16 +14,14 @@
"""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
+# pylint: disable=invalid-name
+DocstringInfo = docstrings.DocstringInfo
+ArgInfo = docstrings.ArgInfo
+KwargInfo = docstrings.KwargInfo
+# pylint: enable=invalid-name
class DocstringsTest(testutils.BaseTestCase):
@@ -48,12 +46,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.',
)
@@ -61,25 +59,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)
@@ -142,6 +140,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.
@@ -205,6 +229,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.
@@ -253,6 +306,56 @@ 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)
+
+ 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/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 8cf121af..99b4a7c6 100644
--- a/fire/fire_test.py
+++ b/fire/fire_test.py
@@ -14,21 +14,14 @@
"""Tests for the fire module."""
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
import os
import sys
-import unittest
+from unittest import mock
import fire
from fire import test_components as tc
from fire import testutils
-import mock
-import six
-
class FireTest(testutils.BaseTestCase):
@@ -185,7 +178,6 @@ 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.')
def testFireKeywordOnlyArgs(self):
with self.assertRaisesFireExit(2):
# Keyword arguments must be passed with flag syntax.
@@ -703,6 +695,27 @@ 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
+ )
+
+ def testClassWithInvalidProperty(self):
+ self.assertEqual(
+ fire.Fire(tc.InvalidProperty, command=['double', '10']), 20
+ )
+
+ 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'])
+
+ def testFireAsyncio(self):
+ self.assertEqual(fire.Fire(tc.py3.WithAsyncio,
+ command=['double', '--count', '10']), 20)
+
if __name__ == '__main__':
testutils.main()
diff --git a/fire/formatting.py b/fire/formatting.py
index 880e2b18..68484c27 100644
--- a/fire/formatting.py
+++ b/fire/formatting.py
@@ -14,13 +14,13 @@
"""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
+ELLIPSIS = '...'
+
+
def Indent(text, spaces=2):
lines = text.split('\n')
return '\n'.join(
@@ -65,3 +65,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..e0f6699d 100644
--- a/fire/formatting_test.py
+++ b/fire/formatting_test.py
@@ -14,23 +14,21 @@
"""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
+LINE_LENGTH = 80
+
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)
@@ -51,6 +49,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/formatting_windows.py b/fire/formatting_windows.py
new file mode 100644
index 00000000..749ab6d0
--- /dev/null
+++ b/fire/formatting_windows.py
@@ -0,0 +1,58 @@
+# Copyright (C) 2018 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""This module is used for enabling formatting on Windows."""
+
+import ctypes
+import os
+import platform
+import subprocess
+import sys
+
+try:
+ import colorama # pylint: disable=g-import-not-at-top
+ HAS_COLORAMA = True
+except ImportError:
+ HAS_COLORAMA = False
+
+
+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'):
+ # Enables native ANSI sequences in console.
+ # Windows 10, 2016, and 2019 only.
+
+ wrap = False
+ kernel32 = ctypes.windll.kernel32
+ enable_virtual_terminal_processing = 0x04
+ 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:
+ 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()
diff --git a/fire/helptext.py b/fire/helptext.py
index 2c360d2b..347278da 100644
--- a/fire/helptext.py
+++ b/fire/helptext.py
@@ -29,22 +29,26 @@
information.
"""
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
+from __future__ import annotations
+
+import collections
+import itertools
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
LINE_LENGTH = 80
+SECTION_INDENTATION = 4
+SUBSECTION_INDENTATION = 4
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.
@@ -83,33 +87,36 @@ 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.
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
+ text = f'{current_command} - {summary}'
else:
text = current_command
return ('NAME', text)
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)
@@ -128,15 +135,11 @@ 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)
-def _DescriptionSection(component, info):
+def _DescriptionSection(component, info) -> tuple[str, str] | None:
"""The "Description" sections of the help string.
Args:
@@ -147,21 +150,44 @@ 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:
return None
+def _CreateKeywordOnlyFlagItem(flag, docstring_info, spec, short_arg):
+ return _CreateFlagItem(
+ 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):
"""The "Args and Flags" sections of the help string."""
args_with_no_defaults = spec.args[:len(spec.args) - len(spec.defaults)]
@@ -176,13 +202,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:
@@ -194,21 +220,60 @@ def _ArgsAndFlagsSections(info, spec, metadata):
('NOTES', 'You can also use flags syntax for POSITIONAL ARGUMENTS')
)
- optional_flag_items = [
- _CreateFlagItem(flag, docstring_info, required=False)
+ unique_short_args = _GetShortFlags(args_with_defaults)
+ positional_flag_items = [
+ _CreateFlagItem(
+ flag, docstring_info, spec, required=False,
+ short_arg=flag[0] in unique_short_args
+ )
for flag in args_with_defaults
]
- required_flag_items = [
- _CreateFlagItem(flag, docstring_info, required=True)
+
+ unique_short_kwonly_flags = _GetShortFlags(spec.kwonlyargs)
+ kwonly_flag_items = [
+ _CreateKeywordOnlyFlagItem(
+ flag, docstring_info, spec,
+ short_arg=flag[0] in unique_short_kwonly_flags
+ )
for flag in spec.kwonlyargs
]
- flag_items = optional_flag_items + required_flag_items
+ flag_items = positional_flag_items + kwonly_flag_items
if spec.varkw:
+ # Include kwargs documented via :key param:
+ documented_kwargs = []
+
+ # 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:
+ short_name = flag.name[0]
+ flag_string = f'-{short_name}, --{flag.name}'
+ else:
+ flag_string = f'--{flag.name}'
+
+ flag_item = _CreateFlagItem(
+ flag.name, docstring_info, spec,
+ flag_string=flag_string)
+ 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)
@@ -278,9 +343,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:
@@ -288,8 +353,8 @@ def _GetArgsAndFlagsString(spec, metadata):
arg_and_flag_strings.append('')
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)
@@ -332,7 +397,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]
@@ -346,64 +411,164 @@ def _GetCurrentCommand(trace=None, include_separators=True):
return current_command
-def _CreateOutputSection(name, content):
- return """{name}
-{content}""".format(name=formatting.Bold(name),
- content=formatting.Indent(content, 4))
+def _CreateOutputSection(name: str, content: str) -> str:
+ return f"""{formatting.Bold(name)}
+{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 = 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))
+
+ 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,
+ flag_string=None, short_arg=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.
- required: Whether the flag is required. Keyword-only arguments (only in
- Python 3) become required flags, whereas normal keyword arguments become
- optional flags.
+ 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.
+ 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.
"""
+ # 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_name=flag,
- flag_name_upper=formatting.Underline(flag.upper()))
+ if not flag_string:
+ flag_name_upper = formatting.Underline(flag.upper())
+ flag_string = f'--{flag}={flag_name_upper}'
if required:
- flag += ' (required)'
- return _CreateItem(flag, description, indent=4)
+ flag_string += ' (required)'
+ if short_arg:
+ short_flag = flag[0]
+ flag_string = f'-{short_flag}, {flag_string}'
+
+ 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 = f'Optional[{arg_type}]'
+
+ 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 = 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))
+
+ 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:
+ return arg_type.__qualname__
+ 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)
+ if flag in spec.kwonlydefaults:
+ return repr(spec.kwonlydefaults[flag])
+ return ''
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
@@ -418,6 +583,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)
@@ -446,9 +614,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)
@@ -464,11 +632,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()
@@ -512,15 +675,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
@@ -529,7 +693,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)
@@ -545,7 +709,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]
@@ -555,18 +719,26 @@ def _GetCallableUsageItems(spec, metadata):
items.append('')
if spec.varargs:
- items.append('[{varargs}]...'.format(varargs=spec.varargs.upper()))
+ items.append(f'[{spec.varargs.upper()}]...')
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 = [f'--{flag}' for flag in itertools.chain(
+ args_with_defaults, _KeywordOnlyArguments(spec, required=False))]
+ required_flags = [
+ f'--{flag}' for flag in _KeywordOnlyArguments(spec, required=True)
+ ]
# Flags section:
availability_lines = []
@@ -598,7 +770,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 1371e2a1..c7098fc4 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 textwrap
@@ -31,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):
@@ -79,9 +75,117 @@ def testHelpTextFunctionWithDefaults(self):
self.assertIn('NAME\n triple', help_screen)
self.assertIn('SYNOPSIS\n triple ', help_screen)
self.assertNotIn('DESCRIPTION', help_screen)
- self.assertIn('FLAGS\n --count=COUNT', help_screen)
+ self.assertIn(
+ 'FLAGS\n -c, --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 ', help_screen)
+ self.assertNotIn('DESCRIPTION', help_screen)
+ self.assertIn(
+ 'FLAGS\n -s, --string=STRING\n'
+ ' Default: \'0001020304050607080910'
+ '1112131415161718192021222324252627282...',
+ 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 ', 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 ', help_screen)
+ self.assertIn('DESCRIPTION\n Function with kwarg', help_screen)
+ self.assertIn(
+ '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.',
+ help_screen)
+
+ 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 ', help_screen)
+ self.assertIn('DESCRIPTION', help_screen)
+ self.assertIn(
+ 'FLAGS\n -c, --count=COUNT\n Type: float\n Default: 0',
+ help_screen)
+ self.assertNotIn('NOTES', help_screen)
+
+ 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 ', help_screen)
+ self.assertNotIn('DESCRIPTION', help_screen)
+ self.assertIn(
+ 'FLAGS\n -v, --value=VALUE\n'
+ ' Type: Optional[int]\n Default: None',
+ help_screen)
+ self.assertNotIn('NOTES', help_screen)
+
+ 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)
+
+ 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 displayed
+ # 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(
@@ -111,7 +215,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 +230,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 +247,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)
@@ -155,6 +262,29 @@ def testHelpTextNoInit(self):
self.assertIn('NAME\n OldStyleEmpty', help_screen)
self.assertIn('SYNOPSIS\n OldStyleEmpty', help_screen)
+ 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=X', output)
+
+ 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 -c, --count=COUNT (required)', output)
+
+ 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 ', 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')
@@ -221,7 +351,8 @@ 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(),
help_output.strip())
@@ -235,7 +366,8 @@ def testHelpTextUnderlineFlag(self):
formatting.Bold('SYNOPSIS') + '\n triple ',
help_screen)
self.assertIn(
- formatting.Bold('FLAGS') + '\n --' + formatting.Underline('count'),
+ formatting.Bold('FLAGS') + '\n -c, --' +
+ formatting.Underline('count'),
help_screen)
def testHelpTextBoldCommandName(self):
@@ -281,6 +413,20 @@ 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 ',
+ 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):
@@ -355,6 +501,20 @@ def testUsageOutputFunctionWithDocstring(self):
textwrap.dedent(expected_output).lstrip('\n'),
usage_output)
+ def testUsageOutputFunctionMixedDefaults(self):
+ component = tc.py3.HelpTextComponent().identity
+ t = trace.FireTrace(component, name='FunctionMixedDefaults')
+ usage_output = helptext.UsageText(component, trace=t, verbose=False)
+ expected_output = """
+ Usage: FunctionMixedDefaults
+ 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/inspectutils.py b/fire/inspectutils.py
index aa130e05..17508e30 100644
--- a/fire/inspectutils.py
+++ b/fire/inspectutils.py
@@ -14,17 +14,14 @@
"""Inspection utility functions for Python Fire."""
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
import inspect
-from fire import docstrings
+import sys
+import types
-import six
+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,
@@ -70,30 +67,95 @@ 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__
elif inspect.ismethod(fn):
# 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
return fn, skip_arg
-def Py2GetArgSpec(fn):
- """A wrapper around getargspec that tries both fn and fn.__call__."""
+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.
+ """
+ # pylint: disable=no-member
+
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
+ sig = inspect._signature_from_callable( # pylint: disable=protected-access # type: ignore
+ 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: # type: ignore
+ args.append(name)
+ 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: # type: ignore
+ varargs = name
+ 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: # type: ignore
+ 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)
+ # pylint: enable=no-member
def GetFullArgSpec(fn):
@@ -102,11 +164,10 @@ def GetFullArgSpec(fn):
fn, skip_arg = _GetArgSpecInfo(fn)
try:
- if six.PY2:
- args, varargs, varkw, defaults = Py2GetArgSpec(fn)
- kwonlyargs = kwonlydefaults = None
- annotations = getattr(fn, '__annotations__', None)
- else:
+ if sys.version_info[0:2] >= (3, 5):
+ (args, varargs, varkw, defaults,
+ kwonlyargs, kwonlydefaults, annotations) = Py3GetFullArgSpec(fn)
+ else: # Specifically Python 3.4.
(args, varargs, varkw, defaults,
kwonlyargs, kwonlydefaults, annotations) = inspect.getfullargspec(fn) # pylint: disable=deprecated-method,no-member
@@ -137,9 +198,10 @@ def GetFullArgSpec(fn):
# Case 3: Other known slot wrappers do not accept args.
return FullArgSpec()
- if skip_arg and args:
+ # In Python 3.5+ Py3GetFullArgSpec uses skip_bound_arg=True already.
+ 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,
kwonlyargs, kwonlydefaults, annotations)
@@ -165,7 +227,7 @@ def GetFileAndLine(component):
try:
unused_code, lineindex = inspect.findsource(component)
lineno = lineindex + 1
- except IOError:
+ except (OSError, IndexError):
lineno = None
return filename, lineno
@@ -191,8 +253,11 @@ def Info(component):
A dict with information about the component.
"""
try:
- from IPython.core import oinspect # pylint: disable=g-import-not-at-top
- inspector = oinspect.Inspector()
+ from IPython.core import oinspect # pylint: disable=import-outside-toplevel,g-import-not-at-top
+ try:
+ inspector = oinspect.Inspector(theme_name="neutral")
+ except TypeError: # Only recent versions of IPython support theme_name.
+ inspector = oinspect.Inspector() # type: ignore
info = inspector.info(component)
# IPython's oinspect.Inspector.info may return ''
@@ -203,12 +268,12 @@ def Info(component):
try:
unused_code, lineindex = inspect.findsource(component)
- info['line'] = lineindex + 1
- except (TypeError, IOError):
- info['line'] = None
+ info['line'] = lineindex + 1 # type: ignore
+ except (TypeError, OSError):
+ info['line'] = None # type: ignore
if 'docstring' in info:
- info['docstring_info'] = docstrings.parse(info['docstring'])
+ info['docstring_info'] = docstrings.parse(info['docstring']) # type: ignore
return info
@@ -264,3 +329,21 @@ 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
+ }
+
+
+def IsCoroutineFunction(fn):
+ try:
+ return inspect.iscoroutinefunction(fn)
+ except: # pylint: disable=bare-except
+ return False
diff --git a/fire/inspectutils_test.py b/fire/inspectutils_test.py
index ea8eb0e2..47de7e72 100644
--- a/fire/inspectutils_test.py
+++ b/fire/inspectutils_test.py
@@ -14,19 +14,12 @@
"""Tests for the inspectutils module."""
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
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):
@@ -40,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'])
@@ -125,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/interact.py b/fire/interact.py
index 9f0a01e6..eccd3990 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
@@ -69,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):
@@ -89,11 +86,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()
diff --git a/fire/interact_test.py b/fire/interact_test.py
index 29fa7597..2f286824 100644
--- a/fire/interact_test.py
+++ b/fire/interact_test.py
@@ -14,15 +14,11 @@
"""Tests for the interact module."""
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
+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/main_test.py b/fire/main_test.py
new file mode 100644
index 00000000..9e1c382b
--- /dev/null
+++ b/fire/main_test.py
@@ -0,0 +1,94 @@
+# 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
+import tempfile
+
+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'])
+
+
+class MainModuleFileTest(testutils.BaseTestCase):
+ """Tests to verify correct import behavior for file executables."""
+
+ def setUp(self):
+ 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()
+
+ self.file2 = tempfile.NamedTemporaryFile() # pylint: disable=consider-using-with
+
+ 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'):
+ dirname = os.path.dirname(self.file.name)
+ with testutils.ChangeDirectory(dirname):
+ with open('tempfile', 'w'):
+ __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.assertRaisesRegex(ValueError,
+ 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'):
+ __main__.main([
+ '__main__.py',
+ 'foobar',
+ ])
+
+ os.remove('foobar')
+
+
+if __name__ == '__main__':
+ testutils.main()
diff --git a/fire/parser.py b/fire/parser.py
index 404e18e7..a335cc2c 100644
--- a/fire/parser.py
+++ b/fire/parser.py
@@ -14,12 +14,14 @@
"""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
+
+if sys.version_info[0:2] < (3, 8):
+ _StrNode = ast.Str # type: ignore # pylint: disable=no-member # deprecated but needed for Python < 3.8
+else:
+ _StrNode = ast.Constant
def CreateParser():
@@ -94,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):
@@ -106,7 +108,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
@@ -127,4 +129,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)
diff --git a/fire/parser_fuzz_test.py b/fire/parser_fuzz_test.py
index af0be038..10f497cf 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
@@ -25,7 +21,6 @@
from hypothesis import settings
from hypothesis import strategies as st
import Levenshtein
-import six
class ParserFuzzTest(testutils.BaseTestCase):
@@ -58,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:
@@ -68,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
@@ -86,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/parser_test.py b/fire/parser_test.py
index 0257be28..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
@@ -117,8 +113,9 @@ 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')
diff --git a/fire/test_components.py b/fire/test_components.py
index e35027c7..887a0dc6 100644
--- a/fire/test_components.py
+++ b/fire/test_components.py
@@ -14,17 +14,11 @@
"""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 six
+import functools
-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
@@ -49,7 +43,7 @@ def function_with_help(help=True): # pylint: disable=redefined-builtin
return help
-class Empty(object):
+class Empty:
pass
@@ -57,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
@@ -78,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
@@ -87,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):
@@ -97,13 +91,20 @@ 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
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
@@ -114,7 +115,7 @@ def triple(self, count=0):
return 3 * count
-class MixedDefaults(object):
+class MixedDefaults:
def ten(self):
return 10
@@ -126,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
@@ -135,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
@@ -153,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):
@@ -172,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):
@@ -190,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'
@@ -199,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
@@ -221,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
@@ -231,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
@@ -243,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.')
@@ -258,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 {}
@@ -267,7 +268,7 @@ def nothing_printable(self):
return {'__do_not_print_me': 1}
-class CircularReference(object):
+class CircularReference:
def create(self):
x = {}
@@ -275,7 +276,7 @@ def create(self):
return x
-class OrderedDictionary(object):
+class OrderedDictionary:
def empty(self):
return collections.OrderedDict()
@@ -287,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):
@@ -303,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
@@ -325,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)
@@ -339,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.
@@ -362,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
@@ -387,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():
@@ -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):
@@ -498,3 +498,71 @@ def off(self):
def set(self, value):
self.pixels[self._row][self._col] = value
return self
+
+
+class DefaultMethod:
+
+ def double(self, number):
+ return 2 * number
+
+ def __getattr__(self, name):
+ def _missing():
+ return 'Undefined function'
+ return _missing
+
+
+class InvalidProperty:
+
+ def double(self, number):
+ return 2 * 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
+
+
+# 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')
+
+
+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
+# pylint: enable=g-doc-args,g-doc-return-or-yield
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_py3.py b/fire/test_components_py3.py
index d705c43a..192302d3 100644
--- a/fire/test_components_py3.py
+++ b/fire/test_components_py3.py
@@ -14,16 +14,88 @@
"""This module has components that use Python 3 specific syntax."""
+import asyncio
+import functools
+from typing import Tuple
+
+# 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
-class KeywordOnly(object):
+class HelpTextComponent:
+
+ def identity(self, *, alpha, beta='0'):
+ return alpha, beta
+
+
+class KeywordOnly:
def double(self, *, count):
return count * 2
def triple(self, *, count):
return count * 3
+
+ def with_default(self, *, x="x"):
+ print("x: " + x)
+
+
+class LruCacheDecoratedMethod:
+
+ @functools.lru_cache()
+ def lru_cache_in_class(self, arg1):
+ return arg1
+
+
+@functools.lru_cache()
+def lru_cache_decorated(arg1):
+ return arg1
+
+
+class WithAsyncio:
+
+ async def double(self, count=0):
+ return 2 * count
+
+
+class WithTypes:
+ """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.
+ """
+ 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:
+ """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.
+ """
+ return 2 * count
+
+ def get_int(self, value: int = None):
+ return 0 if value is None else value
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 0541a9a5..eca37f43 100644
--- a/fire/testutils.py
+++ b/fire/testutils.py
@@ -14,21 +14,17 @@
"""Utilities for Python Fire's tests."""
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
import contextlib
+import io
+import os
import re
import sys
import unittest
+from unittest import mock
from fire import core
from fire import trace
-import mock
-import six
-
class BaseTestCase(unittest.TestCase):
"""Shared test case for Python Fire tests."""
@@ -44,11 +40,12 @@ 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.
"""
- 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):
@@ -80,6 +77,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,13 +87,26 @@ 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):
+ """Context manager to mock a directory change and revert on exit."""
+ cwdir = os.getcwd()
+ os.chdir(directory)
+
+ try:
+ yield directory
+ finally:
+ os.chdir(cwdir)
+
+
# pylint: disable=invalid-name
main = unittest.main
skip = unittest.skip
+skipIf = unittest.skipIf
# pylint: enable=invalid-name
diff --git a/fire/testutils_test.py b/fire/testutils_test.py
index ad604193..4cfc0937 100644
--- a/fire/testutils_test.py
+++ b/fire/testutils_test.py
@@ -14,16 +14,10 @@
"""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
-import six
-
class TestTestUtils(testutils.BaseTestCase):
"""Let's get meta."""
@@ -34,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/trace.py b/fire/trace.py
index 7174f994..601026fd 100644
--- a/fire/trace.py
+++ b/fire/trace.py
@@ -25,11 +25,7 @@
component will be None.
"""
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import pipes
+import shlex
from fire import inspectutils
@@ -42,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
@@ -66,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.
@@ -81,7 +75,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."""
@@ -166,8 +160,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.
@@ -216,10 +210,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)
@@ -245,7 +236,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
@@ -265,7 +256,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.
@@ -305,11 +296,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
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 ad50cb23..81308973 100644
--- a/fire/value_types.py
+++ b/fire/value_types.py
@@ -14,16 +14,12 @@
"""Types of values."""
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
import inspect
-import six
+from fire import inspectutils
-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))
@@ -37,7 +33,7 @@ def IsCommand(component):
def IsValue(component):
- return isinstance(component, VALUE_TYPES)
+ return isinstance(component, VALUE_TYPES) or HasCustomStr(component)
def IsSimpleGroup(component):
@@ -57,3 +53,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 = inspectutils.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
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
diff --git a/pylintrc b/pylintrc
index 37bfa447..8896bb5b 100644
--- a/pylintrc
+++ b/pylintrc
@@ -7,9 +7,6 @@
# pygtk.require().
#init-hook=
-# Profiled execution.
-profile=no
-
# Add to the black list. It should be a base name, not a
# path. You may set this option multiple times.
ignore=
@@ -32,7 +29,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,consider-using-f-string,unspecified-encoding,unnecessary-lambda-assignment,wrong-import-position,ungrouped-imports,deprecated-module
[REPORTS]
@@ -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
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..912c08aa
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,66 @@
+[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 = "david810+fire@gmail.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",
+ "Programming Language :: Python :: 3.14",
+ "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.8",
+ "pytest<=8.4.1",
+ "pytest-pylint<=1.1.2",
+ "pytest-runner<7.0.0",
+ "termcolor<3.2.0",
+ "hypothesis<6.137.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"
+]
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 058f329c..00000000
--- a/setup.cfg
+++ /dev/null
@@ -1,15 +0,0 @@
-[metadata]
-license-file = LICENSE
-
-[wheel]
-universal = 1
-
-[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 5ddbb483..00000000
--- a/setup.py
+++ /dev/null
@@ -1,86 +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 = [
- 'six',
- 'termcolor',
-]
-
-TEST_DEPENDENCIES = [
- 'hypothesis',
- 'mock',
- 'python-Levenshtein',
-]
-
-VERSION = '0.2.2'
-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 :: 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',
-
- 'Operating System :: OS Independent',
- 'Operating System :: POSIX',
- 'Operating System :: MacOS',
- 'Operating System :: Unix',
- ],
-
- keywords='command line interface cli python fire interactive bash tool',
-
- packages=['fire', 'fire.console'],
-
- install_requires=DEPENDENCIES,
- tests_require=TEST_DEPENDENCIES,
-)