Compare commits
80
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
669cf281a5 | ||
|
|
c507c093fa | ||
|
|
2f80522f6d | ||
|
|
665c497693 | ||
|
|
36fcb1995b | ||
|
|
1c1f6dd110 | ||
|
|
2363b5f2b7 | ||
|
|
3578817f16 | ||
|
|
2bfb82ef23 | ||
|
|
668007ae41 | ||
|
|
6dc6531695 | ||
|
|
9104daf7ae | ||
|
|
7538c3dd82 | ||
|
|
7651785f96 | ||
|
|
cb311f16a8 | ||
|
|
8afe885e6d | ||
|
|
5f2fa98518 | ||
|
|
00608beef2 | ||
|
|
e35e650f72 | ||
|
|
f47c938b8e | ||
|
|
42ff6cfa53 | ||
|
|
c11a3fa793 | ||
|
|
c13d87beee | ||
|
|
c99a034998 | ||
|
|
b9aafbdf34 | ||
|
|
855f317c31 | ||
|
|
a81d1d2f13 | ||
|
|
105930e3d9 | ||
|
|
e58c858dbf | ||
|
|
57c777316d | ||
|
|
871ffabb00 | ||
|
|
69f1ef0417 | ||
|
|
1d0465e8c1 | ||
|
|
5867c55439 | ||
|
|
687bf37aa7 | ||
|
|
5f83beecda | ||
|
|
fe1fda46d9 | ||
|
|
53a3498fd2 | ||
|
|
5347858957 | ||
|
|
f37dd2b8a5 | ||
|
|
0a6bb9d754 | ||
|
|
54f91a21c0 | ||
|
|
6e0ca736a9 | ||
|
|
625b305d9d | ||
|
|
80ee2564ac | ||
|
|
b55cebd963 | ||
|
|
eb8f053cee | ||
|
|
021d627c92 | ||
|
|
83a8036ea9 | ||
|
|
6912ccd56f | ||
|
|
9bff9d01ce | ||
|
|
7f166e9089 | ||
|
|
72604f4031 | ||
|
|
fa57593896 | ||
|
|
aff56bb0dc | ||
|
|
7400ca845e | ||
|
|
e76a10c051 | ||
|
|
2c7b03954c | ||
|
|
379dd0a497 | ||
|
|
6c3331fb3e | ||
|
|
a944d2854f | ||
|
|
43c0b8cd39 | ||
|
|
3e98ed525e | ||
|
|
b5053bad7c | ||
|
|
84c5a46d94 | ||
|
|
c43c832e28 | ||
|
|
952e20d1d5 | ||
|
|
3126e2cd70 | ||
|
|
1f366f9760 | ||
|
|
d9982003c6 | ||
|
|
3b04b0edb9 | ||
|
|
b76f1c3dea | ||
|
|
8eb6aa51a9 | ||
|
|
c1f045d9ab | ||
|
|
8ea42297ca | ||
|
|
a0791c9d40 | ||
|
|
f48f93b516 | ||
|
|
309d5717fc | ||
|
|
9eb7886249 | ||
|
|
3db47f8c8e |
+103
@@ -0,0 +1,103 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# dotenv
|
||||
.env
|
||||
|
||||
# virtualenv
|
||||
.venv
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# PyCharm IDE
|
||||
.idea/
|
||||
|
||||
# Type-checking
|
||||
.pytype/
|
||||
+21
-2
@@ -4,11 +4,30 @@ python:
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
# Workaround for testing Python 3.7:
|
||||
# https://github.com/travis-ci/travis-ci/issues/9815
|
||||
matrix:
|
||||
include:
|
||||
- python: 3.7
|
||||
dist: xenial
|
||||
sudo: yes
|
||||
before_install:
|
||||
- pip install --upgrade setuptools pip
|
||||
- pip install --upgrade pylint pytest pytest-pylint pytest-runner
|
||||
install:
|
||||
- pip install hypothesis python-Levenshtein
|
||||
- python setup.py develop
|
||||
script:
|
||||
- python -m pytest
|
||||
- if [[ $TRAVIS_PYTHON_VERSION != 3.6 ]]; then pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py; fi
|
||||
- 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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
include LICENSE
|
||||
@@ -1,19 +1,24 @@
|
||||
# Python Fire
|
||||
_Python Fire is a library for creating command line interfaces (CLIs) from
|
||||
absolutely any Python object._
|
||||
# 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]](doc/benefits.md#simple-cli)
|
||||
- Python Fire is a helpful tool for developing and debugging Python code. [[2]](doc/benefits.md#debugging)
|
||||
- Python Fire is a simple way to create a CLI in Python. [[1]](docs/benefits.md#simple-cli)
|
||||
- Python Fire is a helpful tool for developing and debugging Python code. [[2]](docs/benefits.md#debugging)
|
||||
- Python Fire helps with exploring existing code or turning other people's code
|
||||
into a CLI. [[3]](doc/benefits.md#exploring)
|
||||
- Python Fire makes transitioning between Bash and Python easier. [[4]](doc/benefits.md#bash)
|
||||
into a CLI. [[3]](docs/benefits.md#exploring)
|
||||
- Python Fire makes transitioning between Bash and Python easier. [[4]](docs/benefits.md#bash)
|
||||
- Python Fire makes using a Python REPL easier by setting up the REPL with the
|
||||
modules and variables you'll need already imported and created. [[5]](doc/benefits.md#repl)
|
||||
modules and variables you'll need already imported and created. [[5]](docs/benefits.md#repl)
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
`pip install fire`
|
||||
To install Python Fire with pip, run: `pip install fire`
|
||||
|
||||
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
|
||||
@@ -22,7 +27,7 @@ You can call `Fire` on any Python object:<br>
|
||||
functions, classes, modules, objects, dictionaries, lists, tuples, etc.
|
||||
They all work!
|
||||
|
||||
Here's a simple example.
|
||||
Here's an example of calling Fire on a class.
|
||||
|
||||
```python
|
||||
import fire
|
||||
@@ -45,9 +50,9 @@ python calculator.py double --number=15 # 30
|
||||
```
|
||||
|
||||
To learn how Fire behaves on functions, objects, dicts, lists, etc, and to learn
|
||||
about Fire's other features, see the [Using a Fire CLI page](doc/using-cli.md).
|
||||
about Fire's other features, see the [Using a Fire CLI page](docs/using-cli.md).
|
||||
|
||||
For additional examples, see [The Python Fire Guide](doc/guide.md).
|
||||
For additional examples, see [The Python Fire Guide](docs/guide.md).
|
||||
|
||||
|
||||
## Why is it called Fire?
|
||||
@@ -57,7 +62,7 @@ When you call `Fire`, it fires off (executes) your command.
|
||||
|
||||
## Where can I learn more?
|
||||
|
||||
Please see [The Python Fire Guide](doc/guide.md).
|
||||
Please see [The Python Fire Guide](docs/guide.md).
|
||||
|
||||
|
||||
## Reference
|
||||
@@ -74,14 +79,19 @@ Please see [The Python Fire Guide](doc/guide.md).
|
||||
|
||||
| Using a CLI | Command | Notes
|
||||
| :------------- | :------------------------- | :---------
|
||||
| [Help](doc/using-cli.md#help-flag) | `command -- --help` |
|
||||
| [REPL](doc/using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
|
||||
| [Separator](doc/using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
|
||||
| [Completion](doc/using-cli.md#completion-flag) | `command -- --completion` | Generate a completion script for the CLI.
|
||||
| [Trace](doc/using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
|
||||
| [Verbose](doc/using-cli.md#verbose-flag) | `command -- --verbose` |
|
||||
| [Help](docs/using-cli.md#help-flag) | `command -- --help` |
|
||||
| [REPL](docs/using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
|
||||
| [Separator](docs/using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
|
||||
| [Completion](docs/using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI.
|
||||
| [Trace](docs/using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
|
||||
| [Verbose](docs/using-cli.md#verbose-flag) | `command -- --verbose` |
|
||||
|
||||
_Note that flags are separated from the Fire command by an isolated `--` arg._
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the
|
||||
[Apache 2.0](https://github.com/google/python-fire/blob/master/LICENSE) License.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
| Setup | Command | Notes
|
||||
| :------ | :------------------ | :---------
|
||||
| install | `pip install fire` |
|
||||
|
||||
| Creating a CLI | Command | Notes
|
||||
| :--------------| :--------------------- | :---------
|
||||
| import | `import fire` |
|
||||
| Call | `fire.Fire()` | Turns the current module into a Fire CLI.
|
||||
| Call | `fire.Fire(component)` | Turns `component` into a Fire CLI.
|
||||
|
||||
| Using a CLI | Command | Notes
|
||||
| :------------- | :------------------------- | :---------
|
||||
| [Help](using-cli.md#help-flag) | `command -- --help` |
|
||||
| [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
|
||||
| [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
|
||||
| [Completion](using-cli.md#completion-flag) | `command -- --completion [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._
|
||||
@@ -1,13 +1,14 @@
|
||||
# Benefits of Python Fire
|
||||
|
||||
## Python Fire is a simple way to create a CLI in Python. <a name="simple-cli"></a>
|
||||
<a name="simple-cli"></a>
|
||||
## Create CLIs in Python
|
||||
|
||||
It's dead simple. Simply write the functionality you want exposed at the command
|
||||
line as a function / module / class, and then call Fire. With this addition of a
|
||||
single-line call to Fire, your CLI is ready to go.
|
||||
|
||||
|
||||
## Python Fire is a helpful tool for developing and debugging Python code. <a name="debugging"></a>
|
||||
<a name="debugging"></a>
|
||||
## Develop and debug Python code
|
||||
|
||||
When you're writing a Python library, you probably want to try it out as you go.
|
||||
You could write a main method to check the functionality you're interested in,
|
||||
@@ -23,8 +24,8 @@ a main method. And if you use the `--interactive` flag to enter an IPython REPL
|
||||
then you don't need to load the imports or create your variables; they'll
|
||||
already be ready for use as soon as you start the REPL.
|
||||
|
||||
|
||||
## Python Fire helps with exploring existing code or turning other people's code into a CLI. <a name="exploring"></a>
|
||||
<a name="exploring"></a>
|
||||
## Explore existing code; turn other people's code into a CLI
|
||||
|
||||
You can take an existing module, maybe even one that you don't have access to
|
||||
the source code for, and call `Fire` on it. This lets you easily see what
|
||||
@@ -40,8 +41,8 @@ The auto-generated help strings that Fire provides when you run a Fire CLI
|
||||
allow you to see all the functionality these modules provide in a concise
|
||||
manner.
|
||||
|
||||
|
||||
## Python Fire makes transitioning between Bash and Python easier. <a name="bash"></a>
|
||||
<a name="bash"></a>
|
||||
## Transition between Bash and Python
|
||||
|
||||
Using Fire lets you call Python directly from Bash. So you can mix your Python
|
||||
functions with the unix tools you know and love, like `grep`, `xargs`, `wc`,
|
||||
@@ -51,8 +52,8 @@ Additionally since writing CLIs in Python requires only a single call to Fire,
|
||||
it is now easy to write even one-off scripts that would previously have been in
|
||||
Bash, in Python.
|
||||
|
||||
|
||||
## Python Fire makes using a Python REPL easier by setting up the REPL with the modules and variables you'll need already imported and created. <a name="repl"></a>
|
||||
<a name="repl"></a>
|
||||
## Explore code in a Python REPL
|
||||
|
||||
When you use the `--interactive` flag to enter an IPython REPL, it starts with
|
||||
variables and modules already defined for you. You don't need to waste time
|
||||
@@ -461,13 +461,13 @@ class Building(object):
|
||||
|
||||
def __init__(self, name, stories=1):
|
||||
self.name = name
|
||||
self.stories = 1
|
||||
self.stories = stories
|
||||
|
||||
def climb_stairs(self, stairs_per_story=10):
|
||||
for story in range(self.stories):
|
||||
for stair in range(1, stairs_per_story):
|
||||
yield stair
|
||||
yield 'Phew!'
|
||||
yield 'Phew!'
|
||||
yield 'Done!'
|
||||
|
||||
if __name__ == '__main__':
|
||||
@@ -489,7 +489,7 @@ $ python example.py --name="Sherrerd Hall" climb_stairs --stairs-per-story 10
|
||||
$ python example.py climb-stairs --stairs-per-story 10 --name="Sherrerd Hall"
|
||||
```
|
||||
|
||||
You'll notice that hyphens and underscores (`-` and `_`) are interchangable in
|
||||
You'll notice that hyphens and underscores (`-` and `_`) are interchangeable in
|
||||
member names and flag names.
|
||||
|
||||
You'll also notice that the constructor's arguments can come after the
|
||||
@@ -699,12 +699,13 @@ The complete set of flags available is shown below, in the reference section.
|
||||
|
||||
| Using a CLI | Command | Notes
|
||||
| :------------- | :------------------------- | :---------
|
||||
| [Help](doc/using-cli.md#help-flag) | `command -- --help` |Show help and usage information for the command.
|
||||
| [REPL](doc/using-cli.md#interactive-flag) | `command -- --interactive` | Enter interactive mode.
|
||||
| [Separator](doc/using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
|
||||
| [Completion](doc/using-cli.md#completion-flag) | `command -- --completion` | Generate a completion script for the CLI.
|
||||
| [Trace](doc/using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
|
||||
| [Verbose](doc/using-cli.md#verbose-flag) | `command -- --verbose` | Include private members in the output.
|
||||
| [Help](using-cli.md#help-flag) | `command -- --help` | Show help and usage information for the command.
|
||||
| [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enter interactive mode.
|
||||
| [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
|
||||
| [Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI.
|
||||
| [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
|
||||
| [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` | Include private members in the output.
|
||||
|
||||
_Note that flags are separated from the Fire command by an isolated `--` arg._
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# 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)
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
To install Python Fire with pip, run: `pip install fire`
|
||||
|
||||
To install Python Fire with conda, run: `conda install fire -c conda-forge`
|
||||
|
||||
To install Python Fire from source, first clone the repository and then run:
|
||||
`python setup.py install`
|
||||
|
||||
|
||||
## Basic Usage
|
||||
|
||||
You can call `Fire` on any Python object:<br>
|
||||
functions, classes, modules, objects, dictionaries, lists, tuples, etc.
|
||||
They all work!
|
||||
|
||||
Here's an example of calling Fire on a class.
|
||||
|
||||
```python
|
||||
import fire
|
||||
|
||||
class Calculator(object):
|
||||
"""A simple calculator class."""
|
||||
|
||||
def double(self, number):
|
||||
return 2 * number
|
||||
|
||||
if __name__ == '__main__':
|
||||
fire.Fire(Calculator)
|
||||
```
|
||||
|
||||
Then, from the command line, you can run:
|
||||
|
||||
```bash
|
||||
python calculator.py double 10 # 20
|
||||
python calculator.py double --number=15 # 30
|
||||
```
|
||||
|
||||
To learn how Fire behaves on functions, objects, dicts, lists, etc, and to learn
|
||||
about Fire's other features, see the [Using a Fire CLI page](using-cli.md).
|
||||
|
||||
For additional examples, see [The Python Fire Guide](guide.md).
|
||||
|
||||
|
||||
## Why is it called Fire?
|
||||
|
||||
When you call `Fire`, it fires off (executes) your command.
|
||||
|
||||
|
||||
## Where can I learn more?
|
||||
|
||||
Please see [The Python Fire Guide](guide.md).
|
||||
|
||||
|
||||
## Reference
|
||||
|
||||
| Setup | Command | Notes
|
||||
| :------ | :------------------ | :---------
|
||||
| install | `pip install fire` |
|
||||
|
||||
| Creating a CLI | Command | Notes
|
||||
| :--------------| :--------------------- | :---------
|
||||
| import | `import fire` |
|
||||
| Call | `fire.Fire()` | Turns the current module into a Fire CLI.
|
||||
| Call | `fire.Fire(component)` | Turns `component` into a Fire CLI.
|
||||
|
||||
| Using a CLI | Command | Notes
|
||||
| :------------- | :------------------------- | :---------
|
||||
| [Help](using-cli.md#help-flag) | `command -- --help` |
|
||||
| [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
|
||||
| [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
|
||||
| [Completion](using-cli.md#completion-flag) | `command -- --completion [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._
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the
|
||||
[Apache 2.0](https://github.com/google/python-fire/blob/master/LICENSE) License.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This is not an official Google product.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Installation
|
||||
|
||||
To install Python Fire with pip, run: `pip install fire`
|
||||
|
||||
To install Python Fire with conda, run: `conda install fire -c conda-forge`
|
||||
|
||||
To install Python Fire from source, first clone the repository and then run:
|
||||
`python setup.py install`
|
||||
@@ -0,0 +1,13 @@
|
||||
# Troubleshooting
|
||||
|
||||
This page describes known issues that users of Python Fire have run into. If you
|
||||
have an issue not resolved here, consider opening a
|
||||
[GitHub Issue](https://github.com/google/python-fire/issues).
|
||||
|
||||
### Issue [#19](https://github.com/google/python-fire/issues/19): Don't name your module "cmd"
|
||||
|
||||
If you have a module name that conflicts with the name of a builtin module, then
|
||||
when Fire goes to import the builtin module, it will import your module instead.
|
||||
This will result in an error, possibly an `AttributeError`. Specifically, do not
|
||||
name your module any of the following:
|
||||
sys, linecache, cmd, bdb, repr, os, re, pprint, traceback
|
||||
@@ -1,7 +1,5 @@
|
||||
# Using a Fire CLI
|
||||
|
||||
|
||||
|
||||
## Basic usage
|
||||
|
||||
Every Fire command corresponds to a Python component.
|
||||
@@ -142,6 +140,9 @@ Call `widget -- --completion` to generate a completion script for the Fire CLI
|
||||
run `widget -- --completion > ~/.widget-completion`. You should then source this
|
||||
file; to get permanent completion, source this file from your .bashrc file.
|
||||
|
||||
Call `widget -- --completion fish` to generate a completion script for the Fish
|
||||
shell. Source this file from your fish.config.
|
||||
|
||||
If the commands available in the Fire CLI change, you'll have to regenerate the
|
||||
completion script and source it again.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -74,8 +74,10 @@ class DiffLibWrapper(object):
|
||||
|
||||
self.fromdate = time.ctime(os.stat(fromfile).st_mtime)
|
||||
self.todate = time.ctime(os.stat(tofile).st_mtime)
|
||||
self.fromlines = open(fromfile, 'U').readlines()
|
||||
self.tolines = open(tofile, 'U').readlines()
|
||||
with open(fromfile) as f:
|
||||
self.fromlines = f.readlines()
|
||||
with open(tofile) as f:
|
||||
self.tolines = f.readlines()
|
||||
|
||||
def unified_diff(self, lines=3):
|
||||
return difflib.unified_diff(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
+292
-43
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -18,19 +18,21 @@ from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
from collections import defaultdict
|
||||
from copy import copy
|
||||
import collections
|
||||
import copy
|
||||
import inspect
|
||||
|
||||
from fire import inspectutils
|
||||
import six
|
||||
|
||||
|
||||
def Script(name, component, default_options=None):
|
||||
return _Script(name, _Commands(component), default_options)
|
||||
def Script(name, component, default_options=None, shell='bash'):
|
||||
if shell == 'fish':
|
||||
return _FishScript(name, _Commands(component), default_options)
|
||||
return _BashScript(name, _Commands(component), default_options)
|
||||
|
||||
|
||||
def _Script(name, commands, default_options=None):
|
||||
def _BashScript(name, commands, default_options=None):
|
||||
"""Returns a Bash script registering a completion function for the commands.
|
||||
|
||||
Args:
|
||||
@@ -45,12 +47,9 @@ def _Script(name, commands, default_options=None):
|
||||
completion in Bash.
|
||||
"""
|
||||
default_options = default_options or set()
|
||||
options_map = defaultdict(lambda: copy(default_options))
|
||||
for command in commands:
|
||||
start = (name + ' ' + ' '.join(command[:-1])).strip()
|
||||
completion = _FormatForCommand(command[-1])
|
||||
options_map[start].add(completion)
|
||||
options_map[start.replace('_', '-')].add(completion)
|
||||
global_options, options_map, subcommands_map = _GetMaps(
|
||||
name, commands, default_options
|
||||
)
|
||||
|
||||
bash_completion_template = """# bash completion support for {name}
|
||||
# DO NOT EDIT.
|
||||
@@ -58,46 +57,250 @@ def _Script(name, commands, default_options=None):
|
||||
|
||||
_complete-{identifier}()
|
||||
{{
|
||||
local start cur opts
|
||||
local cur prev opts lastcommand
|
||||
COMPREPLY=()
|
||||
start="${{COMP_WORDS[@]:0:COMP_CWORD}}"
|
||||
prev="${{COMP_WORDS[COMP_CWORD-1]}}"
|
||||
cur="${{COMP_WORDS[COMP_CWORD]}}"
|
||||
lastcommand=$(get_lastcommand)
|
||||
|
||||
opts="{default_options}"
|
||||
GLOBAL_OPTIONS="{global_options}"
|
||||
|
||||
{start_checks}
|
||||
{checks}
|
||||
|
||||
COMPREPLY=( $(compgen -W "${{opts}}" -- ${{cur}}) )
|
||||
return 0
|
||||
}}
|
||||
|
||||
get_lastcommand()
|
||||
{{
|
||||
local lastcommand i
|
||||
|
||||
lastcommand=
|
||||
for ((i=0; i < ${{#COMP_WORDS[@]}}; ++i)); do
|
||||
if [[ ${{COMP_WORDS[i]}} != -* ]] && [[ -n ${{COMP_WORDS[i]}} ]] && [[
|
||||
${{COMP_WORDS[i]}} != $cur ]]; then
|
||||
lastcommand=${{COMP_WORDS[i]}}
|
||||
fi
|
||||
done
|
||||
|
||||
echo $lastcommand
|
||||
}}
|
||||
|
||||
filter_options()
|
||||
{{
|
||||
local opts
|
||||
opts=""
|
||||
for opt in "$@"
|
||||
do
|
||||
if ! option_already_entered $opt; then
|
||||
opts="$opts $opt"
|
||||
fi
|
||||
done
|
||||
|
||||
echo $opts
|
||||
}}
|
||||
|
||||
option_already_entered()
|
||||
{{
|
||||
local opt
|
||||
for opt in ${{COMP_WORDS[@]:0:COMP_CWORD}}
|
||||
do
|
||||
if [ $1 == $opt ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}}
|
||||
|
||||
is_prev_global()
|
||||
{{
|
||||
local opt
|
||||
for opt in $GLOBAL_OPTIONS
|
||||
do
|
||||
if [ $opt == $prev ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}}
|
||||
|
||||
complete -F _complete-{identifier} {command}
|
||||
"""
|
||||
start_check_template = """
|
||||
if [[ "$start" == "{start}" ]] ; then
|
||||
opts="{completions}"
|
||||
fi"""
|
||||
|
||||
start_checks = '\n'.join(
|
||||
start_check_template.format(
|
||||
start=start,
|
||||
completions=' '.join(sorted(options_map[start]))
|
||||
)
|
||||
for start in options_map
|
||||
check_wrapper = """
|
||||
case "${{lastcommand}}" in
|
||||
{lastcommand_checks}
|
||||
esac"""
|
||||
|
||||
lastcommand_check_template = """
|
||||
{command})
|
||||
{opts_assignment}
|
||||
opts=$(filter_options $opts)
|
||||
;;"""
|
||||
|
||||
opts_assignment_subcommand_template = """
|
||||
if is_prev_global; then
|
||||
opts="${{GLOBAL_OPTIONS}}"
|
||||
else
|
||||
opts="{options} ${{GLOBAL_OPTIONS}}"
|
||||
fi"""
|
||||
|
||||
opts_assignment_main_command_template = """
|
||||
opts="{options} ${{GLOBAL_OPTIONS}}" """
|
||||
|
||||
def _GetOptsAssignmentTemplate(command):
|
||||
if command == name:
|
||||
return opts_assignment_main_command_template
|
||||
else:
|
||||
return opts_assignment_subcommand_template
|
||||
|
||||
lines = []
|
||||
for command in set(subcommands_map.keys()).union(set(options_map.keys())):
|
||||
opts_assignment = _GetOptsAssignmentTemplate(command).format(
|
||||
options=' '.join(
|
||||
sorted(options_map[command].union(subcommands_map[command]))
|
||||
),
|
||||
)
|
||||
lines.append(
|
||||
lastcommand_check_template.format(
|
||||
command=command,
|
||||
opts_assignment=opts_assignment)
|
||||
)
|
||||
lastcommand_checks = '\n'.join(lines)
|
||||
|
||||
checks = check_wrapper.format(
|
||||
lastcommand_checks=lastcommand_checks,
|
||||
)
|
||||
|
||||
return (
|
||||
bash_completion_template.format(
|
||||
name=name,
|
||||
command=name,
|
||||
start_checks=start_checks,
|
||||
checks=checks,
|
||||
default_options=' '.join(default_options),
|
||||
identifier=name.replace('/', '').replace('.', '').replace(',', '')
|
||||
identifier=name.replace('/', '').replace('.', '').replace(',', ''),
|
||||
global_options=' '.join(global_options),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _FishScript(name, commands, default_options=None):
|
||||
"""Returns a Fish script registering a completion function for the commands.
|
||||
|
||||
Args:
|
||||
name: The first token in the commands, also the name of the command.
|
||||
commands: A list of all possible commands that tab completion can complete
|
||||
to. Each command is a list or tuple of the string tokens that make up
|
||||
that command.
|
||||
default_options: A dict of options that can be used with any command. Use
|
||||
this if there are flags that can always be appended to a command.
|
||||
Returns:
|
||||
A string which is the Fish script. Source the fish script to enable tab
|
||||
completion in Fish.
|
||||
"""
|
||||
default_options = default_options or set()
|
||||
global_options, options_map, subcommands_map = _GetMaps(
|
||||
name, commands, default_options
|
||||
)
|
||||
|
||||
fish_source = """function __fish_using_command
|
||||
set cmd (commandline -opc)
|
||||
for i in (seq (count $cmd) 1)
|
||||
switch $cmd[$i]
|
||||
case "-*"
|
||||
case "*"
|
||||
if [ $cmd[$i] = $argv[1] ]
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
end
|
||||
end
|
||||
end
|
||||
return 1
|
||||
end
|
||||
|
||||
function __option_entered_check
|
||||
set cmd (commandline -opc)
|
||||
for i in (seq (count $cmd))
|
||||
switch $cmd[$i]
|
||||
case "-*"
|
||||
if [ $cmd[$i] = $argv[1] ]
|
||||
return 1
|
||||
end
|
||||
end
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
function __is_prev_global
|
||||
set cmd (commandline -opc)
|
||||
set global_options {global_options}
|
||||
set prev (count $cmd)
|
||||
|
||||
for opt in $global_options
|
||||
if [ "--$opt" = $cmd[$prev] ]
|
||||
echo $prev
|
||||
return 0
|
||||
end
|
||||
end
|
||||
return 1
|
||||
end
|
||||
|
||||
"""
|
||||
|
||||
subcommand_template = ("complete -c {name} -n '__fish_using_command "
|
||||
"{command}' -f -a {subcommand}\n")
|
||||
flag_template = ("complete -c {name} -n "
|
||||
"'__fish_using_command {command};{prev_global_check} and "
|
||||
"__option_entered_check --{option}' -l {option}\n")
|
||||
|
||||
prev_global_check = ' and __is_prev_global;'
|
||||
for command in set(subcommands_map.keys()).union(set(options_map.keys())):
|
||||
for subcommand in subcommands_map[command]:
|
||||
fish_source += subcommand_template.format(
|
||||
name=name,
|
||||
command=command,
|
||||
subcommand=subcommand,
|
||||
)
|
||||
|
||||
for option in options_map[command].union(global_options):
|
||||
check_needed = command != name
|
||||
fish_source += flag_template.format(
|
||||
name=name,
|
||||
command=command,
|
||||
prev_global_check=prev_global_check if check_needed else '',
|
||||
option=option.lstrip('--'),
|
||||
)
|
||||
|
||||
return fish_source.format(
|
||||
global_options=' '.join(
|
||||
'"{option}"'.format(option=option)
|
||||
for option in global_options
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _IncludeMember(name, verbose):
|
||||
"""Returns whether a member should be included in auto-completion or help.
|
||||
|
||||
Determines whether a member of an object with the specified name should be
|
||||
included in auto-completion or help text(both usage and detailed help).
|
||||
|
||||
If the member starts with '__', it will always be excluded. If the member
|
||||
starts with only one '_', it will be included for all non-string types. If
|
||||
verbose is True, the members, including the private members, are always
|
||||
included.
|
||||
|
||||
Args:
|
||||
name: The name of the member.
|
||||
verbose: Whether to include private members.
|
||||
Returns
|
||||
A boolean value indicating whether the member should be included.
|
||||
|
||||
"""
|
||||
if isinstance(name, six.string_types) and name[:2] == '__':
|
||||
return False
|
||||
if verbose:
|
||||
return True
|
||||
if isinstance(name, six.string_types):
|
||||
@@ -160,18 +363,17 @@ def Completions(component, verbose=False):
|
||||
spec = inspectutils.GetFullArgSpec(component)
|
||||
return _CompletionsFromArgs(spec.args + spec.kwonlyargs)
|
||||
|
||||
elif isinstance(component, (tuple, list)):
|
||||
if isinstance(component, (tuple, list)):
|
||||
return [str(index) for index in range(len(component))]
|
||||
|
||||
elif inspect.isgenerator(component):
|
||||
# TODO: There are currently no commands available for generators.
|
||||
if inspect.isgenerator(component):
|
||||
# TODO(dbieber): There are currently no commands available for generators.
|
||||
return []
|
||||
|
||||
else:
|
||||
return [
|
||||
_FormatForCommand(member_name)
|
||||
for member_name, unused_member in _Members(component, verbose)
|
||||
]
|
||||
return [
|
||||
_FormatForCommand(member_name)
|
||||
for member_name, unused_member in _Members(component, verbose)
|
||||
]
|
||||
|
||||
|
||||
def _FormatForCommand(token):
|
||||
@@ -192,8 +394,8 @@ def _FormatForCommand(token):
|
||||
|
||||
if token.startswith('_'):
|
||||
return token
|
||||
else:
|
||||
return token.replace('_', '-')
|
||||
|
||||
return token.replace('_', '-')
|
||||
|
||||
|
||||
def _Commands(component, depth=3):
|
||||
@@ -210,19 +412,66 @@ def _Commands(component, depth=3):
|
||||
Tuples, each tuple representing one possible command for this CLI.
|
||||
Only traverses the member DAG up to a depth of depth.
|
||||
"""
|
||||
if inspect.isroutine(component) or inspect.isclass(component):
|
||||
for completion in Completions(component):
|
||||
yield (completion,)
|
||||
if inspect.isroutine(component):
|
||||
return # Don't descend into routines.
|
||||
|
||||
if depth < 1:
|
||||
return
|
||||
|
||||
for member_name, member in _Members(component):
|
||||
# TODO: Also skip components we've already seen.
|
||||
# TODO(dbieber): Also skip components we've already seen.
|
||||
member_name = _FormatForCommand(member_name)
|
||||
|
||||
yield (member_name,)
|
||||
|
||||
if inspect.isroutine(member) or inspect.isclass(member):
|
||||
for completion in Completions(member):
|
||||
yield (member_name, completion)
|
||||
continue # Don't descend into routines.
|
||||
|
||||
for command in _Commands(member, depth - 1):
|
||||
yield (member_name,) + command
|
||||
|
||||
|
||||
def _IsOption(arg):
|
||||
return arg.startswith('-')
|
||||
|
||||
|
||||
def _GetMaps(name, commands, default_options):
|
||||
"""Returns sets of subcommands and options for each command.
|
||||
|
||||
Args:
|
||||
name: The first token in the commands, also the name of the command.
|
||||
commands: A list of all possible commands that tab completion can complete
|
||||
to. Each command is a list or tuple of the string tokens that make up
|
||||
that command.
|
||||
default_options: A dict of options that can be used with any command. Use
|
||||
this if there are flags that can always be appended to a command.
|
||||
Returns:
|
||||
global_options: A set of all options of the first token of the command.
|
||||
subcommands_map: A dict storing set of subcommands for each
|
||||
command/subcommand.
|
||||
options_map: A dict storing set of options for each subcommand.
|
||||
"""
|
||||
global_options = copy.copy(default_options)
|
||||
options_map = collections.defaultdict(lambda: copy.copy(default_options))
|
||||
subcommands_map = collections.defaultdict(set)
|
||||
|
||||
for command in commands:
|
||||
if len(command) == 1:
|
||||
if _IsOption(command[0]):
|
||||
global_options.add(command[0])
|
||||
else:
|
||||
subcommands_map[name].add(command[0])
|
||||
|
||||
elif command:
|
||||
subcommand = command[-2]
|
||||
arg = _FormatForCommand(command[-1])
|
||||
|
||||
if _IsOption(arg):
|
||||
args_map = options_map
|
||||
else:
|
||||
args_map = subcommands_map
|
||||
|
||||
args_map[subcommand].add(arg)
|
||||
args_map[subcommand.replace('_', '-')].add(arg)
|
||||
|
||||
return global_options, options_map, subcommands_map
|
||||
|
||||
+60
-6
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -25,18 +25,34 @@ from fire import testutils
|
||||
|
||||
class TabCompletionTest(testutils.BaseTestCase):
|
||||
|
||||
def testCompletionScript(self):
|
||||
# A sanity check test to make sure the completion script satisfies some
|
||||
# basic assumptions.
|
||||
def testCompletionBashScript(self):
|
||||
# A sanity check test to make sure the bash completion script satisfies
|
||||
# some basic assumptions.
|
||||
commands = [
|
||||
['run'],
|
||||
['halt'],
|
||||
['halt', '--now'],
|
||||
]
|
||||
script = completion._Script(name='command', commands=commands) # pylint: disable=protected-access
|
||||
script = completion._BashScript(name='command', commands=commands) # pylint: disable=protected-access
|
||||
self.assertIn('command', script)
|
||||
self.assertIn('halt', script)
|
||||
self.assertIn('"$start" == "command"', script)
|
||||
|
||||
assert_template = '{command})'
|
||||
for last_command in ['command', 'halt']:
|
||||
self.assertIn(assert_template.format(command=last_command), script)
|
||||
|
||||
def testCompletionFishScript(self):
|
||||
# A sanity check test to make sure the fish completion script satisfies
|
||||
# some basic assumptions.
|
||||
commands = [
|
||||
['run'],
|
||||
['halt'],
|
||||
['halt', '--now'],
|
||||
]
|
||||
script = completion._FishScript(name='command', commands=commands) # pylint: disable=protected-access
|
||||
self.assertIn('command', script)
|
||||
self.assertIn('halt', script)
|
||||
self.assertIn('-l now', script)
|
||||
|
||||
def testFnCompletions(self):
|
||||
def example(one, two, three):
|
||||
@@ -98,6 +114,44 @@ class TabCompletionTest(testutils.BaseTestCase):
|
||||
self.assertIn('level3', script)
|
||||
self.assertNotIn('level4', script) # The default depth is 3.
|
||||
|
||||
def testFnScript(self):
|
||||
script = completion.Script('identity', tc.identity)
|
||||
self.assertIn('--arg1', script)
|
||||
self.assertIn('--arg2', script)
|
||||
self.assertIn('--arg3', script)
|
||||
self.assertIn('--arg4', script)
|
||||
|
||||
def testClassScript(self):
|
||||
script = completion.Script('', tc.MixedDefaults)
|
||||
self.assertIn('ten', script)
|
||||
self.assertIn('sum', script)
|
||||
self.assertIn('identity', script)
|
||||
self.assertIn('--alpha', script)
|
||||
self.assertIn('--beta', script)
|
||||
|
||||
def testDeepDictFishScript(self):
|
||||
deepdict = {'level1': {'level2': {'level3': {'level4': {}}}}}
|
||||
script = completion.Script('deepdict', deepdict, shell='fish')
|
||||
self.assertIn('level1', script)
|
||||
self.assertIn('level2', script)
|
||||
self.assertIn('level3', script)
|
||||
self.assertNotIn('level4', script) # The default depth is 3.
|
||||
|
||||
def testFnFishScript(self):
|
||||
script = completion.Script('identity', tc.identity, shell='fish')
|
||||
self.assertIn('arg1', script)
|
||||
self.assertIn('arg2', script)
|
||||
self.assertIn('arg3', script)
|
||||
self.assertIn('arg4', script)
|
||||
|
||||
def testClassFishScript(self):
|
||||
script = completion.Script('', tc.MixedDefaults, shell='fish')
|
||||
self.assertIn('ten', script)
|
||||
self.assertIn('sum', script)
|
||||
self.assertIn('identity', script)
|
||||
self.assertIn('alpha', script)
|
||||
self.assertIn('beta', script)
|
||||
|
||||
def testNonStringDictCompletions(self):
|
||||
completions = completion.Completions({
|
||||
10: 'green',
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
This is the console package from googlecloudsdk, as used by Python Fire.
|
||||
Python Fire does not accept pull requests modifying the console package; rather,
|
||||
changes to console should go through the upstream project googlecloudsdk.
|
||||
@@ -0,0 +1,784 @@
|
||||
# -*- coding: utf-8 -*- #
|
||||
|
||||
# Copyright 2015 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
r"""A module for console attributes, special characters and functions.
|
||||
|
||||
The target architectures {linux, macos, windows} support inline encoding for
|
||||
all attributes except color. Windows requires win32 calls to manipulate the
|
||||
console color state.
|
||||
|
||||
Usage:
|
||||
|
||||
# Get the console attribute state.
|
||||
out = log.out
|
||||
con = console_attr.GetConsoleAttr(out=out)
|
||||
|
||||
# Get the ISO 8879:1986//ENTITIES Box and Line Drawing characters.
|
||||
box = con.GetBoxLineCharacters()
|
||||
# Print an X inside a box.
|
||||
out.write(box.dr)
|
||||
out.write(box.h)
|
||||
out.write(box.dl)
|
||||
out.write('\n')
|
||||
out.write(box.v)
|
||||
out.write('X')
|
||||
out.write(box.v)
|
||||
out.write('\n')
|
||||
out.write(box.ur)
|
||||
out.write(box.h)
|
||||
out.write(box.ul)
|
||||
out.write('\n')
|
||||
|
||||
# Print the bullet characters.
|
||||
for c in con.GetBullets():
|
||||
out.write(c)
|
||||
out.write('\n')
|
||||
|
||||
# Print FAIL in red.
|
||||
out.write('Epic ')
|
||||
con.Colorize('FAIL', 'red')
|
||||
out.write(', my first.')
|
||||
|
||||
# Print italic and bold text.
|
||||
bold = con.GetFontCode(bold=True)
|
||||
italic = con.GetFontCode(italic=True)
|
||||
normal = con.GetFontCode()
|
||||
out.write('This is {bold}bold{normal}, this is {italic}italic{normal},'
|
||||
' and this is normal.\n'.format(bold=bold, italic=italic,
|
||||
normal=normal))
|
||||
|
||||
# Read one character from stdin with echo disabled.
|
||||
c = con.GetRawKey()
|
||||
if c is None:
|
||||
print 'EOF\n'
|
||||
|
||||
# Return the display width of a string that may contain FontCode() chars.
|
||||
display_width = con.DisplayWidth(string)
|
||||
|
||||
# Reset the memoized state.
|
||||
con = console_attr.ResetConsoleAttr()
|
||||
|
||||
# Print the console width and height in characters.
|
||||
width, height = con.GetTermSize()
|
||||
print 'width={width}, height={height}'.format(width=width, height=height)
|
||||
|
||||
# Colorize table data cells.
|
||||
fail = console_attr.Colorizer('FAIL', 'red')
|
||||
pass = console_attr.Colorizer('PASS', 'green')
|
||||
cells = ['label', fail, 'more text', pass, 'end']
|
||||
for cell in cells;
|
||||
if isinstance(cell, console_attr.Colorizer):
|
||||
cell.Render()
|
||||
else:
|
||||
out.write(cell)
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unicodedata
|
||||
|
||||
from fire.console import console_attr_os
|
||||
from fire.console import encoding as encoding_util
|
||||
|
||||
import six
|
||||
|
||||
|
||||
class BoxLineCharacters(object):
|
||||
"""Box/line drawing characters.
|
||||
|
||||
The element names are from ISO 8879:1986//ENTITIES Box and Line Drawing//EN:
|
||||
http://www.w3.org/2003/entities/iso8879doc/isobox.html
|
||||
"""
|
||||
|
||||
|
||||
class BoxLineCharactersUnicode(BoxLineCharacters):
|
||||
"""unicode Box/line drawing characters (cp437 compatible unicode)."""
|
||||
dl = '┐'
|
||||
dr = '┌'
|
||||
h = '─'
|
||||
hd = '┬'
|
||||
hu = '┴'
|
||||
ul = '┘'
|
||||
ur = '└'
|
||||
v = '│'
|
||||
vh = '┼'
|
||||
vl = '┤'
|
||||
vr = '├'
|
||||
d_dl = '╗'
|
||||
d_dr = '╔'
|
||||
d_h = '═'
|
||||
d_hd = '╦'
|
||||
d_hu = '╩'
|
||||
d_ul = '╝'
|
||||
d_ur = '╚'
|
||||
d_v = '║'
|
||||
d_vh = '╬'
|
||||
d_vl = '╣'
|
||||
d_vr = '╠'
|
||||
|
||||
|
||||
class BoxLineCharactersAscii(BoxLineCharacters):
|
||||
"""ASCII Box/line drawing characters."""
|
||||
dl = '+'
|
||||
dr = '+'
|
||||
h = '-'
|
||||
hd = '+'
|
||||
hu = '+'
|
||||
ul = '+'
|
||||
ur = '+'
|
||||
v = '|'
|
||||
vh = '+'
|
||||
vl = '+'
|
||||
vr = '+'
|
||||
d_dl = '#'
|
||||
d_dr = '#'
|
||||
d_h = '='
|
||||
d_hd = '#'
|
||||
d_hu = '#'
|
||||
d_ul = '#'
|
||||
d_ur = '#'
|
||||
d_v = '#'
|
||||
d_vh = '#'
|
||||
d_vl = '#'
|
||||
d_vr = '#'
|
||||
|
||||
|
||||
class ProgressTrackerSymbols(object):
|
||||
"""Characters used by progress trackers."""
|
||||
|
||||
|
||||
class ProgressTrackerSymbolsUnicode(ProgressTrackerSymbols):
|
||||
"""Characters used by progress trackers."""
|
||||
|
||||
@property
|
||||
def spin_marks(self):
|
||||
return ['⠏', '⠛', '⠹', '⠼', '⠶', '⠧']
|
||||
|
||||
success = '✓'
|
||||
failed = 'X'
|
||||
interrupted = '-'
|
||||
not_started = '.'
|
||||
prefix_length = 2
|
||||
|
||||
|
||||
class ProgressTrackerSymbolsAscii(ProgressTrackerSymbols):
|
||||
"""Characters used by progress trackers."""
|
||||
|
||||
@property
|
||||
def spin_marks(self):
|
||||
return ['|', '/', '-', '\\',]
|
||||
|
||||
success = 'OK'
|
||||
failed = 'X'
|
||||
interrupted = '-'
|
||||
not_started = '.'
|
||||
prefix_length = 3
|
||||
|
||||
|
||||
class ConsoleAttr(object):
|
||||
"""Console attribute and special drawing characters and functions accessor.
|
||||
|
||||
Use GetConsoleAttr() to get a global ConsoleAttr object shared by all callers.
|
||||
Use ConsoleAttr() for abstracting multiple consoles.
|
||||
|
||||
If _out is not associated with a console, or if the console properties cannot
|
||||
be determined, the default behavior is ASCII art with no attributes.
|
||||
|
||||
Attributes:
|
||||
_ANSI_COLOR: The ANSI color control sequence dict.
|
||||
_ANSI_COLOR_RESET: The ANSI color reset control sequence string.
|
||||
_csi: The ANSI Control Sequence indicator string, '' if not supported.
|
||||
_encoding: The character encoding.
|
||||
ascii: ASCII art. This is the default.
|
||||
utf8: UTF-8 unicode.
|
||||
win: Windows code page 437.
|
||||
_font_bold: The ANSI bold font embellishment code string.
|
||||
_font_italic: The ANSI italic font embellishment code string.
|
||||
_get_raw_key: A function that reads one keypress from stdin with no echo.
|
||||
_out: The console output file stream.
|
||||
_term: TERM environment variable value.
|
||||
_term_size: The terminal (x, y) dimensions in characters.
|
||||
"""
|
||||
|
||||
_CONSOLE_ATTR_STATE = None
|
||||
|
||||
_ANSI_COLOR = {
|
||||
'red': '31;1m',
|
||||
'yellow': '33;1m',
|
||||
'green': '32m',
|
||||
'blue': '34;1m'
|
||||
}
|
||||
_ANSI_COLOR_RESET = '39;0m'
|
||||
|
||||
_BULLETS_UNICODE = ('▪', '◆', '▸', '▫', '◇', '▹')
|
||||
_BULLETS_WINDOWS = ('■', '≡', '∞', 'Φ', '·') # cp437 compatible unicode
|
||||
_BULLETS_ASCII = ('o', '*', '+', '-')
|
||||
|
||||
def __init__(self, encoding=None):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
encoding: Encoding override.
|
||||
ascii -- ASCII art. This is the default.
|
||||
utf8 -- UTF-8 unicode.
|
||||
win -- Windows code page 437.
|
||||
"""
|
||||
# Normalize the encoding name.
|
||||
if not encoding:
|
||||
encoding = self._GetConsoleEncoding()
|
||||
elif encoding == 'win':
|
||||
encoding = 'cp437'
|
||||
self._encoding = encoding or 'ascii'
|
||||
self._term = os.getenv('TERM', '').lower()
|
||||
|
||||
# ANSI "standard" attributes.
|
||||
if self.SupportsAnsi():
|
||||
# Select Graphic Rendition paramaters from
|
||||
# http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
|
||||
# Italic '3' would be nice here but its not widely supported.
|
||||
self._csi = '\x1b['
|
||||
self._font_bold = '1'
|
||||
self._font_italic = '4'
|
||||
else:
|
||||
self._csi = None
|
||||
self._font_bold = ''
|
||||
self._font_italic = ''
|
||||
|
||||
# Encoded character attributes.
|
||||
if self._encoding == 'utf8':
|
||||
self._box_line_characters = BoxLineCharactersUnicode()
|
||||
self._bullets = self._BULLETS_UNICODE
|
||||
self._progress_tracker_symbols = ProgressTrackerSymbolsUnicode()
|
||||
elif self._encoding == 'cp437':
|
||||
self._box_line_characters = BoxLineCharactersUnicode()
|
||||
self._bullets = self._BULLETS_WINDOWS
|
||||
# Windows does not suport the unicode characters used for the spinner.
|
||||
self._progress_tracker_symbols = ProgressTrackerSymbolsAscii()
|
||||
else:
|
||||
self._box_line_characters = BoxLineCharactersAscii()
|
||||
self._bullets = self._BULLETS_ASCII
|
||||
self._progress_tracker_symbols = ProgressTrackerSymbolsAscii()
|
||||
|
||||
# OS specific attributes.
|
||||
self._get_raw_key = [console_attr_os.GetRawKeyFunction()]
|
||||
self._term_size = console_attr_os.GetTermSize()
|
||||
|
||||
self._display_width_cache = {}
|
||||
|
||||
def _GetConsoleEncoding(self):
|
||||
"""Gets the encoding as declared by the stdout stream.
|
||||
|
||||
Returns:
|
||||
str, The encoding name or None if it could not be determined.
|
||||
"""
|
||||
console_encoding = getattr(sys.stdout, 'encoding', None)
|
||||
if not console_encoding:
|
||||
return None
|
||||
console_encoding = console_encoding.lower()
|
||||
if 'utf-8' in console_encoding:
|
||||
return 'utf8'
|
||||
elif 'cp437' in console_encoding:
|
||||
return 'cp437'
|
||||
return None
|
||||
|
||||
def Colorize(self, string, color, justify=None):
|
||||
"""Generates a colorized string, optionally justified.
|
||||
|
||||
Args:
|
||||
string: The string to write.
|
||||
color: The color name -- must be in _ANSI_COLOR.
|
||||
justify: The justification function, no justification if None. For
|
||||
example, justify=lambda s: s.center(10)
|
||||
|
||||
Returns:
|
||||
str, The colorized string that can be printed to the console.
|
||||
"""
|
||||
if justify:
|
||||
string = justify(string)
|
||||
if self._csi and color in self._ANSI_COLOR:
|
||||
return '{csi}{color_code}{string}{csi}{reset_code}'.format(
|
||||
csi=self._csi,
|
||||
color_code=self._ANSI_COLOR[color],
|
||||
reset_code=self._ANSI_COLOR_RESET,
|
||||
string=string)
|
||||
# TODO: Add elif self._encoding == 'cp437': code here.
|
||||
return string
|
||||
|
||||
def ConvertOutputToUnicode(self, buf):
|
||||
"""Converts a console output string buf to unicode.
|
||||
|
||||
Mainly used for testing. Allows test comparisons in unicode while ensuring
|
||||
that unicode => encoding => unicode works.
|
||||
|
||||
Args:
|
||||
buf: The console output string to convert.
|
||||
|
||||
Returns:
|
||||
The console output string buf converted to unicode.
|
||||
"""
|
||||
if isinstance(buf, six.text_type):
|
||||
buf = buf.encode(self._encoding)
|
||||
return six.text_type(buf, self._encoding, 'replace')
|
||||
|
||||
def GetBoxLineCharacters(self):
|
||||
"""Returns the box/line drawing characters object.
|
||||
|
||||
The element names are from ISO 8879:1986//ENTITIES Box and Line Drawing//EN:
|
||||
http://www.w3.org/2003/entities/iso8879doc/isobox.html
|
||||
|
||||
Returns:
|
||||
A BoxLineCharacters object for the console output device.
|
||||
"""
|
||||
return self._box_line_characters
|
||||
|
||||
def GetBullets(self):
|
||||
"""Returns the bullet characters list.
|
||||
|
||||
Use the list elements in order for best appearance in nested bullet lists,
|
||||
wrapping back to the first element for deep nesting. The list size depends
|
||||
on the console implementation.
|
||||
|
||||
Returns:
|
||||
A tuple of bullet characters.
|
||||
"""
|
||||
return self._bullets
|
||||
|
||||
def GetProgressTrackerSymbols(self):
|
||||
"""Returns the progress tracker characters object.
|
||||
|
||||
Returns:
|
||||
A ProgressTrackerSymbols object for the console output device.
|
||||
"""
|
||||
return self._progress_tracker_symbols
|
||||
|
||||
def GetControlSequenceIndicator(self):
|
||||
"""Returns the control sequence indicator string.
|
||||
|
||||
Returns:
|
||||
The conrol sequence indicator string or None if control sequences are not
|
||||
supported.
|
||||
"""
|
||||
return self._csi
|
||||
|
||||
def GetControlSequenceLen(self, buf):
|
||||
"""Returns the control sequence length at the beginning of buf.
|
||||
|
||||
Used in display width computations. Control sequences have display width 0.
|
||||
|
||||
Args:
|
||||
buf: The string to check for a control sequence.
|
||||
|
||||
Returns:
|
||||
The conrol sequence length at the beginning of buf or 0 if buf does not
|
||||
start with a control sequence.
|
||||
"""
|
||||
if not self._csi or not buf.startswith(self._csi):
|
||||
return 0
|
||||
n = 0
|
||||
for c in buf:
|
||||
n += 1
|
||||
if c.isalpha():
|
||||
break
|
||||
return n
|
||||
|
||||
def GetEncoding(self):
|
||||
"""Returns the current encoding."""
|
||||
return self._encoding
|
||||
|
||||
def GetFontCode(self, bold=False, italic=False):
|
||||
"""Returns a font code string for 0 or more embellishments.
|
||||
|
||||
GetFontCode() with no args returns the default font code string.
|
||||
|
||||
Args:
|
||||
bold: True for bold embellishment.
|
||||
italic: True for italic embellishment.
|
||||
|
||||
Returns:
|
||||
The font code string for the requested embellishments. Write this string
|
||||
to the console output to control the font settings.
|
||||
"""
|
||||
if not self._csi:
|
||||
return ''
|
||||
codes = []
|
||||
if bold:
|
||||
codes.append(self._font_bold)
|
||||
if italic:
|
||||
codes.append(self._font_italic)
|
||||
return '{csi}{codes}m'.format(csi=self._csi, codes=';'.join(codes))
|
||||
|
||||
def GetRawKey(self):
|
||||
"""Reads one key press from stdin with no echo.
|
||||
|
||||
Returns:
|
||||
The key name, None for EOF, <KEY-*> for function keys, otherwise a
|
||||
character.
|
||||
"""
|
||||
return self._get_raw_key[0]()
|
||||
|
||||
def GetTermSize(self):
|
||||
"""Returns the terminal (x, y) dimensions in characters.
|
||||
|
||||
Returns:
|
||||
(x, y): A tuple of the terminal x and y dimensions.
|
||||
"""
|
||||
return self._term_size
|
||||
|
||||
def DisplayWidth(self, buf):
|
||||
"""Returns the display width of buf, handling unicode and ANSI controls.
|
||||
|
||||
Args:
|
||||
buf: The string to count from.
|
||||
|
||||
Returns:
|
||||
The display width of buf, handling unicode and ANSI controls.
|
||||
"""
|
||||
if not isinstance(buf, six.string_types):
|
||||
# Handle non-string objects like Colorizer().
|
||||
return len(buf)
|
||||
|
||||
cached = self._display_width_cache.get(buf, None)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
width = 0
|
||||
max_width = 0
|
||||
i = 0
|
||||
while i < len(buf):
|
||||
if self._csi and buf[i:].startswith(self._csi):
|
||||
i += self.GetControlSequenceLen(buf[i:])
|
||||
elif buf[i] == '\n':
|
||||
# A newline incidates the start of a new line.
|
||||
# Newline characters have 0 width.
|
||||
max_width = max(width, max_width)
|
||||
width = 0
|
||||
i += 1
|
||||
else:
|
||||
width += GetCharacterDisplayWidth(buf[i])
|
||||
i += 1
|
||||
max_width = max(width, max_width)
|
||||
|
||||
self._display_width_cache[buf] = max_width
|
||||
return max_width
|
||||
|
||||
def SplitIntoNormalAndControl(self, buf):
|
||||
"""Returns a list of (normal_string, control_sequence) tuples from buf.
|
||||
|
||||
Args:
|
||||
buf: The input string containing one or more control sequences
|
||||
interspersed with normal strings.
|
||||
|
||||
Returns:
|
||||
A list of (normal_string, control_sequence) tuples.
|
||||
"""
|
||||
if not self._csi or not buf:
|
||||
return [(buf, '')]
|
||||
seq = []
|
||||
i = 0
|
||||
while i < len(buf):
|
||||
c = buf.find(self._csi, i)
|
||||
if c < 0:
|
||||
seq.append((buf[i:], ''))
|
||||
break
|
||||
normal = buf[i:c]
|
||||
i = c + self.GetControlSequenceLen(buf[c:])
|
||||
seq.append((normal, buf[c:i]))
|
||||
return seq
|
||||
|
||||
def SplitLine(self, line, width):
|
||||
"""Splits line into width length chunks.
|
||||
|
||||
Args:
|
||||
line: The line to split.
|
||||
width: The width of each chunk except the last which could be smaller than
|
||||
width.
|
||||
|
||||
Returns:
|
||||
A list of chunks, all but the last with display width == width.
|
||||
"""
|
||||
lines = []
|
||||
chunk = ''
|
||||
w = 0
|
||||
keep = False
|
||||
for normal, control in self.SplitIntoNormalAndControl(line):
|
||||
keep = True
|
||||
while True:
|
||||
n = width - w
|
||||
w += len(normal)
|
||||
if w <= width:
|
||||
break
|
||||
lines.append(chunk + normal[:n])
|
||||
chunk = ''
|
||||
keep = False
|
||||
w = 0
|
||||
normal = normal[n:]
|
||||
chunk += normal + control
|
||||
if chunk or keep:
|
||||
lines.append(chunk)
|
||||
return lines
|
||||
|
||||
def SupportsAnsi(self):
|
||||
return (self._encoding != 'ascii' and
|
||||
('screen' in self._term or 'xterm' in self._term))
|
||||
|
||||
|
||||
class Colorizer(object):
|
||||
"""Resource string colorizer.
|
||||
|
||||
Attributes:
|
||||
_con: ConsoleAttr object.
|
||||
_color: Color name.
|
||||
_string: The string to colorize.
|
||||
_justify: The justification function, no justification if None. For example,
|
||||
justify=lambda s: s.center(10)
|
||||
"""
|
||||
|
||||
def __init__(self, string, color, justify=None):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
string: The string to colorize.
|
||||
color: Color name used to index ConsoleAttr._ANSI_COLOR.
|
||||
justify: The justification function, no justification if None. For
|
||||
example, justify=lambda s: s.center(10)
|
||||
"""
|
||||
self._con = GetConsoleAttr()
|
||||
self._color = color
|
||||
self._string = string
|
||||
self._justify = justify
|
||||
|
||||
def __eq__(self, other):
|
||||
return self._string == six.text_type(other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
def __gt__(self, other):
|
||||
return self._string > six.text_type(other)
|
||||
|
||||
def __lt__(self, other):
|
||||
return self._string < six.text_type(other)
|
||||
|
||||
def __ge__(self, other):
|
||||
return not self < other
|
||||
|
||||
def __le__(self, other):
|
||||
return not self > other
|
||||
|
||||
def __len__(self):
|
||||
return self._con.DisplayWidth(self._string)
|
||||
|
||||
def __str__(self):
|
||||
return self._string
|
||||
|
||||
def Render(self, stream, justify=None):
|
||||
"""Renders the string as self._color on the console.
|
||||
|
||||
Args:
|
||||
stream: The stream to render the string to. The stream given here *must*
|
||||
have the same encoding as sys.stdout for this to work properly.
|
||||
justify: The justification function, self._justify if None.
|
||||
"""
|
||||
stream.write(
|
||||
self._con.Colorize(self._string, self._color, justify or self._justify))
|
||||
|
||||
|
||||
def GetConsoleAttr(encoding=None, reset=False):
|
||||
"""Gets the console attribute state.
|
||||
|
||||
If this is the first call or reset is True or encoding is not None and does
|
||||
not match the current encoding or out is not None and does not match the
|
||||
current out then the state is (re)initialized. Otherwise the current state
|
||||
is returned.
|
||||
|
||||
This call associates the out file stream with the console. All console related
|
||||
output should go to the same stream.
|
||||
|
||||
Args:
|
||||
encoding: Encoding override.
|
||||
ascii -- ASCII. This is the default.
|
||||
utf8 -- UTF-8 unicode.
|
||||
win -- Windows code page 437.
|
||||
reset: Force re-initialization if True.
|
||||
|
||||
Returns:
|
||||
The global ConsoleAttr state object.
|
||||
"""
|
||||
attr = ConsoleAttr._CONSOLE_ATTR_STATE # pylint: disable=protected-access
|
||||
if not reset:
|
||||
if not attr:
|
||||
reset = True
|
||||
elif encoding and encoding != attr.GetEncoding():
|
||||
reset = True
|
||||
if reset:
|
||||
attr = ConsoleAttr(encoding=encoding)
|
||||
ConsoleAttr._CONSOLE_ATTR_STATE = attr # pylint: disable=protected-access
|
||||
return attr
|
||||
|
||||
|
||||
def ResetConsoleAttr(encoding=None):
|
||||
"""Resets the console attribute state to the console default.
|
||||
|
||||
Args:
|
||||
encoding: Reset to this encoding instead of the default.
|
||||
ascii -- ASCII. This is the default.
|
||||
utf8 -- UTF-8 unicode.
|
||||
win -- Windows code page 437.
|
||||
|
||||
Returns:
|
||||
The global ConsoleAttr state object.
|
||||
"""
|
||||
return GetConsoleAttr(encoding=encoding, reset=True)
|
||||
|
||||
|
||||
def GetCharacterDisplayWidth(char):
|
||||
"""Returns the monospaced terminal display width of char.
|
||||
|
||||
Assumptions:
|
||||
- monospaced display
|
||||
- ambiguous or unknown chars default to width 1
|
||||
- ASCII control char width is 1 => don't use this for control chars
|
||||
|
||||
Args:
|
||||
char: The character to determine the display width of.
|
||||
|
||||
Returns:
|
||||
The monospaced terminal display width of char: either 0, 1, or 2.
|
||||
"""
|
||||
if not isinstance(char, six.text_type):
|
||||
# Non-unicode chars have width 1. Don't use this function on control chars.
|
||||
return 1
|
||||
|
||||
# Normalize to avoid special cases.
|
||||
char = unicodedata.normalize('NFC', char)
|
||||
|
||||
if unicodedata.combining(char) != 0:
|
||||
# Modifies the previous character and does not move the cursor.
|
||||
return 0
|
||||
elif unicodedata.category(char) == 'Cf':
|
||||
# Unprintable formatting char.
|
||||
return 0
|
||||
elif unicodedata.east_asian_width(char) in 'FW':
|
||||
# Fullwidth or Wide chars take 2 character positions.
|
||||
return 2
|
||||
else:
|
||||
# Don't use this function on control chars.
|
||||
return 1
|
||||
|
||||
|
||||
def SafeText(data, encoding=None, escape=True):
|
||||
br"""Converts the data to a text string compatible with the given encoding.
|
||||
|
||||
This works the same way as Decode() below except it guarantees that any
|
||||
characters in the resulting text string can be re-encoded using the given
|
||||
encoding (or GetConsoleAttr().GetEncoding() if None is given). This means
|
||||
that the string will be safe to print to sys.stdout (for example) without
|
||||
getting codec exceptions if the user's terminal doesn't support the encoding
|
||||
used by the source of the text.
|
||||
|
||||
Args:
|
||||
data: Any bytes, string, or object that has str() or unicode() methods.
|
||||
encoding: The encoding name to ensure compatibility with. Defaults to
|
||||
GetConsoleAttr().GetEncoding().
|
||||
escape: Replace unencodable characters with a \uXXXX or \xXX equivalent if
|
||||
True. Otherwise replace unencodable characters with an appropriate unknown
|
||||
character, '?' for ASCII, and the unicode unknown replacement character
|
||||
\uFFFE for unicode.
|
||||
|
||||
Returns:
|
||||
A text string representation of the data, but modified to remove any
|
||||
characters that would result in an encoding exception with the target
|
||||
encoding. In the worst case, with escape=False, it will contain only ?
|
||||
characters.
|
||||
"""
|
||||
if data is None:
|
||||
return 'None'
|
||||
encoding = encoding or GetConsoleAttr().GetEncoding()
|
||||
string = encoding_util.Decode(data, encoding=encoding)
|
||||
|
||||
try:
|
||||
# No change needed if the string encodes to the output encoding.
|
||||
string.encode(encoding)
|
||||
return string
|
||||
except UnicodeError:
|
||||
# The string does not encode to the output encoding. Encode it with error
|
||||
# handling then convert it back into a text string (which will be
|
||||
# guaranteed to only contain characters that can be encoded later.
|
||||
return (string
|
||||
.encode(encoding, 'backslashreplace' if escape else 'replace')
|
||||
.decode(encoding))
|
||||
|
||||
|
||||
def EncodeToBytes(data):
|
||||
r"""Encode data to bytes.
|
||||
|
||||
The primary use case is for base64/mime style 7-bit ascii encoding where the
|
||||
encoder input must be bytes. "safe" means that the conversion always returns
|
||||
bytes and will not raise codec exceptions.
|
||||
|
||||
If data is text then an 8-bit ascii encoding is attempted, then the console
|
||||
encoding, and finally utf-8.
|
||||
|
||||
Args:
|
||||
data: Any bytes, string, or object that has str() or unicode() methods.
|
||||
|
||||
Returns:
|
||||
A bytes string representation of the data.
|
||||
"""
|
||||
if data is None:
|
||||
return b''
|
||||
if isinstance(data, bytes):
|
||||
# Already bytes - our work is done.
|
||||
return data
|
||||
|
||||
# Coerce to text that will be converted to bytes.
|
||||
s = six.text_type(data)
|
||||
|
||||
try:
|
||||
# Assume the text can be directly converted to bytes (8-bit ascii).
|
||||
return s.encode('iso-8859-1')
|
||||
except UnicodeEncodeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
# Try the output encoding.
|
||||
return s.encode(GetConsoleAttr().GetEncoding())
|
||||
except UnicodeEncodeError:
|
||||
pass
|
||||
|
||||
# Punt to utf-8.
|
||||
return s.encode('utf-8')
|
||||
|
||||
|
||||
def Decode(data, encoding=None):
|
||||
"""Converts the given string, bytes, or object to a text string.
|
||||
|
||||
Args:
|
||||
data: Any bytes, string, or object that has str() or unicode() methods.
|
||||
encoding: A suggesting encoding used to decode. If this encoding doesn't
|
||||
work, other defaults are tried. Defaults to
|
||||
GetConsoleAttr().GetEncoding().
|
||||
|
||||
Returns:
|
||||
A text string representation of the data.
|
||||
"""
|
||||
encoding = encoding or GetConsoleAttr().GetEncoding()
|
||||
return encoding_util.Decode(data, encoding=encoding)
|
||||
@@ -0,0 +1,260 @@
|
||||
# -*- coding: utf-8 -*- #
|
||||
# Copyright 2015 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""OS specific console_attr helper functions."""
|
||||
# This file contains platform specific code which is not currently handled
|
||||
# by pytype.
|
||||
# pytype: skip-file
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from fire.console import encoding
|
||||
|
||||
|
||||
def GetTermSize():
|
||||
"""Gets the terminal x and y dimensions in characters.
|
||||
|
||||
_GetTermSize*() helper functions taken from:
|
||||
http://stackoverflow.com/questions/263890/
|
||||
|
||||
Returns:
|
||||
(columns, lines): A tuple containing the terminal x and y dimensions.
|
||||
"""
|
||||
xy = None
|
||||
# Believe the first helper that doesn't bail.
|
||||
for get_terminal_size in (_GetTermSizePosix,
|
||||
_GetTermSizeWindows,
|
||||
_GetTermSizeEnvironment,
|
||||
_GetTermSizeTput):
|
||||
try:
|
||||
xy = get_terminal_size()
|
||||
if xy:
|
||||
break
|
||||
except: # pylint: disable=bare-except
|
||||
pass
|
||||
return xy or (80, 24)
|
||||
|
||||
|
||||
def _GetTermSizePosix():
|
||||
"""Returns the Posix terminal x and y dimensions."""
|
||||
# pylint: disable=g-import-not-at-top
|
||||
import fcntl
|
||||
# pylint: disable=g-import-not-at-top
|
||||
import struct
|
||||
# pylint: disable=g-import-not-at-top
|
||||
import termios
|
||||
|
||||
def _GetXY(fd):
|
||||
"""Returns the terminal (x,y) size for fd.
|
||||
|
||||
Args:
|
||||
fd: The terminal file descriptor.
|
||||
|
||||
Returns:
|
||||
The terminal (x,y) size for fd or None on error.
|
||||
"""
|
||||
try:
|
||||
# This magic incantation converts a struct from ioctl(2) containing two
|
||||
# binary shorts to a (rows, columns) int tuple.
|
||||
rc = struct.unpack(b'hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, 'junk'))
|
||||
return (rc[1], rc[0]) if rc else None
|
||||
except: # pylint: disable=bare-except
|
||||
return None
|
||||
|
||||
xy = _GetXY(0) or _GetXY(1) or _GetXY(2)
|
||||
if not xy:
|
||||
fd = None
|
||||
try:
|
||||
fd = os.open(os.ctermid(), os.O_RDONLY)
|
||||
xy = _GetXY(fd)
|
||||
except: # pylint: disable=bare-except
|
||||
xy = None
|
||||
finally:
|
||||
if fd is not None:
|
||||
os.close(fd)
|
||||
return xy
|
||||
|
||||
|
||||
def _GetTermSizeWindows():
|
||||
"""Returns the Windows terminal x and y dimensions."""
|
||||
# pylint:disable=g-import-not-at-top
|
||||
import struct
|
||||
# pylint: disable=g-import-not-at-top
|
||||
from ctypes import create_string_buffer
|
||||
# pylint:disable=g-import-not-at-top
|
||||
from ctypes import windll
|
||||
|
||||
# stdin handle is -10
|
||||
# stdout handle is -11
|
||||
# stderr handle is -12
|
||||
|
||||
h = windll.kernel32.GetStdHandle(-12)
|
||||
csbi = create_string_buffer(22)
|
||||
if not windll.kernel32.GetConsoleScreenBufferInfo(h, csbi):
|
||||
return None
|
||||
(unused_bufx, unused_bufy, unused_curx, unused_cury, unused_wattr,
|
||||
left, top, right, bottom,
|
||||
unused_maxx, unused_maxy) = struct.unpack(b'hhhhHhhhhhh', csbi.raw)
|
||||
x = right - left + 1
|
||||
y = bottom - top + 1
|
||||
return (x, y)
|
||||
|
||||
|
||||
def _GetTermSizeEnvironment():
|
||||
"""Returns the terminal x and y dimensions from the environment."""
|
||||
return (int(os.environ['COLUMNS']), int(os.environ['LINES']))
|
||||
|
||||
|
||||
def _GetTermSizeTput():
|
||||
"""Returns the terminal x and y dimemsions from tput(1)."""
|
||||
import subprocess # pylint: disable=g-import-not-at-top
|
||||
output = encoding.Decode(subprocess.check_output(['tput', 'cols'],
|
||||
stderr=subprocess.STDOUT))
|
||||
cols = int(output)
|
||||
output = encoding.Decode(subprocess.check_output(['tput', 'lines'],
|
||||
stderr=subprocess.STDOUT))
|
||||
rows = int(output)
|
||||
return (cols, rows)
|
||||
|
||||
|
||||
_ANSI_CSI = '\x1b' # ANSI control sequence indicator (ESC)
|
||||
_CONTROL_D = '\x04' # unix EOF (^D)
|
||||
_CONTROL_Z = '\x1a' # Windows EOF (^Z)
|
||||
_WINDOWS_CSI_1 = '\x00' # Windows control sequence indicator #1
|
||||
_WINDOWS_CSI_2 = '\xe0' # Windows control sequence indicator #2
|
||||
|
||||
|
||||
def GetRawKeyFunction():
|
||||
"""Returns a function that reads one keypress from stdin with no echo.
|
||||
|
||||
Returns:
|
||||
A function that reads one keypress from stdin with no echo or a function
|
||||
that always returns None if stdin does not support it.
|
||||
"""
|
||||
# Believe the first helper that doesn't bail.
|
||||
for get_raw_key_function in (_GetRawKeyFunctionPosix,
|
||||
_GetRawKeyFunctionWindows):
|
||||
try:
|
||||
return get_raw_key_function()
|
||||
except: # pylint: disable=bare-except
|
||||
pass
|
||||
return lambda: None
|
||||
|
||||
|
||||
def _GetRawKeyFunctionPosix():
|
||||
"""_GetRawKeyFunction helper using Posix APIs."""
|
||||
# pylint: disable=g-import-not-at-top
|
||||
import tty
|
||||
# pylint: disable=g-import-not-at-top
|
||||
import termios
|
||||
|
||||
def _GetRawKeyPosix():
|
||||
"""Reads and returns one keypress from stdin, no echo, using Posix APIs.
|
||||
|
||||
Returns:
|
||||
The key name, None for EOF, <*> for function keys, otherwise a
|
||||
character.
|
||||
"""
|
||||
ansi_to_key = {
|
||||
'A': '<UP-ARROW>',
|
||||
'B': '<DOWN-ARROW>',
|
||||
'D': '<LEFT-ARROW>',
|
||||
'C': '<RIGHT-ARROW>',
|
||||
'5': '<PAGE-UP>',
|
||||
'6': '<PAGE-DOWN>',
|
||||
'H': '<HOME>',
|
||||
'F': '<END>',
|
||||
'M': '<DOWN-ARROW>',
|
||||
'S': '<PAGE-UP>',
|
||||
'T': '<PAGE-DOWN>',
|
||||
}
|
||||
|
||||
# Flush pending output. sys.stdin.read() would do this, but it's explicitly
|
||||
# bypassed in _GetKeyChar().
|
||||
sys.stdout.flush()
|
||||
|
||||
fd = sys.stdin.fileno()
|
||||
|
||||
def _GetKeyChar():
|
||||
return encoding.Decode(os.read(fd, 1))
|
||||
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(fd)
|
||||
c = _GetKeyChar()
|
||||
if c == _ANSI_CSI:
|
||||
c = _GetKeyChar()
|
||||
while True:
|
||||
if c == _ANSI_CSI:
|
||||
return c
|
||||
if c.isalpha():
|
||||
break
|
||||
prev_c = c
|
||||
c = _GetKeyChar()
|
||||
if c == '~':
|
||||
c = prev_c
|
||||
break
|
||||
return ansi_to_key.get(c, '')
|
||||
except: # pylint:disable=bare-except
|
||||
c = None
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
return None if c in (_CONTROL_D, _CONTROL_Z) else c
|
||||
|
||||
return _GetRawKeyPosix
|
||||
|
||||
|
||||
def _GetRawKeyFunctionWindows():
|
||||
"""_GetRawKeyFunction helper using Windows APIs."""
|
||||
# pylint: disable=g-import-not-at-top
|
||||
import msvcrt
|
||||
|
||||
def _GetRawKeyWindows():
|
||||
"""Reads and returns one keypress from stdin, no echo, using Windows APIs.
|
||||
|
||||
Returns:
|
||||
The key name, None for EOF, <*> for function keys, otherwise a
|
||||
character.
|
||||
"""
|
||||
windows_to_key = {
|
||||
'H': '<UP-ARROW>',
|
||||
'P': '<DOWN-ARROW>',
|
||||
'K': '<LEFT-ARROW>',
|
||||
'M': '<RIGHT-ARROW>',
|
||||
'I': '<PAGE-UP>',
|
||||
'Q': '<PAGE-DOWN>',
|
||||
'G': '<HOME>',
|
||||
'O': '<END>',
|
||||
}
|
||||
|
||||
# Flush pending output. sys.stdin.read() would do this it's explicitly
|
||||
# bypassed in _GetKeyChar().
|
||||
sys.stdout.flush()
|
||||
|
||||
def _GetKeyChar():
|
||||
return encoding.Decode(msvcrt.getch())
|
||||
|
||||
c = _GetKeyChar()
|
||||
# Special function key is a two character sequence; return the second char.
|
||||
if c in (_WINDOWS_CSI_1, _WINDOWS_CSI_2):
|
||||
return windows_to_key.get(_GetKeyChar(), '')
|
||||
return None if c in (_CONTROL_D, _CONTROL_Z) else c
|
||||
|
||||
return _GetRawKeyWindows
|
||||
@@ -0,0 +1,299 @@
|
||||
# -*- coding: utf-8 -*- #
|
||||
# Copyright 2015 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Simple console pager."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
from fire.console import console_attr
|
||||
|
||||
|
||||
class Pager(object):
|
||||
"""A simple console text pager.
|
||||
|
||||
This pager requires the entire contents to be available. The contents are
|
||||
written one page of lines at a time. The prompt is written after each page of
|
||||
lines. A one character response is expected. See HELP_TEXT below for more
|
||||
info.
|
||||
|
||||
The contents are written as is. For example, ANSI control codes will be in
|
||||
effect. This is different from pagers like more(1) which is ANSI control code
|
||||
agnostic and miscalculates line lengths, and less(1) which displays control
|
||||
character names by default.
|
||||
|
||||
Attributes:
|
||||
_attr: The current ConsoleAttr handle.
|
||||
_clear: A string that clears the prompt when written to _out.
|
||||
_contents: The entire contents of the text lines to page.
|
||||
_height: The terminal height in characters.
|
||||
_out: The output stream, log.out (effectively) if None.
|
||||
_prompt: The page break prompt.
|
||||
_search_direction: The search direction command, n:forward, N:reverse.
|
||||
_search_pattern: The current forward/reverse search compiled RE.
|
||||
_width: The termonal width in characters.
|
||||
"""
|
||||
|
||||
HELP_TEXT = """
|
||||
Simple pager commands:
|
||||
|
||||
b, ^B, <PAGE-UP>, <LEFT-ARROW>
|
||||
Back one page.
|
||||
f, ^F, <SPACE>, <PAGE-DOWN>, <RIGHT-ARROW>
|
||||
Forward one page. Does not quit if there are no more lines.
|
||||
g, <HOME>
|
||||
Back to the first page.
|
||||
<number>g
|
||||
Go to <number> lines from the top.
|
||||
G, <END>
|
||||
Forward to the last page.
|
||||
<number>G
|
||||
Go to <number> lines from the bottom.
|
||||
h
|
||||
Print pager command help.
|
||||
j, +, <DOWN-ARROW>
|
||||
Forward one line.
|
||||
k, -, <UP-ARROW>
|
||||
Back one line.
|
||||
/pattern
|
||||
Forward search for pattern.
|
||||
?pattern
|
||||
Backward search for pattern.
|
||||
n
|
||||
Repeat current search.
|
||||
N
|
||||
Repeat current search in the opposite direction.
|
||||
q, Q, ^C, ^D, ^Z
|
||||
Quit return to the caller.
|
||||
any other character
|
||||
Prompt again.
|
||||
|
||||
Hit any key to continue:"""
|
||||
|
||||
PREV_POS_NXT_REPRINT = -1, -1
|
||||
|
||||
def __init__(self, contents, out=None, prompt=None):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
contents: The entire contents of the text lines to page.
|
||||
out: The output stream, log.out (effectively) if None.
|
||||
prompt: The page break prompt, a defalt prompt is used if None..
|
||||
"""
|
||||
self._contents = contents
|
||||
self._out = out or sys.stdout
|
||||
self._search_pattern = None
|
||||
self._search_direction = None
|
||||
|
||||
# prev_pos, prev_next values to force reprint
|
||||
self.prev_pos, self.prev_nxt = self.PREV_POS_NXT_REPRINT
|
||||
# Initialize the console attributes.
|
||||
self._attr = console_attr.GetConsoleAttr()
|
||||
self._width, self._height = self._attr.GetTermSize()
|
||||
|
||||
# Initialize the prompt and the prompt clear string.
|
||||
if not prompt:
|
||||
prompt = '{bold}--({{percent}}%)--{normal}'.format(
|
||||
bold=self._attr.GetFontCode(bold=True),
|
||||
normal=self._attr.GetFontCode())
|
||||
self._clear = '\r{0}\r'.format(' ' * (self._attr.DisplayWidth(prompt) - 6))
|
||||
self._prompt = prompt
|
||||
|
||||
# Initialize a list of lines with long lines split into separate display
|
||||
# lines.
|
||||
self._lines = []
|
||||
for line in contents.splitlines():
|
||||
self._lines += self._attr.SplitLine(line, self._width)
|
||||
|
||||
def _Write(self, s):
|
||||
"""Mockable helper that writes s to self._out."""
|
||||
self._out.write(s)
|
||||
|
||||
def _GetSearchCommand(self, c):
|
||||
"""Consumes a search command and returns the equivalent pager command.
|
||||
|
||||
The search pattern is an RE that is pre-compiled and cached for subsequent
|
||||
/<newline>, ?<newline>, n, or N commands.
|
||||
|
||||
Args:
|
||||
c: The search command char.
|
||||
|
||||
Returns:
|
||||
The pager command char.
|
||||
"""
|
||||
self._Write(c)
|
||||
buf = ''
|
||||
while True:
|
||||
p = self._attr.GetRawKey()
|
||||
if p in (None, '\n', '\r') or len(p) != 1:
|
||||
break
|
||||
self._Write(p)
|
||||
buf += p
|
||||
self._Write('\r' + ' ' * len(buf) + '\r')
|
||||
if buf:
|
||||
try:
|
||||
self._search_pattern = re.compile(buf)
|
||||
except re.error:
|
||||
# Silently ignore pattern errors.
|
||||
self._search_pattern = None
|
||||
return ''
|
||||
self._search_direction = 'n' if c == '/' else 'N'
|
||||
return 'n'
|
||||
|
||||
def _Help(self):
|
||||
"""Print command help and wait for any character to continue."""
|
||||
clear = self._height - (len(self.HELP_TEXT) -
|
||||
len(self.HELP_TEXT.replace('\n', '')))
|
||||
if clear > 0:
|
||||
self._Write('\n' * clear)
|
||||
self._Write(self.HELP_TEXT)
|
||||
self._attr.GetRawKey()
|
||||
self._Write('\n')
|
||||
|
||||
def Run(self):
|
||||
"""Run the pager."""
|
||||
# No paging if the contents are small enough.
|
||||
if len(self._lines) <= self._height:
|
||||
self._Write(self._contents)
|
||||
return
|
||||
|
||||
# We will not always reset previous values.
|
||||
reset_prev_values = True
|
||||
# Save room for the prompt at the bottom of the page.
|
||||
self._height -= 1
|
||||
|
||||
# Loop over all the pages.
|
||||
pos = 0
|
||||
while pos < len(self._lines):
|
||||
# Write a page of lines.
|
||||
nxt = pos + self._height
|
||||
if nxt > len(self._lines):
|
||||
nxt = len(self._lines)
|
||||
pos = nxt - self._height
|
||||
# Checks if the starting position is in between the current printed lines
|
||||
# so we don't need to reprint all the lines.
|
||||
if self.prev_pos < pos < self.prev_nxt:
|
||||
# we start where the previous page ended.
|
||||
self._Write('\n'.join(self._lines[self.prev_nxt:nxt]) + '\n')
|
||||
elif pos != self.prev_pos and nxt != self.prev_nxt:
|
||||
self._Write('\n'.join(self._lines[pos:nxt]) + '\n')
|
||||
|
||||
# Handle the prompt response.
|
||||
percent = self._prompt.format(percent=100 * nxt // len(self._lines))
|
||||
digits = ''
|
||||
while True:
|
||||
# We want to reset prev values if we just exited out of the while loop
|
||||
if reset_prev_values:
|
||||
self.prev_pos, self.prev_nxt = pos, nxt
|
||||
reset_prev_values = False
|
||||
self._Write(percent)
|
||||
c = self._attr.GetRawKey()
|
||||
self._Write(self._clear)
|
||||
|
||||
# Parse the command.
|
||||
if c in (None, # EOF.
|
||||
'q', # Quit.
|
||||
'Q', # Quit.
|
||||
'\x03', # ^C (unix & windows terminal interrupt)
|
||||
'\x1b', # ESC.
|
||||
):
|
||||
# Quit.
|
||||
return
|
||||
elif c in ('/', '?'):
|
||||
c = self._GetSearchCommand(c)
|
||||
elif c.isdigit():
|
||||
# Collect digits for operation count.
|
||||
digits += c
|
||||
continue
|
||||
|
||||
# Set the optional command count.
|
||||
if digits:
|
||||
count = int(digits)
|
||||
digits = ''
|
||||
else:
|
||||
count = 0
|
||||
|
||||
# Finally commit to command c.
|
||||
if c in ('<PAGE-UP>', '<LEFT-ARROW>', 'b', '\x02'):
|
||||
# Previous page.
|
||||
nxt = pos - self._height
|
||||
if nxt < 0:
|
||||
nxt = 0
|
||||
elif c in ('<PAGE-DOWN>', '<RIGHT-ARROW>', 'f', '\x06', ' '):
|
||||
# Next page.
|
||||
if nxt >= len(self._lines):
|
||||
continue
|
||||
nxt = pos + self._height
|
||||
if nxt >= len(self._lines):
|
||||
nxt = pos
|
||||
elif c in ('<HOME>', 'g'):
|
||||
# First page.
|
||||
nxt = count - 1
|
||||
if nxt > len(self._lines) - self._height:
|
||||
nxt = len(self._lines) - self._height
|
||||
if nxt < 0:
|
||||
nxt = 0
|
||||
elif c in ('<END>', 'G'):
|
||||
# Last page.
|
||||
nxt = len(self._lines) - count
|
||||
if nxt > len(self._lines) - self._height:
|
||||
nxt = len(self._lines) - self._height
|
||||
if nxt < 0:
|
||||
nxt = 0
|
||||
elif c == 'h':
|
||||
self._Help()
|
||||
# Special case when we want to reprint the previous display.
|
||||
self.prev_pos, self.prev_nxt = self.PREV_POS_NXT_REPRINT
|
||||
nxt = pos
|
||||
break
|
||||
elif c in ('<DOWN-ARROW>', 'j', '+', '\n', '\r'):
|
||||
# Next line.
|
||||
if nxt >= len(self._lines):
|
||||
continue
|
||||
nxt = pos + 1
|
||||
if nxt >= len(self._lines):
|
||||
nxt = pos
|
||||
elif c in ('<UP-ARROW>', 'k', '-'):
|
||||
# Previous line.
|
||||
nxt = pos - 1
|
||||
if nxt < 0:
|
||||
nxt = 0
|
||||
elif c in ('n', 'N'):
|
||||
# Next pattern match search.
|
||||
if not self._search_pattern:
|
||||
continue
|
||||
nxt = pos
|
||||
i = pos
|
||||
direction = 1 if c == self._search_direction else -1
|
||||
while True:
|
||||
i += direction
|
||||
if i < 0 or i >= len(self._lines):
|
||||
break
|
||||
if self._search_pattern.search(self._lines[i]):
|
||||
nxt = i
|
||||
break
|
||||
else:
|
||||
# Silently ignore everything else.
|
||||
continue
|
||||
if nxt != pos:
|
||||
# We will exit the while loop because position changed so we can reset
|
||||
# prev values.
|
||||
reset_prev_values = True
|
||||
break
|
||||
pos = nxt
|
||||
@@ -0,0 +1,207 @@
|
||||
# -*- coding: utf-8 -*- #
|
||||
|
||||
# Copyright 2015 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""A module for dealing with unknown string and environment encodings."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
|
||||
def Encode(string, encoding=None):
|
||||
"""Encode the text string to a byte string.
|
||||
|
||||
Args:
|
||||
string: str, The text string to encode.
|
||||
encoding: The suggested encoding if known.
|
||||
|
||||
Returns:
|
||||
str, The binary string.
|
||||
"""
|
||||
if string is None:
|
||||
return None
|
||||
if not six.PY2:
|
||||
# In Python 3, the environment sets and gets accept and return text strings
|
||||
# only, and it handles the encoding itself so this is not necessary.
|
||||
return string
|
||||
if isinstance(string, six.binary_type):
|
||||
# Already an encoded byte string, we are done
|
||||
return string
|
||||
|
||||
encoding = encoding or _GetEncoding()
|
||||
return string.encode(encoding)
|
||||
|
||||
|
||||
def Decode(data, encoding=None):
|
||||
"""Returns string with non-ascii characters decoded to UNICODE.
|
||||
|
||||
UTF-8, the suggested encoding, and the usual suspects will be attempted in
|
||||
order.
|
||||
|
||||
Args:
|
||||
data: A string or object that has str() and unicode() methods that may
|
||||
contain an encoding incompatible with the standard output encoding.
|
||||
encoding: The suggested encoding if known.
|
||||
|
||||
Returns:
|
||||
A text string representing the decoded byte string.
|
||||
"""
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
# First we are going to get the data object to be a text string.
|
||||
# Don't use six.string_types here because on Python 3 bytes is not considered
|
||||
# a string type and we want to include that.
|
||||
if isinstance(data, six.text_type) or isinstance(data, six.binary_type):
|
||||
string = data
|
||||
else:
|
||||
# Some non-string type of object.
|
||||
try:
|
||||
string = six.text_type(data)
|
||||
except (TypeError, UnicodeError):
|
||||
# The string cannot be converted to unicode -- default to str() which will
|
||||
# catch objects with special __str__ methods.
|
||||
string = str(data)
|
||||
|
||||
if isinstance(string, six.text_type):
|
||||
# Our work is done here.
|
||||
return string
|
||||
|
||||
try:
|
||||
# Just return the string if its pure ASCII.
|
||||
return string.decode('ascii')
|
||||
except UnicodeError:
|
||||
# The string is not ASCII encoded.
|
||||
pass
|
||||
|
||||
# Try the suggested encoding if specified.
|
||||
if encoding:
|
||||
try:
|
||||
return string.decode(encoding)
|
||||
except UnicodeError:
|
||||
# Bad suggestion.
|
||||
pass
|
||||
|
||||
# Try UTF-8 because the other encodings could be extended ASCII. It would
|
||||
# be exceptional if a valid extended ascii encoding with extended chars
|
||||
# were also a valid UITF-8 encoding.
|
||||
try:
|
||||
return string.decode('utf8')
|
||||
except UnicodeError:
|
||||
# Not a UTF-8 encoding.
|
||||
pass
|
||||
|
||||
# Try the filesystem encoding.
|
||||
try:
|
||||
return string.decode(sys.getfilesystemencoding())
|
||||
except UnicodeError:
|
||||
# string is not encoded for filesystem paths.
|
||||
pass
|
||||
|
||||
# Try the system default encoding.
|
||||
try:
|
||||
return string.decode(sys.getdefaultencoding())
|
||||
except UnicodeError:
|
||||
# string is not encoded using the default encoding.
|
||||
pass
|
||||
|
||||
# We don't know the string encoding.
|
||||
# This works around a Python str.encode() "feature" that throws
|
||||
# an ASCII *decode* exception on str strings that contain 8th bit set
|
||||
# bytes. For example, this sequence throws an exception:
|
||||
# string = '\xdc' # iso-8859-1 'Ü'
|
||||
# string = string.encode('ascii', 'backslashreplace')
|
||||
# even though 'backslashreplace' is documented to handle encoding
|
||||
# errors. We work around the problem by first decoding the str string
|
||||
# from an 8-bit encoding to unicode, selecting any 8-bit encoding that
|
||||
# uses all 256 bytes (such as ISO-8559-1):
|
||||
# string = string.decode('iso-8859-1')
|
||||
# Using this produces a sequence that works:
|
||||
# string = '\xdc'
|
||||
# string = string.decode('iso-8859-1')
|
||||
# string = string.encode('ascii', 'backslashreplace')
|
||||
return string.decode('iso-8859-1')
|
||||
|
||||
|
||||
def GetEncodedValue(env, name, default=None):
|
||||
"""Returns the decoded value of the env var name.
|
||||
|
||||
Args:
|
||||
env: {str: str}, The env dict.
|
||||
name: str, The env var name.
|
||||
default: The value to return if name is not in env.
|
||||
|
||||
Returns:
|
||||
The decoded value of the env var name.
|
||||
"""
|
||||
name = Encode(name)
|
||||
value = env.get(name)
|
||||
if value is None:
|
||||
return default
|
||||
# In Python 3, the environment sets and gets accept and return text strings
|
||||
# only, and it handles the encoding itself so this is not necessary.
|
||||
return Decode(value)
|
||||
|
||||
|
||||
def SetEncodedValue(env, name, value, encoding=None):
|
||||
"""Sets the value of name in env to an encoded value.
|
||||
|
||||
Args:
|
||||
env: {str: str}, The env dict.
|
||||
name: str, The env var name.
|
||||
value: str or unicode, The value for name. If None then name is removed from
|
||||
env.
|
||||
encoding: str, The encoding to use or None to try to infer it.
|
||||
"""
|
||||
# Python 2 *and* 3 unicode support falls apart at filesystem/argv/environment
|
||||
# boundaries. The encoding used for filesystem paths and environment variable
|
||||
# names/values is under user control on most systems. With one of those values
|
||||
# in hand there is no way to tell exactly how the value was encoded. We get
|
||||
# some reasonable hints from sys.getfilesystemencoding() or
|
||||
# sys.getdefaultencoding() and use them to encode values that the receiving
|
||||
# process will have a chance at decoding. Leaving the values as unicode
|
||||
# strings will cause os module Unicode exceptions. What good is a language
|
||||
# unicode model when the module support could care less?
|
||||
name = Encode(name, encoding=encoding)
|
||||
if value is None:
|
||||
env.pop(name, None)
|
||||
return
|
||||
env[name] = Encode(value, encoding=encoding)
|
||||
|
||||
|
||||
def EncodeEnv(env, encoding=None):
|
||||
"""Encodes all the key value pairs in env in preparation for subprocess.
|
||||
|
||||
Args:
|
||||
env: {str: str}, The environment you are going to pass to subprocess.
|
||||
encoding: str, The encoding to use or None to use the default.
|
||||
|
||||
Returns:
|
||||
{bytes: bytes}, The environment to pass to subprocess.
|
||||
"""
|
||||
encoding = encoding or _GetEncoding()
|
||||
return {
|
||||
Encode(k, encoding=encoding): Encode(v, encoding=encoding)
|
||||
for k, v in six.iteritems(env)}
|
||||
|
||||
|
||||
def _GetEncoding():
|
||||
"""Gets the default encoding to use."""
|
||||
return sys.getfilesystemencoding() or sys.getdefaultencoding()
|
||||
+323
-164
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -30,7 +30,7 @@ A Fire CLI command is run by consuming the arguments in the command in order to
|
||||
access a member of current component, call the current component (if it's a
|
||||
function), or instantiate the current component (if it's a class). The target
|
||||
component begins as Component, and at each operation the component becomes the
|
||||
result of the preceeding operation.
|
||||
result of the preceding operation.
|
||||
|
||||
For example "command fn arg1 arg2" might access the "fn" property of the initial
|
||||
target component, and then call that function with arguments 'arg1' and 'arg2'.
|
||||
@@ -44,6 +44,7 @@ The available flags for all Fire CLIs are:
|
||||
-h --help: Provide help and usage information for the command.
|
||||
-i --interactive: Drop into a Python REPL after running the command.
|
||||
--completion: Write the Bash completion script for the tool to stdout.
|
||||
--completion fish: Write the Fish completion script for the tool to stdout.
|
||||
--separator SEPARATOR: Use SEPARATOR in place of the default separator, '-'.
|
||||
--trace: Get the Fire Trace for the command.
|
||||
"""
|
||||
@@ -54,7 +55,9 @@ from __future__ import print_function
|
||||
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
import pipes
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
import types
|
||||
@@ -66,6 +69,8 @@ from fire import inspectutils
|
||||
from fire import interact
|
||||
from fire import parser
|
||||
from fire import trace
|
||||
from fire import value_types
|
||||
from fire.console import console_pager
|
||||
import six
|
||||
|
||||
|
||||
@@ -81,7 +86,8 @@ def Fire(component=None, command=None, name=None):
|
||||
Args:
|
||||
component: The initial target component.
|
||||
command: Optional. If supplied, this is the command executed. If not
|
||||
supplied, then the command is taken from sys.argv instead.
|
||||
supplied, then the command is taken from sys.argv instead. This can be
|
||||
a string or a list of strings; a list of strings is preferred.
|
||||
name: Optional. The name of the command as entered at the command line.
|
||||
Used in interactive mode and for generating the completion script.
|
||||
Returns:
|
||||
@@ -93,18 +99,25 @@ def Fire(component=None, command=None, name=None):
|
||||
to call or class left to instantiate, the resulting current component is
|
||||
the final result.
|
||||
Raises:
|
||||
ValueError: If the command argument is supplied, but not a string or a
|
||||
sequence of arguments.
|
||||
FireExit: When Fire encounters a FireError, Fire will raise a FireExit with
|
||||
code 2. When used with the help or trace flags, Fire will raise a
|
||||
FireExit with code 0 if successful.
|
||||
"""
|
||||
name = name or os.path.basename(sys.argv[0])
|
||||
|
||||
# Get args as a list.
|
||||
if command is None:
|
||||
if isinstance(command, six.string_types):
|
||||
args = shlex.split(command)
|
||||
elif isinstance(command, (list, tuple)):
|
||||
args = command
|
||||
elif command is None:
|
||||
# Use the command line args by default if no command is specified.
|
||||
name = name or sys.argv[0]
|
||||
args = sys.argv[1:]
|
||||
else:
|
||||
# Otherwise use the specified command.
|
||||
args = shlex.split(command)
|
||||
raise ValueError('The command argument must be a string or a sequence of '
|
||||
'arguments.')
|
||||
|
||||
# Determine the calling context.
|
||||
caller = inspect.stack()[1]
|
||||
@@ -118,40 +131,48 @@ def Fire(component=None, command=None, name=None):
|
||||
component_trace = _Fire(component, args, context, name)
|
||||
|
||||
if component_trace.HasError():
|
||||
for help_flag in ['-h', '--help']:
|
||||
if help_flag in component_trace.elements[-1].args:
|
||||
command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
|
||||
print(('WARNING: The proper way to show help is {cmd}.\n'
|
||||
'Showing help anyway.\n').format(cmd=pipes.quote(command)))
|
||||
|
||||
print('Fire trace:\n{trace}\n'.format(trace=component_trace))
|
||||
result = component_trace.GetResult()
|
||||
print(
|
||||
helputils.HelpString(result, component_trace, component_trace.verbose))
|
||||
_DisplayError(component_trace)
|
||||
raise FireExit(2, component_trace)
|
||||
elif component_trace.show_trace and component_trace.show_help:
|
||||
print('Fire trace:\n{trace}\n'.format(trace=component_trace))
|
||||
if component_trace.show_trace and component_trace.show_help:
|
||||
output = ['Fire trace:\n{trace}\n'.format(trace=component_trace)]
|
||||
result = component_trace.GetResult()
|
||||
print(
|
||||
helputils.HelpString(result, component_trace, component_trace.verbose))
|
||||
help_string = helputils.HelpString(
|
||||
result, component_trace, component_trace.verbose)
|
||||
output.append(help_string)
|
||||
Display(output)
|
||||
raise FireExit(0, component_trace)
|
||||
elif component_trace.show_trace:
|
||||
print('Fire trace:\n{trace}'.format(trace=component_trace))
|
||||
if component_trace.show_trace:
|
||||
output = ['Fire trace:\n{trace}'.format(trace=component_trace)]
|
||||
Display(output)
|
||||
raise FireExit(0, component_trace)
|
||||
elif component_trace.show_help:
|
||||
if component_trace.show_help:
|
||||
result = component_trace.GetResult()
|
||||
print(
|
||||
helputils.HelpString(result, component_trace, component_trace.verbose))
|
||||
help_string = helputils.HelpString(
|
||||
result, component_trace, component_trace.verbose)
|
||||
output = [help_string]
|
||||
Display(output)
|
||||
raise FireExit(0, component_trace)
|
||||
else:
|
||||
_PrintResult(component_trace, verbose=component_trace.verbose)
|
||||
result = component_trace.GetResult()
|
||||
return result
|
||||
|
||||
# The command succeeded normally; print the result.
|
||||
_PrintResult(component_trace, verbose=component_trace.verbose)
|
||||
result = component_trace.GetResult()
|
||||
return result
|
||||
|
||||
|
||||
def CompletionScript(name, component):
|
||||
"""Returns the text of the Bash completion script for a Fire CLI."""
|
||||
return completion.Script(name, component)
|
||||
def Display(lines):
|
||||
text = '\n'.join(lines) + '\n'
|
||||
pager = console_pager.Pager(text, out=sys.stderr)
|
||||
try:
|
||||
pager.Run()
|
||||
except: # pylint: disable=bare-except
|
||||
# pager.Run() fails with termios.error(25, 'Inappropriate ioctl for device')
|
||||
# for outputs that don't fit on a single screen in our test environment.
|
||||
pass
|
||||
|
||||
|
||||
def CompletionScript(name, component, shell):
|
||||
"""Returns the text of the completion script for a Fire CLI."""
|
||||
return completion.Script(name, component, shell=shell)
|
||||
|
||||
|
||||
class FireError(Exception):
|
||||
@@ -162,7 +183,7 @@ class FireError(Exception):
|
||||
"""
|
||||
|
||||
|
||||
class FireExit(SystemExit):
|
||||
class FireExit(SystemExit): # pylint: disable=g-bad-exception-name
|
||||
"""An exception raised by Fire to the client in the case of a FireError.
|
||||
|
||||
The trace of the Fire program is available on the `trace` property.
|
||||
@@ -183,9 +204,42 @@ class FireExit(SystemExit):
|
||||
self.trace = component_trace
|
||||
|
||||
|
||||
def _IsHelpShortcut(component_trace, remaining_args):
|
||||
"""Determines if the user is trying to access help without '--' separator.
|
||||
|
||||
For example, mycmd.py --help instead of mycmd.py -- --help.
|
||||
|
||||
Args:
|
||||
component_trace: (FireTrace) The trace for the Fire command.
|
||||
remaining_args: List of remaining args that haven't been consumed yet.
|
||||
Returns:
|
||||
True if help is requested, False otherwise.
|
||||
"""
|
||||
show_help = False
|
||||
if remaining_args:
|
||||
target = remaining_args[0]
|
||||
if target in ('-h', '--help'):
|
||||
# Check if --help would be consumed as a keyword argument, or is a member.
|
||||
component = component_trace.GetResult()
|
||||
if inspect.isclass(component) or inspect.isroutine(component):
|
||||
fn_spec = inspectutils.GetFullArgSpec(component)
|
||||
_, remaining_kwargs, _ = _ParseKeywordArgs(remaining_args, fn_spec)
|
||||
show_help = target in remaining_kwargs
|
||||
else:
|
||||
members = dict(inspect.getmembers(component))
|
||||
show_help = target not in members
|
||||
|
||||
if show_help:
|
||||
component_trace.show_help = True
|
||||
command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
|
||||
print('INFO: Showing help with the command {cmd}.\n'.format(
|
||||
cmd=pipes.quote(command)), file=sys.stderr)
|
||||
return show_help
|
||||
|
||||
|
||||
def _PrintResult(component_trace, verbose=False):
|
||||
"""Prints the result of the Fire call to stdout in a human readable way."""
|
||||
# TODO: Design human readable deserializable serialization method
|
||||
# TODO(dbieber): Design human readable deserializable serialization method
|
||||
# and move serialization to it's own module.
|
||||
result = component_trace.GetResult()
|
||||
|
||||
@@ -198,13 +252,29 @@ def _PrintResult(component_trace, verbose=False):
|
||||
print(_DictAsString(result, verbose))
|
||||
elif isinstance(result, tuple):
|
||||
print(_OneLineResult(result))
|
||||
elif isinstance(result,
|
||||
(bool, six.string_types, six.integer_types, float, complex)):
|
||||
elif isinstance(result, value_types.VALUE_TYPES):
|
||||
print(result)
|
||||
elif result is not None:
|
||||
print(helputils.HelpString(result, component_trace, verbose))
|
||||
|
||||
|
||||
def _DisplayError(component_trace):
|
||||
"""Prints the Fire trace and the error to stdout."""
|
||||
output = []
|
||||
for help_flag in ('-h', '--help'):
|
||||
if help_flag in component_trace.elements[-1].args:
|
||||
command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
|
||||
message = 'INFO: Showing help with the command {cmd}.\n'.format(
|
||||
cmd=pipes.quote(command))
|
||||
output.append(message)
|
||||
output.append('Fire trace:\n{trace}\n'.format(trace=component_trace))
|
||||
result = component_trace.GetResult()
|
||||
help_string = helputils.HelpString(result, component_trace,
|
||||
component_trace.verbose)
|
||||
output.append(help_string)
|
||||
Display(output)
|
||||
|
||||
|
||||
def _DictAsString(result, verbose=False):
|
||||
"""Returns a dict as a string.
|
||||
|
||||
@@ -214,20 +284,25 @@ def _DictAsString(result, verbose=False):
|
||||
Returns:
|
||||
A string representing the dict
|
||||
"""
|
||||
result = {key: value for key, value in result.items()
|
||||
if _ComponentVisible(key, verbose)}
|
||||
|
||||
if not result:
|
||||
# We need to do 2 iterations over the items in the result dict
|
||||
# 1) Getting visible items and the longest key for output formatting
|
||||
# 2) Actually construct the output lines
|
||||
result_visible = {key: value for key, value in result.items()
|
||||
if _ComponentVisible(key, verbose)}
|
||||
|
||||
if not result_visible:
|
||||
return '{}'
|
||||
|
||||
longest_key = max(len(str(key)) for key in result.keys())
|
||||
longest_key = max(len(str(key)) for key in result_visible.keys())
|
||||
format_string = '{{key:{padding}s}} {{value}}'.format(padding=longest_key + 1)
|
||||
|
||||
lines = []
|
||||
for key, value in result.items():
|
||||
line = format_string.format(key=str(key) + ':',
|
||||
value=_OneLineResult(value))
|
||||
lines.append(line)
|
||||
if _ComponentVisible(key, verbose):
|
||||
line = format_string.format(key=str(key) + ':',
|
||||
value=_OneLineResult(value))
|
||||
lines.append(line)
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
@@ -241,13 +316,14 @@ def _ComponentVisible(component, verbose=False):
|
||||
|
||||
def _OneLineResult(result):
|
||||
"""Returns result serialized to a single line string."""
|
||||
# TODO: Ensure line is fewer than eg 120 characters.
|
||||
# TODO(dbieber): Ensure line is fewer than eg 120 characters.
|
||||
if isinstance(result, six.string_types):
|
||||
return str(result).replace('\n', ' ')
|
||||
|
||||
try:
|
||||
return json.dumps(result)
|
||||
except TypeError:
|
||||
# Don't force conversion to ascii.
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
except (TypeError, ValueError):
|
||||
return str(result).replace('\n', ' ')
|
||||
|
||||
|
||||
@@ -321,11 +397,15 @@ def _Fire(component, args, context, name=None):
|
||||
initial_args = remaining_args
|
||||
|
||||
if not remaining_args and (show_help or interactive or show_trace
|
||||
or show_completion):
|
||||
or show_completion is not None):
|
||||
# Don't initialize the final class or call the final function unless
|
||||
# there's a separator after it, and instead process the current component.
|
||||
break
|
||||
|
||||
if _IsHelpShortcut(component_trace, remaining_args):
|
||||
remaining_args = []
|
||||
break
|
||||
|
||||
saved_args = []
|
||||
used_separator = False
|
||||
if separator in remaining_args:
|
||||
@@ -342,20 +422,12 @@ def _Fire(component, args, context, name=None):
|
||||
isclass = inspect.isclass(component)
|
||||
|
||||
try:
|
||||
target = component.__name__
|
||||
filename, lineno = _GetFileAndLine(component)
|
||||
|
||||
component, consumed_args, remaining_args, capacity = _CallCallable(
|
||||
component, remaining_args)
|
||||
|
||||
# Update the trace.
|
||||
if isclass:
|
||||
component_trace.AddInstantiatedClass(
|
||||
component, target, consumed_args, filename, lineno, capacity)
|
||||
else:
|
||||
component_trace.AddCalledRoutine(
|
||||
component, target, consumed_args, filename, lineno, capacity)
|
||||
|
||||
component, remaining_args = _CallAndUpdateTrace(
|
||||
component,
|
||||
remaining_args,
|
||||
component_trace,
|
||||
treatment='class' if isclass else 'routine',
|
||||
target=component.__name__)
|
||||
except FireError as error:
|
||||
component_trace.AddError(error, initial_args)
|
||||
return component_trace
|
||||
@@ -364,7 +436,8 @@ def _Fire(component, args, context, name=None):
|
||||
# If the initial component is a class, keep an instance for use with -i.
|
||||
instance = component
|
||||
|
||||
elif isinstance(component, (list, tuple)) and remaining_args:
|
||||
elif (isinstance(component, (list, tuple)) and remaining_args
|
||||
and not inspectutils.IsNamedTuple(component)):
|
||||
# The component is a tuple or list; we'll try to access a member.
|
||||
arg = remaining_args[0]
|
||||
try:
|
||||
@@ -382,18 +455,39 @@ def _Fire(component, args, context, name=None):
|
||||
component_trace.AddAccessedProperty(
|
||||
component, index, [arg], filename, lineno)
|
||||
|
||||
elif isinstance(component, dict) and remaining_args:
|
||||
elif ((isinstance(component, dict) or inspectutils.IsNamedTuple(component))
|
||||
and remaining_args):
|
||||
# The component is a dict; we'll try to access a member.
|
||||
target = remaining_args[0]
|
||||
if target in component:
|
||||
|
||||
# Allow indexing for namedtuples.
|
||||
try:
|
||||
index = int(target)
|
||||
is_target_int = True
|
||||
except ValueError:
|
||||
is_target_int = False
|
||||
|
||||
if inspectutils.IsNamedTuple(component) and is_target_int:
|
||||
try:
|
||||
component = component[index]
|
||||
except (ValueError, IndexError):
|
||||
error = FireError(
|
||||
'Unable to index into component with argument:', target)
|
||||
component_trace.AddError(error, initial_args)
|
||||
return component_trace
|
||||
elif target in component:
|
||||
component = component[target]
|
||||
elif target.replace('-', '_') in component:
|
||||
component = component[target.replace('-', '_')]
|
||||
else:
|
||||
# The target isn't present in the dict as a string, but maybe it is as
|
||||
# another type.
|
||||
# TODO: Consider alternatives for accessing non-string keys.
|
||||
# TODO(dbieber): Consider alternatives for accessing non-string keys.
|
||||
found_target = False
|
||||
# If the component is a namedtuple, we need to convert it to dict to
|
||||
# be able to use the .items() method.
|
||||
if inspectutils.IsNamedTuple(component):
|
||||
component = component._asdict() # pytype: disable=attribute-error
|
||||
for key, value in component.items():
|
||||
if target == str(key):
|
||||
component = value
|
||||
@@ -419,14 +513,25 @@ def _Fire(component, args, context, name=None):
|
||||
component, consumed_args, remaining_args = _GetMember(
|
||||
component, remaining_args)
|
||||
|
||||
filename, lineno = _GetFileAndLine(component)
|
||||
filename, lineno = inspectutils.GetFileAndLine(component)
|
||||
|
||||
component_trace.AddAccessedProperty(
|
||||
component, target, consumed_args, filename, lineno)
|
||||
|
||||
except FireError as error:
|
||||
component_trace.AddError(error, initial_args)
|
||||
return component_trace
|
||||
if not callable(component):
|
||||
component_trace.AddError(error, initial_args)
|
||||
return component_trace
|
||||
|
||||
# If we can't access the member, try to treat component as a callable.
|
||||
try:
|
||||
component, remaining_args = _CallAndUpdateTrace(component,
|
||||
remaining_args,
|
||||
component_trace,
|
||||
treatment='callable')
|
||||
except FireError as error:
|
||||
component_trace.AddError(error, initial_args)
|
||||
return component_trace
|
||||
|
||||
if used_separator:
|
||||
# Add back in the arguments from after the separator.
|
||||
@@ -452,10 +557,10 @@ def _Fire(component, args, context, name=None):
|
||||
initial_args)
|
||||
return component_trace
|
||||
|
||||
if show_completion:
|
||||
if show_completion is not None:
|
||||
if name is None:
|
||||
raise ValueError('Cannot make completion script without command name')
|
||||
script = CompletionScript(name, initial_component)
|
||||
script = CompletionScript(name, initial_component, shell=show_completion)
|
||||
component_trace.AddCompletionScript(script)
|
||||
|
||||
if interactive:
|
||||
@@ -477,33 +582,6 @@ def _Fire(component, args, context, name=None):
|
||||
return component_trace
|
||||
|
||||
|
||||
def _GetFileAndLine(component):
|
||||
"""Returns the filename and line number of component.
|
||||
|
||||
Args:
|
||||
component: A component to find the source information for, usually a class
|
||||
or routine.
|
||||
Returns:
|
||||
filename: The name of the file where component is defined.
|
||||
lineno: The line number where component is defined.
|
||||
"""
|
||||
if inspect.isbuiltin(component):
|
||||
return None, None
|
||||
|
||||
try:
|
||||
filename = inspect.getsourcefile(component)
|
||||
except TypeError:
|
||||
return None, None
|
||||
|
||||
try:
|
||||
unused_code, lineindex = inspect.findsource(component)
|
||||
lineno = lineindex + 1
|
||||
except IOError:
|
||||
lineno = None
|
||||
|
||||
return filename, lineno
|
||||
|
||||
|
||||
def _GetMember(component, args):
|
||||
"""Returns a subcomponent of component by consuming an arg from args.
|
||||
|
||||
@@ -534,23 +612,44 @@ def _GetMember(component, args):
|
||||
raise FireError('Could not consume arg:', arg)
|
||||
|
||||
|
||||
def _CallCallable(fn, args):
|
||||
"""Calls the function fn by consuming args from args.
|
||||
def _CallAndUpdateTrace(component, args, component_trace, treatment='class',
|
||||
target=None):
|
||||
"""Call the component by consuming args from args, and update the FireTrace.
|
||||
|
||||
The component could be a class, a routine, or a callable object. This function
|
||||
calls the component and adds the appropriate action to component_trace.
|
||||
|
||||
Args:
|
||||
fn: The function to call or class to instantiate.
|
||||
args: Args from which to consume for calling the function.
|
||||
component: The component to call
|
||||
args: Args for calling the component
|
||||
component_trace: FireTrace object that contains action trace
|
||||
treatment: Type of treatment used. Indicating whether we treat the component
|
||||
as a class, a routine, or a callable.
|
||||
target: Target in FireTrace element, default is None. If the value is None,
|
||||
the component itself will be used as target.
|
||||
Returns:
|
||||
component: The object that is the result of the function call.
|
||||
consumed_args: The args that were consumed for the function call.
|
||||
component: The object that is the result of the callable call.
|
||||
remaining_args: The remaining args that haven't been consumed yet.
|
||||
capacity: Whether the call could have taken additional args.
|
||||
"""
|
||||
if not target:
|
||||
target = component
|
||||
filename, lineno = inspectutils.GetFileAndLine(component)
|
||||
fn = component.__call__ if treatment == 'callable' else component
|
||||
parse = _MakeParseFn(fn)
|
||||
(varargs, kwargs), consumed_args, remaining_args, capacity = parse(args)
|
||||
component = fn(*varargs, **kwargs)
|
||||
|
||||
result = fn(*varargs, **kwargs)
|
||||
return result, consumed_args, remaining_args, capacity
|
||||
if treatment == 'class':
|
||||
action = trace.INSTANTIATED_CLASS
|
||||
elif treatment == 'routine':
|
||||
action = trace.CALLED_ROUTINE
|
||||
else:
|
||||
action = trace.CALLED_CALLABLE
|
||||
component_trace.AddCalledComponent(
|
||||
component, target, consumed_args, filename, lineno, capacity,
|
||||
action=action)
|
||||
|
||||
return component, remaining_args
|
||||
|
||||
|
||||
def _MakeParseFn(fn):
|
||||
@@ -565,7 +664,6 @@ def _MakeParseFn(fn):
|
||||
the leftover args from the arguments to the parse function.
|
||||
"""
|
||||
fn_spec = inspectutils.GetFullArgSpec(fn)
|
||||
all_args = fn_spec.args + fn_spec.kwonlyargs
|
||||
metadata = decorators.GetMetadata(fn)
|
||||
|
||||
# Note: num_required_args is the number of positional arguments without
|
||||
@@ -575,7 +673,7 @@ def _MakeParseFn(fn):
|
||||
|
||||
def _ParseFn(args):
|
||||
"""Parses the list of `args` into (varargs, kwargs), remaining_args."""
|
||||
kwargs, remaining_args = _ParseKeywordArgs(args, all_args, fn_spec.varkw)
|
||||
kwargs, remaining_kwargs, remaining_args = _ParseKeywordArgs(args, fn_spec)
|
||||
|
||||
# Note: _ParseArgs modifies kwargs.
|
||||
parsed_args, kwargs, remaining_args, capacity = _ParseArgs(
|
||||
@@ -604,6 +702,7 @@ def _MakeParseFn(fn):
|
||||
varargs[index] = _ParseValue(value, None, None, metadata)
|
||||
|
||||
varargs = parsed_args + varargs
|
||||
remaining_args += remaining_kwargs
|
||||
|
||||
consumed_args = args[:len(args) - len(remaining_args)]
|
||||
return (varargs, kwargs), consumed_args, remaining_args, capacity
|
||||
@@ -635,7 +734,7 @@ def _ParseArgs(fn_args, fn_defaults, num_required_args, kwargs,
|
||||
remaining_args: A list of the supplied args that have not been used yet.
|
||||
capacity: Whether the call could have taken args in place of defaults.
|
||||
Raises:
|
||||
FireError: if additional positional arguments are expected, but none are
|
||||
FireError: If additional positional arguments are expected, but none are
|
||||
available.
|
||||
"""
|
||||
accepts_positional_args = metadata.get(decorators.ACCEPTS_POSITIONAL_ARGS)
|
||||
@@ -670,10 +769,10 @@ def _ParseArgs(fn_args, fn_defaults, num_required_args, kwargs,
|
||||
return parsed_args, kwargs, remaining_args, capacity
|
||||
|
||||
|
||||
def _ParseKeywordArgs(args, fn_args, fn_keywords):
|
||||
def _ParseKeywordArgs(args, fn_spec):
|
||||
"""Parses the supplied arguments for keyword arguments.
|
||||
|
||||
Given a list of arguments, finds occurences of --name value, and uses 'name'
|
||||
Given a list of arguments, finds occurrences of --name value, and uses 'name'
|
||||
as the keyword and 'value' as the value. Constructs and returns a dictionary
|
||||
of these keyword arguments, and returns a list of the remaining arguments.
|
||||
|
||||
@@ -684,71 +783,131 @@ def _ParseKeywordArgs(args, fn_args, fn_keywords):
|
||||
_ParseArgs, which converts them to the appropriate type.
|
||||
|
||||
Args:
|
||||
args: A list of arguments
|
||||
fn_args: A list of argument names that the target function accepts,
|
||||
including positional and named arguments, but not the varargs or kwargs
|
||||
names.
|
||||
fn_keywords: The argument name for **kwargs, or None if **kwargs not used
|
||||
args: A list of arguments.
|
||||
fn_spec: The inspectutils.FullArgSpec describing the given callable.
|
||||
Returns:
|
||||
kwargs: A dictionary mapping keywords to values.
|
||||
remaining_kwargs: A list of the unused kwargs from the original args.
|
||||
remaining_args: A list of the unused arguments from the original args.
|
||||
Raises:
|
||||
FireError: If a single-character flag is passed that could refer to multiple
|
||||
possible args.
|
||||
"""
|
||||
kwargs = {}
|
||||
if args:
|
||||
remaining_args = []
|
||||
skip_argument = False
|
||||
remaining_kwargs = []
|
||||
remaining_args = []
|
||||
fn_keywords = fn_spec.varkw
|
||||
fn_args = fn_spec.args + fn_spec.kwonlyargs
|
||||
|
||||
for index, argument in enumerate(args):
|
||||
if skip_argument:
|
||||
skip_argument = False
|
||||
continue
|
||||
if not args:
|
||||
return kwargs, remaining_kwargs, remaining_args
|
||||
|
||||
arg_consumed = False
|
||||
if argument.startswith('--'):
|
||||
# This is a named argument; get its value from this arg or the next.
|
||||
skip_argument = False
|
||||
|
||||
for index, argument in enumerate(args):
|
||||
if skip_argument:
|
||||
skip_argument = False
|
||||
continue
|
||||
|
||||
if _IsFlag(argument):
|
||||
# This is a named argument. We get its value from this arg or the next.
|
||||
|
||||
# Terminology:
|
||||
# argument: A full token from the command line, e.g. '--alpha=10'
|
||||
# stripped_argument: An argument without leading hyphens.
|
||||
# key: The contents of the stripped argument up to the first equal sign.
|
||||
# "shortcut flag": refers to an argument where the key is just the first
|
||||
# letter of a longer keyword.
|
||||
# keyword: The Python function argument being set by this argument.
|
||||
# value: The unparsed value for that Python function argument.
|
||||
contains_equals = '=' in argument
|
||||
stripped_argument = argument.lstrip('-')
|
||||
if contains_equals:
|
||||
key, value = stripped_argument.split('=', 1)
|
||||
else:
|
||||
key = stripped_argument
|
||||
|
||||
key = key.replace('-', '_')
|
||||
is_bool_syntax = (not contains_equals and
|
||||
(index + 1 == len(args) or _IsFlag(args[index + 1])))
|
||||
|
||||
# Determine the keyword.
|
||||
keyword = '' # Indicates no valid keyword has been found yet.
|
||||
if (key in fn_args
|
||||
or (is_bool_syntax and key.startswith('no') and key[2:] in fn_args)
|
||||
or fn_keywords):
|
||||
keyword = key
|
||||
elif len(key) == 1:
|
||||
# This may be a shortcut flag.
|
||||
matching_fn_args = [arg for arg in fn_args if arg[0] == key]
|
||||
if len(matching_fn_args) == 1:
|
||||
keyword = matching_fn_args[0]
|
||||
elif len(matching_fn_args) > 1:
|
||||
raise FireError("The argument '{}' is ambiguous as it could "
|
||||
"refer to any of the following arguments: {}".format(
|
||||
argument, matching_fn_args))
|
||||
|
||||
# Determine the value.
|
||||
if not keyword:
|
||||
got_argument = False
|
||||
|
||||
keyword = argument[2:]
|
||||
contains_equals = '=' in keyword
|
||||
is_bool_syntax = (
|
||||
not contains_equals and
|
||||
(index + 1 == len(args) or args[index + 1].startswith('--')))
|
||||
if contains_equals:
|
||||
keyword, value = keyword.split('=', 1)
|
||||
got_argument = True
|
||||
elif is_bool_syntax:
|
||||
# Since there's no next arg or the next arg is a Flag, we consider
|
||||
# this flag to be a boolean.
|
||||
got_argument = True
|
||||
if keyword in fn_args:
|
||||
value = 'True'
|
||||
elif keyword.startswith('no'):
|
||||
keyword = keyword[2:]
|
||||
value = 'False'
|
||||
else:
|
||||
value = 'True'
|
||||
elif contains_equals:
|
||||
# Already got the value above.
|
||||
got_argument = True
|
||||
elif is_bool_syntax:
|
||||
# There's no next arg or the next arg is a Flag, so we consider this
|
||||
# flag to be a boolean.
|
||||
got_argument = True
|
||||
if keyword in fn_args:
|
||||
value = 'True'
|
||||
elif keyword.startswith('no'):
|
||||
keyword = keyword[2:]
|
||||
value = 'False'
|
||||
else:
|
||||
if index + 1 < len(args):
|
||||
value = args[index + 1]
|
||||
got_argument = True
|
||||
value = 'True'
|
||||
else:
|
||||
# The assert should pass. Otherwise either contains_equals or
|
||||
# is_bool_syntax would have been True.
|
||||
assert index + 1 < len(args)
|
||||
value = args[index + 1]
|
||||
got_argument = True
|
||||
|
||||
keyword = keyword.replace('-', '_')
|
||||
# In order for us to consume the argument as a keyword arg, we either:
|
||||
# Need to be explicitly expecting the keyword, or we need to be
|
||||
# accepting **kwargs.
|
||||
skip_argument = not contains_equals and not is_bool_syntax
|
||||
if got_argument:
|
||||
kwargs[keyword] = value
|
||||
else:
|
||||
remaining_kwargs.append(argument)
|
||||
if skip_argument:
|
||||
remaining_kwargs.append(args[index + 1])
|
||||
else: # not _IsFlag(argument)
|
||||
remaining_args.append(argument)
|
||||
|
||||
# In order for us to consume the argument as a keyword arg, we either:
|
||||
# Need to be explicitly expecting the keyword, or we need to be
|
||||
# accepting **kwargs.
|
||||
if got_argument and (keyword in fn_args or fn_keywords):
|
||||
kwargs[keyword] = value
|
||||
skip_argument = not contains_equals and not is_bool_syntax
|
||||
arg_consumed = True
|
||||
return kwargs, remaining_kwargs, remaining_args
|
||||
|
||||
if not arg_consumed:
|
||||
# The argument was not consumed, so it is still a remaining argument.
|
||||
remaining_args.append(argument)
|
||||
else:
|
||||
remaining_args = args
|
||||
|
||||
return kwargs, remaining_args
|
||||
def _IsFlag(argument):
|
||||
"""Determines if the argument is a flag argument.
|
||||
|
||||
If it starts with a hyphen and isn't a negative number, it's a flag.
|
||||
|
||||
Args:
|
||||
argument: A command line argument that may or may not be a flag.
|
||||
Returns:
|
||||
A boolean indicating whether the argument is a flag.
|
||||
"""
|
||||
return _IsSingleCharFlag(argument) or _IsMultiCharFlag(argument)
|
||||
|
||||
|
||||
def _IsSingleCharFlag(argument):
|
||||
"""Determines if the argument is a single char flag (e.g. '-a')."""
|
||||
return re.match('^-[a-zA-Z]$', argument) or re.match('^-[a-zA-Z]=', argument)
|
||||
|
||||
|
||||
def _IsMultiCharFlag(argument):
|
||||
"""Determines if the argument is a multi char flag (e.g. '--alpha')."""
|
||||
return argument.startswith('--') or re.match('^-[a-zA-Z]', argument)
|
||||
|
||||
|
||||
def _ParseValue(value, index, arg, metadata):
|
||||
|
||||
+80
-19
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -33,21 +33,26 @@ class CoreTest(testutils.BaseTestCase):
|
||||
self.assertEqual(core._OneLineResult({}), '{}') # pylint: disable=protected-access
|
||||
self.assertEqual(core._OneLineResult({'x': 'y'}), '{"x": "y"}') # pylint: disable=protected-access
|
||||
|
||||
def testOneLineResultCircularRef(self):
|
||||
circular_reference = tc.CircularReference()
|
||||
self.assertEqual(core._OneLineResult(circular_reference.create()), # pylint: disable=protected-access
|
||||
"{'y': {...}}")
|
||||
|
||||
@mock.patch('fire.interact.Embed')
|
||||
def testInteractiveMode(self, mock_embed):
|
||||
core.Fire(tc.TypedProperties, 'alpha')
|
||||
core.Fire(tc.TypedProperties, command=['alpha'])
|
||||
self.assertFalse(mock_embed.called)
|
||||
core.Fire(tc.TypedProperties, 'alpha -- -i')
|
||||
core.Fire(tc.TypedProperties, command=['alpha', '--', '-i'])
|
||||
self.assertTrue(mock_embed.called)
|
||||
|
||||
@mock.patch('fire.interact.Embed')
|
||||
def testInteractiveModeFullArgument(self, mock_embed):
|
||||
core.Fire(tc.TypedProperties, 'alpha -- --interactive')
|
||||
core.Fire(tc.TypedProperties, command=['alpha', '--', '--interactive'])
|
||||
self.assertTrue(mock_embed.called)
|
||||
|
||||
@mock.patch('fire.interact.Embed')
|
||||
def testInteractiveModeVariables(self, mock_embed):
|
||||
core.Fire(tc.WithDefaults, 'double 2 -- -i')
|
||||
core.Fire(tc.WithDefaults, command=['double', '2', '--', '-i'])
|
||||
self.assertTrue(mock_embed.called)
|
||||
(variables, verbose), unused_kwargs = mock_embed.call_args
|
||||
self.assertFalse(verbose)
|
||||
@@ -57,7 +62,8 @@ class CoreTest(testutils.BaseTestCase):
|
||||
|
||||
@mock.patch('fire.interact.Embed')
|
||||
def testInteractiveModeVariablesWithName(self, mock_embed):
|
||||
core.Fire(tc.WithDefaults, 'double 2 -- -i -v', name='D')
|
||||
core.Fire(tc.WithDefaults,
|
||||
command=['double', '2', '--', '-i', '-v'], name='D')
|
||||
self.assertTrue(mock_embed.called)
|
||||
(variables, verbose), unused_kwargs = mock_embed.call_args
|
||||
self.assertTrue(verbose)
|
||||
@@ -66,24 +72,53 @@ class CoreTest(testutils.BaseTestCase):
|
||||
self.assertEqual(variables['D'], tc.WithDefaults)
|
||||
self.assertIsInstance(variables['trace'], trace.FireTrace)
|
||||
|
||||
def testImproperUseOfHelp(self):
|
||||
# This should produce a warning explaining the proper use of help.
|
||||
with self.assertRaisesFireExit(2, 'The proper way to show help.*Usage:'):
|
||||
core.Fire(tc.TypedProperties, 'alpha --help')
|
||||
# TODO(dbieber): Use parameterized tests to break up repetitive tests.
|
||||
def testHelpWithClass(self):
|
||||
with self.assertRaisesFireExit(0, 'Usage:.*ARG1'):
|
||||
core.Fire(tc.InstanceVars, command=['--', '--help'])
|
||||
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*ARG1'):
|
||||
core.Fire(tc.InstanceVars, command=['--help'])
|
||||
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*ARG1'):
|
||||
core.Fire(tc.InstanceVars, command=['-h'])
|
||||
|
||||
def testProperUseOfHelp(self):
|
||||
with self.assertRaisesFireExit(0, 'Usage:.*upper'):
|
||||
core.Fire(tc.TypedProperties, 'gamma -- --help')
|
||||
def testHelpWithMember(self):
|
||||
with self.assertRaisesFireExit(0, 'Usage:.*capitalize'):
|
||||
core.Fire(tc.TypedProperties, command=['gamma', '--', '--help'])
|
||||
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*capitalize'):
|
||||
core.Fire(tc.TypedProperties, command=['gamma', '--help'])
|
||||
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*capitalize'):
|
||||
core.Fire(tc.TypedProperties, command=['gamma', '-h'])
|
||||
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*delta'):
|
||||
core.Fire(tc.TypedProperties, command=['delta', '--help'])
|
||||
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*echo'):
|
||||
core.Fire(tc.TypedProperties, command=['echo', '--help'])
|
||||
|
||||
def testHelpOnErrorInConstructor(self):
|
||||
with self.assertRaisesFireExit(0, 'Usage:.*[VALUE]'):
|
||||
core.Fire(tc.ErrorInConstructor, command=['--', '--help'])
|
||||
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*[VALUE]'):
|
||||
core.Fire(tc.ErrorInConstructor, command=['--help'])
|
||||
|
||||
def testHelpWithNamespaceCollision(self):
|
||||
# Tests cases when calling the help shortcut should not show help.
|
||||
with self.assertOutputMatches(stdout='Docstring.*', stderr=None):
|
||||
core.Fire(tc.WithHelpArg, command=['--help', 'False'])
|
||||
with self.assertOutputMatches(stdout='help in a dict', stderr=None):
|
||||
core.Fire(tc.WithHelpArg, command=['dictionary', '__help'])
|
||||
with self.assertOutputMatches(stdout='{}', stderr=None):
|
||||
core.Fire(tc.WithHelpArg, command=['dictionary', '--help'])
|
||||
with self.assertOutputMatches(stdout='False', stderr=None):
|
||||
core.Fire(tc.function_with_help, command=['False'])
|
||||
|
||||
def testInvalidParameterRaisesFireExit(self):
|
||||
with self.assertRaisesFireExit(2, 'runmisspelled'):
|
||||
core.Fire(tc.Kwargs, 'props --a=1 --b=2 runmisspelled')
|
||||
core.Fire(tc.Kwargs, command=['props', '--a=1', '--b=2', 'runmisspelled'])
|
||||
|
||||
def testErrorRaising(self):
|
||||
# Errors in user code should not be caught; they should surface as normal.
|
||||
# This will lead to exit status code 1 for the client program.
|
||||
with self.assertRaises(ValueError):
|
||||
core.Fire(tc.ErrorRaiser, 'fail')
|
||||
core.Fire(tc.ErrorRaiser, command=['fail'])
|
||||
|
||||
def testFireError(self):
|
||||
error = core.FireError('Example error')
|
||||
@@ -94,10 +129,36 @@ class CoreTest(testutils.BaseTestCase):
|
||||
self.assertIsNotNone(error)
|
||||
|
||||
def testPrintEmptyDict(self):
|
||||
with self.assertStdoutMatches('{}'):
|
||||
core.Fire(tc.EmptyDictOutput, 'totally_empty')
|
||||
with self.assertStdoutMatches('{}'):
|
||||
core.Fire(tc.EmptyDictOutput, 'nothing_printable')
|
||||
with self.assertOutputMatches(stdout='{}', stderr=None):
|
||||
core.Fire(tc.EmptyDictOutput, command=['totally_empty'])
|
||||
with self.assertOutputMatches(stdout='{}', stderr=None):
|
||||
core.Fire(tc.EmptyDictOutput, command=['nothing_printable'])
|
||||
|
||||
def testPrintOrderedDict(self):
|
||||
with self.assertOutputMatches(stdout=r'A:\s+A\s+2:\s+2\s+', stderr=None):
|
||||
core.Fire(tc.OrderedDictionary, command=['non_empty'])
|
||||
with self.assertOutputMatches(stdout='{}'):
|
||||
core.Fire(tc.OrderedDictionary, command=['empty'])
|
||||
|
||||
def testPrintNamedTupleField(self):
|
||||
with self.assertOutputMatches(stdout='11', stderr=None):
|
||||
core.Fire(tc.NamedTuple, command=['point', 'x'])
|
||||
|
||||
def testPrintNamedTupleIndex(self):
|
||||
with self.assertOutputMatches(stdout='22', stderr=None):
|
||||
core.Fire(tc.NamedTuple, command=['point', '1'])
|
||||
|
||||
def testPrintNamedTupleNegativeIndex(self):
|
||||
with self.assertOutputMatches(stdout='11', stderr=None):
|
||||
core.Fire(tc.NamedTuple, command=['point', '-2'])
|
||||
|
||||
def testCallable(self):
|
||||
with self.assertOutputMatches(stdout=r'foo:\s+foo\s+', stderr=None):
|
||||
core.Fire(tc.CallableWithKeywordArgument(), command=['--foo=foo'])
|
||||
with self.assertOutputMatches(stdout=r'foo\s+', stderr=None):
|
||||
core.Fire(tc.CallableWithKeywordArgument(), command=['print_msg', 'foo'])
|
||||
with self.assertOutputMatches(stdout=r'', stderr=None):
|
||||
core.Fire(tc.CallableWithKeywordArgument(), command=[])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
+42
-24
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -24,6 +24,7 @@ from fire import testutils
|
||||
|
||||
|
||||
class NoDefaults(object):
|
||||
"""A class for testing decorated functions without default values."""
|
||||
|
||||
@decorators.SetParseFns(count=int)
|
||||
def double(self, count):
|
||||
@@ -85,21 +86,21 @@ class WithKwargs(object):
|
||||
class WithVarArgs(object):
|
||||
|
||||
@decorators.SetParseFn(str)
|
||||
def example7(self, arg1, arg2=None, *varargs, **kwargs):
|
||||
def example7(self, arg1, arg2=None, *varargs, **kwargs): # pylint: disable=keyword-arg-before-vararg
|
||||
return arg1, arg2, varargs, kwargs
|
||||
|
||||
|
||||
class FireDecoratorsTest(testutils.BaseTestCase):
|
||||
|
||||
def testSetParseFnsNamedArgs(self):
|
||||
self.assertEqual(core.Fire(NoDefaults, 'double 2'), 4)
|
||||
self.assertEqual(core.Fire(NoDefaults, 'triple 4'), 12.0)
|
||||
self.assertEqual(core.Fire(NoDefaults, command=['double', '2']), 4)
|
||||
self.assertEqual(core.Fire(NoDefaults, command=['triple', '4']), 12.0)
|
||||
|
||||
def testSetParseFnsPositionalArgs(self):
|
||||
self.assertEqual(core.Fire(NoDefaults, 'quadruple 5'), 20)
|
||||
self.assertEqual(core.Fire(NoDefaults, command=['quadruple', '5']), 20)
|
||||
|
||||
def testSetParseFnsFnWithPositionalArgs(self):
|
||||
self.assertEqual(core.Fire(double, '5'), 10)
|
||||
self.assertEqual(core.Fire(double, command=['5']), 10)
|
||||
|
||||
def testSetParseFnsDefaultsFromPython(self):
|
||||
# When called from Python, function should behave normally.
|
||||
@@ -109,10 +110,13 @@ class FireDecoratorsTest(testutils.BaseTestCase):
|
||||
|
||||
def testSetParseFnsDefaultsFromFire(self):
|
||||
# Fire should use the decorator to know how to parse string arguments.
|
||||
self.assertEqual(core.Fire(WithDefaults, 'example1'), (10, int))
|
||||
self.assertEqual(core.Fire(WithDefaults, 'example1 10'), (10, float))
|
||||
self.assertEqual(core.Fire(WithDefaults, 'example1 13'), (13, float))
|
||||
self.assertEqual(core.Fire(WithDefaults, 'example1 14.0'), (14, float))
|
||||
self.assertEqual(core.Fire(WithDefaults, command=['example1']), (10, int))
|
||||
self.assertEqual(core.Fire(WithDefaults, command=['example1', '10']),
|
||||
(10, float))
|
||||
self.assertEqual(core.Fire(WithDefaults, command=['example1', '13']),
|
||||
(13, float))
|
||||
self.assertEqual(core.Fire(WithDefaults, command=['example1', '14.0']),
|
||||
(14, float))
|
||||
|
||||
def testSetParseFnsNamedDefaultsFromPython(self):
|
||||
# When called from Python, function should behave normally.
|
||||
@@ -122,33 +126,47 @@ class FireDecoratorsTest(testutils.BaseTestCase):
|
||||
|
||||
def testSetParseFnsNamedDefaultsFromFire(self):
|
||||
# Fire should use the decorator to know how to parse string arguments.
|
||||
self.assertEqual(core.Fire(WithDefaults, 'example2'), (10, int))
|
||||
self.assertEqual(core.Fire(WithDefaults, 'example2 10'), (10, float))
|
||||
self.assertEqual(core.Fire(WithDefaults, 'example2 13'), (13, float))
|
||||
self.assertEqual(core.Fire(WithDefaults, 'example2 14.0'), (14, float))
|
||||
self.assertEqual(core.Fire(WithDefaults, command=['example2']), (10, int))
|
||||
self.assertEqual(core.Fire(WithDefaults, command=['example2', '10']),
|
||||
(10, float))
|
||||
self.assertEqual(core.Fire(WithDefaults, command=['example2', '13']),
|
||||
(13, float))
|
||||
self.assertEqual(core.Fire(WithDefaults, command=['example2', '14.0']),
|
||||
(14, float))
|
||||
|
||||
def testSetParseFnsPositionalAndNamed(self):
|
||||
self.assertEqual(core.Fire(MixedArguments, 'example3 10 10'), (10, '10'))
|
||||
self.assertEqual(core.Fire(MixedArguments, ['example3', '10', '10']),
|
||||
(10, '10'))
|
||||
|
||||
def testSetParseFnsOnlySomeTypes(self):
|
||||
self.assertEqual(core.Fire(PartialParseFn, 'example4 10 10'), ('10', 10))
|
||||
self.assertEqual(core.Fire(PartialParseFn, 'example5 10 10'), (10, '10'))
|
||||
self.assertEqual(
|
||||
core.Fire(PartialParseFn, command=['example4', '10', '10']), ('10', 10))
|
||||
self.assertEqual(
|
||||
core.Fire(PartialParseFn, command=['example5', '10', '10']), (10, '10'))
|
||||
|
||||
def testSetParseFnsForKeywordArgs(self):
|
||||
self.assertEqual(core.Fire(WithKwargs, 'example6'), ('default', 0))
|
||||
self.assertEqual(
|
||||
core.Fire(WithKwargs, 'example6 --herring "red"'), ('default', 0))
|
||||
core.Fire(WithKwargs, command=['example6']), ('default', 0))
|
||||
self.assertEqual(
|
||||
core.Fire(WithKwargs, 'example6 --mode train'), ('train', 0))
|
||||
self.assertEqual(core.Fire(WithKwargs, 'example6 --mode 3'), ('3', 0))
|
||||
core.Fire(WithKwargs, command=['example6', '--herring', '"red"']),
|
||||
('default', 0))
|
||||
self.assertEqual(
|
||||
core.Fire(WithKwargs, 'example6 --mode -1 --count 10'), ('-1', 10))
|
||||
core.Fire(WithKwargs, command=['example6', '--mode', 'train']),
|
||||
('train', 0))
|
||||
self.assertEqual(core.Fire(WithKwargs, command=['example6', '--mode', '3']),
|
||||
('3', 0))
|
||||
self.assertEqual(
|
||||
core.Fire(WithKwargs, 'example6 --count -2'), ('default', -2))
|
||||
core.Fire(WithKwargs,
|
||||
command=['example6', '--mode', '-1', '--count', '10']),
|
||||
('-1', 10))
|
||||
self.assertEqual(
|
||||
core.Fire(WithKwargs, command=['example6', '--count', '-2']),
|
||||
('default', -2))
|
||||
|
||||
def testSetParseFn(self):
|
||||
self.assertEqual(
|
||||
core.Fire(WithVarArgs, 'example7 1 --arg2=2 3 4 --kwarg=5'),
|
||||
core.Fire(WithVarArgs,
|
||||
command=['example7', '1', '--arg2=2', '3', '4', '--kwarg=5']),
|
||||
('1', '2', ('3', '4'), {'kwarg': '5'}))
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,695 @@
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Docstring parsing module for Python Fire.
|
||||
|
||||
The following features of docstrings are not supported.
|
||||
TODO(dbieber): Support these features.
|
||||
- numpy docstrings may begin with the function signature.
|
||||
- whitespace may be important for proper structuring of a docstring
|
||||
- I've seen `argname` (with single backticks) as a style of documenting
|
||||
arguments. The `argname` appears on one line, and the description on the next.
|
||||
- .. Sphinx directives such as .. note:: are not understood.
|
||||
- After a section ends, future contents may be included in the section. E.g.
|
||||
:returns: This is what is returned.
|
||||
Example: An example goes here.
|
||||
- @param is sometimes used. E.g.
|
||||
@param argname (type) Description
|
||||
@return (type) Description
|
||||
- The true signature of a function is not used by the docstring parser. It could
|
||||
be useful for determining whether something is a section header or an argument
|
||||
for example.
|
||||
- This example confuses types as part of the docstrings.
|
||||
Parameters
|
||||
argname : argtype
|
||||
Arg description
|
||||
- If there's no blank line after the summary, the description will be slurped
|
||||
up into the summary.
|
||||
- "Examples" should be its own section type. aka "Usage".
|
||||
- "Notes" should be a section type.
|
||||
- Some people put parenthesis around their types in RST format, e.g.
|
||||
:param (type) paramname:
|
||||
- :rtype: directive (return type)
|
||||
- Also ":rtype str" with no closing ":" has come up.
|
||||
- Return types are not supported.
|
||||
- "# Returns" as a section title style
|
||||
- ":raises ExceptionType: Description" ignores the ExceptionType currently.
|
||||
- "Defaults to X" occurs sometimes.
|
||||
- "True | False" indicates bool type.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
|
||||
import collections
|
||||
import re
|
||||
|
||||
import enum
|
||||
|
||||
|
||||
class DocstringInfo(
|
||||
collections.namedtuple(
|
||||
'DocstringInfo',
|
||||
('summary', 'description', 'args', 'returns', 'yields', 'raises'))):
|
||||
pass
|
||||
DocstringInfo.__new__.__defaults__ = (None,) * len(DocstringInfo._fields)
|
||||
|
||||
|
||||
class ArgInfo(
|
||||
collections.namedtuple(
|
||||
'ArgInfo',
|
||||
('name', 'type', 'description'))):
|
||||
pass
|
||||
ArgInfo.__new__.__defaults__ = (None,) * len(ArgInfo._fields)
|
||||
|
||||
|
||||
class Namespace(dict):
|
||||
"""A dict with attribute (dot-notation) access enabled."""
|
||||
|
||||
def __getattr__(self, key):
|
||||
if key not in self:
|
||||
self[key] = Namespace()
|
||||
return self[key]
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
self[key] = value
|
||||
|
||||
def __delattr__(self, key):
|
||||
if key in self:
|
||||
del self[key]
|
||||
|
||||
|
||||
class Sections(enum.Enum):
|
||||
ARGS = 0
|
||||
RETURNS = 1
|
||||
YIELDS = 2
|
||||
RAISES = 3
|
||||
TYPE = 4
|
||||
|
||||
|
||||
class Formats(enum.Enum):
|
||||
GOOGLE = 0
|
||||
NUMPY = 1
|
||||
RST = 2
|
||||
|
||||
|
||||
SECTION_TITLES = {
|
||||
Sections.ARGS: ('argument', 'arg', 'parameter', 'param'),
|
||||
Sections.RETURNS: ('return',),
|
||||
Sections.YIELDS: ('yield',),
|
||||
Sections.RAISES: ('raise', 'except', 'exception', 'throw', 'error', 'warn'),
|
||||
Sections.TYPE: ('type',), # rst-only
|
||||
}
|
||||
|
||||
|
||||
def parse(docstring):
|
||||
"""Returns DocstringInfo about the given docstring.
|
||||
|
||||
This parser aims to parse Google, numpy, and rst formatted docstrings. These
|
||||
are the three most common docstring styles at the time of this writing.
|
||||
|
||||
This parser aims to be permissive, working even when the docstring deviates
|
||||
from the strict recommendations of these styles.
|
||||
|
||||
This parser does not aim to fully extract all structured information from a
|
||||
docstring, since there are simply too many ways to structure information in a
|
||||
docstring. Sometimes content will remain as unstructured text and simply gets
|
||||
included in the description.
|
||||
|
||||
The Google docstring style guide is available at:
|
||||
https://github.com/google/styleguide/blob/gh-pages/pyguide.md
|
||||
|
||||
The numpy docstring style guide is available at:
|
||||
https://numpydoc.readthedocs.io/en/latest/format.html
|
||||
|
||||
Information about the rST docstring format is available at:
|
||||
https://www.python.org/dev/peps/pep-0287/
|
||||
The full set of directives such as param and type for rST docstrings are at:
|
||||
http://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html
|
||||
|
||||
Note: This function does not claim to handle all docstrings well. A list of
|
||||
limitations is available at the top of the file. It does aim to run without
|
||||
crashing in O(n) time on all strings on length n. If you find a string that
|
||||
causes this to crash or run unacceptably slowly, please consider submitting
|
||||
a pull request.
|
||||
|
||||
Args:
|
||||
docstring: The docstring to parse.
|
||||
Returns:
|
||||
A DocstringInfo containing information about the docstring.
|
||||
"""
|
||||
if docstring is None:
|
||||
return DocstringInfo()
|
||||
|
||||
lines = docstring.strip().split('\n')
|
||||
lines_len = len(lines)
|
||||
state = Namespace() # TODO(dbieber): Switch to an explicit class.
|
||||
|
||||
# Variables in state include:
|
||||
state.section.title = None
|
||||
state.section.indentation = None
|
||||
state.section.line1_indentation = None
|
||||
state.section.format = None
|
||||
state.summary.permitted = True
|
||||
state.summary.lines = []
|
||||
state.description.lines = []
|
||||
state.args = []
|
||||
state.current_arg = None
|
||||
state.returns.lines = []
|
||||
state.yields.lines = []
|
||||
state.raises.lines = []
|
||||
|
||||
for index, line in enumerate(lines):
|
||||
has_next = index + 1 < lines_len
|
||||
next_line = lines[index + 1] if has_next else None
|
||||
line_info = _create_line_info(line, next_line)
|
||||
_consume_line(line_info, state)
|
||||
|
||||
summary = ' '.join(state.summary.lines) if state.summary.lines else None
|
||||
description = _join_lines(state.description.lines)
|
||||
returns = _join_lines(state.returns.lines)
|
||||
yields = _join_lines(state.yields.lines)
|
||||
raises = _join_lines(state.raises.lines)
|
||||
|
||||
args = [
|
||||
ArgInfo(
|
||||
name=arg.name,
|
||||
type=_cast_to_known_type(_join_lines(arg.type.lines)),
|
||||
description=_join_lines(arg.description.lines),
|
||||
)
|
||||
for arg in state.args
|
||||
]
|
||||
|
||||
return DocstringInfo(
|
||||
summary=summary,
|
||||
description=description,
|
||||
args=args or None,
|
||||
returns=returns,
|
||||
raises=raises,
|
||||
yields=yields,
|
||||
)
|
||||
|
||||
|
||||
def _join_lines(lines):
|
||||
"""Joins lines with the appropriate connective whitespace.
|
||||
|
||||
This puts a single space between consecutive lines, unless there's a blank
|
||||
line, in which case a full blank line is included.
|
||||
|
||||
Args:
|
||||
lines: A list of lines to join.
|
||||
Returns:
|
||||
A string, the lines joined together.
|
||||
"""
|
||||
# TODO(dbieber): Add parameters for variations in whitespace handling.
|
||||
if not lines:
|
||||
return None
|
||||
|
||||
started = False
|
||||
group_texts = [] # Full text of each section.
|
||||
group_lines = [] # Lines within the current section.
|
||||
for line in lines:
|
||||
stripped_line = line.strip()
|
||||
if stripped_line:
|
||||
started = True
|
||||
group_lines.append(stripped_line)
|
||||
else:
|
||||
if started:
|
||||
group_text = ' '.join(group_lines)
|
||||
group_texts.append(group_text)
|
||||
group_lines = []
|
||||
|
||||
if group_lines: # Process the final group.
|
||||
group_text = ' '.join(group_lines)
|
||||
group_texts.append(group_text)
|
||||
|
||||
return '\n\n'.join(group_texts)
|
||||
|
||||
|
||||
def _get_or_create_arg_by_name(state, name):
|
||||
"""Gets or creates a new Arg.
|
||||
|
||||
These Arg objects (Namespaces) are turned into the ArgInfo namedtuples
|
||||
returned by parse. Each Arg object is used to collect the name, type, and
|
||||
description of a single argument to the docstring's function.
|
||||
|
||||
Args:
|
||||
state: The state of the parser.
|
||||
name: The name of the arg to create.
|
||||
Returns:
|
||||
The new Arg.
|
||||
"""
|
||||
for arg in state.args:
|
||||
if arg.name == name:
|
||||
return arg
|
||||
arg = Namespace() # TODO(dbieber): Switch to an explicit class.
|
||||
arg.name = name
|
||||
arg.type.lines = []
|
||||
arg.description.lines = []
|
||||
state.args.append(arg)
|
||||
return arg
|
||||
|
||||
|
||||
def _is_arg_name(name):
|
||||
"""Returns whether name is a valid arg name.
|
||||
|
||||
This is used to prevent multiple words (plaintext) from being 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.
|
||||
|
||||
Args:
|
||||
name: The name of the potential arg.
|
||||
Returns:
|
||||
True if name looks like an arg name, False otherwise.
|
||||
"""
|
||||
name = name.strip()
|
||||
return (name
|
||||
and ' ' not in name
|
||||
and ':' not in name)
|
||||
|
||||
|
||||
def _as_arg_name_and_type(text):
|
||||
"""Returns text as a name and type, if text looks like an arg name and type.
|
||||
|
||||
Example:
|
||||
_as_arg_name_and_type("foo (int)") == "foo", "int"
|
||||
|
||||
Args:
|
||||
text: The text, which may or may not be an arg name and type.
|
||||
Returns:
|
||||
The arg name and type, if text looks like an arg name and type.
|
||||
None otherwise.
|
||||
"""
|
||||
tokens = text.split()
|
||||
if len(tokens) < 2:
|
||||
return None
|
||||
if _is_arg_name(tokens[0]):
|
||||
type_token = ' '.join(tokens[1:])
|
||||
type_token = type_token.lstrip('{([').rstrip('])}')
|
||||
return tokens[0], type_token
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _as_arg_names(names_str):
|
||||
"""Converts names_str to a list of arg names.
|
||||
|
||||
Example:
|
||||
_as_arg_names("a, b, c") == ["a", "b", "c"]
|
||||
|
||||
Args:
|
||||
names_str: A string with multiple space or comma separated arg names.
|
||||
Returns:
|
||||
A list of arg names, or None if names_str doesn't look like a list of arg
|
||||
names.
|
||||
"""
|
||||
names = re.split(',| ', names_str)
|
||||
names = [name.strip() for name in names if name.strip()]
|
||||
for name in names:
|
||||
if not _is_arg_name(name):
|
||||
return None
|
||||
if not names:
|
||||
return None
|
||||
return names
|
||||
|
||||
|
||||
def _cast_to_known_type(name):
|
||||
"""Canonicalizes a string representing a type if possible.
|
||||
|
||||
# TODO(dbieber): Support additional canonicalization, such as string/str, and
|
||||
# boolean/bool.
|
||||
|
||||
Example:
|
||||
_cast_to_known_type("str.") == "str"
|
||||
|
||||
Args:
|
||||
name: A string representing a type, or None.
|
||||
Returns:
|
||||
A canonicalized version of the type string.
|
||||
"""
|
||||
if name is None:
|
||||
return None
|
||||
return name.rstrip('.')
|
||||
|
||||
|
||||
def _consume_google_args_line(line_info, state):
|
||||
"""Consume a single line from a Google args section."""
|
||||
split_line = line_info.remaining.split(':', 1)
|
||||
if len(split_line) > 1:
|
||||
first, second = split_line # first is either the "arg" or "arg (type)"
|
||||
if _is_arg_name(first.strip()):
|
||||
arg = _get_or_create_arg_by_name(state, first.strip())
|
||||
arg.description.lines.append(second.strip())
|
||||
state.current_arg = arg
|
||||
else:
|
||||
arg_name_and_type = _as_arg_name_and_type(first)
|
||||
if arg_name_and_type:
|
||||
arg_name, type_str = arg_name_and_type
|
||||
arg = _get_or_create_arg_by_name(state, arg_name)
|
||||
arg.type.lines.append(type_str)
|
||||
arg.description.lines.append(second.strip())
|
||||
else:
|
||||
if state.current_arg:
|
||||
state.current_arg.description.lines.append(split_line[0])
|
||||
else:
|
||||
if state.current_arg:
|
||||
state.current_arg.description.lines.append(split_line[0])
|
||||
|
||||
|
||||
def _consume_line(line_info, state):
|
||||
"""Consumes one line of text, updating the state accordingly.
|
||||
|
||||
When _consume_line is called, part of the line may already have been processed
|
||||
for header information.
|
||||
|
||||
Args:
|
||||
line_info: Information about the current and next line of the docstring.
|
||||
state: The state of the docstring parser.
|
||||
"""
|
||||
_update_section_state(line_info, state)
|
||||
|
||||
if state.section.title is None:
|
||||
if state.summary.permitted:
|
||||
if line_info.remaining:
|
||||
state.summary.lines.append(line_info.remaining)
|
||||
elif state.summary.lines:
|
||||
state.summary.permitted = False
|
||||
else:
|
||||
# We're past the end of the summary.
|
||||
# Additions now contribute to the description.
|
||||
state.description.lines.append(line_info.remaining)
|
||||
else:
|
||||
state.summary.permitted = False
|
||||
|
||||
if state.section.new and state.section.format == Formats.RST:
|
||||
# The current line starts with an RST directive, e.g. ":param arg:".
|
||||
directive = _get_directive(line_info)
|
||||
directive_tokens = directive.split() # pytype: disable=attribute-error
|
||||
if state.section.title == Sections.ARGS:
|
||||
name = directive_tokens[-1]
|
||||
arg = _get_or_create_arg_by_name(state, name)
|
||||
if len(directive_tokens) == 3:
|
||||
# A param directive of the form ":param type arg:".
|
||||
arg.type.lines.append(directive_tokens[1])
|
||||
state.current_arg = arg
|
||||
elif state.section.title == Sections.TYPE:
|
||||
name = directive_tokens[-1]
|
||||
arg = _get_or_create_arg_by_name(state, name)
|
||||
state.current_arg = arg
|
||||
|
||||
if (state.section.format == Formats.NUMPY and
|
||||
_line_is_hyphens(line_info.remaining)):
|
||||
# Skip this all-hyphens line, which is part of the numpy section header.
|
||||
return
|
||||
|
||||
if state.section.title == Sections.ARGS:
|
||||
if state.section.format == Formats.GOOGLE:
|
||||
_consume_google_args_line(line_info, state)
|
||||
elif state.section.format == Formats.RST:
|
||||
state.current_arg.description.lines.append(line_info.remaining.strip())
|
||||
elif state.section.format == Formats.NUMPY:
|
||||
line_stripped = line_info.remaining.strip()
|
||||
if _is_arg_name(line_stripped):
|
||||
# Token on it's own line can either be the last word of the description
|
||||
# of the previous arg, or a new arg. TODO: Whitespace can distinguish.
|
||||
arg = _get_or_create_arg_by_name(state, line_stripped)
|
||||
state.current_arg = arg
|
||||
elif ':' in line_stripped:
|
||||
possible_args, type_data = line_stripped.split(':', 1)
|
||||
arg_names = _as_arg_names(possible_args) # re.split(' |,', s)
|
||||
if arg_names:
|
||||
for arg_name in arg_names:
|
||||
arg = _get_or_create_arg_by_name(state, arg_name)
|
||||
arg.type.lines.append(type_data)
|
||||
state.current_arg = arg # TODO(dbieber): Multiple current args.
|
||||
else: # Just an ordinary line.
|
||||
if state.current_arg:
|
||||
state.current_arg.description.lines.append(
|
||||
line_info.remaining.strip())
|
||||
else:
|
||||
# TODO(dbieber): If not a blank line, add it to the description.
|
||||
pass
|
||||
else: # Just an ordinary line.
|
||||
if state.current_arg:
|
||||
state.current_arg.description.lines.append(
|
||||
line_info.remaining.strip())
|
||||
else:
|
||||
# TODO(dbieber): If not a blank line, add it to the description.
|
||||
pass
|
||||
|
||||
elif state.section.title == Sections.RETURNS:
|
||||
state.returns.lines.append(line_info.remaining.strip())
|
||||
elif state.section.title == Sections.YIELDS:
|
||||
state.yields.lines.append(line_info.remaining.strip())
|
||||
elif state.section.title == Sections.RAISES:
|
||||
state.raises.lines.append(line_info.remaining.strip())
|
||||
elif state.section.title == Sections.TYPE:
|
||||
if state.section.format == Formats.RST:
|
||||
assert state.current_arg is not None
|
||||
state.current_arg.type.lines.append(line_info.remaining.strip())
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
def _create_line_info(line, next_line):
|
||||
"""Returns information about the current and next line of the docstring."""
|
||||
line_info = Namespace() # TODO(dbieber): Switch to an explicit class.
|
||||
line_info.line = line
|
||||
line_info.stripped = line.strip()
|
||||
line_info.remaining = line_info.stripped
|
||||
line_info.indentation = len(line) - len(line.lstrip())
|
||||
line_info.next.line = next_line
|
||||
line_info.next.stripped = next_line.strip() if next_line else None
|
||||
line_info.next.indentation = (
|
||||
len(next_line) - len(next_line.lstrip()) if next_line else None)
|
||||
# Note: This counts all whitespace equally.
|
||||
return line_info
|
||||
|
||||
|
||||
def _update_section_state(line_info, state):
|
||||
"""Uses line_info to determine the current section of the docstring.
|
||||
|
||||
Updates state and line_info.remaining.
|
||||
|
||||
Args:
|
||||
line_info: Information about the current line.
|
||||
state: The state of the parser.
|
||||
"""
|
||||
section_updated = False
|
||||
|
||||
google_section_permitted = _google_section_permitted(line_info, state)
|
||||
google_section = google_section_permitted and _google_section(line_info)
|
||||
if google_section:
|
||||
state.section.format = Formats.GOOGLE
|
||||
state.section.title = google_section
|
||||
line_info.remaining = _get_after_google_header(line_info)
|
||||
section_updated = True
|
||||
|
||||
rst_section = _rst_section(line_info)
|
||||
if rst_section:
|
||||
state.section.format = Formats.RST
|
||||
state.section.title = rst_section
|
||||
line_info.remaining = _get_after_directive(line_info)
|
||||
section_updated = True
|
||||
|
||||
numpy_section = _numpy_section(line_info)
|
||||
if numpy_section:
|
||||
state.section.format = Formats.NUMPY
|
||||
state.section.title = numpy_section
|
||||
line_info.remaining = ''
|
||||
section_updated = True
|
||||
|
||||
if section_updated:
|
||||
state.section.new = True
|
||||
state.section.indentation = line_info.indentation
|
||||
state.section.line1_indentation = line_info.next.indentation
|
||||
else:
|
||||
state.section.new = False
|
||||
|
||||
|
||||
def _google_section_permitted(line_info, state):
|
||||
"""Returns whether a new google section is permitted to start here.
|
||||
|
||||
Q: Why might a new Google section not be allowed?
|
||||
A: If we're in the middle of a Google "Args" section, then lines that start
|
||||
"param:" will usually be a new arg, rather than a new section.
|
||||
We use whitespace to determine when the Args section has actually ended.
|
||||
|
||||
A Google section ends when either:
|
||||
- A new google section begins at either
|
||||
- indentation less than indentation of line 1 of the previous section
|
||||
- or <= indentation of the previous section
|
||||
- Or the docstring terminates.
|
||||
|
||||
Args:
|
||||
line_info: Information about the current line.
|
||||
state: The state of the parser.
|
||||
Returns:
|
||||
True or False, indicating whether a new Google section is permitted at the
|
||||
current line.
|
||||
"""
|
||||
if state.section.indentation is None: # We're not in a section yet.
|
||||
return True
|
||||
return (line_info.indentation <= state.section.indentation
|
||||
or line_info.indentation < state.section.line1_indentation)
|
||||
|
||||
|
||||
def _matches_section_title(title, section_title):
|
||||
"""Returns whether title is a match for a specific section_title.
|
||||
|
||||
Example:
|
||||
_matches_section_title('Yields', 'yield') == True
|
||||
|
||||
Args:
|
||||
title: The title to check for matching.
|
||||
section_title: A specific known section title to check against.
|
||||
"""
|
||||
title = title.lower()
|
||||
section_title = section_title.lower()
|
||||
return section_title in (title, title[:-1]) # Supports plurals / some typos.
|
||||
|
||||
|
||||
def _matches_section(title, section):
|
||||
"""Returns whether title is a match any known title for a specific section.
|
||||
|
||||
Example:
|
||||
_matches_section_title('Yields', Sections.YIELDS) == True
|
||||
_matches_section_title('param', Sections.Args) == True
|
||||
|
||||
Args:
|
||||
title: The title to check for matching.
|
||||
section: A specific section to check all possible titles for.
|
||||
Returns:
|
||||
True or False, indicating whether title is a match for the specified
|
||||
section.
|
||||
"""
|
||||
for section_title in SECTION_TITLES[section]:
|
||||
if _matches_section_title(title, section_title):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _section_from_possible_title(possible_title):
|
||||
"""Returns a section matched by the possible title, or None if none match.
|
||||
|
||||
Args:
|
||||
possible_title: A string that may be the title of a new section.
|
||||
Returns:
|
||||
A Section type if one matches, or None if no section type matches.
|
||||
"""
|
||||
for section in SECTION_TITLES:
|
||||
if _matches_section(possible_title, section):
|
||||
return section
|
||||
return None
|
||||
|
||||
|
||||
def _google_section(line_info):
|
||||
"""Checks whether the current line is the start of a new Google-style section.
|
||||
|
||||
This docstring is a Google-style docstring. Google-style sections look like
|
||||
this:
|
||||
|
||||
Section Name:
|
||||
section body goes here
|
||||
|
||||
Args:
|
||||
line_info: Information about the current line.
|
||||
Returns:
|
||||
A Section type if one matches, or None if no section type matches.
|
||||
"""
|
||||
colon_index = line_info.remaining.find(':')
|
||||
possible_title = line_info.remaining[:colon_index]
|
||||
return _section_from_possible_title(possible_title)
|
||||
|
||||
|
||||
def _get_after_google_header(line_info):
|
||||
"""Gets the remainder of the line, after a Google header."""
|
||||
colon_index = line_info.remaining.find(':')
|
||||
return line_info.remaining[colon_index + 1:]
|
||||
|
||||
|
||||
def _get_directive(line_info):
|
||||
"""Gets a directive from the start of the line.
|
||||
|
||||
If the line is ":param str foo: Description of foo", then
|
||||
_get_directive(line_info) returns "param str foo".
|
||||
|
||||
Args:
|
||||
line_info: Information about the current line.
|
||||
Returns:
|
||||
The contents of a directive, or None if the line doesn't start with a
|
||||
directive.
|
||||
"""
|
||||
if line_info.stripped.startswith(':'):
|
||||
return line_info.stripped.split(':', 2)[1]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _get_after_directive(line_info):
|
||||
"""Gets the remainder of the line, after a directive."""
|
||||
sections = line_info.stripped.split(':', 2)
|
||||
if len(sections) > 2:
|
||||
return sections[-1]
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
def _rst_section(line_info):
|
||||
"""Checks whether the current line is the start of a new RST-style section.
|
||||
|
||||
RST uses directives to specify information. An RST directive, which we refer
|
||||
to as a section here, are surrounded with colons. For example, :param name:.
|
||||
|
||||
Args:
|
||||
line_info: Information about the current line.
|
||||
Returns:
|
||||
A Section type if one matches, or None if no section type matches.
|
||||
"""
|
||||
directive = _get_directive(line_info)
|
||||
if directive:
|
||||
possible_title = directive.split()[0]
|
||||
return _section_from_possible_title(possible_title)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _line_is_hyphens(line):
|
||||
"""Returns whether the line is entirely hyphens (and not blank)."""
|
||||
return line and not line.strip('-')
|
||||
|
||||
|
||||
def _numpy_section(line_info):
|
||||
"""Checks whether the current line is the start of a new numpy-style section.
|
||||
|
||||
Numpy style sections are followed by a full line of hyphens, for example:
|
||||
|
||||
Section Name
|
||||
------------
|
||||
Section body goes here.
|
||||
|
||||
Args:
|
||||
line_info: Information about the current line.
|
||||
Returns:
|
||||
A Section type if one matches, or None if no section type matches.
|
||||
"""
|
||||
next_line_is_hyphens = _line_is_hyphens(line_info.next.stripped)
|
||||
if next_line_is_hyphens:
|
||||
possible_title = line_info.remaining
|
||||
return _section_from_possible_title(possible_title)
|
||||
else:
|
||||
return None
|
||||
@@ -0,0 +1,40 @@
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Fuzz tests for the docstring parser module."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
from fire import docstrings
|
||||
from fire import testutils
|
||||
|
||||
from hypothesis import example
|
||||
from hypothesis import given
|
||||
from hypothesis import settings
|
||||
from hypothesis import strategies as st
|
||||
|
||||
|
||||
class DocstringsFuzzTest(testutils.BaseTestCase):
|
||||
|
||||
@settings(max_examples=1000, deadline=1000)
|
||||
@given(st.text(min_size=1))
|
||||
@example('This is a one-line docstring.')
|
||||
def test_fuzz_parse(self, value):
|
||||
docstrings.parse(value)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
testutils.main()
|
||||
@@ -0,0 +1,237 @@
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for fire docstrings module."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
from fire import docstrings
|
||||
from fire import testutils
|
||||
|
||||
|
||||
DocstringInfo = docstrings.DocstringInfo # pylint: disable=invalid-name
|
||||
ArgInfo = docstrings.ArgInfo # pylint: disable=invalid-name
|
||||
|
||||
|
||||
class DocstringsTest(testutils.BaseTestCase):
|
||||
|
||||
def test_one_line_simple(self):
|
||||
docstring = """A simple one line docstring."""
|
||||
docstring_info = docstrings.parse(docstring)
|
||||
expected_docstring_info = DocstringInfo(
|
||||
summary='A simple one line docstring.',
|
||||
)
|
||||
self.assertEqual(docstring_info, expected_docstring_info)
|
||||
|
||||
def test_one_line_simple_whitespace(self):
|
||||
docstring = """
|
||||
A simple one line docstring.
|
||||
"""
|
||||
docstring_info = docstrings.parse(docstring)
|
||||
expected_docstring_info = DocstringInfo(
|
||||
summary='A simple one line docstring.',
|
||||
)
|
||||
self.assertEqual(docstring_info, expected_docstring_info)
|
||||
|
||||
def test_one_line_too_long(self):
|
||||
# pylint: disable=line-too-long
|
||||
docstring = """A one line docstring thats both a little too verbose and a little too long so it keeps going well beyond a reasonable length for a one-liner.
|
||||
"""
|
||||
# pylint: enable=line-too-long
|
||||
docstring_info = docstrings.parse(docstring)
|
||||
expected_docstring_info = DocstringInfo(
|
||||
summary='A one line docstring thats both a little too verbose and '
|
||||
'a little too long so it keeps going well beyond a reasonable length '
|
||||
'for a one-liner.',
|
||||
)
|
||||
self.assertEqual(docstring_info, expected_docstring_info)
|
||||
|
||||
def test_one_line_runs_over(self):
|
||||
# pylint: disable=line-too-long
|
||||
docstring = """A one line docstring thats both a little too verbose and a little too long
|
||||
so it runs onto a second line.
|
||||
"""
|
||||
# pylint: enable=line-too-long
|
||||
docstring_info = docstrings.parse(docstring)
|
||||
expected_docstring_info = DocstringInfo(
|
||||
summary='A one line docstring thats both a little too verbose and '
|
||||
'a little too long so it runs onto a second line.',
|
||||
)
|
||||
self.assertEqual(docstring_info, expected_docstring_info)
|
||||
|
||||
def test_one_line_runs_over_whitespace(self):
|
||||
docstring = """
|
||||
A one line docstring thats both a little too verbose and a little too long
|
||||
so it runs onto a second line.
|
||||
"""
|
||||
docstring_info = docstrings.parse(docstring)
|
||||
expected_docstring_info = DocstringInfo(
|
||||
summary='A one line docstring thats both a little too verbose and '
|
||||
'a little too long so it runs onto a second line.',
|
||||
)
|
||||
self.assertEqual(docstring_info, expected_docstring_info)
|
||||
|
||||
def test_google_format_args_only(self):
|
||||
docstring = """One line description.
|
||||
|
||||
Args:
|
||||
arg1: arg1_description
|
||||
arg2: arg2_description
|
||||
"""
|
||||
docstring_info = docstrings.parse(docstring)
|
||||
expected_docstring_info = DocstringInfo(
|
||||
summary='One line description.',
|
||||
args=[
|
||||
ArgInfo(name='arg1', description='arg1_description'),
|
||||
ArgInfo(name='arg2', description='arg2_description'),
|
||||
]
|
||||
)
|
||||
self.assertEqual(docstring_info, expected_docstring_info)
|
||||
|
||||
def test_google_format_arg_named_args(self):
|
||||
docstring = """
|
||||
Args:
|
||||
args: arg_description
|
||||
"""
|
||||
docstring_info = docstrings.parse(docstring)
|
||||
expected_docstring_info = DocstringInfo(
|
||||
args=[
|
||||
ArgInfo(name='args', description='arg_description'),
|
||||
]
|
||||
)
|
||||
self.assertEqual(docstring_info, expected_docstring_info)
|
||||
|
||||
def test_google_format_typed_args_and_returns(self):
|
||||
docstring = """Docstring summary.
|
||||
|
||||
This is a longer description of the docstring. It spans multiple lines, as
|
||||
is allowed.
|
||||
|
||||
Args:
|
||||
param1 (int): The first parameter.
|
||||
param2 (str): The second parameter.
|
||||
|
||||
Returns:
|
||||
bool: The return value. True for success, False otherwise.
|
||||
"""
|
||||
docstring_info = docstrings.parse(docstring)
|
||||
expected_docstring_info = DocstringInfo(
|
||||
summary='Docstring summary.',
|
||||
description='This is a longer description of the docstring. It spans '
|
||||
'multiple lines, as is allowed.',
|
||||
args=[
|
||||
ArgInfo(name='param1', type='int',
|
||||
description='The first parameter.'),
|
||||
ArgInfo(name='param2', type='str',
|
||||
description='The second parameter.'),
|
||||
],
|
||||
returns='bool: The return value. True for success, False otherwise.'
|
||||
)
|
||||
self.assertEqual(docstring_info, expected_docstring_info)
|
||||
|
||||
def test_rst_format_typed_args_and_returns(self):
|
||||
docstring = """Docstring summary.
|
||||
|
||||
This is a longer description of the docstring. It spans across multiple
|
||||
lines.
|
||||
|
||||
:param arg1: Description of arg1.
|
||||
:type arg1: str.
|
||||
:param arg2: Description of arg2.
|
||||
:type arg2: bool.
|
||||
:returns: int -- description of the return value.
|
||||
:raises: AttributeError, KeyError
|
||||
"""
|
||||
docstring_info = docstrings.parse(docstring)
|
||||
expected_docstring_info = DocstringInfo(
|
||||
summary='Docstring summary.',
|
||||
description='This is a longer description of the docstring. It spans '
|
||||
'across multiple lines.',
|
||||
args=[
|
||||
ArgInfo(name='arg1', type='str',
|
||||
description='Description of arg1.'),
|
||||
ArgInfo(name='arg2', type='bool',
|
||||
description='Description of arg2.'),
|
||||
],
|
||||
returns='int -- description of the return value.',
|
||||
raises='AttributeError, KeyError',
|
||||
)
|
||||
self.assertEqual(docstring_info, expected_docstring_info)
|
||||
|
||||
def test_numpy_format_typed_args_and_returns(self):
|
||||
docstring = """Docstring summary.
|
||||
|
||||
This is a longer description of the docstring. It spans across multiple
|
||||
lines.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
param1 : int
|
||||
The first parameter.
|
||||
param2 : str
|
||||
The second parameter.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if successful, False otherwise.
|
||||
"""
|
||||
docstring_info = docstrings.parse(docstring)
|
||||
expected_docstring_info = DocstringInfo(
|
||||
summary='Docstring summary.',
|
||||
description='This is a longer description of the docstring. It spans '
|
||||
'across multiple lines.',
|
||||
args=[
|
||||
ArgInfo(name='param1', type='int',
|
||||
description='The first parameter.'),
|
||||
ArgInfo(name='param2', type='str',
|
||||
description='The second parameter.'),
|
||||
],
|
||||
# TODO(dbieber): Support return type.
|
||||
returns='bool True if successful, False otherwise.',
|
||||
)
|
||||
self.assertEqual(docstring_info, expected_docstring_info)
|
||||
|
||||
def test_multisection_docstring(self):
|
||||
docstring = """Docstring summary.
|
||||
|
||||
This is the first section of a docstring description.
|
||||
|
||||
This is the second section of a docstring description. This docstring
|
||||
description has just two sections.
|
||||
"""
|
||||
docstring_info = docstrings.parse(docstring)
|
||||
expected_docstring_info = DocstringInfo(
|
||||
summary='Docstring summary.',
|
||||
description='This is the first section of a docstring description.\n\n'
|
||||
'This is the second section of a docstring description. This docstring '
|
||||
'description has just two sections.',
|
||||
)
|
||||
self.assertEqual(docstring_info, expected_docstring_info)
|
||||
|
||||
def test_ill_formed_docstring(self):
|
||||
docstring = """Docstring summary.
|
||||
|
||||
args: raises ::
|
||||
:
|
||||
pathological docstrings should not fail, and ideally should behave
|
||||
reasonably.
|
||||
"""
|
||||
docstrings.parse(docstring)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
testutils.main()
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
+426
-157
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -18,6 +18,7 @@ from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
@@ -36,383 +37,651 @@ class FireTest(testutils.BaseTestCase):
|
||||
fire.Fire(tc.Empty)
|
||||
fire.Fire(tc.OldStyleEmpty)
|
||||
fire.Fire(tc.WithInit)
|
||||
# Test both passing command as a sequence and as a string.
|
||||
self.assertEqual(fire.Fire(tc.NoDefaults, command='triple 4'), 12)
|
||||
self.assertEqual(fire.Fire(tc.WithDefaults, command=('double', '2')), 4)
|
||||
self.assertEqual(fire.Fire(tc.WithDefaults, command=['triple', '4']), 12)
|
||||
self.assertEqual(fire.Fire(tc.OldStyleWithDefaults,
|
||||
command=['double', '2']), 4)
|
||||
self.assertEqual(fire.Fire(tc.OldStyleWithDefaults,
|
||||
command=['triple', '4']), 12)
|
||||
|
||||
def testFirePositionalCommand(self):
|
||||
# Test passing command as a positional argument.
|
||||
self.assertEqual(fire.Fire(tc.NoDefaults, 'double 2'), 4)
|
||||
self.assertEqual(fire.Fire(tc.NoDefaults, 'triple 4'), 12)
|
||||
self.assertEqual(fire.Fire(tc.WithDefaults, 'double 2'), 4)
|
||||
self.assertEqual(fire.Fire(tc.WithDefaults, 'triple 4'), 12)
|
||||
self.assertEqual(fire.Fire(tc.OldStyleWithDefaults, 'double 2'), 4)
|
||||
self.assertEqual(fire.Fire(tc.OldStyleWithDefaults, 'triple 4'), 12)
|
||||
self.assertEqual(fire.Fire(tc.NoDefaults, ['double', '2']), 4)
|
||||
|
||||
def testFireInvalidCommandArg(self):
|
||||
with self.assertRaises(ValueError):
|
||||
# This is not a valid command.
|
||||
fire.Fire(tc.WithDefaults, command=10)
|
||||
|
||||
def testFireDefaultName(self):
|
||||
with mock.patch.object(sys, 'argv',
|
||||
[os.path.join('python-fire', 'fire',
|
||||
'base_filename.py')]):
|
||||
with self.assertOutputMatches(stdout='Usage: base_filename.py',
|
||||
stderr=None):
|
||||
fire.Fire(tc.Empty)
|
||||
|
||||
def testFireNoArgs(self):
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, 'ten'), 10)
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, command=['ten']), 10)
|
||||
|
||||
def testFireExceptions(self):
|
||||
# Exceptions of Fire are printed to stderr and a FireExit is raised.
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.Empty, 'nomethod') # Member doesn't exist.
|
||||
fire.Fire(tc.Empty, command=['nomethod']) # Member doesn't exist.
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.NoDefaults, 'double') # Missing argument.
|
||||
fire.Fire(tc.NoDefaults, command=['double']) # Missing argument.
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.TypedProperties, 'delta x') # Missing key.
|
||||
fire.Fire(tc.TypedProperties, command=['delta', 'x']) # Missing key.
|
||||
|
||||
# Exceptions of the target components are still raised.
|
||||
with self.assertRaises(ZeroDivisionError):
|
||||
fire.Fire(tc.NumberDefaults, 'reciprocal 0.0')
|
||||
fire.Fire(tc.NumberDefaults, command=['reciprocal', '0.0'])
|
||||
|
||||
def testFireNamedArgs(self):
|
||||
self.assertEqual(fire.Fire(tc.WithDefaults, 'double --count 5'), 10)
|
||||
self.assertEqual(fire.Fire(tc.WithDefaults, 'triple --count 5'), 15)
|
||||
self.assertEqual(fire.Fire(tc.OldStyleWithDefaults, 'double --count 5'), 10)
|
||||
self.assertEqual(fire.Fire(tc.OldStyleWithDefaults, 'triple --count 5'), 15)
|
||||
self.assertEqual(fire.Fire(tc.WithDefaults,
|
||||
command=['double', '--count', '5']), 10)
|
||||
self.assertEqual(fire.Fire(tc.WithDefaults,
|
||||
command=['triple', '--count', '5']), 15)
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.OldStyleWithDefaults, command=['double', '--count', '5']),
|
||||
10)
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.OldStyleWithDefaults, command=['triple', '--count', '5']),
|
||||
15)
|
||||
|
||||
def testFireNamedArgsSingleHyphen(self):
|
||||
self.assertEqual(fire.Fire(tc.WithDefaults,
|
||||
command=['double', '-count', '5']), 10)
|
||||
self.assertEqual(fire.Fire(tc.WithDefaults,
|
||||
command=['triple', '-count', '5']), 15)
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.OldStyleWithDefaults, command=['double', '-count', '5']),
|
||||
10)
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.OldStyleWithDefaults, command=['triple', '-count', '5']),
|
||||
15)
|
||||
|
||||
def testFireNamedArgsWithEquals(self):
|
||||
self.assertEqual(fire.Fire(tc.WithDefaults, 'double --count=5'), 10)
|
||||
self.assertEqual(fire.Fire(tc.WithDefaults, 'triple --count=5'), 15)
|
||||
self.assertEqual(fire.Fire(tc.WithDefaults,
|
||||
command=['double', '--count=5']), 10)
|
||||
self.assertEqual(fire.Fire(tc.WithDefaults,
|
||||
command=['triple', '--count=5']), 15)
|
||||
|
||||
def testFireNamedArgsWithEqualsSingleHyphen(self):
|
||||
self.assertEqual(fire.Fire(tc.WithDefaults,
|
||||
command=['double', '-count=5']), 10)
|
||||
self.assertEqual(fire.Fire(tc.WithDefaults,
|
||||
command=['triple', '-count=5']), 15)
|
||||
|
||||
def testFireAllNamedArgs(self):
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum 1 2'), 5)
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum --alpha 1 2'), 5)
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum --beta 1 2'), 4)
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum 1 --alpha 2'), 4)
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum 1 --beta 2'), 5)
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum --alpha 1 --beta 2'), 5)
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum --beta 1 --alpha 2'), 4)
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, command=['sum', '1', '2']), 5)
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults,
|
||||
command=['sum', '--alpha', '1', '2']), 5)
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults,
|
||||
command=['sum', '--beta', '1', '2']), 4)
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults,
|
||||
command=['sum', '1', '--alpha', '2']), 4)
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults,
|
||||
command=['sum', '1', '--beta', '2']), 5)
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['sum', '--alpha', '1', '--beta', '2']), 5)
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['sum', '--beta', '1', '--alpha', '2']), 4)
|
||||
|
||||
def testFireAllNamedArgsOneMissing(self):
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum'), 0)
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum 1'), 1)
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum --alpha 1'), 1)
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, 'sum --beta 2'), 4)
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, command=['sum']), 0)
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, command=['sum', '1']), 1)
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults,
|
||||
command=['sum', '--alpha', '1']), 1)
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults,
|
||||
command=['sum', '--beta', '2']), 4)
|
||||
|
||||
def testFirePartialNamedArgs(self):
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, 'identity 1 2'), (1, 2))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults, 'identity --alpha 1 2'), (1, 2))
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, 'identity --beta 1 2'), (2, 1))
|
||||
fire.Fire(tc.MixedDefaults, command=['identity', '1', '2']), (1, 2))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults, 'identity 1 --alpha 2'), (2, 1))
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, 'identity 1 --beta 2'), (1, 2))
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '--alpha', '1', '2']), (1, 2))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults, 'identity --alpha 1 --beta 2'), (1, 2))
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '--beta', '1', '2']), (2, 1))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults, 'identity --beta 1 --alpha 2'), (2, 1))
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '1', '--alpha', '2']), (2, 1))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '1', '--beta', '2']), (1, 2))
|
||||
self.assertEqual(
|
||||
fire.Fire(
|
||||
tc.MixedDefaults,
|
||||
command=['identity', '--alpha', '1', '--beta', '2']), (1, 2))
|
||||
self.assertEqual(
|
||||
fire.Fire(
|
||||
tc.MixedDefaults,
|
||||
command=['identity', '--beta', '1', '--alpha', '2']), (2, 1))
|
||||
|
||||
def testFirePartialNamedArgsOneMissing(self):
|
||||
# Errors are written to standard out and a FireExit is raised.
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.MixedDefaults, 'identity') # Identity needs an arg.
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity']) # Identity needs an arg.
|
||||
|
||||
with self.assertRaisesFireExit(2):
|
||||
# Identity needs a value for alpha.
|
||||
fire.Fire(tc.MixedDefaults, 'identity --beta 2')
|
||||
fire.Fire(tc.MixedDefaults, command=['identity', '--beta', '2'])
|
||||
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, 'identity 1'), (1, '0'))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults, 'identity --alpha 1'), (1, '0'))
|
||||
fire.Fire(tc.MixedDefaults, command=['identity', '1']), (1, '0'))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults, command=['identity', '--alpha', '1']),
|
||||
(1, '0'))
|
||||
|
||||
def testFireAnnotatedArgs(self):
|
||||
self.assertEqual(fire.Fire(tc.Annotations, 'double 5'), 10)
|
||||
self.assertEqual(fire.Fire(tc.Annotations, 'triple 5'), 15)
|
||||
self.assertEqual(fire.Fire(tc.Annotations, command=['double', '5']), 10)
|
||||
self.assertEqual(fire.Fire(tc.Annotations, command=['triple', '5']), 15)
|
||||
|
||||
@unittest.skipIf(six.PY2, 'Keyword-only arguments not in Python 2.')
|
||||
def testFireKeywordOnlyArgs(self):
|
||||
with self.assertRaisesFireExit(2):
|
||||
# Keyword arguments must be passed with flag syntax.
|
||||
fire.Fire(tc.py3.KeywordOnly, 'double 5')
|
||||
fire.Fire(tc.py3.KeywordOnly, command=['double', '5'])
|
||||
|
||||
self.assertEqual(fire.Fire(tc.py3.KeywordOnly, 'double --count 5'), 10)
|
||||
self.assertEqual(fire.Fire(tc.py3.KeywordOnly, 'triple --count 5'), 15)
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.py3.KeywordOnly, command=['double', '--count', '5']), 10)
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.py3.KeywordOnly, command=['triple', '--count', '5']), 15)
|
||||
|
||||
def testFireProperties(self):
|
||||
self.assertEqual(fire.Fire(tc.TypedProperties, 'alpha'), True)
|
||||
self.assertEqual(fire.Fire(tc.TypedProperties, 'beta'), (1, 2, 3))
|
||||
self.assertEqual(fire.Fire(tc.TypedProperties, command=['alpha']), True)
|
||||
self.assertEqual(fire.Fire(tc.TypedProperties, command=['beta']), (1, 2, 3))
|
||||
|
||||
def testFireRecursion(self):
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.TypedProperties, 'charlie double hello'), 'hellohello')
|
||||
self.assertEqual(fire.Fire(tc.TypedProperties, 'charlie triple w'), 'www')
|
||||
fire.Fire(tc.TypedProperties,
|
||||
command=['charlie', 'double', 'hello']), 'hellohello')
|
||||
self.assertEqual(fire.Fire(tc.TypedProperties,
|
||||
command=['charlie', 'triple', 'w']), 'www')
|
||||
|
||||
def testFireVarArgs(self):
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.VarArgs, 'cumsums a b c d'), ['a', 'ab', 'abc', 'abcd'])
|
||||
self.assertEqual(fire.Fire(tc.VarArgs, 'cumsums 1 2 3 4'), [1, 3, 6, 10])
|
||||
fire.Fire(tc.VarArgs,
|
||||
command=['cumsums', 'a', 'b', 'c', 'd']),
|
||||
['a', 'ab', 'abc', 'abcd'])
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.VarArgs, command=['cumsums', '1', '2', '3', '4']),
|
||||
[1, 3, 6, 10])
|
||||
|
||||
def testFireVarArgsWithNamedArgs(self):
|
||||
self.assertEqual(fire.Fire(tc.VarArgs, 'varchars 1 2 c d'), (1, 2, 'cd'))
|
||||
self.assertEqual(fire.Fire(tc.VarArgs, 'varchars 3 4 c d e'), (3, 4, 'cde'))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.VarArgs, command=['varchars', '1', '2', 'c', 'd']),
|
||||
(1, 2, 'cd'))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.VarArgs, command=['varchars', '3', '4', 'c', 'd', 'e']),
|
||||
(3, 4, 'cde'))
|
||||
|
||||
def testFireKeywordArgs(self):
|
||||
self.assertEqual(fire.Fire(tc.Kwargs, 'props --name David --age 24'),
|
||||
{'name': 'David', 'age': 24})
|
||||
self.assertEqual(
|
||||
fire.Fire(
|
||||
tc.Kwargs,
|
||||
command=['props', '--name', 'David', '--age', '24']),
|
||||
{'name': 'David', 'age': 24})
|
||||
# Run this test both with a list command and a string command.
|
||||
self.assertEqual(
|
||||
fire.Fire(
|
||||
tc.Kwargs,
|
||||
command=['props', '--message',
|
||||
'"This is a message it has -- in it"']), # Quotes stripped
|
||||
{'message': 'This is a message it has -- in it'})
|
||||
self.assertEqual(
|
||||
fire.Fire(
|
||||
tc.Kwargs,
|
||||
command=['props', '--message',
|
||||
'This is a message it has -- in it']),
|
||||
{'message': 'This is a message it has -- in it'})
|
||||
self.assertEqual(
|
||||
fire.Fire(
|
||||
tc.Kwargs,
|
||||
command='props --message "This is a message it has -- in it"'),
|
||||
{'message': 'This is a message it has -- in it'})
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.Kwargs,
|
||||
'props --message "This is a message it has -- in it"'),
|
||||
{'message': 'This is a message it has -- in it'})
|
||||
self.assertEqual(fire.Fire(tc.Kwargs, 'upper --alpha A --beta B'),
|
||||
'ALPHA BETA')
|
||||
self.assertEqual(fire.Fire(tc.Kwargs, 'upper --alpha A --beta B - lower'),
|
||||
'alpha beta')
|
||||
command=['upper', '--alpha', 'A', '--beta', 'B']),
|
||||
'ALPHA BETA')
|
||||
self.assertEqual(
|
||||
fire.Fire(
|
||||
tc.Kwargs,
|
||||
command=['upper', '--alpha', 'A', '--beta', 'B', '-', 'lower']),
|
||||
'alpha beta')
|
||||
|
||||
def testFireKeywordArgsWithMissingPositionalArgs(self):
|
||||
self.assertEqual(fire.Fire(tc.Kwargs, 'run Hello World --cell is'),
|
||||
('Hello', 'World', {'cell': 'is'}))
|
||||
self.assertEqual(fire.Fire(tc.Kwargs, 'run Hello --cell ok'),
|
||||
('Hello', None, {'cell': 'ok'}))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.Kwargs, command=['run', 'Hello', 'World', '--cell', 'is']),
|
||||
('Hello', 'World', {'cell': 'is'}))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.Kwargs, command=['run', 'Hello', '--cell', 'ok']),
|
||||
('Hello', None, {'cell': 'ok'}))
|
||||
|
||||
def testFireObject(self):
|
||||
self.assertEqual(fire.Fire(tc.WithDefaults(), 'double --count 5'), 10)
|
||||
self.assertEqual(fire.Fire(tc.WithDefaults(), 'triple --count 5'), 15)
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.WithDefaults(), command=['double', '--count', '5']), 10)
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.WithDefaults(), command=['triple', '--count', '5']), 15)
|
||||
|
||||
def testFireDict(self):
|
||||
component = {
|
||||
'double': lambda x=0: 2 * x,
|
||||
'cheese': 'swiss',
|
||||
}
|
||||
self.assertEqual(fire.Fire(component, 'double 5'), 10)
|
||||
self.assertEqual(fire.Fire(component, 'cheese'), 'swiss')
|
||||
self.assertEqual(fire.Fire(component, command=['double', '5']), 10)
|
||||
self.assertEqual(fire.Fire(component, command=['cheese']), 'swiss')
|
||||
|
||||
def testFireObjectWithDict(self):
|
||||
self.assertEqual(fire.Fire(tc.TypedProperties, 'delta echo'), 'E')
|
||||
self.assertEqual(fire.Fire(tc.TypedProperties, 'delta echo lower'), 'e')
|
||||
self.assertIsInstance(fire.Fire(tc.TypedProperties, 'delta nest'), dict)
|
||||
self.assertEqual(fire.Fire(tc.TypedProperties, 'delta nest 0'), 'a')
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.TypedProperties, command=['delta', 'echo']), 'E')
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.TypedProperties, command=['delta', 'echo', 'lower']), 'e')
|
||||
self.assertIsInstance(
|
||||
fire.Fire(tc.TypedProperties, command=['delta', 'nest']), dict)
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.TypedProperties, command=['delta', 'nest', '0']), 'a')
|
||||
|
||||
def testFireList(self):
|
||||
component = ['zero', 'one', 'two', 'three']
|
||||
self.assertEqual(fire.Fire(component, '2'), 'two')
|
||||
self.assertEqual(fire.Fire(component, '3'), 'three')
|
||||
self.assertEqual(fire.Fire(component, '-1'), 'three')
|
||||
self.assertEqual(fire.Fire(component, command=['2']), 'two')
|
||||
self.assertEqual(fire.Fire(component, command=['3']), 'three')
|
||||
self.assertEqual(fire.Fire(component, command=['-1']), 'three')
|
||||
|
||||
def testFireObjectWithList(self):
|
||||
self.assertEqual(fire.Fire(tc.TypedProperties, 'echo 0'), 'alex')
|
||||
self.assertEqual(fire.Fire(tc.TypedProperties, 'echo 1'), 'bethany')
|
||||
self.assertEqual(fire.Fire(tc.TypedProperties, command=['echo', '0']),
|
||||
'alex')
|
||||
self.assertEqual(fire.Fire(tc.TypedProperties, command=['echo', '1']),
|
||||
'bethany')
|
||||
|
||||
def testFireObjectWithTuple(self):
|
||||
self.assertEqual(fire.Fire(tc.TypedProperties, 'fox 0'), 'carry')
|
||||
self.assertEqual(fire.Fire(tc.TypedProperties, 'fox 1'), 'divide')
|
||||
self.assertEqual(fire.Fire(tc.TypedProperties, command=['fox', '0']),
|
||||
'carry')
|
||||
self.assertEqual(fire.Fire(tc.TypedProperties, command=['fox', '1']),
|
||||
'divide')
|
||||
|
||||
def testFireNoComponent(self):
|
||||
self.assertEqual(fire.Fire(command='tc WithDefaults double 10'), 20)
|
||||
self.assertEqual(fire.Fire(command=['tc', 'WithDefaults', 'double', '10']),
|
||||
20)
|
||||
last_char = lambda text: text[-1] # pylint: disable=unused-variable
|
||||
self.assertEqual(fire.Fire(command='last_char "Hello"'), 'o')
|
||||
self.assertEqual(fire.Fire(command='last-char "World"'), 'd')
|
||||
self.assertEqual(fire.Fire(command=['last_char', '"Hello"']), 'o')
|
||||
self.assertEqual(fire.Fire(command=['last-char', '"World"']), 'd')
|
||||
rset = lambda count=0: set(range(count)) # pylint: disable=unused-variable
|
||||
self.assertEqual(fire.Fire(command='rset 5'), {0, 1, 2, 3, 4})
|
||||
self.assertEqual(fire.Fire(command=['rset', '5']), {0, 1, 2, 3, 4})
|
||||
|
||||
def testFireUnderscores(self):
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.Underscores, 'underscore-example'), 'fish fingers')
|
||||
fire.Fire(tc.Underscores,
|
||||
command=['underscore-example']), 'fish fingers')
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.Underscores, 'underscore_example'), 'fish fingers')
|
||||
fire.Fire(tc.Underscores,
|
||||
command=['underscore_example']), 'fish fingers')
|
||||
|
||||
def testFireUnderscoresInArg(self):
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.Underscores, 'underscore-function example'), 'example')
|
||||
fire.Fire(tc.Underscores,
|
||||
command=['underscore-function', 'example']), 'example')
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.Underscores, 'underscore_function --underscore-arg=score'),
|
||||
fire.Fire(tc.Underscores,
|
||||
command=['underscore_function', '--underscore-arg=score']),
|
||||
'score')
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.Underscores, 'underscore_function --underscore_arg=score'),
|
||||
fire.Fire(tc.Underscores,
|
||||
command=['underscore_function', '--underscore_arg=score']),
|
||||
'score')
|
||||
|
||||
def testBoolParsing(self):
|
||||
self.assertEqual(fire.Fire(tc.BoolConverter, 'as-bool True'), True)
|
||||
self.assertEqual(fire.Fire(tc.BoolConverter, 'as-bool False'), False)
|
||||
self.assertEqual(fire.Fire(tc.BoolConverter, 'as-bool --arg=True'), True)
|
||||
self.assertEqual(fire.Fire(tc.BoolConverter, 'as-bool --arg=False'), False)
|
||||
self.assertEqual(fire.Fire(tc.BoolConverter, 'as-bool --arg'), True)
|
||||
self.assertEqual(fire.Fire(tc.BoolConverter, 'as-bool --noarg'), False)
|
||||
self.assertEqual(fire.Fire(tc.BoolConverter, command=['as-bool', 'True']),
|
||||
True)
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.BoolConverter, command=['as-bool', 'False']), False)
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.BoolConverter, command=['as-bool', '--arg=True']), True)
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.BoolConverter, command=['as-bool', '--arg=False']), False)
|
||||
self.assertEqual(fire.Fire(tc.BoolConverter, command=['as-bool', '--arg']),
|
||||
True)
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.BoolConverter, command=['as-bool', '--noarg']), False)
|
||||
|
||||
def testBoolParsingContinued(self):
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults, 'identity True False'), (True, False))
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', 'True', 'False']), (True, False))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults, 'identity --alpha=False 10'), (False, 10))
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '--alpha=False', '10']), (False, 10))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults, 'identity --alpha --beta 10'), (True, 10))
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '--alpha', '--beta', '10']), (True, 10))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults, 'identity --alpha --beta=10'), (True, 10))
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '--alpha', '--beta=10']), (True, 10))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults, 'identity --noalpha --beta'), (False, True))
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '--noalpha', '--beta']), (False, True))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults, 'identity 10 --beta'), (10, True))
|
||||
fire.Fire(tc.MixedDefaults, command=['identity', '10', '--beta']),
|
||||
(10, True))
|
||||
|
||||
def testBoolParsingSingleHyphen(self):
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '-alpha=False', '10']), (False, 10))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '-alpha', '-beta', '10']), (True, 10))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '-alpha', '-beta=10']), (True, 10))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '-noalpha', '-beta']), (False, True))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '-alpha', '-10', '-beta']), (-10, True))
|
||||
|
||||
def testBoolParsingLessExpectedCases(self):
|
||||
# Note: Does not return (True, 10).
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults, 'identity --alpha 10'), (10, '0'))
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '--alpha', '10']), (10, '0'))
|
||||
# To get (True, 10), use one of the following:
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults, 'identity --alpha --beta=10'), (True, 10))
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '--alpha', '--beta=10']),
|
||||
(True, 10))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults, 'identity True 10'), (True, 10))
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', 'True', '10']), (True, 10))
|
||||
|
||||
# Note: Does not return ('--test', '0').
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, 'identity --alpha --test'),
|
||||
(True, '--test'))
|
||||
# Note: Does not return (True, '--test') or ('--test', 0).
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.MixedDefaults, command=['identity', '--alpha', '--test'])
|
||||
|
||||
self.assertEqual(
|
||||
fire.Fire(
|
||||
tc.MixedDefaults,
|
||||
command=['identity', '--alpha', 'True', '"--test"']),
|
||||
(True, '--test'))
|
||||
# To get ('--test', '0'), use one of the following:
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, 'identity --alpha=--test'),
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '--alpha=--test']),
|
||||
('--test', '0'))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults, r'identity --alpha \"--test\"'),
|
||||
fire.Fire(tc.MixedDefaults, command=r'identity --alpha \"--test\"'),
|
||||
('--test', '0'))
|
||||
|
||||
def testSingleCharFlagParsing(self):
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '-a']), (True, '0'))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '-a', '--beta=10']), (True, 10))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '-a', '-b']), (True, True))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '-a', '42', '-b']), (42, True))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '-a', '42', '-b', '10']), (42, 10))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '--alpha', 'True', '-b', '10']),
|
||||
(True, 10))
|
||||
with self.assertRaisesFireExit(2):
|
||||
# This test attempts to use an ambiguous shortcut flag on a function with
|
||||
# a naming conflict for the shortcut, triggering a FireError.
|
||||
fire.Fire(tc.SimilarArgNames, command=['identity', '-b'])
|
||||
|
||||
def testSingleCharFlagParsingEqualSign(self):
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '-a=True']), (True, '0'))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '-a=3', '--beta=10']), (3, 10))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '-a=False', '-b=15']), (False, 15))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '-a', '42', '-b=12']), (42, 12))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '-a=42', '-b', '10']), (42, 10))
|
||||
|
||||
def testSingleCharFlagParsingExactMatch(self):
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.SimilarArgNames,
|
||||
command=['identity2', '-a']), (True, None))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.SimilarArgNames,
|
||||
command=['identity2', '-a=10']), (10, None))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.SimilarArgNames,
|
||||
command=['identity2', '--a']), (True, None))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.SimilarArgNames,
|
||||
command=['identity2', '-alpha']), (None, True))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.SimilarArgNames,
|
||||
command=['identity2', '-a', '-alpha']), (True, True))
|
||||
|
||||
def testSingleCharFlagParsingCapitalLetter(self):
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.CapitalizedArgNames,
|
||||
command=['sum', '-D', '5', '-G', '10']), 15)
|
||||
|
||||
def testBoolParsingWithNo(self):
|
||||
# In these examples --nothing always refers to the nothing argument:
|
||||
def fn1(thing, nothing):
|
||||
return thing, nothing
|
||||
|
||||
self.assertEqual(fire.Fire(fn1, '--thing --nothing'), (True, True))
|
||||
self.assertEqual(fire.Fire(fn1, '--thing --nonothing'), (True, False))
|
||||
self.assertEqual(fire.Fire(fn1, command=['--thing', '--nothing']),
|
||||
(True, True))
|
||||
self.assertEqual(fire.Fire(fn1, command=['--thing', '--nonothing']),
|
||||
(True, False))
|
||||
|
||||
with self.assertRaisesFireExit(2):
|
||||
# In this case nothing=False (since rightmost setting of a flag gets
|
||||
# precedence), but it errors because thing has no value.
|
||||
fire.Fire(fn1, '--nothing --nonothing')
|
||||
fire.Fire(fn1, command=['--nothing', '--nonothing'])
|
||||
|
||||
# In these examples, --nothing sets thing=False:
|
||||
def fn2(thing, **kwargs):
|
||||
return thing, kwargs
|
||||
self.assertEqual(fire.Fire(fn2, '--thing'), (True, {}))
|
||||
self.assertEqual(fire.Fire(fn2, '--nothing'), (False, {}))
|
||||
self.assertEqual(fire.Fire(fn2, command=['--thing']), (True, {}))
|
||||
self.assertEqual(fire.Fire(fn2, command=['--nothing']), (False, {}))
|
||||
with self.assertRaisesFireExit(2):
|
||||
# In this case, nothing=True, but it errors because thing has no value.
|
||||
fire.Fire(fn2, '--nothing=True')
|
||||
self.assertEqual(fire.Fire(fn2, '--nothing --nothing=True'),
|
||||
fire.Fire(fn2, command=['--nothing=True'])
|
||||
self.assertEqual(fire.Fire(fn2, command=['--nothing', '--nothing=True']),
|
||||
(False, {'nothing': True}))
|
||||
|
||||
def fn3(arg, **kwargs):
|
||||
return arg, kwargs
|
||||
self.assertEqual(fire.Fire(fn3, '--arg=value --thing'),
|
||||
self.assertEqual(fire.Fire(fn3, command=['--arg=value', '--thing']),
|
||||
('value', {'thing': True}))
|
||||
self.assertEqual(fire.Fire(fn3, '--arg=value --nothing'),
|
||||
self.assertEqual(fire.Fire(fn3, command=['--arg=value', '--nothing']),
|
||||
('value', {'thing': False}))
|
||||
self.assertEqual(fire.Fire(fn3, '--arg=value --nonothing'),
|
||||
self.assertEqual(fire.Fire(fn3, command=['--arg=value', '--nonothing']),
|
||||
('value', {'nothing': False}))
|
||||
|
||||
def testTraceFlag(self):
|
||||
with self.assertRaisesFireExit(0, 'Fire trace:\n'):
|
||||
fire.Fire(tc.BoolConverter, 'as-bool True -- --trace')
|
||||
fire.Fire(tc.BoolConverter, command=['as-bool', 'True', '--', '--trace'])
|
||||
with self.assertRaisesFireExit(0, 'Fire trace:\n'):
|
||||
fire.Fire(tc.BoolConverter, 'as-bool True -- -t')
|
||||
fire.Fire(tc.BoolConverter, command=['as-bool', 'True', '--', '-t'])
|
||||
with self.assertRaisesFireExit(0, 'Fire trace:\n'):
|
||||
fire.Fire(tc.BoolConverter, '-- --trace')
|
||||
fire.Fire(tc.BoolConverter, command=['--', '--trace'])
|
||||
|
||||
def testHelpFlag(self):
|
||||
with self.assertRaisesFireExit(0):
|
||||
fire.Fire(tc.BoolConverter, 'as-bool True -- --help')
|
||||
fire.Fire(tc.BoolConverter, command=['as-bool', 'True', '--', '--help'])
|
||||
with self.assertRaisesFireExit(0):
|
||||
fire.Fire(tc.BoolConverter, 'as-bool True -- -h')
|
||||
fire.Fire(tc.BoolConverter, command=['as-bool', 'True', '--', '-h'])
|
||||
with self.assertRaisesFireExit(0):
|
||||
fire.Fire(tc.BoolConverter, '-- --help')
|
||||
fire.Fire(tc.BoolConverter, command=['--', '--help'])
|
||||
|
||||
def testHelpFlagAndTraceFlag(self):
|
||||
with self.assertRaisesFireExit(0, 'Fire trace:\n.*Usage:'):
|
||||
fire.Fire(tc.BoolConverter, 'as-bool True -- --help --trace')
|
||||
fire.Fire(tc.BoolConverter,
|
||||
command=['as-bool', 'True', '--', '--help', '--trace'])
|
||||
with self.assertRaisesFireExit(0, 'Fire trace:\n.*Usage:'):
|
||||
fire.Fire(tc.BoolConverter, 'as-bool True -- -h -t')
|
||||
fire.Fire(tc.BoolConverter, command=['as-bool', 'True', '--', '-h', '-t'])
|
||||
with self.assertRaisesFireExit(0, 'Fire trace:\n.*Usage:'):
|
||||
fire.Fire(tc.BoolConverter, '-- -h --trace')
|
||||
fire.Fire(tc.BoolConverter, command=['--', '-h', '--trace'])
|
||||
|
||||
def testTabCompletionNoName(self):
|
||||
with self.assertRaises(ValueError):
|
||||
fire.Fire(tc.NoDefaults, '-- --completion')
|
||||
completion_script = fire.Fire(tc.NoDefaults, command=['--', '--completion'])
|
||||
self.assertIn('double', completion_script)
|
||||
self.assertIn('triple', completion_script)
|
||||
|
||||
def testTabCompletion(self):
|
||||
completion_script = fire.Fire(tc.NoDefaults, '-- --completion', name='c')
|
||||
completion_script = fire.Fire(
|
||||
tc.NoDefaults, command=['--', '--completion'], name='c')
|
||||
self.assertIn('double', completion_script)
|
||||
self.assertIn('triple', completion_script)
|
||||
|
||||
def testTabCompletionWithDict(self):
|
||||
actions = {'multiply': lambda a, b: a * b}
|
||||
completion_script = fire.Fire(actions, '-- --completion', name='actCLI')
|
||||
completion_script = fire.Fire(
|
||||
actions, command=['--', '--completion'], name='actCLI')
|
||||
self.assertIn('actCLI', completion_script)
|
||||
self.assertIn('multiply', completion_script)
|
||||
|
||||
def testBasicSeparator(self):
|
||||
# '-' is the default separator.
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, 'identity + _'), ('+', '_'))
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, 'identity _ + -'), ('_', '+'))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '+', '_']), ('+', '_'))
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '_', '+', '-']), ('_', '+'))
|
||||
|
||||
# If we change the separator we can use '-' as an argument.
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults, 'identity - _ -- --separator &'),
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity', '-', '_', '--', '--separator', '&']),
|
||||
('-', '_'))
|
||||
|
||||
# The separator triggers a function call, but there aren't enough arguments.
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.MixedDefaults, 'identity - _ +')
|
||||
fire.Fire(tc.MixedDefaults, command=['identity', '-', '_', '+'])
|
||||
|
||||
def testNonComparable(self):
|
||||
"""Fire should work with classes that disallow comparisons."""
|
||||
self.assertIsInstance(fire.Fire(tc.NonComparable, ''), tc.NonComparable)
|
||||
# Make sure this test passes both with a string command or a list command.
|
||||
self.assertIsInstance(
|
||||
fire.Fire(tc.NonComparable, command=''), tc.NonComparable)
|
||||
self.assertIsInstance(
|
||||
fire.Fire(tc.NonComparable, command=[]), tc.NonComparable)
|
||||
|
||||
# The first separator instantiates the NonComparable object.
|
||||
# The second separator causes Fire to check if the separator was necessary.
|
||||
self.assertIsInstance(fire.Fire(tc.NonComparable, '- -'), tc.NonComparable)
|
||||
self.assertIsInstance(
|
||||
fire.Fire(tc.NonComparable, command=['-', '-']), tc.NonComparable)
|
||||
|
||||
def testExtraSeparators(self):
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.ReturnsObj, 'get-obj arg1 arg2 - - as-bool True'), True)
|
||||
fire.Fire(
|
||||
tc.ReturnsObj,
|
||||
command=['get-obj', 'arg1', 'arg2', '-', '-', 'as-bool', 'True']),
|
||||
True)
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.ReturnsObj, 'get-obj arg1 arg2 - - - as-bool True'), True)
|
||||
fire.Fire(
|
||||
tc.ReturnsObj,
|
||||
command=['get-obj', 'arg1', 'arg2', '-', '-', '-', 'as-bool',
|
||||
'True']),
|
||||
True)
|
||||
|
||||
def testSeparatorForChaining(self):
|
||||
# Without a separator all args are consumed by get_obj.
|
||||
self.assertIsInstance(
|
||||
fire.Fire(tc.ReturnsObj, 'get-obj arg1 arg2 as-bool True'),
|
||||
fire.Fire(tc.ReturnsObj,
|
||||
command=['get-obj', 'arg1', 'arg2', 'as-bool', 'True']),
|
||||
tc.BoolConverter)
|
||||
# With a separator only the preceeding args are consumed by get_obj.
|
||||
# With a separator only the preceding args are consumed by get_obj.
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.ReturnsObj, 'get-obj arg1 arg2 - as-bool True'), True)
|
||||
fire.Fire(
|
||||
tc.ReturnsObj,
|
||||
command=['get-obj', 'arg1', 'arg2', '-', 'as-bool', 'True']), True)
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.ReturnsObj,
|
||||
'get-obj arg1 arg2 & as-bool True -- --separator &'),
|
||||
command=['get-obj', 'arg1', 'arg2', '&', 'as-bool', 'True',
|
||||
'--', '--separator', '&']),
|
||||
True)
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.ReturnsObj,
|
||||
'get-obj arg1 $$ as-bool True -- --separator $$'),
|
||||
command=['get-obj', 'arg1', '$$', 'as-bool', 'True', '--',
|
||||
'--separator', '$$']),
|
||||
True)
|
||||
|
||||
def testNegativeNumbers(self):
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['sum', '--alpha', '-3', '--beta', '-4']), -11)
|
||||
|
||||
def testFloatForExpectedInt(self):
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.MixedDefaults, 'sum --alpha 2.2 --beta 3.0'), 8.2)
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['sum', '--alpha', '2.2', '--beta', '3.0']), 8.2)
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.NumberDefaults, 'integer_reciprocal --divisor 5.0'), 0.2)
|
||||
fire.Fire(
|
||||
tc.NumberDefaults,
|
||||
command=['integer_reciprocal', '--divisor', '5.0']), 0.2)
|
||||
self.assertEqual(
|
||||
fire.Fire(tc.NumberDefaults, 'integer_reciprocal 4.0'), 0.25)
|
||||
fire.Fire(tc.NumberDefaults, command=['integer_reciprocal', '4.0']),
|
||||
0.25)
|
||||
|
||||
def testClassInstantiation(self):
|
||||
self.assertIsInstance(fire.Fire(tc.InstanceVars, '--arg1=a1 --arg2=a2'),
|
||||
self.assertIsInstance(fire.Fire(tc.InstanceVars,
|
||||
command=['--arg1=a1', '--arg2=a2']),
|
||||
tc.InstanceVars)
|
||||
with self.assertRaisesFireExit(2):
|
||||
# Cannot instantiate a class with positional args.
|
||||
fire.Fire(tc.InstanceVars, 'a1 a2')
|
||||
fire.Fire(tc.InstanceVars, command=['a1', 'a2'])
|
||||
|
||||
def testTraceErrors(self):
|
||||
# Class needs additional value but runs out of args.
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.InstanceVars, 'a1')
|
||||
fire.Fire(tc.InstanceVars, command=['a1'])
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.InstanceVars, '--arg1=a1')
|
||||
fire.Fire(tc.InstanceVars, command=['--arg1=a1'])
|
||||
|
||||
# Routine needs additional value but runs out of args.
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.InstanceVars, 'a1 a2 - run b1')
|
||||
fire.Fire(tc.InstanceVars, command=['a1', 'a2', '-', 'run', 'b1'])
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.InstanceVars, '--arg1=a1 --arg2=a2 - run b1')
|
||||
fire.Fire(tc.InstanceVars,
|
||||
command=['--arg1=a1', '--arg2=a2', '-', 'run b1'])
|
||||
|
||||
# Extra args cannot be consumed.
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.InstanceVars, 'a1 a2 - run b1 b2 b3')
|
||||
fire.Fire(tc.InstanceVars,
|
||||
command=['a1', 'a2', '-', 'run', 'b1', 'b2', 'b3'])
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.InstanceVars, '--arg1=a1 --arg2=a2 - run b1 b2 b3')
|
||||
fire.Fire(
|
||||
tc.InstanceVars,
|
||||
command=['--arg1=a1', '--arg2=a2', '-', 'run', 'b1', 'b2', 'b3'])
|
||||
|
||||
# Cannot find member to access.
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.InstanceVars, 'a1 a2 - jog')
|
||||
fire.Fire(tc.InstanceVars, command=['a1', 'a2', '-', 'jog'])
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.InstanceVars, '--arg1=a1 --arg2=a2 - jog')
|
||||
fire.Fire(tc.InstanceVars, command=['--arg1=a1', '--arg2=a2', '-', 'jog'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -0,0 +1,473 @@
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""helptext is the new, work in progress, help text module for Fire.
|
||||
|
||||
This is a fork of, and is intended to replace, helputils.
|
||||
|
||||
Utility for producing help strings for use in Fire CLIs.
|
||||
|
||||
Can produce help strings suitable for display in Fire CLIs for any type of
|
||||
Python object, module, class, or function.
|
||||
|
||||
There are two types of informative strings: Usage and Help screens.
|
||||
|
||||
Usage screens are shown when the user accesses a group or accesses a command
|
||||
without calling it. A Usage screen shows information about how to use that group
|
||||
or command. Usage screens are typically short and show the minimal information
|
||||
necessary for the user to determine how to proceed.
|
||||
|
||||
Help screens are shown when the user requests help with the help flag (--help).
|
||||
Help screens are shown in a less-style console view, and contain detailed help
|
||||
information.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import inspect
|
||||
|
||||
from fire import completion
|
||||
from fire import docstrings
|
||||
from fire import inspectutils
|
||||
from fire import value_types
|
||||
|
||||
|
||||
def Text(component, trace=None, verbose=False):
|
||||
"""Returns the text to show for a supplied component.
|
||||
|
||||
The component can be any Python class, object, function, module, etc.
|
||||
|
||||
Args:
|
||||
component: The component to determine the help string for.
|
||||
trace: The Fire trace leading to this component.
|
||||
verbose: Whether to include private members in the help string.
|
||||
Returns:
|
||||
String suitable for display giving information about the component.
|
||||
"""
|
||||
info = inspectutils.Info(component)
|
||||
info['docstring_info'] = docstrings.parse(info['docstring'])
|
||||
|
||||
is_error_screen = False
|
||||
if trace:
|
||||
is_error_screen = trace.HasError()
|
||||
|
||||
if is_error_screen:
|
||||
return UsageText(info, trace, verbose=verbose)
|
||||
else:
|
||||
return HelpText(info, trace, verbose=verbose)
|
||||
|
||||
|
||||
def GetArgsAngFlags(component):
|
||||
"""Returns all types of arguments and flags of a component."""
|
||||
spec = inspectutils.GetFullArgSpec(component)
|
||||
args = spec.args
|
||||
if spec.defaults is None:
|
||||
num_defaults = 0
|
||||
else:
|
||||
num_defaults = len(spec.defaults)
|
||||
args_with_no_defaults = args[:len(args) - num_defaults]
|
||||
args_with_defaults = args[len(args) - num_defaults:]
|
||||
flags = args_with_defaults + spec.kwonlyargs
|
||||
return args_with_no_defaults, args_with_defaults, flags
|
||||
|
||||
|
||||
def GetSummaryAndDescription(docstring_info):
|
||||
"""Retrieves summary and description for help text generation."""
|
||||
|
||||
# To handle both empty string and None
|
||||
summary = docstring_info.summary if docstring_info.summary else None
|
||||
description = (
|
||||
docstring_info.description if docstring_info.description else None)
|
||||
return summary, description
|
||||
|
||||
|
||||
def GetCurrentCommand(trace=None):
|
||||
"""Returns current command for the purpose of generating help text."""
|
||||
if trace:
|
||||
current_command = trace.GetCommand()
|
||||
else:
|
||||
current_command = ''
|
||||
|
||||
return current_command
|
||||
|
||||
|
||||
def HelpText(component, info, trace=None, verbose=False):
|
||||
if inspect.isroutine(component) or inspect.isclass(component):
|
||||
return HelpTextForFunction(component, info, trace)
|
||||
else:
|
||||
return HelpTextForObject(component, info, trace, verbose)
|
||||
|
||||
|
||||
def HelpTextForFunction(component, info, trace=None, verbose=False):
|
||||
"""Returns detail help text for a function component.
|
||||
|
||||
Args:
|
||||
component: Current component to generate help text for.
|
||||
info: Info containing metadata of component.
|
||||
trace: FireTrace object that leads to current component.
|
||||
verbose: Whether to display help text in verbose mode.
|
||||
|
||||
Returns:
|
||||
Formatted help text for display.
|
||||
"""
|
||||
# TODO(joejoevictor): Implement verbose related output
|
||||
del verbose
|
||||
|
||||
current_command = GetCurrentCommand(trace)
|
||||
summary, description = GetSummaryAndDescription(info['docstring_info'])
|
||||
spec = inspectutils.GetFullArgSpec(component)
|
||||
args = spec.args
|
||||
|
||||
args_with_no_defaults, args_with_defaults, flags = GetArgsAngFlags(component)
|
||||
del args_with_defaults
|
||||
|
||||
output_template = """NAME
|
||||
{name_section}
|
||||
|
||||
SYNOPSIS
|
||||
{synopsis_section}
|
||||
|
||||
DESCRIPTION
|
||||
{description_section}
|
||||
{args_and_flags_section}
|
||||
NOTES
|
||||
You could also use flags syntax for POSITIONAL ARGUMENTS
|
||||
"""
|
||||
|
||||
# Name section
|
||||
name_section_template = '{current_command}{command_summary}'
|
||||
command_summary_str = ' - ' + summary if summary else ''
|
||||
name_section = name_section_template.format(
|
||||
current_command=current_command, command_summary=command_summary_str)
|
||||
|
||||
args_and_flags = ''
|
||||
if args_with_no_defaults:
|
||||
items = [arg.upper() for arg in args_with_no_defaults]
|
||||
args_and_flags = ' '.join(items)
|
||||
|
||||
synopsis_flag_template = '[--{flag_name}={flag_name_upper}]'
|
||||
if flags:
|
||||
items = [
|
||||
synopsis_flag_template.format(
|
||||
flag_name=flag, flag_name_upper=flag.upper()) for flag in flags
|
||||
]
|
||||
args_and_flags = args_and_flags + ' '.join(items)
|
||||
|
||||
# Synopsis section
|
||||
synopsis_section_template = '{current_command} {args_and_flags}'
|
||||
positional_arguments = '|'.join(args)
|
||||
if positional_arguments:
|
||||
positional_arguments = ' ' + positional_arguments
|
||||
synopsis_section = synopsis_section_template.format(
|
||||
current_command=current_command, args_and_flags=args_and_flags)
|
||||
|
||||
# Description section
|
||||
description_section = description if description else summary
|
||||
|
||||
args_and_flags_section = ''
|
||||
|
||||
# Positional arguments and flags section
|
||||
pos_arg_template = """
|
||||
POSITIONAL ARGUMENTS
|
||||
{items}
|
||||
"""
|
||||
pos_arg_items = []
|
||||
for arg in args_with_no_defaults:
|
||||
item_template = ' {arg_name}\n {arg_description}\n'
|
||||
arg_description = None
|
||||
for arg_in_docstring in info['docstring_info'].args:
|
||||
if arg_in_docstring.name == arg:
|
||||
arg_description = arg_in_docstring.description
|
||||
|
||||
item = item_template.format(
|
||||
arg_name=arg.upper(), arg_description=arg_description)
|
||||
pos_arg_items.append(item)
|
||||
if pos_arg_items:
|
||||
args_and_flags_section += pos_arg_template.format(
|
||||
items='\n'.join(pos_arg_items).rstrip('\n'))
|
||||
|
||||
flags_template = """
|
||||
FLAGS
|
||||
{items}
|
||||
"""
|
||||
flag_items = []
|
||||
for flag in flags:
|
||||
item_template = ' --{flag_name}\n {flag_description}\n'
|
||||
flag_description = None
|
||||
for arg_in_docstring in info['docstring_info'].args:
|
||||
if arg_in_docstring.name == flag:
|
||||
flag_description = arg_in_docstring.description
|
||||
|
||||
item = item_template.format(
|
||||
flag_name=flag, flag_description=flag_description)
|
||||
flag_items.append(item)
|
||||
if flag_items:
|
||||
args_and_flags_section += flags_template.format(
|
||||
items='\n'.join(flag_items).rstrip('\n'))
|
||||
|
||||
return output_template.format(
|
||||
name_section=name_section,
|
||||
synopsis_section=synopsis_section,
|
||||
description_section=description_section,
|
||||
args_and_flags_section=args_and_flags_section)
|
||||
|
||||
|
||||
def HelpTextForObject(component, info, trace=None, verbose=False):
|
||||
"""Generates help text for python objects.
|
||||
|
||||
Args:
|
||||
component: Current component to generate help text for.
|
||||
info: Info containing metadata of component.
|
||||
trace: FireTrace object that leads to current component.
|
||||
verbose: Whether to display help text in verbose mode.
|
||||
|
||||
Returns:
|
||||
Formatted help text for display.
|
||||
"""
|
||||
|
||||
output_template = """NAME
|
||||
{current_command} - {command_summary}
|
||||
|
||||
SYNOPSIS
|
||||
{synopsis}
|
||||
|
||||
DESCRIPTION
|
||||
{command_description}
|
||||
{detail_section}
|
||||
"""
|
||||
|
||||
current_command = GetCurrentCommand(trace)
|
||||
|
||||
docstring_info = info['docstring_info']
|
||||
command_summary = docstring_info.summary if docstring_info.summary else ''
|
||||
if docstring_info.description:
|
||||
command_description = docstring_info.description
|
||||
else:
|
||||
command_description = ''
|
||||
|
||||
groups = []
|
||||
commands = []
|
||||
values = []
|
||||
members = completion._Members(component, verbose) # pylint: disable=protected-access
|
||||
for member_name, member in members:
|
||||
if value_types.IsGroup(member):
|
||||
groups.append((member_name, member))
|
||||
if value_types.IsCommand(member):
|
||||
commands.append((member_name, member))
|
||||
if value_types.IsValue(member):
|
||||
values.append((member_name, member))
|
||||
|
||||
possible_actions = []
|
||||
# TODO(joejoevictor): Add global flags to here. Also, if it's a callable,
|
||||
# there will be additional flags.
|
||||
possible_flags = ''
|
||||
detail_section_string = ''
|
||||
item_template = """
|
||||
{name}
|
||||
{command_summary}
|
||||
"""
|
||||
|
||||
if groups:
|
||||
# TODO(joejoevictor): Add missing GROUPS section handling
|
||||
possible_actions.append('GROUP')
|
||||
if commands:
|
||||
possible_actions.append('COMMAND')
|
||||
commands_str_template = """
|
||||
COMMANDS
|
||||
COMMAND is one of the followings:
|
||||
{items}
|
||||
"""
|
||||
command_item_strings = []
|
||||
for command_name, command in commands:
|
||||
command_docstring_info = docstrings.parse(
|
||||
inspectutils.Info(command)['docstring'])
|
||||
command_item_strings.append(
|
||||
item_template.format(
|
||||
name=command_name,
|
||||
command_summary=command_docstring_info.summary))
|
||||
detail_section_string += commands_str_template.format(
|
||||
items=('\n'.join(command_item_strings)).rstrip('\n'))
|
||||
|
||||
if values:
|
||||
possible_actions.append('VALUES')
|
||||
values_str_template = """
|
||||
VALUES
|
||||
VALUE is one of the followings:
|
||||
{items}
|
||||
"""
|
||||
value_item_strings = []
|
||||
for value_name, value in values:
|
||||
del value
|
||||
init_docstring_info = docstrings.parse(
|
||||
inspectutils.Info(component.__class__.__init__)['docstring'])
|
||||
for arg_info in init_docstring_info.args:
|
||||
if arg_info.name == value_name:
|
||||
value_item_strings.append(
|
||||
item_template.format(
|
||||
name=value_name, command_summary=arg_info.description))
|
||||
detail_section_string += values_str_template.format(
|
||||
items=('\n'.join(value_item_strings)).rstrip('\n'))
|
||||
|
||||
possible_actions_string = ' ' + (' | '.join(possible_actions))
|
||||
|
||||
synopsis_template = '{current_command}{possible_actions}{possible_flags}'
|
||||
synopsis_string = synopsis_template.format(
|
||||
current_command=current_command,
|
||||
possible_actions=possible_actions_string,
|
||||
possible_flags=possible_flags)
|
||||
|
||||
return output_template.format(
|
||||
current_command=current_command,
|
||||
command_summary=command_summary,
|
||||
synopsis=synopsis_string,
|
||||
command_description=command_description,
|
||||
detail_section=detail_section_string)
|
||||
|
||||
|
||||
def UsageText(component, trace=None, verbose=False):
|
||||
if inspect.isroutine(component) or inspect.isclass(component):
|
||||
return UsageTextForFunction(component, trace)
|
||||
else:
|
||||
return UsageTextForObject(component, trace, verbose)
|
||||
|
||||
|
||||
def UsageTextForFunction(component, trace=None):
|
||||
"""Returns usage text for function objects.
|
||||
|
||||
Args:
|
||||
component: The component to determine the usage text for.
|
||||
trace: The Fire trace object containing all metadata of current execution.
|
||||
|
||||
Returns:
|
||||
String suitable for display in error screen.
|
||||
"""
|
||||
|
||||
output_template = """Usage: {current_command} {args_and_flags}
|
||||
{availability_lines}
|
||||
For detailed information on this command, run:
|
||||
{current_command}{hyphen_hyphen} --help
|
||||
"""
|
||||
|
||||
if trace:
|
||||
command = trace.GetCommand()
|
||||
is_help_an_arg = trace.NeedsSeparatingHyphenHyphen()
|
||||
else:
|
||||
command = None
|
||||
is_help_an_arg = False
|
||||
|
||||
if not command:
|
||||
command = ''
|
||||
|
||||
spec = inspectutils.GetFullArgSpec(component)
|
||||
args = spec.args
|
||||
if spec.defaults is None:
|
||||
num_defaults = 0
|
||||
else:
|
||||
num_defaults = len(spec.defaults)
|
||||
args_with_no_defaults = args[:len(args) - num_defaults]
|
||||
args_with_defaults = args[len(args) - num_defaults:]
|
||||
flags = args_with_defaults + spec.kwonlyargs
|
||||
|
||||
items = [arg.upper() for arg in args_with_no_defaults]
|
||||
if flags:
|
||||
items.append('<flags>')
|
||||
availability_lines = (
|
||||
'\nAvailable flags: '
|
||||
+ ' | '.join('--' + flag for flag in flags) + '\n')
|
||||
else:
|
||||
availability_lines = ''
|
||||
args_and_flags = ' '.join(items)
|
||||
|
||||
hyphen_hyphen = ' --' if is_help_an_arg else ''
|
||||
|
||||
return output_template.format(
|
||||
current_command=command,
|
||||
args_and_flags=args_and_flags,
|
||||
availability_lines=availability_lines,
|
||||
hyphen_hyphen=hyphen_hyphen)
|
||||
|
||||
|
||||
def UsageTextForObject(component, trace=None, verbose=False):
|
||||
"""Returns help text for usage screen for objects.
|
||||
|
||||
Construct help text for usage screen to inform the user about error occurred
|
||||
and correct syntax for invoking the object.
|
||||
|
||||
Args:
|
||||
component: The component to determine the usage text for.
|
||||
trace: The Fire trace object containing all metadata of current execution.
|
||||
verbose: Whether to include private members in the usage text.
|
||||
Returns:
|
||||
String suitable for display in error screen.
|
||||
"""
|
||||
output_template = """Usage: {current_command} <{possible_actions}>
|
||||
{availability_lines}
|
||||
|
||||
For detailed information on this command, run:
|
||||
{current_command} --help
|
||||
"""
|
||||
if trace:
|
||||
command = trace.GetCommand()
|
||||
else:
|
||||
command = None
|
||||
|
||||
if not command:
|
||||
command = ''
|
||||
|
||||
groups = []
|
||||
commands = []
|
||||
values = []
|
||||
|
||||
members = completion._Members(component, verbose) # pylint: disable=protected-access
|
||||
for member_name, member in members:
|
||||
if value_types.IsGroup(member):
|
||||
groups.append(member_name)
|
||||
if value_types.IsCommand(member):
|
||||
commands.append(member_name)
|
||||
if value_types.IsValue(member):
|
||||
values.append(member_name)
|
||||
|
||||
possible_actions = []
|
||||
availability_lines = []
|
||||
availability_lint_format = '{header:20s}{choices}'
|
||||
if groups:
|
||||
possible_actions.append('groups')
|
||||
groups_string = ' | '.join(groups)
|
||||
groups_text = availability_lint_format.format(
|
||||
header='available groups:',
|
||||
choices=groups_string)
|
||||
availability_lines.append(groups_text)
|
||||
if commands:
|
||||
possible_actions.append('commands')
|
||||
commands_string = ' | '.join(commands)
|
||||
commands_text = availability_lint_format.format(
|
||||
header='available commands:',
|
||||
choices=commands_string)
|
||||
availability_lines.append(commands_text)
|
||||
if values:
|
||||
possible_actions.append('values')
|
||||
values_string = ' | '.join(values)
|
||||
values_text = availability_lint_format.format(
|
||||
header='available values:',
|
||||
choices=values_string)
|
||||
availability_lines.append(values_text)
|
||||
possible_actions_string = '|'.join(possible_actions)
|
||||
availability_lines_string = '\n'.join(availability_lines)
|
||||
|
||||
return output_template.format(
|
||||
current_command=command,
|
||||
possible_actions=possible_actions_string,
|
||||
availability_lines=availability_lines_string)
|
||||
@@ -0,0 +1,231 @@
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for the helptext module."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import textwrap
|
||||
|
||||
from fire import docstrings
|
||||
from fire import helptext
|
||||
from fire import inspectutils
|
||||
from fire import test_components as tc
|
||||
from fire import testutils
|
||||
from fire import trace
|
||||
|
||||
|
||||
class HelpScreenTest(testutils.BaseTestCase):
|
||||
|
||||
def testHelpScreen(self):
|
||||
component = tc.ClassWithDocstring()
|
||||
t = trace.FireTrace(component, name='ClassWithDocstring')
|
||||
info = inspectutils.Info(component)
|
||||
info['docstring_info'] = docstrings.parse(info['docstring'])
|
||||
help_output = helptext.HelpText(component, info, t)
|
||||
expected_output = """
|
||||
NAME
|
||||
ClassWithDocstring - Test class for testing help text output.
|
||||
|
||||
SYNOPSIS
|
||||
ClassWithDocstring COMMAND | VALUES
|
||||
|
||||
DESCRIPTION
|
||||
This is some detail description of this test class.
|
||||
|
||||
COMMANDS
|
||||
COMMAND is one of the followings:
|
||||
|
||||
print_msg
|
||||
Prints a message.
|
||||
|
||||
VALUES
|
||||
VALUE is one of the followings:
|
||||
|
||||
message
|
||||
The default message to print.
|
||||
|
||||
"""
|
||||
self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
|
||||
|
||||
def testHelpScreenForFunctionDocstringWithLineBreak(self):
|
||||
component = tc.ClassWithMultilineDocstring.example_generator
|
||||
t = trace.FireTrace(component, name='example_generator')
|
||||
info = inspectutils.Info(component)
|
||||
info['docstring_info'] = docstrings.parse(info['docstring'])
|
||||
help_output = helptext.HelpText(component, info, t)
|
||||
expected_output = """
|
||||
NAME
|
||||
example_generator - Generators have a ``Yields`` section instead of a ``Returns`` section.
|
||||
|
||||
SYNOPSIS
|
||||
example_generator N
|
||||
|
||||
DESCRIPTION
|
||||
Generators have a ``Yields`` section instead of a ``Returns`` section.
|
||||
|
||||
POSITIONAL ARGUMENTS
|
||||
N
|
||||
The upper limit of the range to generate, from 0 to `n` - 1.
|
||||
|
||||
NOTES
|
||||
You could also use flags syntax for POSITIONAL ARGUMENTS
|
||||
"""
|
||||
self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
|
||||
|
||||
def testHelpScreenForFunctionFunctionWithDefaultArgs(self):
|
||||
component = tc.WithDefaults().double
|
||||
t = trace.FireTrace(component, name='double')
|
||||
info = inspectutils.Info(component)
|
||||
info['docstring_info'] = docstrings.parse(info['docstring'])
|
||||
help_output = helptext.HelpText(component, info, t)
|
||||
expected_output = """
|
||||
NAME
|
||||
double - Returns the input multiplied by 2.
|
||||
|
||||
SYNOPSIS
|
||||
double [--count=COUNT]
|
||||
|
||||
DESCRIPTION
|
||||
Returns the input multiplied by 2.
|
||||
|
||||
FLAGS
|
||||
--count
|
||||
Input number that you want to double.
|
||||
|
||||
NOTES
|
||||
You could also use flags syntax for POSITIONAL ARGUMENTS
|
||||
"""
|
||||
self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
|
||||
|
||||
|
||||
class UsageTest(testutils.BaseTestCase):
|
||||
|
||||
def testUsageOutput(self):
|
||||
component = tc.NoDefaults()
|
||||
t = trace.FireTrace(component, name='NoDefaults')
|
||||
usage_output = helptext.UsageText(component, trace=t, verbose=False)
|
||||
expected_output = '''
|
||||
Usage: NoDefaults <commands>
|
||||
available commands: double | triple
|
||||
|
||||
For detailed information on this command, run:
|
||||
NoDefaults --help
|
||||
'''
|
||||
|
||||
self.assertEqual(
|
||||
usage_output,
|
||||
textwrap.dedent(expected_output).lstrip('\n'))
|
||||
|
||||
def testUsageOutputVerbose(self):
|
||||
component = tc.NoDefaults()
|
||||
t = trace.FireTrace(component, name='NoDefaults')
|
||||
usage_output = helptext.UsageText(component, trace=t, verbose=True)
|
||||
expected_output = '''
|
||||
Usage: NoDefaults <commands>
|
||||
available commands: double | triple
|
||||
|
||||
For detailed information on this command, run:
|
||||
NoDefaults --help
|
||||
'''
|
||||
self.assertEqual(
|
||||
usage_output,
|
||||
textwrap.dedent(expected_output).lstrip('\n'))
|
||||
|
||||
def testUsageOutputMethod(self):
|
||||
component = tc.NoDefaults().double
|
||||
t = trace.FireTrace(component, name='NoDefaults')
|
||||
t.AddAccessedProperty(component, 'double', ['double'], None, None)
|
||||
usage_output = helptext.UsageText(component, trace=t, verbose=True)
|
||||
expected_output = '''
|
||||
Usage: NoDefaults double COUNT
|
||||
|
||||
For detailed information on this command, run:
|
||||
NoDefaults double --help
|
||||
'''
|
||||
self.assertEqual(
|
||||
usage_output,
|
||||
textwrap.dedent(expected_output).lstrip('\n'))
|
||||
|
||||
def testUsageOutputFunctionWithHelp(self):
|
||||
component = tc.function_with_help
|
||||
t = trace.FireTrace(component, name='function_with_help')
|
||||
usage_output = helptext.UsageText(component, trace=t, verbose=True)
|
||||
expected_output = '''
|
||||
Usage: function_with_help <flags>
|
||||
|
||||
Available flags: --help
|
||||
|
||||
For detailed information on this command, run:
|
||||
function_with_help -- --help
|
||||
'''
|
||||
self.assertEqual(
|
||||
usage_output,
|
||||
textwrap.dedent(expected_output).lstrip('\n'))
|
||||
|
||||
def testUsageOutputFunctionWithDocstring(self):
|
||||
component = tc.multiplier_with_docstring
|
||||
t = trace.FireTrace(component, name='multiplier_with_docstring')
|
||||
usage_output = helptext.UsageText(component, trace=t, verbose=True)
|
||||
expected_output = '''
|
||||
Usage: multiplier_with_docstring NUM <flags>
|
||||
|
||||
Available flags: --rate
|
||||
|
||||
For detailed information on this command, run:
|
||||
multiplier_with_docstring --help
|
||||
'''
|
||||
self.assertEqual(
|
||||
usage_output,
|
||||
textwrap.dedent(expected_output).lstrip('\n'))
|
||||
|
||||
@testutils.skip('The functionality is not implemented yet')
|
||||
def testUsageOutputCallable(self):
|
||||
# This is both a group and a command!
|
||||
component = tc.CallableWithKeywordArgument
|
||||
t = trace.FireTrace(component, name='CallableWithKeywordArgument')
|
||||
usage_output = helptext.UsageText(component, trace=t, verbose=True)
|
||||
# TODO(zuhaohen): We need to handle the case for keyword args as well
|
||||
# i.e. __call__ method of CallableWithKeywordArgument
|
||||
expected_output = '''
|
||||
Usage: CallableWithKeywordArgument <commands>
|
||||
|
||||
Available commands: print_msg
|
||||
|
||||
For detailed information on this command, run:
|
||||
CallableWithKeywordArgument -- --help
|
||||
'''
|
||||
self.assertEqual(
|
||||
usage_output,
|
||||
textwrap.dedent(expected_output).lstrip('\n'))
|
||||
|
||||
def testUsageOutputConstructorWithParameter(self):
|
||||
component = tc.InstanceVars
|
||||
t = trace.FireTrace(component, name='InstanceVars')
|
||||
usage_output = helptext.UsageText(component, trace=t, verbose=True)
|
||||
expected_output = '''
|
||||
Usage: InstanceVars ARG1 ARG2
|
||||
|
||||
For detailed information on this command, run:
|
||||
InstanceVars --help
|
||||
'''
|
||||
self.assertEqual(
|
||||
usage_output,
|
||||
textwrap.dedent(expected_output).lstrip('\n'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
testutils.main()
|
||||
+90
-29
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,17 @@
|
||||
|
||||
Can produce help strings suitable for display in Fire CLIs for any type of
|
||||
Python object, module, class, or function.
|
||||
|
||||
There are two types of informative strings: Usage and Help screens.
|
||||
|
||||
Usage screens are shown when the user accesses a group or accesses a command
|
||||
without calling it. A Usage screen shows information about how to use that group
|
||||
or command. Usage screens are typically short and show the minimal information
|
||||
necessary for the user to determine how to proceed.
|
||||
|
||||
Help screens are shown when the user requests help with the help flag (--help).
|
||||
Help screens are shown in a less-style console view, and contain detailed help
|
||||
information.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
@@ -25,6 +36,7 @@ from __future__ import print_function
|
||||
import inspect
|
||||
|
||||
from fire import completion
|
||||
from fire import docstrings
|
||||
from fire import inspectutils
|
||||
|
||||
|
||||
@@ -54,7 +66,7 @@ def _DisplayValue(info, field, padding):
|
||||
value = info.get(field)
|
||||
|
||||
if value is None:
|
||||
return
|
||||
return None
|
||||
|
||||
skip_doc_types = ('dict', 'list', 'unicode', 'int', 'float', 'bool')
|
||||
|
||||
@@ -76,6 +88,23 @@ def _DisplayValue(info, field, padding):
|
||||
return value
|
||||
|
||||
|
||||
def _GetFields(trace=None):
|
||||
"""Returns the field names to include in the help text for a component."""
|
||||
del trace # Unused.
|
||||
return [
|
||||
'type_name',
|
||||
'string_form',
|
||||
'file',
|
||||
'line',
|
||||
'docstring',
|
||||
'init_docstring',
|
||||
'class_docstring',
|
||||
'call_docstring',
|
||||
'length',
|
||||
'usage',
|
||||
]
|
||||
|
||||
|
||||
def HelpString(component, trace=None, verbose=False):
|
||||
"""Returns a help string for a supplied component.
|
||||
|
||||
@@ -90,27 +119,33 @@ def HelpString(component, trace=None, verbose=False):
|
||||
"""
|
||||
info = inspectutils.Info(component)
|
||||
info['usage'] = UsageString(component, trace, verbose)
|
||||
info['docstring_info'] = docstrings.parse(info['docstring'])
|
||||
|
||||
fields = [
|
||||
'type_name',
|
||||
'string_form',
|
||||
'file',
|
||||
'line',
|
||||
return _HelpText(info, trace)
|
||||
|
||||
'docstring',
|
||||
'init_docstring',
|
||||
'class_docstring',
|
||||
'call_docstring',
|
||||
'length',
|
||||
|
||||
'usage',
|
||||
]
|
||||
def _HelpText(info, trace=None):
|
||||
"""Returns help text.
|
||||
|
||||
max_size = max(
|
||||
len(_NormalizeField(field)) + 1
|
||||
for field in fields
|
||||
if field in info and info[field])
|
||||
format_string = '{{field:{max_size}s}} {{value}}'.format(max_size=max_size)
|
||||
This was a copy of previous HelpString function and will be removed once the
|
||||
correct text formatters are implemented.
|
||||
|
||||
Args:
|
||||
info: The IR object containing metadata of an object.
|
||||
trace: The Fire trace object containing all metadata of current execution.
|
||||
Returns:
|
||||
String suitable for display giving information about the component.
|
||||
"""
|
||||
fields = _GetFields(trace)
|
||||
|
||||
try:
|
||||
max_size = max(
|
||||
len(_NormalizeField(field)) + 1
|
||||
for field in fields
|
||||
if field in info and info[field])
|
||||
format_string = '{{field:{max_size}s}} {{value}}'.format(max_size=max_size)
|
||||
except ValueError:
|
||||
return ''
|
||||
|
||||
lines = []
|
||||
for field in fields:
|
||||
@@ -126,6 +161,26 @@ def HelpString(component, trace=None, verbose=False):
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def GetSummaryAndDescription(docstring_info):
|
||||
"""Retrieves summary and description for help text generation."""
|
||||
|
||||
# To handle both empty string and None
|
||||
summary = docstring_info.summary if docstring_info.summary else None
|
||||
description = (
|
||||
docstring_info.description if docstring_info.description else None)
|
||||
return summary, description
|
||||
|
||||
|
||||
def GetCurrentCommand(trace=None):
|
||||
"""Returns current command for the purpose of generating help text."""
|
||||
if trace:
|
||||
current_command = trace.GetCommand()
|
||||
else:
|
||||
current_command = ''
|
||||
|
||||
return current_command
|
||||
|
||||
|
||||
def _UsageStringFromFullArgSpec(command, spec):
|
||||
"""Get a usage string from the FullArgSpec for the given command.
|
||||
|
||||
@@ -180,23 +235,29 @@ def _UsageStringFromFullArgSpec(command, spec):
|
||||
|
||||
def UsageString(component, trace=None, verbose=False):
|
||||
"""Returns a string showing how to use the component as a Fire command."""
|
||||
command = trace.GetCommand() + ' ' if trace else ''
|
||||
if trace:
|
||||
command = trace.GetCommand()
|
||||
else:
|
||||
command = None
|
||||
|
||||
if command:
|
||||
command += ' '
|
||||
else:
|
||||
command = ''
|
||||
|
||||
if inspect.isroutine(component) or inspect.isclass(component):
|
||||
spec = inspectutils.GetFullArgSpec(component)
|
||||
return _UsageStringFromFullArgSpec(command, spec)
|
||||
|
||||
elif isinstance(component, (list, tuple)):
|
||||
if isinstance(component, (list, tuple)):
|
||||
length = len(component)
|
||||
if length == 0:
|
||||
return command
|
||||
elif length == 1:
|
||||
if length == 1:
|
||||
return command + '[0]'
|
||||
else:
|
||||
return command + '[0..{cap}]'.format(cap=length - 1)
|
||||
return command + '[0..{cap}]'.format(cap=length - 1)
|
||||
|
||||
else:
|
||||
completions = completion.Completions(component, verbose)
|
||||
if command:
|
||||
completions = [''] + completions
|
||||
return '\n'.join(command + end for end in completions)
|
||||
completions = completion.Completions(component, verbose)
|
||||
if command:
|
||||
completions = [''] + completions
|
||||
return '\n'.join(command + end for end in completions)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -43,7 +43,8 @@ class HelpUtilsTest(testutils.BaseTestCase):
|
||||
self.assertIn('Type: NoDefaults', helpstring)
|
||||
self.assertIn('String form: <fire.test_components.NoDefaults object at ',
|
||||
helpstring)
|
||||
self.assertIn('test_components.py', helpstring)
|
||||
# TODO(dbieber): We comment this out since it only works with IPython:
|
||||
# self.assertIn('test_components.py', helpstring)
|
||||
self.assertIn('Usage: double\n'
|
||||
' triple', helpstring)
|
||||
|
||||
@@ -83,7 +84,6 @@ class HelpUtilsTest(testutils.BaseTestCase):
|
||||
helpstring = helputils.HelpString('test'.upper)
|
||||
self.assertIn('Type: builtin_function_or_method', helpstring)
|
||||
self.assertIn('String form: <built-in method upper of', helpstring)
|
||||
self.assertIn('Usage: [VARS ...] [--KWARGS ...]', helpstring)
|
||||
|
||||
def testHelpStringIntType(self):
|
||||
helpstring = helputils.HelpString(int)
|
||||
|
||||
+94
-11
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -20,7 +20,6 @@ from __future__ import print_function
|
||||
|
||||
import inspect
|
||||
|
||||
import IPython
|
||||
import six
|
||||
|
||||
|
||||
@@ -64,7 +63,7 @@ def _GetArgSpecInfo(fn):
|
||||
fn: The function or class of interest.
|
||||
Returns:
|
||||
A tuple with the following two items:
|
||||
fn: The function to use for determing the arg spec of this function.
|
||||
fn: The function to use for determining the arg spec of this function.
|
||||
skip_arg: Whether the first argument will be supplied automatically, and
|
||||
hence should be skipped when supplying args from a Fire command.
|
||||
"""
|
||||
@@ -74,11 +73,12 @@ def _GetArgSpecInfo(fn):
|
||||
skip_arg = True
|
||||
if six.PY2 and hasattr(fn, '__init__'):
|
||||
fn = fn.__init__
|
||||
else:
|
||||
elif inspect.ismethod(fn):
|
||||
# If the function is a bound method, we skip the `self` argument.
|
||||
is_method = inspect.ismethod(fn)
|
||||
skip_arg = is_method and fn.__self__ is not None
|
||||
|
||||
skip_arg = fn.__self__ is not None
|
||||
elif inspect.isbuiltin(fn):
|
||||
# If the function is a bound builtin, we skip the `self` argument.
|
||||
skip_arg = fn.__self__ is not None
|
||||
return fn, skip_arg
|
||||
|
||||
|
||||
@@ -103,8 +103,7 @@ def GetFullArgSpec(fn):
|
||||
# Are there other cases?
|
||||
if inspect.isbuiltin(fn):
|
||||
return FullArgSpec(varargs='vars', varkw='kwargs')
|
||||
else:
|
||||
return FullArgSpec()
|
||||
return FullArgSpec()
|
||||
|
||||
if skip_arg and args:
|
||||
args.pop(0) # Remove 'self' or 'cls' from the list of arguments.
|
||||
@@ -113,6 +112,33 @@ def GetFullArgSpec(fn):
|
||||
kwonlyargs, kwonlydefaults, annotations)
|
||||
|
||||
|
||||
def GetFileAndLine(component):
|
||||
"""Returns the filename and line number of component.
|
||||
|
||||
Args:
|
||||
component: A component to find the source information for, usually a class
|
||||
or routine.
|
||||
Returns:
|
||||
filename: The name of the file where component is defined.
|
||||
lineno: The line number where component is defined.
|
||||
"""
|
||||
if inspect.isbuiltin(component):
|
||||
return None, None
|
||||
|
||||
try:
|
||||
filename = inspect.getsourcefile(component)
|
||||
except TypeError:
|
||||
return None, None
|
||||
|
||||
try:
|
||||
unused_code, lineindex = inspect.findsource(component)
|
||||
lineno = lineindex + 1
|
||||
except IOError:
|
||||
lineno = None
|
||||
|
||||
return filename, lineno
|
||||
|
||||
|
||||
def Info(component):
|
||||
"""Returns a dict with information about the given component.
|
||||
|
||||
@@ -132,8 +158,12 @@ def Info(component):
|
||||
Returns:
|
||||
A dict with information about the component.
|
||||
"""
|
||||
inspector = IPython.core.oinspect.Inspector()
|
||||
info = inspector.info(component)
|
||||
try:
|
||||
from IPython.core import oinspect # pylint: disable=g-import-not-at-top
|
||||
inspector = oinspect.Inspector()
|
||||
info = inspector.info(component)
|
||||
except ImportError:
|
||||
info = _InfoBackup(component)
|
||||
|
||||
try:
|
||||
unused_code, lineindex = inspect.findsource(component)
|
||||
@@ -142,3 +172,56 @@ def Info(component):
|
||||
info['line'] = None
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def _InfoBackup(component):
|
||||
"""Returns a dict with information about the given component.
|
||||
|
||||
This function is to be called only in the case that IPython's
|
||||
oinspect module is not available. The info dict it produces may
|
||||
contain less information that contained in the info dict produced
|
||||
by oinspect.
|
||||
|
||||
Args:
|
||||
component: The component to analyze.
|
||||
Returns:
|
||||
A dict with information about the component.
|
||||
"""
|
||||
info = {}
|
||||
|
||||
info['type_name'] = type(component).__name__
|
||||
info['string_form'] = str(component)
|
||||
|
||||
filename, lineno = GetFileAndLine(component)
|
||||
info['file'] = filename
|
||||
info['line'] = lineno
|
||||
info['docstring'] = inspect.getdoc(component)
|
||||
|
||||
try:
|
||||
info['length'] = str(len(component))
|
||||
except (TypeError, AttributeError):
|
||||
pass
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def IsNamedTuple(component):
|
||||
"""Return true if the component is a namedtuple.
|
||||
|
||||
Unfortunately, Python offers no native way to check for a namedtuple type.
|
||||
Instead, we need to use a simple hack which should suffice for our case.
|
||||
namedtuples are internally implemented as tuples, therefore we need to:
|
||||
1. Check if the component is an instance of tuple.
|
||||
2. Check if the component has a _fields attribute which regular tuples do
|
||||
not have.
|
||||
|
||||
Args:
|
||||
component: The component to analyze.
|
||||
Returns:
|
||||
True if the component is a namedtuple or False otherwise.
|
||||
"""
|
||||
if not isinstance(component, tuple):
|
||||
return False
|
||||
|
||||
has_fields = bool(getattr(component, '_fields', None))
|
||||
return has_fields
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -56,8 +56,6 @@ class InspectUtilsTest(testutils.BaseTestCase):
|
||||
spec = inspectutils.GetFullArgSpec('test'.upper)
|
||||
self.assertEqual(spec.args, [])
|
||||
self.assertEqual(spec.defaults, ())
|
||||
self.assertEqual(spec.varargs, 'vars')
|
||||
self.assertEqual(spec.varkw, 'kwargs')
|
||||
self.assertEqual(spec.kwonlyargs, [])
|
||||
self.assertEqual(spec.kwonlydefaults, {})
|
||||
self.assertEqual(spec.annotations, {})
|
||||
|
||||
+15
-6
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -12,17 +12,20 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""This module enables interactive mode in Python Fire."""
|
||||
"""This module enables interactive mode in Python Fire.
|
||||
|
||||
It uses IPython as an optional dependency. When IPython is installed, the
|
||||
interactive flag will use IPython's REPL. When IPython is not installed, the
|
||||
interactive flag will start a Python REPL with the builtin `code` module's
|
||||
InteractiveConsole class.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import code
|
||||
import inspect
|
||||
|
||||
import IPython
|
||||
|
||||
|
||||
def Embed(variables, verbose=False):
|
||||
"""Drops into a Python REPL with variables available as local variables.
|
||||
@@ -33,7 +36,11 @@ def Embed(variables, verbose=False):
|
||||
verbose: Whether to include 'hidden' members, those keys starting with _.
|
||||
"""
|
||||
print(_AvailableString(variables, verbose))
|
||||
_EmbedIPython(variables)
|
||||
|
||||
try:
|
||||
_EmbedIPython(variables)
|
||||
except ImportError:
|
||||
_EmbedCode(variables)
|
||||
|
||||
|
||||
def _AvailableString(variables, verbose=False):
|
||||
@@ -82,9 +89,11 @@ def _EmbedIPython(variables, argv=None):
|
||||
Values are variable values.
|
||||
argv: The argv to use for starting ipython. Defaults to an empty list.
|
||||
"""
|
||||
import IPython # pylint: disable=g-import-not-at-top
|
||||
argv = argv or []
|
||||
IPython.start_ipython(argv=argv, user_ns=variables)
|
||||
|
||||
|
||||
def _EmbedCode(variables):
|
||||
import code # pylint: disable=g-import-not-at-top
|
||||
code.InteractiveConsole(variables).interact()
|
||||
|
||||
+16
-9
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -24,22 +24,29 @@ from fire import testutils
|
||||
import mock
|
||||
|
||||
|
||||
try:
|
||||
import IPython # pylint: disable=unused-import, g-import-not-at-top
|
||||
INTERACT_METHOD = 'IPython.start_ipython'
|
||||
except ImportError:
|
||||
INTERACT_METHOD = 'code.InteractiveConsole'
|
||||
|
||||
|
||||
class InteractTest(testutils.BaseTestCase):
|
||||
|
||||
@mock.patch('IPython.start_ipython')
|
||||
def testInteract(self, mock_ipython):
|
||||
self.assertFalse(mock_ipython.called)
|
||||
@mock.patch(INTERACT_METHOD)
|
||||
def testInteract(self, mock_interact_method):
|
||||
self.assertFalse(mock_interact_method.called)
|
||||
interact.Embed({})
|
||||
self.assertTrue(mock_ipython.called)
|
||||
self.assertTrue(mock_interact_method.called)
|
||||
|
||||
@mock.patch('IPython.start_ipython')
|
||||
def testInteractVariables(self, mock_ipython):
|
||||
self.assertFalse(mock_ipython.called)
|
||||
@mock.patch(INTERACT_METHOD)
|
||||
def testInteractVariables(self, mock_interact_method):
|
||||
self.assertFalse(mock_interact_method.called)
|
||||
interact.Embed({
|
||||
'count': 10,
|
||||
'mock': mock,
|
||||
})
|
||||
self.assertTrue(mock_ipython.called)
|
||||
self.assertTrue(mock_interact_method.called)
|
||||
|
||||
if __name__ == '__main__':
|
||||
testutils.main()
|
||||
|
||||
+6
-3
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -27,10 +27,10 @@ def CreateParser():
|
||||
parser.add_argument('--verbose', '-v', action='store_true')
|
||||
parser.add_argument('--interactive', '-i', action='store_true')
|
||||
parser.add_argument('--separator', default='-')
|
||||
parser.add_argument('--completion', action='store_true')
|
||||
parser.add_argument('--completion', nargs='?', const='bash', type=str)
|
||||
parser.add_argument('--help', '-h', action='store_true')
|
||||
parser.add_argument('--trace', '-t', action='store_true')
|
||||
# TODO: Consider allowing name to be passed as an argument.
|
||||
# TODO(dbieber): Consider allowing name to be passed as an argument.
|
||||
return parser
|
||||
|
||||
|
||||
@@ -94,6 +94,9 @@ def _LiteralEval(value):
|
||||
SyntaxError: If the value string has a syntax error.
|
||||
"""
|
||||
root = ast.parse(value, mode='eval')
|
||||
if isinstance(root.body, ast.BinOp): # pytype: disable=attribute-error
|
||||
raise ValueError(value)
|
||||
|
||||
for node in ast.walk(root):
|
||||
for field, child in ast.iter_fields(node):
|
||||
if isinstance(child, list):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -30,7 +30,8 @@ import six
|
||||
|
||||
class ParserFuzzTest(testutils.BaseTestCase):
|
||||
|
||||
@given(st.text(min_size=1), settings=settings.Settings(max_examples=10000))
|
||||
@settings(max_examples=10000)
|
||||
@given(st.text(min_size=1))
|
||||
@example('True')
|
||||
@example(r'"test\t\t\a\\a"')
|
||||
@example(r' "test\t\t\a\\a" ')
|
||||
@@ -67,8 +68,8 @@ class ParserFuzzTest(testutils.BaseTestCase):
|
||||
raise
|
||||
|
||||
try:
|
||||
uvalue = unicode(value)
|
||||
uresult = unicode(result)
|
||||
uvalue = six.text_type(value)
|
||||
uresult = six.text_type(result)
|
||||
except UnicodeDecodeError:
|
||||
# This is not what we're testing.
|
||||
return
|
||||
|
||||
+6
-2
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -69,10 +69,12 @@ class ParserTest(testutils.BaseTestCase):
|
||||
|
||||
def testDefaultParseValueNumbers(self):
|
||||
self.assertEqual(parser.DefaultParseValue('23'), 23)
|
||||
self.assertEqual(parser.DefaultParseValue('-23'), -23)
|
||||
self.assertEqual(parser.DefaultParseValue('23.0'), 23.0)
|
||||
self.assertIsInstance(parser.DefaultParseValue('23'), int)
|
||||
self.assertIsInstance(parser.DefaultParseValue('23.0'), float)
|
||||
self.assertEqual(parser.DefaultParseValue('23.5'), 23.5)
|
||||
self.assertEqual(parser.DefaultParseValue('-23.5'), -23.5)
|
||||
|
||||
def testDefaultParseValueStringNumbers(self):
|
||||
self.assertEqual(parser.DefaultParseValue("'23'"), '23')
|
||||
@@ -127,13 +129,15 @@ class ParserTest(testutils.BaseTestCase):
|
||||
# If it can't be parsed, we treat it as a string. This behavior may change.
|
||||
self.assertEqual(
|
||||
parser.DefaultParseValue('[(A, 2, "3"), 5'), '[(A, 2, "3"), 5')
|
||||
|
||||
self.assertEqual(parser.DefaultParseValue('x=10'), 'x=10')
|
||||
|
||||
def testDefaultParseValueSyntaxError(self):
|
||||
# If it can't be parsed, we treat it as a string.
|
||||
self.assertEqual(parser.DefaultParseValue('"'), '"')
|
||||
|
||||
def testDefaultParseValueIgnoreBinOp(self):
|
||||
self.assertEqual(parser.DefaultParseValue('2017-10-10'), '2017-10-10')
|
||||
self.assertEqual(parser.DefaultParseValue('1+1'), '1+1')
|
||||
|
||||
if __name__ == '__main__':
|
||||
testutils.main()
|
||||
|
||||
+155
-5
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -18,18 +18,36 @@ from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import collections
|
||||
|
||||
import six
|
||||
|
||||
if six.PY3:
|
||||
from fire import test_components_py3 as py3 # pylint: disable=unused-import,no-name-in-module
|
||||
from fire import test_components_py3 as py3 # pylint: disable=unused-import,no-name-in-module,g-import-not-at-top
|
||||
|
||||
|
||||
def identity(arg1, arg2, arg3=10, arg4=20, *arg5, **arg6):
|
||||
def identity(arg1, arg2, arg3=10, arg4=20, *arg5, **arg6): # pylint: disable=keyword-arg-before-vararg
|
||||
return arg1, arg2, arg3, arg4, arg5, arg6
|
||||
|
||||
identity.__annotations__ = {'arg2': int, 'arg4': int}
|
||||
|
||||
|
||||
def multiplier_with_docstring(num, rate=2):
|
||||
"""Multiplies num by rate.
|
||||
|
||||
Args:
|
||||
num (int): the num you want to multiply
|
||||
rate (int): the rate for multiplication
|
||||
Returns:
|
||||
Multiplication of num by rate
|
||||
"""
|
||||
return num * rate
|
||||
|
||||
|
||||
def function_with_help(help=True): # pylint: disable=redefined-builtin
|
||||
return help
|
||||
|
||||
|
||||
class Empty(object):
|
||||
pass
|
||||
|
||||
@@ -44,6 +62,21 @@ class WithInit(object):
|
||||
pass
|
||||
|
||||
|
||||
class ErrorInConstructor(object):
|
||||
|
||||
def __init__(self, value='value'):
|
||||
self.value = value
|
||||
raise ValueError('Error in constructor')
|
||||
|
||||
|
||||
class WithHelpArg(object):
|
||||
"""Test class for testing when class has a help= arg."""
|
||||
|
||||
def __init__(self, help=True): # pylint: disable=redefined-builtin
|
||||
self.has_help = help
|
||||
self.dictionary = {'__help': 'help in a dict'}
|
||||
|
||||
|
||||
class NoDefaults(object):
|
||||
|
||||
def double(self, count):
|
||||
@@ -54,8 +87,17 @@ class NoDefaults(object):
|
||||
|
||||
|
||||
class WithDefaults(object):
|
||||
"""Class with functions that have default arguments."""
|
||||
|
||||
def double(self, count=0):
|
||||
"""Returns the input multiplied by 2.
|
||||
|
||||
Args:
|
||||
count: Input number that you want to double.
|
||||
|
||||
Returns:
|
||||
A number that is the double of count.s
|
||||
"""
|
||||
return 2 * count
|
||||
|
||||
def triple(self, count=0):
|
||||
@@ -83,6 +125,21 @@ class MixedDefaults(object):
|
||||
return alpha, beta
|
||||
|
||||
|
||||
class SimilarArgNames(object):
|
||||
|
||||
def identity(self, bool_one=False, bool_two=False):
|
||||
return bool_one, bool_two
|
||||
|
||||
def identity2(self, a=None, alpha=None):
|
||||
return a, alpha
|
||||
|
||||
|
||||
class CapitalizedArgNames(object):
|
||||
|
||||
def sum(self, Delta=1.0, Gamma=2.0): # pylint: disable=invalid-name
|
||||
return Delta + Gamma
|
||||
|
||||
|
||||
class Annotations(object):
|
||||
|
||||
def double(self, count=0):
|
||||
@@ -96,6 +153,7 @@ class Annotations(object):
|
||||
|
||||
|
||||
class TypedProperties(object):
|
||||
"""Test class for testing Python Fire with properties of various types."""
|
||||
|
||||
def __init__(self):
|
||||
self.alpha = True
|
||||
@@ -114,7 +172,7 @@ class TypedProperties(object):
|
||||
|
||||
|
||||
class VarArgs(object):
|
||||
"""Test class G for testing Python Fire."""
|
||||
"""Test class for testing Python Fire with a property with varargs."""
|
||||
|
||||
def cumsums(self, *items):
|
||||
total = None
|
||||
@@ -127,7 +185,7 @@ class VarArgs(object):
|
||||
sums.append(total)
|
||||
return sums
|
||||
|
||||
def varchars(self, alpha=0, beta=0, *chars):
|
||||
def varchars(self, alpha=0, beta=0, *chars): # pylint: disable=keyword-arg-before-vararg
|
||||
return alpha, beta, ''.join(chars)
|
||||
|
||||
|
||||
@@ -206,3 +264,95 @@ class EmptyDictOutput(object):
|
||||
|
||||
def nothing_printable(self):
|
||||
return {'__do_not_print_me': 1}
|
||||
|
||||
|
||||
class CircularReference(object):
|
||||
|
||||
def create(self):
|
||||
x = {}
|
||||
x['y'] = x
|
||||
return x
|
||||
|
||||
|
||||
class OrderedDictionary(object):
|
||||
|
||||
def empty(self):
|
||||
return collections.OrderedDict()
|
||||
|
||||
def non_empty(self):
|
||||
ordered_dict = collections.OrderedDict()
|
||||
ordered_dict['A'] = 'A'
|
||||
ordered_dict[2] = 2
|
||||
return ordered_dict
|
||||
|
||||
|
||||
class NamedTuple(object):
|
||||
|
||||
def point(self):
|
||||
"""Point example straight from Python docs."""
|
||||
# pylint: disable=invalid-name
|
||||
Point = collections.namedtuple('Point', ['x', 'y'])
|
||||
return Point(11, y=22)
|
||||
|
||||
|
||||
class CallableWithKeywordArgument(object):
|
||||
"""Test class for supporting callable."""
|
||||
|
||||
def __call__(self, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
print('%s: %s' % (key, value))
|
||||
|
||||
def print_msg(self, msg):
|
||||
print(msg)
|
||||
|
||||
|
||||
class ClassWithDocstring(object):
|
||||
"""Test class for testing help text output.
|
||||
|
||||
This is some detail description of this test class.
|
||||
"""
|
||||
|
||||
def __init__(self, message='Hello!'):
|
||||
"""Constructor of the test class.
|
||||
|
||||
Constructs a new ClassWithDocstring object.
|
||||
|
||||
Args:
|
||||
message: The default message to print.
|
||||
"""
|
||||
self.message = message
|
||||
|
||||
def print_msg(self, msg=None):
|
||||
"""Prints a message."""
|
||||
if msg is None:
|
||||
msg = self.message
|
||||
print(msg)
|
||||
|
||||
|
||||
class ClassWithMultilineDocstring(object):
|
||||
"""Test class for testing help text output with multiline docstring.
|
||||
|
||||
This is a test class that has a long docstring description that spans across
|
||||
multiple lines for testing line breaking in help text.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def example_generator(n):
|
||||
"""Generators have a ``Yields`` section instead of a ``Returns`` section.
|
||||
|
||||
Args:
|
||||
n (int): The upper limit of the range to generate, from 0 to `n` - 1.
|
||||
|
||||
Yields:
|
||||
int: The next number in the range of 0 to `n` - 1.
|
||||
|
||||
Examples:
|
||||
Examples should be written in doctest format, and should illustrate how
|
||||
to use the function.
|
||||
|
||||
>>> print([i for i in example_generator(4)])
|
||||
[0, 1, 2, 3]
|
||||
|
||||
"""
|
||||
for i in range(n):
|
||||
yield i
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
+43
-22
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -34,17 +34,44 @@ class BaseTestCase(unittest.TestCase):
|
||||
"""Shared test case for Python Fire tests."""
|
||||
|
||||
@contextlib.contextmanager
|
||||
def assertStdoutMatches(self, regexp):
|
||||
"""Asserts that the context generates stdout matching regexp."""
|
||||
stdout = six.StringIO()
|
||||
with mock.patch.object(sys, 'stdout', stdout):
|
||||
yield
|
||||
value = stdout.getvalue()
|
||||
if not re.search(regexp, value, re.DOTALL | re.MULTILINE):
|
||||
raise AssertionError('Expected %r to match %r' % (value, regexp))
|
||||
def assertOutputMatches(self, stdout='.*', stderr='.*', capture=True):
|
||||
"""Asserts that the context generates stdout and stderr matching regexps.
|
||||
|
||||
Note: If wrapped code raises an exception, stdout and stderr will not be
|
||||
checked.
|
||||
|
||||
Args:
|
||||
stdout: (str) regexp to match against stdout (None will check no stdout)
|
||||
stderr: (str) regexp to match against stderr (None will check no stderr)
|
||||
capture: (bool, default True) do not bubble up stdout or stderr
|
||||
Yields:
|
||||
Yields to the wrapped context.
|
||||
"""
|
||||
stdout_fp = six.StringIO()
|
||||
stderr_fp = six.StringIO()
|
||||
try:
|
||||
with mock.patch.object(sys, 'stdout', stdout_fp):
|
||||
with mock.patch.object(sys, 'stderr', stderr_fp):
|
||||
yield
|
||||
finally:
|
||||
if not capture:
|
||||
sys.stdout.write(stdout_fp.getvalue())
|
||||
sys.stderr.write(stderr_fp.getvalue())
|
||||
|
||||
for name, regexp, fp in [('stdout', stdout, stdout_fp),
|
||||
('stderr', stderr, stderr_fp)]:
|
||||
value = fp.getvalue()
|
||||
if regexp is None:
|
||||
if value:
|
||||
raise AssertionError('%s: Expected no output. Got: %r' %
|
||||
(name, value))
|
||||
else:
|
||||
if not re.search(regexp, value, re.DOTALL | re.MULTILINE):
|
||||
raise AssertionError('%s: Expected %r to match %r' %
|
||||
(name, value, regexp))
|
||||
|
||||
@contextlib.contextmanager
|
||||
def assertRaisesFireExit(self, code, regexp=None):
|
||||
def assertRaisesFireExit(self, code, regexp='.*'):
|
||||
"""Asserts that a FireExit error is raised in the context.
|
||||
|
||||
Allows tests to check that Fire's wrapper around SystemExit is raised
|
||||
@@ -56,11 +83,8 @@ class BaseTestCase(unittest.TestCase):
|
||||
Yields:
|
||||
Yields to the wrapped context.
|
||||
"""
|
||||
if regexp is None:
|
||||
regexp = '.*'
|
||||
with self.assertRaises(core.FireExit):
|
||||
stdout = six.StringIO()
|
||||
with mock.patch.object(sys, 'stdout', stdout):
|
||||
with self.assertOutputMatches(stderr=regexp):
|
||||
with self.assertRaises(core.FireExit):
|
||||
try:
|
||||
yield
|
||||
except core.FireExit as exc:
|
||||
@@ -68,13 +92,10 @@ class BaseTestCase(unittest.TestCase):
|
||||
raise AssertionError('Incorrect exit code: %r != %r' % (exc.code,
|
||||
code))
|
||||
self.assertIsInstance(exc.trace, trace.FireTrace)
|
||||
stdout.flush()
|
||||
stdout.seek(0)
|
||||
value = stdout.getvalue()
|
||||
if not re.search(regexp, value, re.DOTALL | re.MULTILINE):
|
||||
raise AssertionError('Expected %r to match %r' % (value, regexp))
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
unittest.main()
|
||||
# pylint: disable=invalid-name
|
||||
main = unittest.main
|
||||
skip = unittest.skip
|
||||
# pylint: enable=invalid-name
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# 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 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."""
|
||||
|
||||
def testNoCheckOnException(self):
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertOutputMatches(stdout='blah'):
|
||||
raise ValueError()
|
||||
|
||||
def testCheckStdoutOrStderrNone(self):
|
||||
with six.assertRaisesRegex(self, AssertionError, 'stdout:'):
|
||||
with self.assertOutputMatches(stdout=None):
|
||||
print('blah')
|
||||
|
||||
with six.assertRaisesRegex(self, AssertionError, 'stderr:'):
|
||||
with self.assertOutputMatches(stderr=None):
|
||||
print('blah', file=sys.stderr)
|
||||
|
||||
with six.assertRaisesRegex(self, AssertionError, 'stderr:'):
|
||||
with self.assertOutputMatches(stdout='apple', stderr=None):
|
||||
print('apple')
|
||||
print('blah', file=sys.stderr)
|
||||
|
||||
def testCorrectOrderingOfAssertRaises(self):
|
||||
# Check to make sure FireExit tests are correct.
|
||||
with self.assertOutputMatches(stdout='Yep.*first.*second'):
|
||||
with self.assertRaises(ValueError):
|
||||
print('Yep, this is the first line.\nThis is the second.')
|
||||
raise ValueError()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
testutils.main()
|
||||
+40
-34
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -30,10 +30,12 @@ from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import pipes
|
||||
from fire import inspectutils
|
||||
|
||||
INITIAL_COMPONENT = 'Initial component'
|
||||
INSTANTIATED_CLASS = 'Instantiated class'
|
||||
CALLED_ROUTINE = 'Called routine'
|
||||
CALLED_CALLABLE = 'Called callable'
|
||||
ACCESSED_PROPERTY = 'Accessed property'
|
||||
COMPLETION_SCRIPT = 'Generated completion script'
|
||||
INTERACTIVE_MODE = 'Entered interactive mode'
|
||||
@@ -63,7 +65,9 @@ class FireTrace(object):
|
||||
|
||||
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.
|
||||
@@ -76,6 +80,7 @@ class FireTrace(object):
|
||||
for element in reversed(self.elements):
|
||||
if not element.HasError():
|
||||
return element
|
||||
return None
|
||||
|
||||
def HasError(self):
|
||||
"""Returns whether the Fire execution encountered a Fire usage error."""
|
||||
@@ -92,44 +97,24 @@ class FireTrace(object):
|
||||
)
|
||||
self.elements.append(element)
|
||||
|
||||
def AddCalledRoutine(self, component, target, args, filename, lineno,
|
||||
capacity):
|
||||
"""Adds an element to the trace indicating that a routine was called.
|
||||
def AddCalledComponent(self, component, target, args, filename, lineno,
|
||||
capacity, action=CALLED_CALLABLE):
|
||||
"""Adds an element to the trace indicating that a component was called.
|
||||
|
||||
Also applies to instantiating a class.
|
||||
|
||||
Args:
|
||||
component: The result of calling the routine.
|
||||
target: The name of the routine.
|
||||
args: The args consumed in order to call this routine.
|
||||
filename: The file in which the routine is defined, or None if N/A.
|
||||
lineno: The line number on which the routine is defined, or None if N/A.
|
||||
capacity: (bool) Whether the routine could have accepted additional args.
|
||||
component: The result of calling the callable.
|
||||
target: The name of the callable.
|
||||
args: The args consumed in order to call this callable.
|
||||
filename: The file in which the callable is defined, or None if N/A.
|
||||
lineno: The line number on which the callable is defined, or None if N/A.
|
||||
capacity: (bool) Whether the callable could have accepted additional args.
|
||||
action: The value to include as the action in the FireTraceElement.
|
||||
"""
|
||||
element = FireTraceElement(
|
||||
component=component,
|
||||
action=CALLED_ROUTINE,
|
||||
target=target,
|
||||
args=args,
|
||||
filename=filename,
|
||||
lineno=lineno,
|
||||
capacity=capacity,
|
||||
)
|
||||
self.elements.append(element)
|
||||
|
||||
def AddInstantiatedClass(self, component, target, args, filename, lineno,
|
||||
capacity):
|
||||
"""Adds an element to the trace indicating that a class was instantiated.
|
||||
|
||||
Args:
|
||||
component: The result of instantiating the class.
|
||||
target: The name of the class.
|
||||
args: The args consumed in order to instantiate the class.
|
||||
filename: The file in which the class is defined, or None if N/A.
|
||||
lineno: The line number on which the class is defined, or None if N/A.
|
||||
capacity: (bool) Whether cls.__init__ could have accepted additional args.
|
||||
"""
|
||||
element = FireTraceElement(
|
||||
component=component,
|
||||
action=INSTANTIATED_CLASS,
|
||||
action=action,
|
||||
target=target,
|
||||
args=args,
|
||||
filename=filename,
|
||||
@@ -233,6 +218,27 @@ class FireTrace(object):
|
||||
for index, element in enumerate(self.elements)
|
||||
)
|
||||
|
||||
def NeedsSeparatingHyphenHyphen(self, flag='help'):
|
||||
"""Returns whether a the trace need '--' before '--help'.
|
||||
|
||||
'--' is needed when the component takes keyword arguments, when the value of
|
||||
flag matches one of the argument of the component, or the component takes in
|
||||
keyword-only arguments(e.g. argument with default value).
|
||||
|
||||
Args:
|
||||
flag: the flag available for the trace
|
||||
|
||||
Returns:
|
||||
True for needed '--', False otherwise.
|
||||
|
||||
"""
|
||||
element = self.GetLastHealthyElement()
|
||||
component = element.component
|
||||
spec = inspectutils.GetFullArgSpec(component)
|
||||
return (spec.varkw is not None
|
||||
or flag in spec.args
|
||||
or flag in spec.kwonlyargs)
|
||||
|
||||
|
||||
class FireTraceElement(object):
|
||||
"""A FireTraceElement represents a single step taken by a Fire execution.
|
||||
|
||||
+21
-7
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -51,10 +51,20 @@ class FireTraceTest(testutils.BaseTestCase):
|
||||
str(t),
|
||||
'1. Initial component\n2. Accessed property "prop" (sample.py:12)')
|
||||
|
||||
def testAddCalledCallable(self):
|
||||
t = trace.FireTrace('initial object')
|
||||
args = ('example', 'args')
|
||||
t.AddCalledComponent('result', 'cell', args, 'sample.py', 10, False,
|
||||
action=trace.CALLED_CALLABLE)
|
||||
self.assertEqual(
|
||||
str(t),
|
||||
'1. Initial component\n2. Called callable "cell" (sample.py:10)')
|
||||
|
||||
def testAddCalledRoutine(self):
|
||||
t = trace.FireTrace('initial object')
|
||||
args = ('example', 'args')
|
||||
t.AddCalledRoutine('result', 'run', args, 'sample.py', 12, False)
|
||||
t.AddCalledComponent('result', 'run', args, 'sample.py', 12, False,
|
||||
action=trace.CALLED_ROUTINE)
|
||||
self.assertEqual(
|
||||
str(t),
|
||||
'1. Initial component\n2. Called routine "run" (sample.py:12)')
|
||||
@@ -62,8 +72,9 @@ class FireTraceTest(testutils.BaseTestCase):
|
||||
def testAddInstantiatedClass(self):
|
||||
t = trace.FireTrace('initial object')
|
||||
args = ('example', 'args')
|
||||
t.AddInstantiatedClass(
|
||||
'Classname', 'classname', args, 'sample.py', 12, False)
|
||||
t.AddCalledComponent(
|
||||
'Classname', 'classname', args, 'sample.py', 12, False,
|
||||
action=trace.INSTANTIATED_CLASS)
|
||||
target = """1. Initial component
|
||||
2. Instantiated class "classname" (sample.py:12)"""
|
||||
self.assertEqual(str(t), target)
|
||||
@@ -85,19 +96,22 @@ class FireTraceTest(testutils.BaseTestCase):
|
||||
def testGetCommand(self):
|
||||
t = trace.FireTrace('initial object')
|
||||
args = ('example', 'args')
|
||||
t.AddCalledRoutine('result', 'run', args, 'sample.py', 12, False)
|
||||
t.AddCalledComponent('result', 'run', args, 'sample.py', 12, False,
|
||||
action=trace.CALLED_ROUTINE)
|
||||
self.assertEqual(t.GetCommand(), 'example args')
|
||||
|
||||
def testGetCommandWithQuotes(self):
|
||||
t = trace.FireTrace('initial object')
|
||||
args = ('example', 'spaced arg')
|
||||
t.AddCalledRoutine('result', 'run', args, 'sample.py', 12, False)
|
||||
t.AddCalledComponent('result', 'run', args, 'sample.py', 12, False,
|
||||
action=trace.CALLED_ROUTINE)
|
||||
self.assertEqual(t.GetCommand(), "example 'spaced arg'")
|
||||
|
||||
def testGetCommandWithFlagQuotes(self):
|
||||
t = trace.FireTrace('initial object')
|
||||
args = ('--example=spaced arg',)
|
||||
t.AddCalledRoutine('result', 'run', args, 'sample.py', 12, False)
|
||||
t.AddCalledComponent('result', 'run', args, 'sample.py', 12, False,
|
||||
action=trace.CALLED_ROUTINE)
|
||||
self.assertEqual(t.GetCommand(), "--example='spaced arg'")
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Types of values."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import inspect
|
||||
|
||||
import six
|
||||
|
||||
|
||||
VALUE_TYPES = (bool, six.string_types, six.integer_types, float, complex)
|
||||
|
||||
|
||||
def IsGroup(component):
|
||||
# TODO(dbieber): Check if there are any subcomponents.
|
||||
return not IsCommand(component) and not IsValue(component)
|
||||
|
||||
|
||||
def IsCommand(component):
|
||||
return inspect.isroutine(component) or inspect.isclass(component)
|
||||
|
||||
|
||||
def IsValue(component):
|
||||
return isinstance(component, VALUE_TYPES)
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
site_name: Python Fire
|
||||
theme: readthedocs
|
||||
markdown_extensions: [fenced_code]
|
||||
pages:
|
||||
- Overview: index.md
|
||||
- Installation: installation.md
|
||||
- Benefits: benefits.md
|
||||
- The Python Fire Guide: guide.md
|
||||
- Using a CLI: using-cli.md
|
||||
- Troubleshooting: troubleshooting.md
|
||||
- Reference: api.md
|
||||
@@ -32,7 +32,7 @@ enable=indexing-exception,old-raise-syntax
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time.
|
||||
disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored
|
||||
disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
[metadata]
|
||||
license-file = LICENSE
|
||||
|
||||
[wheel]
|
||||
universal = 1
|
||||
|
||||
[aliases]
|
||||
test=pytest
|
||||
test = pytest
|
||||
|
||||
[tool:pytest]
|
||||
addopts = --ignore=fire/test_components_py3.py --ignore=fire/parser_fuzz_test.py
|
||||
|
||||
[pytype]
|
||||
inputs = .
|
||||
output = .pytype
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2017 Google Inc.
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -27,10 +27,9 @@ it fires off your command.
|
||||
""".strip()
|
||||
|
||||
SHORT_DESCRIPTION = """
|
||||
A library for automatically generating commane line interfaces.""".strip()
|
||||
A library for automatically generating command line interfaces.""".strip()
|
||||
|
||||
DEPENDENCIES = [
|
||||
'ipython',
|
||||
'six',
|
||||
]
|
||||
|
||||
@@ -40,18 +39,18 @@ TEST_DEPENDENCIES = [
|
||||
'python-Levenshtein',
|
||||
]
|
||||
|
||||
VERSION = '0.1.3'
|
||||
URL = 'https://github.com/google/python-fire'
|
||||
|
||||
setup(
|
||||
name='fire',
|
||||
version='0.1.0',
|
||||
|
||||
version=VERSION,
|
||||
description=SHORT_DESCRIPTION,
|
||||
long_description=LONG_DESCRIPTION,
|
||||
|
||||
url='https://github.com/google/python-fire',
|
||||
url=URL,
|
||||
|
||||
author='David Bieber',
|
||||
author_email='dbieber@google.com',
|
||||
|
||||
license='Apache Software License',
|
||||
|
||||
classifiers=[
|
||||
@@ -69,6 +68,7 @@ setup(
|
||||
'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',
|
||||
|
||||
Reference in New Issue
Block a user