Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fb8b53e27 | ||
|
|
a44898e4db | ||
|
|
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 | ||
|
|
5e65a31858 | ||
|
|
ee1f573889 | ||
|
|
ea80933cb3 | ||
|
|
51fc990ddc | ||
|
|
4198d5a42b | ||
|
|
05d59337d8 | ||
|
|
35a3ec9981 | ||
|
|
c399562dd8 | ||
|
|
c3f158a811 | ||
|
|
babf4f19bb | ||
|
|
6feced71c6 | ||
|
|
d5f56381e9 | ||
|
|
1239bdd23b | ||
|
|
d0968d7b04 | ||
|
|
88f65747e2 | ||
|
|
8e294783b1 | ||
|
|
59d9095efb | ||
|
|
f8ad6b7f78 | ||
|
|
63aba49c50 |
+100
@@ -0,0 +1,100 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# dotenv
|
||||
.env
|
||||
|
||||
# virtualenv
|
||||
.venv
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# PyCharm IDE
|
||||
.idea/
|
||||
+14
-1
@@ -4,8 +4,21 @@ 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 setup.py develop
|
||||
script: nosetests --ignore-files=parser_fuzz_test.py
|
||||
script:
|
||||
- python -m pytest # Run the tests without IPython.
|
||||
- pip install ipython
|
||||
- python -m pytest # Now run the tests with IPython.
|
||||
- pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
include LICENSE
|
||||
@@ -1,18 +1,25 @@
|
||||
# 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
|
||||
|
||||
@@ -20,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
|
||||
@@ -43,7 +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](docs/guide.md).
|
||||
|
||||
|
||||
## Why is it called Fire?
|
||||
@@ -51,6 +60,11 @@ about Fire's other features, see the [Using a Fire CLI page](doc/using-cli.md).
|
||||
When you call `Fire`, it fires off (executes) your command.
|
||||
|
||||
|
||||
## Where can I learn more?
|
||||
|
||||
Please see [The Python Fire Guide](docs/guide.md).
|
||||
|
||||
|
||||
## Reference
|
||||
|
||||
| Setup | Command | Notes
|
||||
@@ -65,14 +79,19 @@ When you call `Fire`, it fires off (executes) your command.
|
||||
|
||||
| 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
|
||||
+714
@@ -0,0 +1,714 @@
|
||||
## The Python Fire Guide
|
||||
|
||||
### Introduction
|
||||
|
||||
Welcome to the Python Fire guide! Python Fire is a Python library that will turn
|
||||
any Python component into a command line interface with just a single call to
|
||||
`Fire`.
|
||||
|
||||
Let's get started!
|
||||
|
||||
### Installation
|
||||
|
||||
To install Python Fire from pypi, run:
|
||||
|
||||
`pip install fire`
|
||||
|
||||
Alternatively, to install Python Fire from source, clone the source and run:
|
||||
|
||||
`python setup.py install`
|
||||
|
||||
### Hello World
|
||||
|
||||
##### Version 1: `fire.Fire()`
|
||||
|
||||
The easiest way to use Fire is to take any Python program, and then simply call
|
||||
`fire.Fire()` at the end of the program. This will expose the full contents of
|
||||
the program to the command line.
|
||||
|
||||
```python
|
||||
import fire
|
||||
|
||||
def hello(name):
|
||||
return 'Hello {name}!'.format(name=name)
|
||||
|
||||
if __name__ == '__main__':
|
||||
fire.Fire()
|
||||
```
|
||||
|
||||
Here's how we can run our program from the command line:
|
||||
|
||||
```bash
|
||||
$ python example.py hello World
|
||||
Hello World!
|
||||
```
|
||||
|
||||
##### Version 2: `fire.Fire(<fn>)`
|
||||
|
||||
Let's modify our program slightly to only expose the `hello` function to the
|
||||
command line.
|
||||
|
||||
```python
|
||||
import fire
|
||||
|
||||
def hello(name):
|
||||
return 'Hello {name}!'.format(name=name)
|
||||
|
||||
if __name__ == '__main__':
|
||||
fire.Fire(hello)
|
||||
```
|
||||
|
||||
Here's how we can run this from the command line:
|
||||
|
||||
```bash
|
||||
$ python example.py World
|
||||
Hello World!
|
||||
```
|
||||
|
||||
Notice we no longer have to specify to run the `hello` function, because we
|
||||
called `fire.Fire(hello)`.
|
||||
|
||||
##### Version 3: Using a main
|
||||
|
||||
We can alternatively write this program like this:
|
||||
|
||||
```python
|
||||
import fire
|
||||
|
||||
def hello(name):
|
||||
return 'Hello {name}!'.format(name=name)
|
||||
|
||||
def main():
|
||||
fire.Fire(hello)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||
|
||||
Or if we're using
|
||||
[entry points](https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points),
|
||||
then simply this:
|
||||
|
||||
```python
|
||||
import fire
|
||||
|
||||
def hello(name):
|
||||
return 'Hello {name}!'.format(name=name)
|
||||
|
||||
def main():
|
||||
fire.Fire(hello)
|
||||
```
|
||||
|
||||
### Exposing Multiple Commands
|
||||
|
||||
In the previous example, we exposed a single function to the command line. Now
|
||||
we'll look at ways of exposing multiple functions to the command line.
|
||||
|
||||
##### Version 1: `fire.Fire()`
|
||||
|
||||
The simplest way to expose multiple commands is to write multiple functions, and
|
||||
then call Fire.
|
||||
|
||||
```python
|
||||
import fire
|
||||
|
||||
def add(x, y):
|
||||
return x + y
|
||||
|
||||
def multiply(x, y):
|
||||
return x * y
|
||||
|
||||
if __name__ == '__main__':
|
||||
fire.Fire()
|
||||
```
|
||||
|
||||
We can use this like so:
|
||||
|
||||
```bash
|
||||
$ python example.py add 10 20
|
||||
30
|
||||
$ python example.py multiply 10 20
|
||||
200
|
||||
```
|
||||
|
||||
You'll notice that Fire correctly parsed `10` and `20` as numbers, rather than
|
||||
as strings. Read more about [argument parsing here](#argument-parsing).
|
||||
|
||||
##### Version 2: `fire.Fire(<dict>)`
|
||||
|
||||
In version 1 we exposed all the program's functionality to the command line. By
|
||||
using a dict, we can selectively expose functions to the command line.
|
||||
|
||||
```python
|
||||
import fire
|
||||
|
||||
def add(x, y):
|
||||
return x + y
|
||||
|
||||
def multiply(x, y):
|
||||
return x * y
|
||||
|
||||
if __name__ == '__main__':
|
||||
fire.Fire({
|
||||
'add': add,
|
||||
'multiply': multiply,
|
||||
})
|
||||
```
|
||||
|
||||
We can use this in the same way as before:
|
||||
|
||||
```bash
|
||||
$ python example.py add 10 20
|
||||
30
|
||||
$ python example.py multiply 10 20
|
||||
200
|
||||
```
|
||||
|
||||
##### Version 3: `fire.Fire(<object>)`
|
||||
|
||||
Fire also works on objects, as in this variant. This is a good way to expose
|
||||
multiple commands.
|
||||
|
||||
```python
|
||||
import fire
|
||||
|
||||
class Calculator(object):
|
||||
|
||||
def add(self, x, y):
|
||||
return x + y
|
||||
|
||||
def multiply(self, x, y):
|
||||
return x * y
|
||||
|
||||
if __name__ == '__main__':
|
||||
calculator = Calculator()
|
||||
fire.Fire(calculator)
|
||||
```
|
||||
|
||||
We can use this in the same way as before:
|
||||
|
||||
```bash
|
||||
$ python example.py add 10 20
|
||||
30
|
||||
$ python example.py multiply 10 20
|
||||
200
|
||||
```
|
||||
|
||||
|
||||
##### Version 4: `fire.Fire(<class>)`
|
||||
|
||||
Fire also works on classes. This is another good way to expose multiple
|
||||
commands.
|
||||
|
||||
```python
|
||||
import fire
|
||||
|
||||
class Calculator(object):
|
||||
|
||||
def add(self, x, y):
|
||||
return x + y
|
||||
|
||||
def multiply(self, x, y):
|
||||
return x * y
|
||||
|
||||
if __name__ == '__main__':
|
||||
fire.Fire(Calculator)
|
||||
```
|
||||
|
||||
We can use this in the same way as before:
|
||||
|
||||
```bash
|
||||
$ python example.py add 10 20
|
||||
30
|
||||
$ python example.py multiply 10 20
|
||||
200
|
||||
```
|
||||
|
||||
Why might you prefer a class over an object? One reason is that you can pass
|
||||
arguments for constructing the class too, as in this broken calculator example.
|
||||
|
||||
```python
|
||||
import fire
|
||||
|
||||
class BrokenCalculator(object):
|
||||
|
||||
def __init__(self, offset=1):
|
||||
self._offset = offset
|
||||
|
||||
def add(self, x, y):
|
||||
return x + y + self._offset
|
||||
|
||||
def multiply(self, x, y):
|
||||
return x * y + self._offset
|
||||
|
||||
if __name__ == '__main__':
|
||||
fire.Fire(BrokenCalculator)
|
||||
```
|
||||
|
||||
When you use a broken calculator, you get wrong answers:
|
||||
|
||||
```bash
|
||||
$ python example.py add 10 20
|
||||
31
|
||||
$ python example.py multiply 10 20
|
||||
201
|
||||
```
|
||||
|
||||
But you can always fix it:
|
||||
|
||||
```bash
|
||||
$ python example.py add 10 20 --offset=0
|
||||
30
|
||||
$ python example.py multiply 10 20 --offset=0
|
||||
200
|
||||
```
|
||||
|
||||
Unlike calling ordinary functions, which can be done both with positional
|
||||
arguments and named arguments (--flag syntax), arguments to \_\_init\_\_
|
||||
functions must be passed with the --flag syntax. See the section on
|
||||
[calling functions](#calling-functions) for more.
|
||||
|
||||
### Grouping Commands
|
||||
|
||||
Here's an example of how you might make a command line interface with grouped
|
||||
commands.
|
||||
|
||||
```python
|
||||
class IngestionStage(object):
|
||||
|
||||
def run(self):
|
||||
return 'Ingesting! Nom nom nom...'
|
||||
|
||||
class DigestionStage(object):
|
||||
|
||||
def run(self, volume=1):
|
||||
return ' '.join(['Burp!'] * volume)
|
||||
|
||||
def status(self):
|
||||
return 'Satiated.'
|
||||
|
||||
class Pipeline(object):
|
||||
|
||||
def __init__(self):
|
||||
self.ingestion = IngestionStage()
|
||||
self.digestion = DigestionStage()
|
||||
|
||||
def run(self):
|
||||
self.ingestion.run()
|
||||
self.digestion.run()
|
||||
|
||||
if __name__ == '__main__':
|
||||
fire.Fire(Pipeline)
|
||||
```
|
||||
|
||||
Here's how this looks at the command line:
|
||||
|
||||
```bash
|
||||
$ python example.py run
|
||||
Ingesting! Nom nom nom...
|
||||
Burp!
|
||||
$ python example.py ingestion run
|
||||
Ingesting! Nom nom nom...
|
||||
$ python example.py digestion run
|
||||
Burp!
|
||||
$ python example.py digestion status
|
||||
Satiated.
|
||||
```
|
||||
|
||||
You can nest your commands in arbitrarily complex ways, if you're feeling grumpy
|
||||
or adventurous.
|
||||
|
||||
|
||||
### Accessing Properties
|
||||
|
||||
In the examples we've looked at so far, our invocations of `python example.py`
|
||||
have all run some function from the example program. In this example, we simply
|
||||
access a property.
|
||||
|
||||
```python
|
||||
from airports import airports
|
||||
|
||||
import fire
|
||||
|
||||
class Airport(object):
|
||||
|
||||
def __init__(self, code):
|
||||
self.code = code
|
||||
self.name = dict(airports).get(self.code)
|
||||
self.city = self.name.split(',')[0] if self.name else None
|
||||
|
||||
if __name__ == '__main__':
|
||||
fire.Fire(Airport)
|
||||
```
|
||||
|
||||
Now we can use this program to learn about airport codes!
|
||||
|
||||
```bash
|
||||
$ python example.py --code=JFK code
|
||||
JFK
|
||||
$ python example.py --code=SJC name
|
||||
San Jose-Sunnyvale-Santa Clara, CA - Norman Y. Mineta San Jose International (SJC)
|
||||
$ python example.py --code=ALB city
|
||||
Albany-Schenectady-Troy
|
||||
```
|
||||
|
||||
By the way, you can find this
|
||||
[airports module here](https://github.com/trendct-data/airports.py).
|
||||
|
||||
### Chaining Function Calls
|
||||
|
||||
When you run a Fire CLI, you can take all the same actions on the _result_ of
|
||||
the call to Fire that you can take on the original object passed in.
|
||||
|
||||
For example, we can use our Airport CLI from the previous example like this:
|
||||
|
||||
```bash
|
||||
$ python example.py --code=ALB city upper
|
||||
ALBANY-SCHENECTADY-TROY
|
||||
```
|
||||
|
||||
This works since `upper` is a method on all strings.
|
||||
|
||||
So, if you want to set up your functions to chain nicely, all you have to do is
|
||||
have a class whose methods return self. Here's an example.
|
||||
|
||||
```python
|
||||
import fire
|
||||
|
||||
class BinaryCanvas(object):
|
||||
"""A canvas with which to make binary art, one bit at a time."""
|
||||
|
||||
def __init__(self, size=10):
|
||||
self.pixels = [[0] * size for _ in range(size)]
|
||||
self._size = size
|
||||
self._row = 0 # The row of the cursor.
|
||||
self._col = 0 # The column of the cursor.
|
||||
|
||||
def __str__(self):
|
||||
return '\n'.join(' '.join(str(pixel) for pixel in row) for row in self.pixels)
|
||||
|
||||
def show(self):
|
||||
print(self)
|
||||
return self
|
||||
|
||||
def move(self, row, col):
|
||||
self._row = row % self._size
|
||||
self._col = col % self._size
|
||||
return self
|
||||
|
||||
def on(self):
|
||||
return self.set(1)
|
||||
|
||||
def off(self):
|
||||
return self.set(0)
|
||||
|
||||
def set(self, value):
|
||||
self.pixels[self._row][self._col] = value
|
||||
return self
|
||||
|
||||
if __name__ == '__main__':
|
||||
fire.Fire(BinaryCanvas)
|
||||
```
|
||||
|
||||
Now we can draw stuff :).
|
||||
|
||||
```bash
|
||||
$ python example.py move 3 3 on move 3 6 on move 6 3 on move 6 6 on move 7 4 on move 7 5 on __str__
|
||||
0 0 0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0 0 0
|
||||
0 0 0 1 0 0 1 0 0 0
|
||||
0 0 0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0 0 0
|
||||
0 0 0 1 0 0 1 0 0 0
|
||||
0 0 0 0 1 1 0 0 0 0
|
||||
0 0 0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0 0 0
|
||||
```
|
||||
|
||||
It's supposed to be a smiley face.
|
||||
|
||||
### Can we make an even simpler example than Hello World?
|
||||
|
||||
Yes, this program is even simpler than our original Hello World example.
|
||||
|
||||
```python
|
||||
import fire
|
||||
english = 'Hello World'
|
||||
spanish = 'Hola Mundo'
|
||||
fire.Fire()
|
||||
```
|
||||
|
||||
You can use it like this:
|
||||
|
||||
```bash
|
||||
$ python example.py english
|
||||
Hello World
|
||||
$ python example.py spanish
|
||||
Hola Mundo
|
||||
```
|
||||
|
||||
### Calling Functions
|
||||
|
||||
Arguments to a constructor are passed by name using flag syntax `--name=value`.
|
||||
|
||||
For example, consider this simple class:
|
||||
|
||||
```python
|
||||
import fire
|
||||
|
||||
class Building(object):
|
||||
|
||||
def __init__(self, name, stories=1):
|
||||
self.name = name
|
||||
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 'Done!'
|
||||
|
||||
if __name__ == '__main__':
|
||||
fire.Fire(Building)
|
||||
```
|
||||
|
||||
We can instantiate it as follows: `python example.py --name="Sherrerd Hall"`
|
||||
|
||||
Arguments to other functions may be passed positionally or by name using flag
|
||||
syntax.
|
||||
|
||||
To instantiate a `Building` and then run the `climb_stairs` function, the
|
||||
following commands are all valid:
|
||||
|
||||
```bash
|
||||
$ python example.py --name="Sherrerd Hall" --stories=3 climb_stairs 10
|
||||
$ python example.py --name="Sherrerd Hall" climb_stairs --stairs_per_story=10
|
||||
$ 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 interchangeable in
|
||||
member names and flag names.
|
||||
|
||||
You'll also notice that the constructor's arguments can come after the
|
||||
function's arguments or before the function.
|
||||
|
||||
You'll also notice that the equal sign between the flag name and its value is
|
||||
optional.
|
||||
|
||||
##### Functions with `*varargs` and `**kwargs`
|
||||
|
||||
Fire supports functions that take \*varargs or \*\*kwargs. Here's an example:
|
||||
|
||||
```python
|
||||
import fire
|
||||
|
||||
def order_by_length(*items):
|
||||
"""Orders items by length, breaking ties alphabetically."""
|
||||
sorted_items = sorted(items, key=lambda item: (len(str(item)), str(item)))
|
||||
return ' '.join(sorted_items)
|
||||
|
||||
if __name__ == '__main__':
|
||||
fire.Fire(order_by_length)
|
||||
```
|
||||
|
||||
To use it, we run:
|
||||
|
||||
```bash
|
||||
$ python example.py dog cat elephant
|
||||
cat dog elephant
|
||||
```
|
||||
|
||||
You can use a separator to indicate that you're done providing arguments to a
|
||||
function. All arguments after the separator will be used to process the result
|
||||
of the function, rather than being passed to the function itself. The default
|
||||
separator is the hyphen `-`.
|
||||
|
||||
Here's an example where we use a separator.
|
||||
|
||||
```bash
|
||||
$ python example.py dog cat elephant - upper
|
||||
CAT DOG ELEPHANT
|
||||
```
|
||||
|
||||
Without the separator, upper would have been treated as another argument.
|
||||
|
||||
```bash
|
||||
$ python example.py dog cat elephant upper
|
||||
cat dog upper elephant
|
||||
```
|
||||
|
||||
You can change the separator with the `--separator` flag. Flags are always
|
||||
separated from your Fire command by an isolated `--`. Here's an example where we
|
||||
change the separator.
|
||||
|
||||
```bash
|
||||
$ python example.py dog cat elephant X upper -- --separator=X
|
||||
CAT DOG ELEPHANT
|
||||
```
|
||||
|
||||
Separators can be useful when a function accepts \*varargs, \*\*kwargs, or
|
||||
default values that you don't want to specify. It is also important to remember
|
||||
to change the separator if you want to pass `-` as an argument.
|
||||
|
||||
|
||||
### Argument Parsing
|
||||
|
||||
The types of the arguments are determined by their values, rather than by the
|
||||
function signature where they're used. You can pass any Python literal from the
|
||||
command line: numbers, strings, tuples, lists, dictionaries, (sets are only
|
||||
supported in some versions of Python). You can also nest the collections
|
||||
arbitrarily as long as they only contain literals.
|
||||
|
||||
To demonstrate this, we'll make a small example program that tells us the type
|
||||
of any argument we give it:
|
||||
|
||||
```python
|
||||
import fire
|
||||
fire.Fire(lambda obj: type(obj).__name__)
|
||||
```
|
||||
|
||||
And we'll use it like so:
|
||||
|
||||
```bash
|
||||
$ python example.py 10
|
||||
int
|
||||
$ python example.py 10.0
|
||||
float
|
||||
$ python example.py hello
|
||||
str
|
||||
$ python example.py '(1,2)'
|
||||
tuple
|
||||
$ python example.py [1,2]
|
||||
list
|
||||
$ python example.py True
|
||||
bool
|
||||
$ python example.py {name: David}
|
||||
dict
|
||||
```
|
||||
|
||||
You'll notice in that last example that bare-words are automatically replaced
|
||||
with strings.
|
||||
|
||||
Be careful with your quotes! If you want to pass the string `"10"`, rather than
|
||||
the int `10`, you'll need to either escape or quote your quotes. Otherwise Bash
|
||||
will eat your quotes and pass an unquoted `10` to your Python program, where
|
||||
Fire will interpret it as a number.
|
||||
|
||||
|
||||
```bash
|
||||
$ python example.py 10
|
||||
int
|
||||
$ python example.py "10"
|
||||
int
|
||||
$ python example.py '"10"'
|
||||
str
|
||||
$ python example.py "'10'"
|
||||
str
|
||||
$ python example.py \"10\"
|
||||
str
|
||||
```
|
||||
|
||||
Be careful with your quotes! Remember that Bash processes your arguments first,
|
||||
and then Fire parses the result of that.
|
||||
If you wanted to pass the dict `{"name": "David Bieber"}` to your program, you
|
||||
might try this:
|
||||
|
||||
```bash
|
||||
$ python example.py '{"name": "David Bieber"}' # Good! Do this.
|
||||
dict
|
||||
$ python example.py {"name":'"David Bieber"'} # Okay.
|
||||
dict
|
||||
$ python example.py {"name":"David Bieber"} # Wrong. This is parsed as a string.
|
||||
str
|
||||
$ python example.py {"name": "David Bieber"} # Wrong. This isn't even treated as a single argument.
|
||||
<error>
|
||||
$ python example.py '{"name": "Justin Bieber"}' # Wrong. This is not the Bieber you're looking for. (The syntax is fine though :))
|
||||
dict
|
||||
```
|
||||
|
||||
##### Boolean Arguments
|
||||
|
||||
The tokens `True` and `False` are parsed as boolean values.
|
||||
|
||||
You may also specify booleans via flag syntax `--name` and `--noname`, which set
|
||||
`name` to `True` and `False` respectively.
|
||||
|
||||
Continuing the previous example, we could run any of the following:
|
||||
|
||||
```bash
|
||||
$ python example.py --obj=True
|
||||
bool
|
||||
$ python example.py --obj=False
|
||||
bool
|
||||
$ python example.py --obj
|
||||
bool
|
||||
$ python example.py --noobj
|
||||
bool
|
||||
```
|
||||
|
||||
Be careful with boolean flags! If a token other than another flag immediately
|
||||
follows a flag that's supposed to be a boolean, the flag will take on the value
|
||||
of the token rather than the boolean value. You can resolve this: by putting a
|
||||
separator after your last flag, by explicitly stating the value of the boolean
|
||||
flag (as in `--obj=True`), or by making sure there's another flag after any
|
||||
boolean flag argument.
|
||||
|
||||
|
||||
### Using Fire Flags
|
||||
|
||||
Fire CLIs all come with a number of flags. These flags should be separated from
|
||||
the Fire command by an isolated `--`. If there is at least one isolated `--`
|
||||
argument, then arguments after the final isolated `--` are treated as flags,
|
||||
whereas all arguments before the final isolated `--` are considered part of the
|
||||
Fire command.
|
||||
|
||||
One useful flag is the `--interactive` flag. Use the `--interactive` flag on any
|
||||
CLI to enter a Python REPL with all the modules and variables used in the
|
||||
context where `Fire` was called already available to you for use. Other useful
|
||||
variables, such as the result of the Fire command will also be available. Use
|
||||
this feature like this: `python example.py -- --interactive`.
|
||||
|
||||
You can add the help flag to any command to see help and usage information. Fire
|
||||
incorporates your docstrings into the help and usage information that it
|
||||
generates. Fire will try to provide help even if you omit the isolated `--`
|
||||
separating the flags from the Fire command, but may not always be able to, since
|
||||
`help` is a valid argument name. Use this feature like this:
|
||||
`python example.py -- --help`.
|
||||
|
||||
The complete set of flags available is shown below, in the reference section.
|
||||
|
||||
|
||||
### Reference
|
||||
|
||||
| Setup | Command | Notes
|
||||
| :------ | :------------------ | :---------
|
||||
| install | `pip install fire` |
|
||||
|
||||
##### Creating a CLI
|
||||
|
||||
| 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.
|
||||
|
||||
##### Flags
|
||||
|
||||
| Using a CLI | Command | Notes
|
||||
| :------------- | :------------------------- | :---------
|
||||
| [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._
|
||||
|
||||
|
||||
### Disclaimer
|
||||
|
||||
Python Fire is not an official Google product.
|
||||
@@ -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.
|
||||
@@ -12,12 +12,14 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from fire.examples.cipher import cipher
|
||||
"""Tests for the cipher module."""
|
||||
|
||||
import unittest
|
||||
from fire import testutils
|
||||
|
||||
from examples.cipher import cipher
|
||||
|
||||
|
||||
class CipherTest(unittest.TestCase):
|
||||
class CipherTest(testutils.BaseTestCase):
|
||||
|
||||
def testCipher(self):
|
||||
self.assertEqual(cipher.rot13('Hello world!'), 'Uryyb jbeyq!')
|
||||
@@ -29,4 +31,4 @@ class CipherTest(unittest.TestCase):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.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.
|
||||
@@ -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.
|
||||
@@ -12,15 +12,17 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for the diff and difffull modules."""
|
||||
|
||||
import tempfile
|
||||
|
||||
from fire.examples.diff import diff
|
||||
from fire.examples.diff import difffull
|
||||
from fire import testutils
|
||||
|
||||
import unittest
|
||||
from examples.diff import diff
|
||||
from examples.diff import difffull
|
||||
|
||||
|
||||
class DiffTest(unittest.TestCase):
|
||||
class DiffTest(testutils.BaseTestCase):
|
||||
"""The purpose of these tests is to ensure the difflib wrappers works.
|
||||
|
||||
It is not the goal of these tests to exhaustively test difflib functionality.
|
||||
@@ -30,8 +32,8 @@ class DiffTest(unittest.TestCase):
|
||||
self.file1 = file1 = tempfile.NamedTemporaryFile()
|
||||
self.file2 = file2 = tempfile.NamedTemporaryFile()
|
||||
|
||||
file1.write('test\ntest1\n')
|
||||
file2.write('test\ntest2\nextraline\n')
|
||||
file1.write(b'test\ntest1\n')
|
||||
file2.write(b'test\ntest2\nextraline\n')
|
||||
|
||||
file1.flush()
|
||||
file2.flush()
|
||||
@@ -90,4 +92,4 @@ class DiffTest(unittest.TestCase):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.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.
|
||||
|
||||
@@ -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.
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import fire
|
||||
|
||||
from fire.examples.widget import widget
|
||||
from examples.widget import widget
|
||||
|
||||
|
||||
class Collector(object):
|
||||
@@ -28,7 +28,7 @@ class Collector(object):
|
||||
|
||||
def collect_widgets(self):
|
||||
"""Returns all the widgets the Collector wants."""
|
||||
return [widget.Widget() for _ in xrange(self.desired_widget_count)]
|
||||
return [widget.Widget() for _ in range(self.desired_widget_count)]
|
||||
|
||||
|
||||
def 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.
|
||||
@@ -12,13 +12,15 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from fire.examples.widget import collector
|
||||
from fire.examples.widget import widget
|
||||
"""Tests for the collector module."""
|
||||
|
||||
import unittest
|
||||
from fire import testutils
|
||||
|
||||
from examples.widget import collector
|
||||
from examples.widget import widget
|
||||
|
||||
|
||||
class CollectorTest(unittest.TestCase):
|
||||
class CollectorTest(testutils.BaseTestCase):
|
||||
|
||||
def testCollectorHasWidget(self):
|
||||
col = collector.Collector()
|
||||
@@ -26,12 +28,12 @@ class CollectorTest(unittest.TestCase):
|
||||
|
||||
def testCollectorWantsMoreWidgets(self):
|
||||
col = collector.Collector()
|
||||
self.assertEquals(col.desired_widget_count, 10)
|
||||
self.assertEqual(col.desired_widget_count, 10)
|
||||
|
||||
def testCollectorGetsWantedWidgets(self):
|
||||
col = collector.Collector()
|
||||
self.assertEquals(len(col.collect_widgets()), 10)
|
||||
self.assertEqual(len(col.collect_widgets()), 10)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.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.
|
||||
@@ -21,7 +21,7 @@ class Widget(object):
|
||||
|
||||
def whack(self, n=1):
|
||||
"""Prints "whack!" n times."""
|
||||
return ' '.join('whack!' for _ in xrange(n))
|
||||
return ' '.join('whack!' for _ in range(n))
|
||||
|
||||
def bang(self, noise='bang'):
|
||||
"""Makes a loud noise."""
|
||||
|
||||
@@ -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,12 +12,14 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from fire.examples.widget import widget
|
||||
"""Tests for the widget module."""
|
||||
|
||||
import unittest
|
||||
from fire import testutils
|
||||
|
||||
from examples.widget import widget
|
||||
|
||||
|
||||
class WidgetTest(unittest.TestCase):
|
||||
class WidgetTest(testutils.BaseTestCase):
|
||||
|
||||
def testWidgetWhack(self):
|
||||
toy = widget.Widget()
|
||||
@@ -31,4 +33,4 @@ class WidgetTest(unittest.TestCase):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
testutils.main()
|
||||
|
||||
+2
-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.
|
||||
@@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Python Fire module for third_party."""
|
||||
"""The Python Fire module."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
|
||||
+293
-45
@@ -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):
|
||||
@@ -157,21 +360,20 @@ def Completions(component, verbose=False):
|
||||
A list of completions for a command that would so far return the component.
|
||||
"""
|
||||
if inspect.isroutine(component) or inspect.isclass(component):
|
||||
fn_args = inspectutils.GetArgSpec(component).args
|
||||
return _CompletionsFromArgs(fn_args)
|
||||
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,20 +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
|
||||
|
||||
+65
-12
@@ -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,32 +12,47 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for the completion module."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
|
||||
|
||||
from fire import completion
|
||||
from fire import test_components as tc
|
||||
|
||||
import unittest
|
||||
from fire import testutils
|
||||
|
||||
|
||||
class TabCompletionTest(unittest.TestCase):
|
||||
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)
|
||||
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):
|
||||
@@ -99,6 +114,44 @@ class TabCompletionTest(unittest.TestCase):
|
||||
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',
|
||||
@@ -137,4 +190,4 @@ class TabCompletionTest(unittest.TestCase):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
testutils.main()
|
||||
|
||||
@@ -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()
|
||||
+371
-189
@@ -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,9 +55,12 @@ from __future__ import print_function
|
||||
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
import pipes
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
import types
|
||||
|
||||
from fire import completion
|
||||
from fire import decorators
|
||||
@@ -65,11 +69,13 @@ 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
|
||||
|
||||
|
||||
def Fire(component=None, command=None, name=None):
|
||||
"""This function, Fire, is the main entrypoint for Fire.
|
||||
"""This function, Fire, is the main entrypoint for Python Fire.
|
||||
|
||||
Executes a command either from the `command` argument or from sys.argv by
|
||||
recursively traversing the target object `component`'s members consuming
|
||||
@@ -80,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:
|
||||
@@ -91,18 +98,26 @@ def Fire(component=None, command=None, name=None):
|
||||
it's a class). When all arguments are consumed and there's no function left
|
||||
to call or class left to instantiate, the resulting current component is
|
||||
the final result.
|
||||
If a Fire error is encountered, the Fire Trace is displayed to stdout and
|
||||
None is returned.
|
||||
If the trace command line argument is supplied, the FireTrace is returned.
|
||||
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]
|
||||
@@ -116,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))
|
||||
return None
|
||||
_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))
|
||||
output = ['Fire trace:\n{trace}\n'.format(trace=component_trace)]
|
||||
result = component_trace.GetResult()
|
||||
print(
|
||||
helputils.HelpString(result, component_trace, component_trace.verbose))
|
||||
return component_trace
|
||||
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))
|
||||
return component_trace
|
||||
output = ['Fire trace:\n{trace}'.format(trace=component_trace)]
|
||||
Display(output)
|
||||
raise FireExit(0, component_trace)
|
||||
elif component_trace.show_help:
|
||||
result = component_trace.GetResult()
|
||||
print(
|
||||
helputils.HelpString(result, component_trace, component_trace.verbose))
|
||||
return None
|
||||
help_string = helputils.HelpString(
|
||||
result, component_trace, component_trace.verbose)
|
||||
output = [help_string]
|
||||
Display(output)
|
||||
raise FireExit(0, component_trace)
|
||||
else:
|
||||
# The command succeeded normally; print the result.
|
||||
_PrintResult(component_trace, verbose=component_trace.verbose)
|
||||
result = component_trace.GetResult()
|
||||
return result
|
||||
|
||||
|
||||
def 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):
|
||||
@@ -160,19 +183,67 @@ class FireError(Exception):
|
||||
"""
|
||||
|
||||
|
||||
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.
|
||||
|
||||
This exception inherits from SystemExit, so clients may explicitly catch it
|
||||
with `except SystemExit` or `except FireExit`. If not caught, this exception
|
||||
will cause the client program to exit without a stacktrace.
|
||||
"""
|
||||
|
||||
def __init__(self, code, component_trace):
|
||||
"""Constructs a FireExit exception.
|
||||
|
||||
Args:
|
||||
code: (int) Exit code for the Fire CLI.
|
||||
component_trace: (FireTrace) The trace for the Fire command.
|
||||
"""
|
||||
super(FireExit, self).__init__(code)
|
||||
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()
|
||||
|
||||
if isinstance(result, list):
|
||||
for i in result:
|
||||
print(_OneLineResult(i))
|
||||
elif isinstance(result, set):
|
||||
for i in result:
|
||||
print(_OneLineResult(i))
|
||||
elif inspect.isgenerator(result):
|
||||
if isinstance(result, (list, set, types.GeneratorType)):
|
||||
for i in result:
|
||||
print(_OneLineResult(i))
|
||||
elif inspect.isgeneratorfunction(result):
|
||||
@@ -181,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.
|
||||
|
||||
@@ -197,17 +284,24 @@ def _DictAsString(result, verbose=False):
|
||||
Returns:
|
||||
A string representing the dict
|
||||
"""
|
||||
longest_key = max(
|
||||
len(str(key)) for key in result.keys()
|
||||
if _ComponentVisible(key, verbose)
|
||||
)
|
||||
|
||||
# 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_visible.keys())
|
||||
format_string = '{{key:{padding}s}} {{value}}'.format(padding=longest_key + 1)
|
||||
|
||||
lines = []
|
||||
for key, value in result.items():
|
||||
if _ComponentVisible(key, verbose):
|
||||
line = format_string.format(
|
||||
key=str(key) + ':', value=_OneLineResult(value))
|
||||
line = format_string.format(key=str(key) + ':',
|
||||
value=_OneLineResult(value))
|
||||
lines.append(line)
|
||||
return '\n'.join(lines)
|
||||
|
||||
@@ -222,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', ' ')
|
||||
|
||||
|
||||
@@ -302,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:
|
||||
@@ -323,29 +422,22 @@ 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
|
||||
|
||||
if last_component == initial_component:
|
||||
if last_component is initial_component:
|
||||
# If the initial component is a class, keep an instance for use with -i.
|
||||
instance = component
|
||||
|
||||
elif isinstance(component, (list, tuple)) and remaining_args:
|
||||
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:
|
||||
@@ -363,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()
|
||||
for key, value in component.items():
|
||||
if target == str(key):
|
||||
component = value
|
||||
@@ -382,7 +495,7 @@ def _Fire(component, args, context, name=None):
|
||||
break
|
||||
if not found_target:
|
||||
error = FireError(
|
||||
'Cannot find target in dict', target, component)
|
||||
'Cannot find target in dict:', target, component)
|
||||
component_trace.AddError(error, initial_args)
|
||||
return component_trace
|
||||
|
||||
@@ -400,18 +513,25 @@ def _Fire(component, args, context, name=None):
|
||||
component, consumed_args, remaining_args = _GetMember(
|
||||
component, remaining_args)
|
||||
|
||||
try:
|
||||
filename, lineno = _GetFileAndLine(component)
|
||||
except TypeError:
|
||||
filename = None
|
||||
lineno = None
|
||||
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.
|
||||
@@ -421,26 +541,26 @@ def _Fire(component, args, context, name=None):
|
||||
or inspect.isroutine(last_component)):
|
||||
remaining_args = saved_args
|
||||
component_trace.AddSeparator()
|
||||
elif component != last_component:
|
||||
elif component is not last_component:
|
||||
remaining_args = [separator] + saved_args
|
||||
else:
|
||||
# It was an unnecessary separator.
|
||||
remaining_args = saved_args
|
||||
|
||||
if component == last_component and remaining_args == initial_args:
|
||||
if component is last_component and remaining_args == initial_args:
|
||||
# We're making no progress.
|
||||
break
|
||||
|
||||
if remaining_args:
|
||||
component_trace.AddError(
|
||||
FireError('Could not consume arguments', remaining_args),
|
||||
FireError('Could not consume arguments:', remaining_args),
|
||||
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:
|
||||
@@ -462,32 +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.
|
||||
Raises:
|
||||
TypeError: If component is not a module, class, method, function, traceback,
|
||||
frame, or code object then the inspect module will raise this error.
|
||||
"""
|
||||
if inspect.isbuiltin(component):
|
||||
return None, None
|
||||
|
||||
filename = inspect.getsourcefile(component)
|
||||
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.
|
||||
|
||||
@@ -515,26 +609,47 @@ def _GetMember(component, args):
|
||||
if arg_name in members:
|
||||
return members[arg_name], [arg], args[1:]
|
||||
|
||||
raise FireError('Could not consume arg', arg)
|
||||
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):
|
||||
@@ -548,31 +663,37 @@ def _MakeParseFn(fn):
|
||||
can then be called with fn(*varargs, **kwargs). The remaining_args are
|
||||
the leftover args from the arguments to the parse function.
|
||||
"""
|
||||
fn_args, fn_varargs, fn_keywords, fn_defaults = inspectutils.GetArgSpec(fn)
|
||||
fn_spec = inspectutils.GetFullArgSpec(fn)
|
||||
metadata = decorators.GetMetadata(fn)
|
||||
|
||||
# Note: num_required_args is the number of arguments without default values.
|
||||
# All of these arguments are required.
|
||||
num_required_args = len(fn_args) - len(fn_defaults)
|
||||
# Note: num_required_args is the number of positional arguments without
|
||||
# default values. All of these arguments are required.
|
||||
num_required_args = len(fn_spec.args) - len(fn_spec.defaults)
|
||||
required_kwonly = set(fn_spec.kwonlyargs) - set(fn_spec.kwonlydefaults)
|
||||
|
||||
def _ParseFn(args):
|
||||
"""Parses the list of `args` into (varargs, kwargs), remaining_args."""
|
||||
kwargs, remaining_args = _ParseKeywordArgs(args, fn_args, fn_keywords)
|
||||
kwargs, remaining_kwargs, remaining_args = _ParseKeywordArgs(args, fn_spec)
|
||||
|
||||
# Note: _ParseArgs modifies kwargs.
|
||||
parsed_args, kwargs, remaining_args, capacity = _ParseArgs(
|
||||
fn_args, fn_defaults, num_required_args, kwargs, remaining_args,
|
||||
metadata)
|
||||
fn_spec.args, fn_spec.defaults, num_required_args, kwargs,
|
||||
remaining_args, metadata)
|
||||
|
||||
if fn_varargs or fn_keywords:
|
||||
if fn_spec.varargs or fn_spec.varkw:
|
||||
# If we're allowed *varargs or **kwargs, there's always capacity.
|
||||
capacity = True
|
||||
|
||||
if fn_keywords is None and kwargs:
|
||||
raise FireError('Unexpected kwargs present', kwargs)
|
||||
extra_kw = set(kwargs) - set(fn_spec.kwonlyargs)
|
||||
if fn_spec.varkw is None and extra_kw:
|
||||
raise FireError('Unexpected kwargs present:', extra_kw)
|
||||
|
||||
missing_kwonly = set(required_kwonly) - set(kwargs)
|
||||
if missing_kwonly:
|
||||
raise FireError('Missing required flags:', missing_kwonly)
|
||||
|
||||
# If we accept *varargs, then use all remaining arguments for *varargs.
|
||||
if fn_varargs is not None:
|
||||
if fn_spec.varargs is not None:
|
||||
varargs, remaining_args = remaining_args, []
|
||||
else:
|
||||
varargs = []
|
||||
@@ -581,8 +702,9 @@ 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)]
|
||||
consumed_args = args[:len(args) - len(remaining_args)]
|
||||
return (varargs, kwargs), consumed_args, remaining_args, capacity
|
||||
|
||||
return _ParseFn
|
||||
@@ -612,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)
|
||||
@@ -647,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.
|
||||
|
||||
@@ -661,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):
|
||||
|
||||
+97
-18
@@ -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,41 +12,47 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for the core module."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
from fire import core
|
||||
from fire import test_components as tc
|
||||
from fire import testutils
|
||||
from fire import trace
|
||||
import mock
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
class CoreTest(unittest.TestCase):
|
||||
class CoreTest(testutils.BaseTestCase):
|
||||
|
||||
def testOneLineResult(self):
|
||||
self.assertEqual(core._OneLineResult(1), '1')
|
||||
self.assertEqual(core._OneLineResult('hello'), 'hello')
|
||||
self.assertEqual(core._OneLineResult({}), '{}')
|
||||
self.assertEqual(core._OneLineResult({'x': 'y'}), '{"x": "y"}')
|
||||
self.assertEqual(core._OneLineResult(1), '1') # pylint: disable=protected-access
|
||||
self.assertEqual(core._OneLineResult('hello'), 'hello') # pylint: disable=protected-access
|
||||
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)
|
||||
@@ -56,7 +62,8 @@ class CoreTest(unittest.TestCase):
|
||||
|
||||
@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)
|
||||
@@ -65,14 +72,53 @@ class CoreTest(unittest.TestCase):
|
||||
self.assertEqual(variables['D'], tc.WithDefaults)
|
||||
self.assertIsInstance(variables['trace'], trace.FireTrace)
|
||||
|
||||
def testImproperUseOfHelp(self):
|
||||
# This should produce a warning and return None.
|
||||
self.assertIsNone(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 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, 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')
|
||||
@@ -82,5 +128,38 @@ class CoreTest(unittest.TestCase):
|
||||
error = core.FireError('Example error', 'value')
|
||||
self.assertIsNotNone(error)
|
||||
|
||||
def testPrintEmptyDict(self):
|
||||
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__':
|
||||
unittest.main()
|
||||
testutils.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.
|
||||
|
||||
+65
-41
@@ -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,19 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for the decorators module."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
from fire import core
|
||||
from fire import decorators
|
||||
|
||||
import unittest
|
||||
from fire import testutils
|
||||
|
||||
|
||||
class A(object):
|
||||
class NoDefaults(object):
|
||||
"""A class for testing decorated functions without default values."""
|
||||
|
||||
@decorators.SetParseFns(count=int)
|
||||
def double(self, count):
|
||||
@@ -42,7 +44,7 @@ def double(count):
|
||||
return 2 * count
|
||||
|
||||
|
||||
class B(object):
|
||||
class WithDefaults(object):
|
||||
|
||||
@decorators.SetParseFns(float)
|
||||
def example1(self, arg1=10):
|
||||
@@ -53,14 +55,14 @@ class B(object):
|
||||
return arg1, type(arg1)
|
||||
|
||||
|
||||
class C(object):
|
||||
class MixedArguments(object):
|
||||
|
||||
@decorators.SetParseFns(float, arg2=str)
|
||||
def example3(self, arg1, arg2):
|
||||
return arg1, arg2
|
||||
|
||||
|
||||
class D(object):
|
||||
class PartialParseFn(object):
|
||||
|
||||
@decorators.SetParseFns(arg1=str)
|
||||
def example4(self, arg1, arg2):
|
||||
@@ -71,7 +73,7 @@ class D(object):
|
||||
return arg1, arg2
|
||||
|
||||
|
||||
class E(object):
|
||||
class WithKwargs(object):
|
||||
|
||||
@decorators.SetParseFns(mode=str, count=int)
|
||||
def example6(self, **kwargs):
|
||||
@@ -81,70 +83,92 @@ class E(object):
|
||||
)
|
||||
|
||||
|
||||
class F(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(unittest.TestCase):
|
||||
class FireDecoratorsTest(testutils.BaseTestCase):
|
||||
|
||||
def testSetParseFnsNamedArgs(self):
|
||||
self.assertEqual(core.Fire(A, 'double 2'), 4)
|
||||
self.assertEqual(core.Fire(A, '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(A, '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.
|
||||
self.assertTupleEqual(B().example1(), (10, int))
|
||||
self.assertEqual(B().example1(5), (5, int))
|
||||
self.assertEqual(B().example1(12.0), (12, float))
|
||||
self.assertTupleEqual(WithDefaults().example1(), (10, int))
|
||||
self.assertEqual(WithDefaults().example1(5), (5, int))
|
||||
self.assertEqual(WithDefaults().example1(12.0), (12, float))
|
||||
|
||||
def testSetParseFnsDefaultsFromFire(self):
|
||||
# Fire should use the decorator to know how to parse string arguments.
|
||||
self.assertEqual(core.Fire(B, 'example1'), (10, int))
|
||||
self.assertEqual(core.Fire(B, 'example1 10'), (10, float))
|
||||
self.assertEqual(core.Fire(B, 'example1 13'), (13, float))
|
||||
self.assertEqual(core.Fire(B, '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.
|
||||
self.assertTupleEqual(B().example2(), (10, int))
|
||||
self.assertEqual(B().example2(5), (5, int))
|
||||
self.assertEqual(B().example2(12.0), (12, float))
|
||||
self.assertTupleEqual(WithDefaults().example2(), (10, int))
|
||||
self.assertEqual(WithDefaults().example2(5), (5, int))
|
||||
self.assertEqual(WithDefaults().example2(12.0), (12, float))
|
||||
|
||||
def testSetParseFnsNamedDefaultsFromFire(self):
|
||||
# Fire should use the decorator to know how to parse string arguments.
|
||||
self.assertEqual(core.Fire(B, 'example2'), (10, int))
|
||||
self.assertEqual(core.Fire(B, 'example2 10'), (10, float))
|
||||
self.assertEqual(core.Fire(B, 'example2 13'), (13, float))
|
||||
self.assertEqual(core.Fire(B, '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(C, 'example3 10 10'), (10, '10'))
|
||||
self.assertEqual(core.Fire(MixedArguments, ['example3', '10', '10']),
|
||||
(10, '10'))
|
||||
|
||||
def testSetParseFnsOnlySomeTypes(self):
|
||||
self.assertEqual(core.Fire(D, 'example4 10 10'), ('10', 10))
|
||||
self.assertEqual(core.Fire(D, '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(E, 'example6'), ('default', 0))
|
||||
self.assertEqual(core.Fire(E, 'example6 --herring "red"'), ('default', 0))
|
||||
self.assertEqual(core.Fire(E, 'example6 --mode train'), ('train', 0))
|
||||
self.assertEqual(core.Fire(E, 'example6 --mode 3'), ('3', 0))
|
||||
self.assertEqual(core.Fire(E, 'example6 --mode -1 --count 10'), ('-1', 10))
|
||||
self.assertEqual(core.Fire(E, 'example6 --count -2'), ('default', -2))
|
||||
self.assertEqual(
|
||||
core.Fire(WithKwargs, command=['example6']), ('default', 0))
|
||||
self.assertEqual(
|
||||
core.Fire(WithKwargs, command=['example6', '--herring', '"red"']),
|
||||
('default', 0))
|
||||
self.assertEqual(
|
||||
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,
|
||||
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(F, 'example7 1 --arg2=2 3 4 --kwarg=5'),
|
||||
('1', '2', ('3', '4'), {'kwarg': '5'}))
|
||||
self.assertEqual(
|
||||
core.Fire(WithVarArgs,
|
||||
command=['example7', '1', '--arg2=2', '3', '4', '--kwarg=5']),
|
||||
('1', '2', ('3', '4'), {'kwarg': '5'}))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
testutils.main()
|
||||
|
||||
@@ -0,0 +1,695 @@
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Docstring parsing module for Python Fire.
|
||||
|
||||
The following features of docstrings are not supported.
|
||||
TODO(dbieber): Support these features.
|
||||
- numpy docstrings may begin with the function signature.
|
||||
- whitespace may be important for proper structuring of a docstring
|
||||
- I've seen `argname` (with single backticks) as a style of documenting
|
||||
arguments. The `argname` appears on one line, and the description on the next.
|
||||
- .. Sphinx directives such as .. note:: are not understood.
|
||||
- After a section ends, future contents may be included in the section. E.g.
|
||||
:returns: This is what is returned.
|
||||
Example: An example goes here.
|
||||
- @param is sometimes used. E.g.
|
||||
@param argname (type) Description
|
||||
@return (type) Description
|
||||
- The true signature of a function is not used by the docstring parser. It could
|
||||
be useful for determining whether something is a section header or an argument
|
||||
for example.
|
||||
- This example confuses types as part of the docstrings.
|
||||
Parameters
|
||||
argname : argtype
|
||||
Arg description
|
||||
- If there's no blank line after the summary, the description will be slurped
|
||||
up into the summary.
|
||||
- "Examples" should be its own section type. aka "Usage".
|
||||
- "Notes" should be a section type.
|
||||
- Some people put parenthesis around their types in RST format, e.g.
|
||||
:param (type) paramname:
|
||||
- :rtype: directive (return type)
|
||||
- Also ":rtype str" with no closing ":" has come up.
|
||||
- Return types are not supported.
|
||||
- "# Returns" as a section title style
|
||||
- ":raises ExceptionType: Description" ignores the ExceptionType currently.
|
||||
- "Defaults to X" occurs sometimes.
|
||||
- "True | False" indicates bool type.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
|
||||
import collections
|
||||
import re
|
||||
|
||||
import enum
|
||||
|
||||
|
||||
class DocstringInfo(
|
||||
collections.namedtuple(
|
||||
'DocstringInfo',
|
||||
('summary', 'description', 'args', 'returns', 'yields', 'raises'))):
|
||||
pass
|
||||
DocstringInfo.__new__.__defaults__ = (None,) * len(DocstringInfo._fields)
|
||||
|
||||
|
||||
class ArgInfo(
|
||||
collections.namedtuple(
|
||||
'ArgInfo',
|
||||
('name', 'type', 'description'))):
|
||||
pass
|
||||
ArgInfo.__new__.__defaults__ = (None,) * len(ArgInfo._fields)
|
||||
|
||||
|
||||
class Namespace(dict):
|
||||
"""A dict with attribute (dot-notation) access enabled."""
|
||||
|
||||
def __getattr__(self, key):
|
||||
if key not in self:
|
||||
self[key] = Namespace()
|
||||
return self.get(key)
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
self[key] = value
|
||||
|
||||
def __delattr__(self, key):
|
||||
if key in self:
|
||||
del self[key]
|
||||
|
||||
|
||||
class Sections(enum.Enum):
|
||||
ARGS = 0
|
||||
RETURNS = 1
|
||||
YIELDS = 2
|
||||
RAISES = 3
|
||||
TYPE = 4
|
||||
|
||||
|
||||
class Formats(enum.Enum):
|
||||
GOOGLE = 0
|
||||
NUMPY = 1
|
||||
RST = 2
|
||||
|
||||
|
||||
SECTION_TITLES = {
|
||||
Sections.ARGS: ('argument', 'arg', 'parameter', 'param'),
|
||||
Sections.RETURNS: ('return',),
|
||||
Sections.YIELDS: ('yield',),
|
||||
Sections.RAISES: ('raise', 'except', 'exception', 'throw', 'error', 'warn'),
|
||||
Sections.TYPE: ('type',), # rst-only
|
||||
}
|
||||
|
||||
|
||||
def parse(docstring):
|
||||
"""Returns DocstringInfo about the given docstring.
|
||||
|
||||
This parser aims to parse Google, numpy, and rst formatted docstrings. These
|
||||
are the three most common docstring styles at the time of this writing.
|
||||
|
||||
This parser aims to be permissive, working even when the docstring deviates
|
||||
from the strict recommendations of these styles.
|
||||
|
||||
This parser does not aim to fully extract all structured information from a
|
||||
docstring, since there are simply too many ways to structure information in a
|
||||
docstring. Sometimes content will remain as unstructured text and simply gets
|
||||
included in the description.
|
||||
|
||||
The Google docstring style guide is available at:
|
||||
https://github.com/google/styleguide/blob/gh-pages/pyguide.md
|
||||
|
||||
The numpy docstring style guide is available at:
|
||||
https://numpydoc.readthedocs.io/en/latest/format.html
|
||||
|
||||
Information about the rST docstring format is available at:
|
||||
https://www.python.org/dev/peps/pep-0287/
|
||||
The full set of directives such as param and type for rST docstrings are at:
|
||||
http://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html
|
||||
|
||||
Note: This function does not claim to handle all docstrings well. A list of
|
||||
limitations is available at the top of the file. It does aim to run without
|
||||
crashing in O(n) time on all strings on length n. If you find a string that
|
||||
causes this to crash or run unacceptably slowly, please consider submitting
|
||||
a pull request.
|
||||
|
||||
Args:
|
||||
docstring: The docstring to parse.
|
||||
Returns:
|
||||
A DocstringInfo containing information about the docstring.
|
||||
"""
|
||||
if docstring is None:
|
||||
return DocstringInfo()
|
||||
|
||||
lines = docstring.strip().split('\n')
|
||||
lines_len = len(lines)
|
||||
state = Namespace() # TODO(dbieber): Switch to an explicit class.
|
||||
|
||||
# Variables in state include:
|
||||
state.section.title = None
|
||||
state.section.indentation = None
|
||||
state.section.line1_indentation = None
|
||||
state.section.format = None
|
||||
state.summary.permitted = True
|
||||
state.summary.lines = []
|
||||
state.description.lines = []
|
||||
state.args = []
|
||||
state.current_arg = None
|
||||
state.returns.lines = []
|
||||
state.yields.lines = []
|
||||
state.raises.lines = []
|
||||
|
||||
for index, line in enumerate(lines):
|
||||
has_next = index + 1 < lines_len
|
||||
next_line = lines[index + 1] if has_next else None
|
||||
line_info = _create_line_info(line, next_line)
|
||||
_consume_line(line_info, state)
|
||||
|
||||
summary = ' '.join(state.summary.lines) if state.summary.lines else None
|
||||
description = _join_lines(state.description.lines)
|
||||
returns = _join_lines(state.returns.lines)
|
||||
yields = _join_lines(state.yields.lines)
|
||||
raises = _join_lines(state.raises.lines)
|
||||
|
||||
args = [
|
||||
ArgInfo(
|
||||
name=arg.name,
|
||||
type=_cast_to_known_type(_join_lines(arg.type.lines)),
|
||||
description=_join_lines(arg.description.lines),
|
||||
)
|
||||
for arg in state.args
|
||||
]
|
||||
|
||||
return DocstringInfo(
|
||||
summary=summary,
|
||||
description=description,
|
||||
args=args or None,
|
||||
returns=returns,
|
||||
raises=raises,
|
||||
yields=yields,
|
||||
)
|
||||
|
||||
|
||||
def _join_lines(lines):
|
||||
"""Joins lines with the appropriate connective whitespace.
|
||||
|
||||
This puts a single space between consecutive lines, unless there's a blank
|
||||
line, in which case a full blank line is included.
|
||||
|
||||
Args:
|
||||
lines: A list of lines to join.
|
||||
Returns:
|
||||
A string, the lines joined together.
|
||||
"""
|
||||
# TODO(dbieber): Add parameters for variations in whitespace handling.
|
||||
if not lines:
|
||||
return None
|
||||
|
||||
started = False
|
||||
group_texts = [] # Full text of each section.
|
||||
group_lines = [] # Lines within the current section.
|
||||
for line in lines:
|
||||
stripped_line = line.strip()
|
||||
if stripped_line:
|
||||
started = True
|
||||
group_lines.append(stripped_line)
|
||||
else:
|
||||
if started:
|
||||
group_text = ' '.join(group_lines)
|
||||
group_texts.append(group_text)
|
||||
group_lines = []
|
||||
|
||||
if group_lines: # Process the final group.
|
||||
group_text = ' '.join(group_lines)
|
||||
group_texts.append(group_text)
|
||||
|
||||
return '\n\n'.join(group_texts)
|
||||
|
||||
|
||||
def _get_or_create_arg_by_name(state, name):
|
||||
"""Gets or creates a new Arg.
|
||||
|
||||
These Arg objects (Namespaces) are turned into the ArgInfo namedtuples
|
||||
returned by parse. Each Arg object is used to collect the name, type, and
|
||||
description of a single argument to the docstring's function.
|
||||
|
||||
Args:
|
||||
state: The state of the parser.
|
||||
name: The name of the arg to create.
|
||||
Returns:
|
||||
The new Arg.
|
||||
"""
|
||||
for arg in state.args:
|
||||
if arg.name == name:
|
||||
return arg
|
||||
arg = Namespace() # TODO(dbieber): Switch to an explicit class.
|
||||
arg.name = name
|
||||
arg.type.lines = []
|
||||
arg.description.lines = []
|
||||
state.args.append(arg)
|
||||
return arg
|
||||
|
||||
|
||||
def _is_arg_name(name):
|
||||
"""Returns whether name is a valid arg name.
|
||||
|
||||
This is used to prevent multiple words (plaintext) from being 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()
|
||||
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.
|
||||
@@ -12,16 +12,21 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests importing the fire module."""
|
||||
|
||||
import sys
|
||||
|
||||
import fire
|
||||
|
||||
import unittest
|
||||
from fire import testutils
|
||||
import mock
|
||||
|
||||
|
||||
class FireImportTest(unittest.TestCase):
|
||||
class FireImportTest(testutils.BaseTestCase):
|
||||
"""Tests importing Fire."""
|
||||
|
||||
def testFire(self):
|
||||
fire.Fire()
|
||||
with mock.patch.object(sys, 'argv', ['commandname']):
|
||||
fire.Fire()
|
||||
|
||||
def testFireMethods(self):
|
||||
self.assertIsNotNone(fire.Fire)
|
||||
@@ -32,4 +37,4 @@ class FireImportTest(unittest.TestCase):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
testutils.main()
|
||||
|
||||
+493
-176
@@ -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,360 +12,677 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for the fire module."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import fire
|
||||
from fire import test_components as tc
|
||||
from fire import trace
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import fire
|
||||
from fire import test_components as tc
|
||||
from fire import testutils
|
||||
|
||||
class FireTest(unittest.TestCase):
|
||||
import mock
|
||||
import six
|
||||
|
||||
|
||||
class FireTest(testutils.BaseTestCase):
|
||||
|
||||
def testFire(self):
|
||||
fire.Fire(tc.Empty)
|
||||
fire.Fire(tc.OldStyleEmpty)
|
||||
fire.Fire(tc.WithInit)
|
||||
with mock.patch.object(sys, 'argv', ['progname']):
|
||||
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 None is returned.
|
||||
self.assertIsNone(fire.Fire(tc.Empty, 'nomethod')) # Member doesn't exist.
|
||||
self.assertIsNone(fire.Fire(tc.NoDefaults, 'double')) # Missing argument.
|
||||
self.assertIsNone(fire.Fire(tc.TypedProperties, 'delta x')) # Missing key.
|
||||
# Exceptions of Fire are printed to stderr and a FireExit is raised.
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.Empty, command=['nomethod']) # Member doesn't exist.
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.NoDefaults, command=['double']) # Missing argument.
|
||||
with self.assertRaisesFireExit(2):
|
||||
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):
|
||||
# By default, errors are written to standard out and None is returned.
|
||||
self.assertIsNone( # Identity needs an arg.
|
||||
fire.Fire(tc.MixedDefaults, 'identity'))
|
||||
self.assertIsNone( # Identity needs a value for alpha.
|
||||
fire.Fire(tc.MixedDefaults, 'identity --beta 2'))
|
||||
# Errors are written to standard out and a FireExit is raised.
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.MixedDefaults,
|
||||
command=['identity']) # Identity needs an arg.
|
||||
|
||||
with self.assertRaisesFireExit(2):
|
||||
# Identity needs a value for alpha.
|
||||
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, 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, command=['double', '5'])
|
||||
|
||||
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))
|
||||
|
||||
# In the next example nothing=False (since rightmost setting of a flag gets
|
||||
# precedence), but it errors because thing has no value.
|
||||
self.assertEqual(fire.Fire(fn1, '--nothing --nonothing'), None)
|
||||
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, 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, {}))
|
||||
# In the next one, nothing=True, but it errors because thing has no value.
|
||||
self.assertEqual(fire.Fire(fn2, '--nothing=True'), None)
|
||||
self.assertEqual(fire.Fire(fn2, '--nothing --nothing=True'),
|
||||
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, 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):
|
||||
self.assertIsInstance(
|
||||
fire.Fire(tc.BoolConverter, 'as-bool True -- --trace'), trace.FireTrace)
|
||||
self.assertIsInstance(
|
||||
fire.Fire(tc.BoolConverter, 'as-bool True -- -t'), trace.FireTrace)
|
||||
self.assertIsInstance(
|
||||
fire.Fire(tc.BoolConverter, '-- --trace'), trace.FireTrace)
|
||||
with self.assertRaisesFireExit(0, 'Fire trace:\n'):
|
||||
fire.Fire(tc.BoolConverter, command=['as-bool', 'True', '--', '--trace'])
|
||||
with self.assertRaisesFireExit(0, 'Fire trace:\n'):
|
||||
fire.Fire(tc.BoolConverter, command=['as-bool', 'True', '--', '-t'])
|
||||
with self.assertRaisesFireExit(0, 'Fire trace:\n'):
|
||||
fire.Fire(tc.BoolConverter, command=['--', '--trace'])
|
||||
|
||||
def testHelpFlag(self):
|
||||
self.assertIsNone(fire.Fire(tc.BoolConverter, 'as-bool True -- --help'))
|
||||
self.assertIsNone(fire.Fire(tc.BoolConverter, 'as-bool True -- -h'))
|
||||
self.assertIsNone(fire.Fire(tc.BoolConverter, '-- --help'))
|
||||
with self.assertRaisesFireExit(0):
|
||||
fire.Fire(tc.BoolConverter, command=['as-bool', 'True', '--', '--help'])
|
||||
with self.assertRaisesFireExit(0):
|
||||
fire.Fire(tc.BoolConverter, command=['as-bool', 'True', '--', '-h'])
|
||||
with self.assertRaisesFireExit(0):
|
||||
fire.Fire(tc.BoolConverter, command=['--', '--help'])
|
||||
|
||||
def testHelpFlagAndTraceFlag(self):
|
||||
self.assertIsInstance(
|
||||
fire.Fire(tc.BoolConverter, 'as-bool True -- --help --trace'),
|
||||
trace.FireTrace)
|
||||
self.assertIsInstance(
|
||||
fire.Fire(tc.BoolConverter, 'as-bool True -- -h -t'), trace.FireTrace)
|
||||
self.assertIsInstance(
|
||||
fire.Fire(tc.BoolConverter, '-- -h --trace'), trace.FireTrace)
|
||||
with self.assertRaisesFireExit(0, 'Fire trace:\n.*Usage:'):
|
||||
fire.Fire(tc.BoolConverter,
|
||||
command=['as-bool', 'True', '--', '--help', '--trace'])
|
||||
with self.assertRaisesFireExit(0, 'Fire trace:\n.*Usage:'):
|
||||
fire.Fire(tc.BoolConverter, command=['as-bool', 'True', '--', '-h', '-t'])
|
||||
with self.assertRaisesFireExit(0, 'Fire trace:\n.*Usage:'):
|
||||
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.
|
||||
self.assertEqual(fire.Fire(tc.MixedDefaults, 'identity - _ +'), None)
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.MixedDefaults, command=['identity', '-', '_', '+'])
|
||||
|
||||
def testNonComparable(self):
|
||||
"""Fire should work with classes that disallow comparisons."""
|
||||
# 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, 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)
|
||||
# Cannot instantiate a class with positional args by default.
|
||||
self.assertIsNone(fire.Fire(tc.InstanceVars, 'a1 a2'))
|
||||
with self.assertRaisesFireExit(2):
|
||||
# Cannot instantiate a class with positional args.
|
||||
fire.Fire(tc.InstanceVars, command=['a1', 'a2'])
|
||||
|
||||
def testTraceErrors(self):
|
||||
# Class needs additional value but runs out of args.
|
||||
self.assertIsNone(fire.Fire(tc.InstanceVars, 'a1'))
|
||||
self.assertIsNone(fire.Fire(tc.InstanceVars, '--arg1=a1'))
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.InstanceVars, command=['a1'])
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.InstanceVars, command=['--arg1=a1'])
|
||||
|
||||
# Routine needs additional value but runs out of args.
|
||||
self.assertIsNone(fire.Fire(tc.InstanceVars, 'a1 a2 - run b1'))
|
||||
self.assertIsNone(
|
||||
fire.Fire(tc.InstanceVars, '--arg1=a1 --arg2=a2 - run b1'))
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.InstanceVars, command=['a1', 'a2', '-', 'run', 'b1'])
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.InstanceVars,
|
||||
command=['--arg1=a1', '--arg2=a2', '-', 'run b1'])
|
||||
|
||||
# Extra args cannot be consumed.
|
||||
self.assertIsNone(fire.Fire(tc.InstanceVars, 'a1 a2 - run b1 b2 b3'))
|
||||
self.assertIsNone(
|
||||
fire.Fire(tc.InstanceVars, '--arg1=a1 --arg2=a2 - run b1 b2 b3'))
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.InstanceVars,
|
||||
command=['a1', 'a2', '-', 'run', 'b1', 'b2', 'b3'])
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(
|
||||
tc.InstanceVars,
|
||||
command=['--arg1=a1', '--arg2=a2', '-', 'run', 'b1', 'b2', 'b3'])
|
||||
|
||||
# Cannot find member to access.
|
||||
self.assertIsNone(fire.Fire(tc.InstanceVars, 'a1 a2 - jog'))
|
||||
self.assertIsNone(fire.Fire(tc.InstanceVars, '--arg1=a1 --arg2=a2 - jog'))
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.InstanceVars, command=['a1', 'a2', '-', 'jog'])
|
||||
with self.assertRaisesFireExit(2):
|
||||
fire.Fire(tc.InstanceVars, command=['--arg1=a1', '--arg2=a2', '-', 'jog'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
testutils.main()
|
||||
|
||||
+488
-45
@@ -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,7 +36,9 @@ 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 _NormalizeField(field):
|
||||
@@ -54,7 +67,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 +89,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.
|
||||
|
||||
@@ -89,28 +119,45 @@ def HelpString(component, trace=None, verbose=False):
|
||||
String suitable for display giving information about the component.
|
||||
"""
|
||||
info = inspectutils.Info(component)
|
||||
# TODO(dbieber): Stop using UsageString in favor of UsageText.
|
||||
info['usage'] = UsageString(component, trace, verbose)
|
||||
info['docstring_info'] = docstrings.parse(info['docstring'])
|
||||
|
||||
fields = [
|
||||
'type_name',
|
||||
'string_form',
|
||||
'file',
|
||||
'line',
|
||||
is_error_screen = False
|
||||
if trace:
|
||||
is_error_screen = trace.HasError()
|
||||
|
||||
'docstring',
|
||||
'init_docstring',
|
||||
'class_docstring',
|
||||
'call_docstring',
|
||||
'length',
|
||||
if is_error_screen:
|
||||
# TODO(dbieber): Call UsageText instead of CommonHelpText once ready.
|
||||
return _CommonHelpText(info, trace)
|
||||
else:
|
||||
return _HelpText(info, trace)
|
||||
|
||||
'usage',
|
||||
]
|
||||
|
||||
max_size = max(
|
||||
len(_NormalizeField(field)) + 1
|
||||
for field in fields
|
||||
if field in info and info[field])
|
||||
format_string = '{{field:{max_size}s}} {{value}}'.format(max_size=max_size)
|
||||
def _CommonHelpText(info, trace=None):
|
||||
"""Returns help text.
|
||||
|
||||
This was a copy of previous HelpString function and will be removed once the
|
||||
correct text formatters are implemented.
|
||||
|
||||
Args:
|
||||
info: The IR object containing metadata of an object.
|
||||
trace: The Fire trace object containing all metadata of current execution.
|
||||
Returns:
|
||||
String suitable for display giving information about the component.
|
||||
"""
|
||||
# TODO(joejoevictor): Currently this is just a copy of existing
|
||||
# HelpString method. We will reimplement this further in later CLs.
|
||||
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,26 +173,408 @@ def HelpString(component, trace=None, verbose=False):
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def _UsageStringFromFnDetails(command, args, varargs, keywords, defaults):
|
||||
"""Get a usage string from the function details for the given command.
|
||||
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
|
||||
|
||||
if spec.defaults is None:
|
||||
num_defaults = 0
|
||||
else:
|
||||
num_defaults = len(spec.defaults)
|
||||
args_with_no_defaults = args[:len(args) - num_defaults]
|
||||
|
||||
# TODO(joejoevictor): Generate flag section using these
|
||||
# args_with_defaults = args[len(args) - num_defaults:]
|
||||
# flags = args_with_defaults + spec.kwonlyargs
|
||||
|
||||
output_template = """NAME
|
||||
{name_section}
|
||||
|
||||
SYNOPSIS
|
||||
{synopsis_section}
|
||||
|
||||
DESCRIPTION
|
||||
{description_section}
|
||||
{args_and_flags_section}
|
||||
NOTES
|
||||
You could also use flags syntax for POSITIONAL ARGUMENTS
|
||||
"""
|
||||
|
||||
# Name section
|
||||
name_section_template = '{current_command}{command_summary}'
|
||||
command_summary_str = ' - ' + summary if summary else ''
|
||||
name_section = name_section_template.format(
|
||||
current_command=current_command, command_summary=command_summary_str)
|
||||
|
||||
items = [arg.upper() for arg in args_with_no_defaults]
|
||||
args_and_flags = ' '.join(items)
|
||||
|
||||
# Synopsis section
|
||||
synopsis_section_template = '{current_command} {args_and_flags}'
|
||||
positional_arguments = '|'.join(args)
|
||||
if positional_arguments:
|
||||
positional_arguments = ' ' + positional_arguments
|
||||
synopsis_section = synopsis_section_template.format(
|
||||
current_command=current_command, args_and_flags=args_and_flags)
|
||||
|
||||
# Description section
|
||||
description_section = description if description else summary
|
||||
|
||||
args_and_flags_section = ''
|
||||
|
||||
# Positional arguments and flags section
|
||||
|
||||
pos_arg_template = """
|
||||
POSITIONAL ARGUMENTS
|
||||
{items}
|
||||
"""
|
||||
pos_arg_items = []
|
||||
for arg in args_with_no_defaults:
|
||||
item_template = ' {arg_name}\n {arg_description}\n'
|
||||
arg_description = None
|
||||
for arg_in_docstring in info['docstring_info'].args:
|
||||
if arg_in_docstring.name == arg:
|
||||
arg_description = arg_in_docstring.description
|
||||
|
||||
item = item_template.format(
|
||||
arg_name=arg.upper(), arg_description=arg_description)
|
||||
pos_arg_items.append(item)
|
||||
if pos_arg_items:
|
||||
args_and_flags_section += pos_arg_template.format(
|
||||
items='\n'.join(pos_arg_items).rstrip('\n'))
|
||||
|
||||
return output_template.format(
|
||||
name_section=name_section,
|
||||
synopsis_section=synopsis_section,
|
||||
description_section=description_section,
|
||||
args_and_flags_section=args_and_flags_section)
|
||||
|
||||
|
||||
def HelpTextForObject(component, info, trace=None, verbose=False):
|
||||
"""Generates help text for python objects.
|
||||
|
||||
Args:
|
||||
component: Current component to generate help text for.
|
||||
info: Info containing metadata of component.
|
||||
trace: FireTrace object that leads to current component.
|
||||
verbose: Whether to display help text in verbose mode.
|
||||
|
||||
Returns:
|
||||
Formatted help text for display.
|
||||
"""
|
||||
|
||||
output_template = """NAME
|
||||
{current_command} - {command_summary}
|
||||
|
||||
SYNOPSIS
|
||||
{synopsis}
|
||||
|
||||
DESCRIPTION
|
||||
{command_description}
|
||||
{detail_section}
|
||||
"""
|
||||
|
||||
current_command = GetCurrentCommand(trace)
|
||||
|
||||
docstring_info = info['docstring_info']
|
||||
command_summary = docstring_info.summary if docstring_info.summary else ''
|
||||
if docstring_info.description:
|
||||
command_description = docstring_info.description
|
||||
else:
|
||||
command_description = ''
|
||||
|
||||
groups = []
|
||||
commands = []
|
||||
values = []
|
||||
members = completion._Members(component, verbose) # pylint: disable=protected-access
|
||||
for member_name, member in members:
|
||||
if value_types.IsGroup(member):
|
||||
groups.append((member_name, member))
|
||||
if value_types.IsCommand(member):
|
||||
commands.append((member_name, member))
|
||||
if value_types.IsValue(member):
|
||||
values.append((member_name, member))
|
||||
|
||||
possible_actions = []
|
||||
# TODO(joejoevictor): Add global flags to here. Also, if it's a callable,
|
||||
# there will be additional flags.
|
||||
possible_flags = ''
|
||||
detail_section_string = ''
|
||||
item_template = """
|
||||
{name}
|
||||
{command_summary}
|
||||
"""
|
||||
|
||||
if groups:
|
||||
# TODO(joejoevictor): Add missing GROUPS section handling
|
||||
possible_actions.append('GROUP')
|
||||
if commands:
|
||||
possible_actions.append('COMMAND')
|
||||
commands_str_template = """
|
||||
COMMANDS
|
||||
COMMAND is one of the followings:
|
||||
{items}
|
||||
"""
|
||||
command_item_strings = []
|
||||
for command_name, command in commands:
|
||||
command_docstring_info = docstrings.parse(
|
||||
inspectutils.Info(command)['docstring'])
|
||||
command_item_strings.append(
|
||||
item_template.format(
|
||||
name=command_name,
|
||||
command_summary=command_docstring_info.summary))
|
||||
detail_section_string += commands_str_template.format(
|
||||
items=('\n'.join(command_item_strings)).rstrip('\n'))
|
||||
|
||||
if values:
|
||||
possible_actions.append('VALUES')
|
||||
values_str_template = """
|
||||
VALUES
|
||||
VALUE is one of the followings:
|
||||
{items}
|
||||
"""
|
||||
value_item_strings = []
|
||||
for value_name, value in values:
|
||||
del value
|
||||
init_docstring_info = docstrings.parse(
|
||||
inspectutils.Info(component.__class__.__init__)['docstring'])
|
||||
for arg_info in init_docstring_info.args:
|
||||
if arg_info.name == value_name:
|
||||
value_item_strings.append(
|
||||
item_template.format(
|
||||
name=value_name, command_summary=arg_info.description))
|
||||
detail_section_string += values_str_template.format(
|
||||
items=('\n'.join(value_item_strings)).rstrip('\n'))
|
||||
|
||||
possible_actions_string = ' ' + (' | '.join(possible_actions))
|
||||
|
||||
synopsis_template = '{current_command}{possible_actions}{possible_flags}'
|
||||
synopsis_string = synopsis_template.format(
|
||||
current_command=current_command,
|
||||
possible_actions=possible_actions_string,
|
||||
possible_flags=possible_flags)
|
||||
|
||||
return output_template.format(
|
||||
current_command=current_command,
|
||||
command_summary=command_summary,
|
||||
synopsis=synopsis_string,
|
||||
command_description=command_description,
|
||||
detail_section=detail_section_string)
|
||||
|
||||
|
||||
def UsageText(component, trace=None, verbose=False):
|
||||
if inspect.isroutine(component) or inspect.isclass(component):
|
||||
return UsageTextForFunction(component, trace)
|
||||
else:
|
||||
return UsageTextForObject(component, trace, verbose)
|
||||
|
||||
|
||||
def UsageTextForFunction(component, trace=None):
|
||||
"""Returns usage text for function objects.
|
||||
|
||||
Args:
|
||||
component: The component to determine the usage text for.
|
||||
trace: The Fire trace object containing all metadata of current execution.
|
||||
|
||||
Returns:
|
||||
String suitable for display in error screen.
|
||||
"""
|
||||
|
||||
output_template = """Usage: {current_command} {args_and_flags}
|
||||
{availability_lines}
|
||||
For detailed information on this command, run:
|
||||
{current_command}{hyphen_hyphen} --help
|
||||
"""
|
||||
|
||||
if trace:
|
||||
command = trace.GetCommand()
|
||||
is_help_an_arg = trace.NeedsSeparatingHyphenHyphen()
|
||||
else:
|
||||
command = None
|
||||
is_help_an_arg = False
|
||||
|
||||
if not command:
|
||||
command = ''
|
||||
|
||||
spec = inspectutils.GetFullArgSpec(component)
|
||||
args = spec.args
|
||||
if spec.defaults is None:
|
||||
num_defaults = 0
|
||||
else:
|
||||
num_defaults = len(spec.defaults)
|
||||
args_with_no_defaults = args[:len(args) - num_defaults]
|
||||
args_with_defaults = args[len(args) - num_defaults:]
|
||||
flags = args_with_defaults + spec.kwonlyargs
|
||||
|
||||
items = [arg.upper() for arg in args_with_no_defaults]
|
||||
if flags:
|
||||
items.append('<flags>')
|
||||
availability_lines = (
|
||||
'\nAvailable flags: '
|
||||
+ ' | '.join('--' + flag for flag in flags) + '\n')
|
||||
else:
|
||||
availability_lines = ''
|
||||
args_and_flags = ' '.join(items)
|
||||
|
||||
hyphen_hyphen = ' --' if is_help_an_arg else ''
|
||||
|
||||
return output_template.format(
|
||||
current_command=command,
|
||||
args_and_flags=args_and_flags,
|
||||
availability_lines=availability_lines,
|
||||
hyphen_hyphen=hyphen_hyphen)
|
||||
|
||||
|
||||
def UsageTextForObject(component, trace=None, verbose=False):
|
||||
"""Returns help text for usage screen for objects.
|
||||
|
||||
Construct help text for usage screen to inform the user about error occurred
|
||||
and correct syntax for invoking the object.
|
||||
|
||||
Args:
|
||||
component: The component to determine the usage text for.
|
||||
trace: The Fire trace object containing all metadata of current execution.
|
||||
verbose: Whether to include private members in the usage text.
|
||||
Returns:
|
||||
String suitable for display in error screen.
|
||||
"""
|
||||
output_template = """Usage: {current_command} <{possible_actions}>
|
||||
{availability_lines}
|
||||
|
||||
For detailed information on this command, run:
|
||||
{current_command} --help
|
||||
"""
|
||||
if trace:
|
||||
command = trace.GetCommand()
|
||||
else:
|
||||
command = None
|
||||
|
||||
if not command:
|
||||
command = ''
|
||||
|
||||
groups = []
|
||||
commands = []
|
||||
values = []
|
||||
|
||||
members = completion._Members(component, verbose) # pylint: disable=protected-access
|
||||
for member_name, member in members:
|
||||
if value_types.IsGroup(member):
|
||||
groups.append(member_name)
|
||||
if value_types.IsCommand(member):
|
||||
commands.append(member_name)
|
||||
if value_types.IsValue(member):
|
||||
values.append(member_name)
|
||||
|
||||
possible_actions = []
|
||||
availability_lines = []
|
||||
availability_lint_format = '{header:20s}{choices}'
|
||||
if groups:
|
||||
possible_actions.append('groups')
|
||||
groups_string = ' | '.join(groups)
|
||||
groups_text = availability_lint_format.format(
|
||||
header='available groups:',
|
||||
choices=groups_string)
|
||||
availability_lines.append(groups_text)
|
||||
if commands:
|
||||
possible_actions.append('commands')
|
||||
commands_string = ' | '.join(commands)
|
||||
commands_text = availability_lint_format.format(
|
||||
header='available commands:',
|
||||
choices=commands_string)
|
||||
availability_lines.append(commands_text)
|
||||
if values:
|
||||
possible_actions.append('values')
|
||||
values_string = ' | '.join(values)
|
||||
values_text = availability_lint_format.format(
|
||||
header='available values:',
|
||||
choices=values_string)
|
||||
availability_lines.append(values_text)
|
||||
possible_actions_string = '|'.join(possible_actions)
|
||||
availability_lines_string = '\n'.join(availability_lines)
|
||||
|
||||
return output_template.format(
|
||||
current_command=command,
|
||||
possible_actions=possible_actions_string,
|
||||
availability_lines=availability_lines_string)
|
||||
|
||||
|
||||
def _HelpText(info, trace=None):
|
||||
"""Returns help text for extensive help screen.
|
||||
|
||||
Construct help text for help screen when user explicitly requesting help by
|
||||
having -h, --help in the command sequence.
|
||||
|
||||
Args:
|
||||
info: The IR object containing metadata of an object.
|
||||
trace: The Fire trace object containing all metadata of current execution.
|
||||
Returns:
|
||||
String suitable for display in extensive help screen.
|
||||
"""
|
||||
|
||||
# TODO(joejoevictor): Implement real help text construction.
|
||||
return _CommonHelpText(info, trace)
|
||||
|
||||
|
||||
def _UsageStringFromFullArgSpec(command, spec):
|
||||
"""Get a usage string from the FullArgSpec for the given command.
|
||||
|
||||
The strings look like:
|
||||
command --arg ARG [--opt OPT] [VAR ...] [--KWARGS ...]
|
||||
|
||||
Args:
|
||||
command: The command leading up to the function.
|
||||
args: The args accepted by the function.
|
||||
varargs: If not None, a string naming the *varargs variable used by the fn.
|
||||
keywords: If not None, a string naming the **kwargs varargs used by the fn.
|
||||
defaults: The default values for args accepted by the function.
|
||||
spec: a FullArgSpec object describing the function.
|
||||
Returns:
|
||||
The usage string for the function.
|
||||
"""
|
||||
num_required_args = len(args) - len(defaults)
|
||||
num_required_args = len(spec.args) - len(spec.defaults)
|
||||
|
||||
help_flags = []
|
||||
help_positional = []
|
||||
for index, arg in enumerate(args):
|
||||
for index, arg in enumerate(spec.args):
|
||||
flag = arg.replace('_', '-')
|
||||
if index < num_required_args:
|
||||
help_flags.append('--{flag} {value}'.format(flag=flag, value=arg.upper()))
|
||||
@@ -155,13 +584,21 @@ def _UsageStringFromFnDetails(command, args, varargs, keywords, defaults):
|
||||
flag=flag, value=arg.upper()))
|
||||
help_positional.append('[{value}]'.format(value=arg.upper()))
|
||||
|
||||
if varargs:
|
||||
help_flags.append('[{var} ...]'.format(var=varargs.upper()))
|
||||
help_positional.append('[{var} ...]'.format(var=varargs.upper()))
|
||||
if spec.varargs:
|
||||
help_flags.append('[{var} ...]'.format(var=spec.varargs.upper()))
|
||||
help_positional.append('[{var} ...]'.format(var=spec.varargs.upper()))
|
||||
|
||||
if keywords:
|
||||
help_flags.append('[--{kwarg} ...]'.format(kwarg=keywords.upper()))
|
||||
help_positional.append('[--{kwarg} ...]'.format(kwarg=keywords.upper()))
|
||||
for arg in spec.kwonlyargs:
|
||||
if arg in spec.kwonlydefaults:
|
||||
arg_str = '[--{flag} {value}]'.format(flag=arg, value=arg.upper())
|
||||
else:
|
||||
arg_str = '--{flag} {value}'.format(flag=arg, value=arg.upper())
|
||||
help_flags.append(arg_str)
|
||||
help_positional.append(arg_str)
|
||||
|
||||
if spec.varkw:
|
||||
help_flags.append('[--{kwarg} ...]'.format(kwarg=spec.varkw.upper()))
|
||||
help_positional.append('[--{kwarg} ...]'.format(kwarg=spec.varkw.upper()))
|
||||
|
||||
commands_flags = command + ' '.join(help_flags)
|
||||
commands_positional = command + ' '.join(help_positional)
|
||||
@@ -175,23 +612,29 @@ def _UsageStringFromFnDetails(command, args, varargs, keywords, defaults):
|
||||
|
||||
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):
|
||||
args, varargs, keywords, defaults = inspectutils.GetArgSpec(component)
|
||||
return _UsageStringFromFnDetails(command, args, varargs, keywords, defaults)
|
||||
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)
|
||||
|
||||
+189
-8
@@ -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,18 +12,25 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for the helputils module."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import textwrap
|
||||
|
||||
from fire import docstrings
|
||||
from fire import helputils
|
||||
from fire import inspectutils
|
||||
from fire import test_components as tc
|
||||
from fire import testutils
|
||||
from fire import trace
|
||||
import six
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
class HelpUtilsTest(unittest.TestCase):
|
||||
class HelpUtilsTest(testutils.BaseTestCase):
|
||||
|
||||
def testHelpStringClass(self):
|
||||
helpstring = helputils.HelpString(tc.NoDefaults)
|
||||
@@ -40,7 +47,8 @@ class HelpUtilsTest(unittest.TestCase):
|
||||
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)
|
||||
|
||||
@@ -80,7 +88,6 @@ class HelpUtilsTest(unittest.TestCase):
|
||||
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)
|
||||
@@ -120,9 +127,183 @@ class HelpUtilsTest(unittest.TestCase):
|
||||
self.assertIn('Type: type\n', helpstring)
|
||||
self.assertIn('String form: ', helpstring)
|
||||
self.assertIn('fire.test_components.OldStyleEmpty', helpstring)
|
||||
self.assertIn('fire/test_components.py\n', helpstring)
|
||||
self.assertIn(os.path.join('fire', 'test_components.py'), helpstring)
|
||||
self.assertIn('Line: ', helpstring)
|
||||
|
||||
|
||||
class HelpScreenTest(testutils.BaseTestCase):
|
||||
|
||||
def testHelpScreen(self):
|
||||
component = tc.ClassWithDocstring()
|
||||
t = trace.FireTrace(component, name='ClassWithDocstring')
|
||||
info = inspectutils.Info(component)
|
||||
info['docstring_info'] = docstrings.parse(info['docstring'])
|
||||
help_output = helputils.HelpText(component, info, t)
|
||||
expected_output = """
|
||||
NAME
|
||||
ClassWithDocstring - Test class for testing help text output.
|
||||
|
||||
SYNOPSIS
|
||||
ClassWithDocstring COMMAND | VALUES
|
||||
|
||||
DESCRIPTION
|
||||
This is some detail description of this test class.
|
||||
|
||||
COMMANDS
|
||||
COMMAND is one of the followings:
|
||||
|
||||
print_msg
|
||||
Prints a message.
|
||||
|
||||
VALUES
|
||||
VALUE is one of the followings:
|
||||
|
||||
message
|
||||
The default message to print.
|
||||
|
||||
"""
|
||||
self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
|
||||
|
||||
def testHelpScreenWithLineBreak(self):
|
||||
component = tc.ClassWithMultilineDocstring.example_generator
|
||||
t = trace.FireTrace(component, name='example_generator')
|
||||
info = inspectutils.Info(component)
|
||||
info['docstring_info'] = docstrings.parse(info['docstring'])
|
||||
help_output = helputils.HelpText(component, info, t)
|
||||
expected_output = """
|
||||
NAME
|
||||
example_generator - Generators have a ``Yields`` section instead of a ``Returns`` section.
|
||||
|
||||
SYNOPSIS
|
||||
example_generator N
|
||||
|
||||
DESCRIPTION
|
||||
Generators have a ``Yields`` section instead of a ``Returns`` section.
|
||||
|
||||
POSITIONAL ARGUMENTS
|
||||
N
|
||||
The upper limit of the range to generate, from 0 to `n` - 1.
|
||||
|
||||
NOTES
|
||||
You could also use flags syntax for POSITIONAL ARGUMENTS
|
||||
"""
|
||||
self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
|
||||
|
||||
|
||||
class UsageTest(testutils.BaseTestCase):
|
||||
|
||||
def testUsageOutput(self):
|
||||
component = tc.NoDefaults()
|
||||
t = trace.FireTrace(component, name='NoDefaults')
|
||||
usage_output = helputils.UsageText(component, trace=t, verbose=False)
|
||||
expected_output = '''
|
||||
Usage: NoDefaults <commands>
|
||||
available commands: double | triple
|
||||
|
||||
For detailed information on this command, run:
|
||||
NoDefaults --help
|
||||
'''
|
||||
|
||||
self.assertEqual(
|
||||
usage_output,
|
||||
textwrap.dedent(expected_output).lstrip('\n'))
|
||||
|
||||
def testUsageOutputVerbose(self):
|
||||
component = tc.NoDefaults()
|
||||
t = trace.FireTrace(component, name='NoDefaults')
|
||||
usage_output = helputils.UsageText(component, trace=t, verbose=True)
|
||||
expected_output = '''
|
||||
Usage: NoDefaults <commands>
|
||||
available commands: double | triple
|
||||
|
||||
For detailed information on this command, run:
|
||||
NoDefaults --help
|
||||
'''
|
||||
self.assertEqual(
|
||||
usage_output,
|
||||
textwrap.dedent(expected_output).lstrip('\n'))
|
||||
|
||||
def testUsageOutputMethod(self):
|
||||
component = tc.NoDefaults().double
|
||||
t = trace.FireTrace(component, name='NoDefaults')
|
||||
t.AddAccessedProperty(component, 'double', ['double'], None, None)
|
||||
usage_output = helputils.UsageText(component, trace=t, verbose=True)
|
||||
expected_output = '''
|
||||
Usage: NoDefaults double COUNT
|
||||
|
||||
For detailed information on this command, run:
|
||||
NoDefaults double --help
|
||||
'''
|
||||
self.assertEqual(
|
||||
usage_output,
|
||||
textwrap.dedent(expected_output).lstrip('\n'))
|
||||
|
||||
def testUsageOutputFunctionWithHelp(self):
|
||||
component = tc.function_with_help
|
||||
t = trace.FireTrace(component, name='function_with_help')
|
||||
usage_output = helputils.UsageText(component, trace=t, verbose=True)
|
||||
expected_output = '''
|
||||
Usage: function_with_help <flags>
|
||||
|
||||
Available flags: --help
|
||||
|
||||
For detailed information on this command, run:
|
||||
function_with_help -- --help
|
||||
'''
|
||||
self.assertEqual(
|
||||
usage_output,
|
||||
textwrap.dedent(expected_output).lstrip('\n'))
|
||||
|
||||
def testUsageOutputFunctionWithDocstring(self):
|
||||
component = tc.multiplier_with_docstring
|
||||
t = trace.FireTrace(component, name='multiplier_with_docstring')
|
||||
usage_output = helputils.UsageText(component, trace=t, verbose=True)
|
||||
expected_output = '''
|
||||
Usage: multiplier_with_docstring NUM <flags>
|
||||
|
||||
Available flags: --rate
|
||||
|
||||
For detailed information on this command, run:
|
||||
multiplier_with_docstring --help
|
||||
'''
|
||||
self.assertEqual(
|
||||
usage_output,
|
||||
textwrap.dedent(expected_output).lstrip('\n'))
|
||||
|
||||
@testutils.skip('The functionality is not implemented yet')
|
||||
def testUsageOutputCallable(self):
|
||||
# This is both a group and a command!
|
||||
component = tc.CallableWithKeywordArgument
|
||||
t = trace.FireTrace(component, name='CallableWithKeywordArgument')
|
||||
usage_output = helputils.UsageText(component, trace=t, verbose=True)
|
||||
# TODO(zuhaohen): We need to handle the case for keyword args as well
|
||||
# i.e. __call__ method of CallableWithKeywordArgument
|
||||
expected_output = '''
|
||||
Usage: CallableWithKeywordArgument <commands>
|
||||
|
||||
Available commands: print_msg
|
||||
|
||||
For detailed information on this command, run:
|
||||
CallableWithKeywordArgument -- --help
|
||||
'''
|
||||
self.assertEqual(
|
||||
usage_output,
|
||||
textwrap.dedent(expected_output).lstrip('\n'))
|
||||
|
||||
def testUsageOutputConstructorWithParameter(self):
|
||||
component = tc.InstanceVars
|
||||
t = trace.FireTrace(component, name='InstanceVars')
|
||||
usage_output = helputils.UsageText(component, trace=t, verbose=True)
|
||||
expected_output = '''
|
||||
Usage: InstanceVars ARG1 ARG2
|
||||
|
||||
For detailed information on this command, run:
|
||||
InstanceVars --help
|
||||
'''
|
||||
self.assertEqual(
|
||||
usage_output,
|
||||
textwrap.dedent(expected_output).lstrip('\n'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
testutils.main()
|
||||
|
||||
+137
-38
@@ -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,11 +20,35 @@ from __future__ import print_function
|
||||
|
||||
import inspect
|
||||
|
||||
import IPython
|
||||
import six
|
||||
|
||||
|
||||
def _GetArgSpecFnInfo(fn):
|
||||
class FullArgSpec(object):
|
||||
"""The arguments of a function, as in Python 3's inspect.FullArgSpec."""
|
||||
|
||||
def __init__(self, args=None, varargs=None, varkw=None, defaults=None,
|
||||
kwonlyargs=None, kwonlydefaults=None, annotations=None):
|
||||
"""Constructs a FullArgSpec with each provided attribute, or the default.
|
||||
|
||||
Args:
|
||||
args: A list of the argument names accepted by the function.
|
||||
varargs: The name of the *varargs argument or None if there isn't one.
|
||||
varkw: The name of the **kwargs argument or None if there isn't one.
|
||||
defaults: A tuple of the defaults for the arguments that accept defaults.
|
||||
kwonlyargs: A list of argument names that must be passed with a keyword.
|
||||
kwonlydefaults: A dictionary of keyword only arguments and their defaults.
|
||||
annotations: A dictionary of arguments and their annotated types.
|
||||
"""
|
||||
self.args = args or []
|
||||
self.varargs = varargs
|
||||
self.varkw = varkw
|
||||
self.defaults = defaults or ()
|
||||
self.kwonlyargs = kwonlyargs or []
|
||||
self.kwonlydefaults = kwonlydefaults or {}
|
||||
self.annotations = annotations or {}
|
||||
|
||||
|
||||
def _GetArgSpecInfo(fn):
|
||||
"""Gives information pertaining to computing the ArgSpec of fn.
|
||||
|
||||
Determines if the first arg is supplied automatically when fn is called.
|
||||
@@ -39,7 +63,7 @@ def _GetArgSpecFnInfo(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.
|
||||
"""
|
||||
@@ -49,52 +73,70 @@ def _GetArgSpecFnInfo(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
|
||||
|
||||
|
||||
def GetArgSpec(fn):
|
||||
"""Returns information about the function signature.
|
||||
def GetFullArgSpec(fn):
|
||||
"""Returns a FullArgSpec describing the given callable."""
|
||||
|
||||
Args:
|
||||
fn: The function to analyze.
|
||||
Returns:
|
||||
A named tuple of type inspect.ArgSpec with the following fields:
|
||||
args: A list of the argument names accepted by the function.
|
||||
varargs: The name of the *varargs argument or None if there isn't one.
|
||||
keywords: The name of the **kwargs argument or None if there isn't one.
|
||||
defaults: A tuple of the defaults for the arguments that accept defaults.
|
||||
"""
|
||||
fn, skip_arg = _GetArgSpecFnInfo(fn)
|
||||
fn, skip_arg = _GetArgSpecInfo(fn)
|
||||
|
||||
try:
|
||||
argspec = inspect.getargspec(fn)
|
||||
args = argspec.args
|
||||
defaults = argspec.defaults or ()
|
||||
varargs = argspec.varargs
|
||||
keywords = argspec.keywords
|
||||
if six.PY2:
|
||||
args, varargs, varkw, defaults = inspect.getargspec(fn) # pylint: disable=deprecated-method
|
||||
kwonlyargs = kwonlydefaults = None
|
||||
annotations = getattr(fn, '__annotations__', None)
|
||||
else:
|
||||
(args, varargs, varkw, defaults,
|
||||
kwonlyargs, kwonlydefaults, annotations) = inspect.getfullargspec(fn) # pylint: disable=deprecated-method,no-member
|
||||
|
||||
except TypeError:
|
||||
args = []
|
||||
defaults = ()
|
||||
# If we can't get the argspec, how do we know if the fn should take args?
|
||||
# 1. If it's a builtin, it can take args.
|
||||
# 2. If it's an implicit __init__ function (a 'slot wrapper'), take no args.
|
||||
# Are there other cases?
|
||||
varargs = 'vars' if inspect.isbuiltin(fn) else None
|
||||
keywords = 'kwargs' if inspect.isbuiltin(fn) else None
|
||||
if inspect.isbuiltin(fn):
|
||||
return FullArgSpec(varargs='vars', varkw='kwargs')
|
||||
return FullArgSpec()
|
||||
|
||||
if skip_arg:
|
||||
args = args[1:] # Remove self.
|
||||
if skip_arg and args:
|
||||
args.pop(0) # Remove 'self' or 'cls' from the list of arguments.
|
||||
|
||||
return inspect.ArgSpec(
|
||||
args=args,
|
||||
varargs=varargs,
|
||||
keywords=keywords,
|
||||
defaults=defaults)
|
||||
return FullArgSpec(args, varargs, varkw, defaults,
|
||||
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):
|
||||
@@ -116,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)
|
||||
@@ -126,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
|
||||
|
||||
+68
-50
@@ -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,65 +12,83 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for the inspectutils module."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
from fire import inspectutils
|
||||
from fire import test_components as tc
|
||||
import six
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from fire import inspectutils
|
||||
from fire import test_components as tc
|
||||
from fire import testutils
|
||||
|
||||
class InspectUtilsTest(unittest.TestCase):
|
||||
import six
|
||||
|
||||
def testGetArgSpecReturnType(self):
|
||||
# Asserts that the named tuple returned by GetArgSpec has the appropriate
|
||||
# fields.
|
||||
argspec = inspectutils.GetArgSpec(tc.identity)
|
||||
args, varargs, keywords, defaults = argspec
|
||||
self.assertEqual(argspec.args, args)
|
||||
self.assertEqual(argspec.defaults, defaults)
|
||||
self.assertEqual(argspec.varargs, varargs)
|
||||
self.assertEqual(argspec.keywords, keywords)
|
||||
|
||||
def testGetArgSpec(self):
|
||||
args, varargs, keywords, defaults = inspectutils.GetArgSpec(tc.identity)
|
||||
self.assertEqual(args, ['arg1', 'arg2'])
|
||||
self.assertEqual(defaults, (10,))
|
||||
self.assertEqual(varargs, 'arg3')
|
||||
self.assertEqual(keywords, 'arg4')
|
||||
class InspectUtilsTest(testutils.BaseTestCase):
|
||||
|
||||
def testGetArgSpecBuiltin(self):
|
||||
args, varargs, keywords, defaults = inspectutils.GetArgSpec('test'.upper)
|
||||
self.assertEqual(args, [])
|
||||
self.assertEqual(defaults, ())
|
||||
self.assertEqual(varargs, 'vars')
|
||||
self.assertEqual(keywords, 'kwargs')
|
||||
def testGetFullArgSpec(self):
|
||||
spec = inspectutils.GetFullArgSpec(tc.identity)
|
||||
self.assertEqual(spec.args, ['arg1', 'arg2', 'arg3', 'arg4'])
|
||||
self.assertEqual(spec.defaults, (10, 20))
|
||||
self.assertEqual(spec.varargs, 'arg5')
|
||||
self.assertEqual(spec.varkw, 'arg6')
|
||||
self.assertEqual(spec.kwonlyargs, [])
|
||||
self.assertEqual(spec.kwonlydefaults, {})
|
||||
self.assertEqual(spec.annotations, {'arg2': int, 'arg4': int})
|
||||
|
||||
def testGetArgSpecSlotWrapper(self):
|
||||
args, varargs, keywords, defaults = inspectutils.GetArgSpec(tc.NoDefaults)
|
||||
self.assertEqual(args, [])
|
||||
self.assertEqual(defaults, ())
|
||||
self.assertEqual(varargs, None)
|
||||
self.assertEqual(keywords, None)
|
||||
@unittest.skipIf(six.PY2, 'No keyword arguments in python 2')
|
||||
def testGetFullArgSpecPy3(self):
|
||||
spec = inspectutils.GetFullArgSpec(tc.py3.identity)
|
||||
self.assertEqual(spec.args, ['arg1', 'arg2', 'arg3', 'arg4'])
|
||||
self.assertEqual(spec.defaults, (10, 20))
|
||||
self.assertEqual(spec.varargs, 'arg5')
|
||||
self.assertEqual(spec.varkw, 'arg10')
|
||||
self.assertEqual(spec.kwonlyargs, ['arg6', 'arg7', 'arg8', 'arg9'])
|
||||
self.assertEqual(spec.kwonlydefaults, {'arg8': 30, 'arg9': 40})
|
||||
self.assertEqual(spec.annotations,
|
||||
{'arg2': int, 'arg4': int, 'arg7': int, 'arg9': int})
|
||||
|
||||
def testGetArgSpecClassNoInit(self):
|
||||
args, varargs, keywords, defaults = inspectutils.GetArgSpec(
|
||||
tc.OldStyleEmpty)
|
||||
self.assertEqual(args, [])
|
||||
self.assertEqual(defaults, ())
|
||||
self.assertEqual(varargs, None)
|
||||
self.assertEqual(keywords, None)
|
||||
def testGetFullArgSpecFromBuiltin(self):
|
||||
spec = inspectutils.GetFullArgSpec('test'.upper)
|
||||
self.assertEqual(spec.args, [])
|
||||
self.assertEqual(spec.defaults, ())
|
||||
self.assertEqual(spec.kwonlyargs, [])
|
||||
self.assertEqual(spec.kwonlydefaults, {})
|
||||
self.assertEqual(spec.annotations, {})
|
||||
|
||||
def testGetArgSpecMethod(self):
|
||||
args, varargs, keywords, defaults = inspectutils.GetArgSpec(
|
||||
tc.NoDefaults().double)
|
||||
self.assertEqual(args, ['count'])
|
||||
self.assertEqual(defaults, ())
|
||||
self.assertEqual(varargs, None)
|
||||
self.assertEqual(keywords, None)
|
||||
def testGetFullArgSpecFromSlotWrapper(self):
|
||||
spec = inspectutils.GetFullArgSpec(tc.NoDefaults)
|
||||
self.assertEqual(spec.args, [])
|
||||
self.assertEqual(spec.defaults, ())
|
||||
self.assertEqual(spec.varargs, None)
|
||||
self.assertEqual(spec.varkw, None)
|
||||
self.assertEqual(spec.kwonlyargs, [])
|
||||
self.assertEqual(spec.kwonlydefaults, {})
|
||||
self.assertEqual(spec.annotations, {})
|
||||
|
||||
def testGetFullArgSpecFromClassNoInit(self):
|
||||
spec = inspectutils.GetFullArgSpec(tc.OldStyleEmpty)
|
||||
self.assertEqual(spec.args, [])
|
||||
self.assertEqual(spec.defaults, ())
|
||||
self.assertEqual(spec.varargs, None)
|
||||
self.assertEqual(spec.varkw, None)
|
||||
self.assertEqual(spec.kwonlyargs, [])
|
||||
self.assertEqual(spec.kwonlydefaults, {})
|
||||
self.assertEqual(spec.annotations, {})
|
||||
|
||||
def testGetFullArgSpecFromMethod(self):
|
||||
spec = inspectutils.GetFullArgSpec(tc.NoDefaults().double)
|
||||
self.assertEqual(spec.args, ['count'])
|
||||
self.assertEqual(spec.defaults, ())
|
||||
self.assertEqual(spec.varargs, None)
|
||||
self.assertEqual(spec.varkw, None)
|
||||
self.assertEqual(spec.kwonlyargs, [])
|
||||
self.assertEqual(spec.kwonlydefaults, {})
|
||||
self.assertEqual(spec.annotations, {})
|
||||
|
||||
def testInfoOne(self):
|
||||
info = inspectutils.Info(1)
|
||||
@@ -82,7 +100,7 @@ class InspectUtilsTest(unittest.TestCase):
|
||||
def testInfoClass(self):
|
||||
info = inspectutils.Info(tc.NoDefaults)
|
||||
self.assertEqual(info.get('type_name'), 'type')
|
||||
self.assertIn('fire/test_components.py', info.get('file'))
|
||||
self.assertIn(os.path.join('fire', 'test_components.py'), info.get('file'))
|
||||
self.assertGreater(info.get('line'), 0)
|
||||
|
||||
def testInfoClassNoInit(self):
|
||||
@@ -91,9 +109,9 @@ class InspectUtilsTest(unittest.TestCase):
|
||||
self.assertEqual(info.get('type_name'), 'classobj')
|
||||
else:
|
||||
self.assertEqual(info.get('type_name'), 'type')
|
||||
self.assertIn('fire/test_components.py', info.get('file'))
|
||||
self.assertIn(os.path.join('fire', 'test_components.py'), info.get('file'))
|
||||
self.assertGreater(info.get('line'), 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
testutils.main()
|
||||
|
||||
+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()
|
||||
|
||||
+21
-12
@@ -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,32 +12,41 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for the interact module."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
from fire import interact
|
||||
from fire import testutils
|
||||
|
||||
import mock
|
||||
|
||||
import unittest
|
||||
|
||||
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(unittest.TestCase):
|
||||
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__':
|
||||
unittest.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):
|
||||
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.
|
||||
@@ -12,11 +12,14 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Fuzz tests for the parser module."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
from fire import parser
|
||||
from fire import testutils
|
||||
from hypothesis import example
|
||||
from hypothesis import given
|
||||
from hypothesis import settings
|
||||
@@ -24,12 +27,11 @@ from hypothesis import strategies as st
|
||||
import Levenshtein
|
||||
import six
|
||||
|
||||
import unittest
|
||||
|
||||
class ParserFuzzTest(testutils.BaseTestCase):
|
||||
|
||||
class ParserFuzzTest(unittest.TestCase):
|
||||
|
||||
@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" ')
|
||||
@@ -66,8 +68,8 @@ class ParserFuzzTest(unittest.TestCase):
|
||||
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
|
||||
@@ -94,4 +96,4 @@ class ParserFuzzTest(unittest.TestCase):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
testutils.main()
|
||||
|
||||
+11
-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,16 +12,17 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for the parser module."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
from fire import parser
|
||||
|
||||
import unittest
|
||||
from fire import testutils
|
||||
|
||||
|
||||
class ParserTest(unittest.TestCase):
|
||||
class ParserTest(testutils.BaseTestCase):
|
||||
|
||||
def testCreateParser(self):
|
||||
self.assertIsNotNone(parser.CreateParser())
|
||||
@@ -68,10 +69,12 @@ class ParserTest(unittest.TestCase):
|
||||
|
||||
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')
|
||||
@@ -126,13 +129,15 @@ class ParserTest(unittest.TestCase):
|
||||
# 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__':
|
||||
unittest.main()
|
||||
testutils.main()
|
||||
|
||||
+187
-8
@@ -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,22 +12,47 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Thie module has componenets that are used for testing Python Fire."""
|
||||
"""This module has components that are used for testing Python Fire."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import collections
|
||||
|
||||
def identity(arg1, arg2=10, *arg3, **arg4):
|
||||
return arg1, arg2, arg3, arg4
|
||||
import six
|
||||
|
||||
if six.PY3:
|
||||
from fire import test_components_py3 as py3 # pylint: disable=unused-import,no-name-in-module,g-import-not-at-top
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
class OldStyleEmpty: # pylint: disable=old-style-class
|
||||
class OldStyleEmpty: # pylint: disable=old-style-class,no-init
|
||||
pass
|
||||
|
||||
|
||||
@@ -37,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):
|
||||
@@ -55,7 +95,7 @@ class WithDefaults(object):
|
||||
return 3 * count
|
||||
|
||||
|
||||
class OldStyleWithDefaults: # pylint: disable=old-style-class
|
||||
class OldStyleWithDefaults: # pylint: disable=old-style-class,no-init
|
||||
|
||||
def double(self, count=0):
|
||||
return 2 * count
|
||||
@@ -76,7 +116,35 @@ 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):
|
||||
return 2 * count
|
||||
|
||||
def triple(self, count=0):
|
||||
return 3 * count
|
||||
|
||||
double.__annotations__ = {'count': float}
|
||||
triple.__annotations__ = {'count': float}
|
||||
|
||||
|
||||
class TypedProperties(object):
|
||||
"""Test class for testing Python Fire with properties of various types."""
|
||||
|
||||
def __init__(self):
|
||||
self.alpha = True
|
||||
@@ -91,10 +159,11 @@ class TypedProperties(object):
|
||||
}
|
||||
self.echo = ['alex', 'bethany']
|
||||
self.fox = ('carry', 'divide')
|
||||
self.gamma = 'myexcitingstring'
|
||||
|
||||
|
||||
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
|
||||
@@ -107,7 +176,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)
|
||||
|
||||
|
||||
@@ -168,3 +237,113 @@ class ErrorRaiser(object):
|
||||
|
||||
def fail(self):
|
||||
raise ValueError('This error is part of a test.')
|
||||
|
||||
|
||||
class NonComparable(object):
|
||||
|
||||
def __eq__(self, other):
|
||||
raise ValueError('Instances of this class cannot be compared.')
|
||||
|
||||
def __ne__(self, other):
|
||||
raise ValueError('Instances of this class cannot be compared.')
|
||||
|
||||
|
||||
class EmptyDictOutput(object):
|
||||
|
||||
def totally_empty(self):
|
||||
return {}
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright (C) 2018 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""This module has components that use Python 3 specific syntax."""
|
||||
|
||||
|
||||
def identity(arg1, arg2: int, arg3=10, arg4: int = 20, *arg5,
|
||||
arg6, arg7: int, arg8=30, arg9: int = 40, **arg10):
|
||||
return arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10
|
||||
|
||||
|
||||
class KeywordOnly(object):
|
||||
|
||||
def double(self, *, count):
|
||||
return count * 2
|
||||
|
||||
def triple(self, *, count):
|
||||
return count * 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.
|
||||
@@ -12,22 +12,29 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for the test_components module."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
from fire import test_components as tc
|
||||
|
||||
import unittest
|
||||
from fire import testutils
|
||||
|
||||
|
||||
class TestComponentsTest(unittest.TestCase):
|
||||
class TestComponentsTest(testutils.BaseTestCase):
|
||||
"""Tests to verify that the test components are importable and okay."""
|
||||
|
||||
def testTestComponents(self):
|
||||
self.assertIsNotNone(tc.Empty)
|
||||
self.assertIsNotNone(tc.OldStyleEmpty)
|
||||
|
||||
def testNonComparable(self):
|
||||
with self.assertRaises(ValueError):
|
||||
tc.NonComparable() != 2 # pylint: disable=expression-not-assigned
|
||||
with self.assertRaises(ValueError):
|
||||
tc.NonComparable() == 2 # pylint: disable=expression-not-assigned
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
testutils.main()
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# 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.
|
||||
|
||||
"""Utilities for Python Fire's tests."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import contextlib
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from fire import core
|
||||
from fire import trace
|
||||
|
||||
import mock
|
||||
import six
|
||||
|
||||
|
||||
class BaseTestCase(unittest.TestCase):
|
||||
"""Shared test case for Python Fire tests."""
|
||||
|
||||
@contextlib.contextmanager
|
||||
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='.*'):
|
||||
"""Asserts that a FireExit error is raised in the context.
|
||||
|
||||
Allows tests to check that Fire's wrapper around SystemExit is raised
|
||||
and that a regexp is matched in the output.
|
||||
|
||||
Args:
|
||||
code: The status code that the FireExit should contain.
|
||||
regexp: stdout must match this regex.
|
||||
Yields:
|
||||
Yields to the wrapped context.
|
||||
"""
|
||||
with self.assertOutputMatches(stderr=regexp):
|
||||
with self.assertRaises(core.FireExit):
|
||||
try:
|
||||
yield
|
||||
except core.FireExit as exc:
|
||||
if exc.code != code:
|
||||
raise AssertionError('Incorrect exit code: %r != %r' % (exc.code,
|
||||
code))
|
||||
self.assertIsInstance(exc.trace, trace.FireTrace)
|
||||
raise
|
||||
|
||||
|
||||
# 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()
|
||||
+38
-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'
|
||||
@@ -76,6 +78,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 +95,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 +216,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.
|
||||
|
||||
+27
-12
@@ -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,16 +12,17 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for the trace module."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
from fire import testutils
|
||||
from fire import trace
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
class FireTraceTest(unittest.TestCase):
|
||||
class FireTraceTest(testutils.BaseTestCase):
|
||||
|
||||
def testFireTraceInitialization(self):
|
||||
t = trace.FireTrace(10)
|
||||
@@ -50,10 +51,20 @@ class FireTraceTest(unittest.TestCase):
|
||||
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)')
|
||||
@@ -61,8 +72,9 @@ class FireTraceTest(unittest.TestCase):
|
||||
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)
|
||||
@@ -84,23 +96,26 @@ class FireTraceTest(unittest.TestCase):
|
||||
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'")
|
||||
|
||||
|
||||
class FireTraceElementTest(unittest.TestCase):
|
||||
class FireTraceElementTest(testutils.BaseTestCase):
|
||||
|
||||
def testFireTraceElementHasError(self):
|
||||
el = trace.FireTraceElement()
|
||||
@@ -136,4 +151,4 @@ class FireTraceElementTest(unittest.TestCase):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
testutils.main()
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,228 @@
|
||||
[MASTER]
|
||||
|
||||
# Specify a configuration file.
|
||||
#rcfile=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Profiled execution.
|
||||
profile=no
|
||||
|
||||
# Add <file or directory> to the black list. It should be a base name, not a
|
||||
# path. You may set this option multiple times.
|
||||
ignore=
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Enable 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.
|
||||
enable=indexing-exception,old-raise-syntax
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time.
|
||||
disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, msvs
|
||||
# (visual studio) and html
|
||||
output-format=text
|
||||
|
||||
# Include message's id in output
|
||||
include-ids=no
|
||||
|
||||
# Put messages in a separate file for each module / package specified on the
|
||||
# command line instead of printing them on stdout. Reports (if any) will be
|
||||
# written in a file name "pylint_global.[txt|html]".
|
||||
files-output=no
|
||||
|
||||
# Tells whether to display a full report or only the messages
|
||||
reports=yes
|
||||
|
||||
# Python expression which should return a note less than 10 (10 is the highest
|
||||
# note). You have access to the variables errors warning, statement which
|
||||
# respectively contain the number of errors / warnings messages and the total
|
||||
# number of statements analyzed. This is used by the global evaluation report
|
||||
# (R0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Add a comment according to your evaluation note. This is used by the global
|
||||
# evaluation report (R0004).
|
||||
comment=no
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# A regular expression matching names used for dummy variables (i.e. not used).
|
||||
dummy-variables-rgx=\*{0,2}(_$|unused_|dummy_)
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid to define new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# List of builtins function names that should not be used, separated by a comma
|
||||
bad-functions=map,filter,apply,input,reduce
|
||||
|
||||
# Regular expression which should only match correct module names
|
||||
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
|
||||
|
||||
# Regular expression which should only match correct module level names
|
||||
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
|
||||
|
||||
# Regular expression which should only match correct class names
|
||||
class-rgx=[A-Z_][a-zA-Z0-9]+$
|
||||
|
||||
# Regular expression which should only match correct function names
|
||||
function-rgx=^(?:(?P<camel_case>_?[A-Z][a-zA-Z0-9]*)|(?P<snake_case>_?[a-z][a-z0-9_]*))$
|
||||
|
||||
# Regular expression which should only match correct method names
|
||||
method-rgx=^(?:(?P<exempt>__[a-z0-9_]+__|next)|(?P<camel_case>_{0,2}(?:test|assert)?[A-Z][a-zA-Z0-9]*)|(?:_{0,2}[a-z][a-z0-9_]*))$
|
||||
|
||||
# Regular expression which should only match correct instance attribute names
|
||||
attr-rgx=^_{0,2}[a-z][a-z0-9_]*$
|
||||
|
||||
# Regular expression which should only match correct argument names
|
||||
argument-rgx=^[a-z][a-z0-9_]*$
|
||||
|
||||
# Regular expression which should only match correct variable names
|
||||
variable-rgx=^[a-z][a-z0-9_]*$
|
||||
|
||||
# Regular expression which should only match correct list comprehension /
|
||||
# generator expression variable names
|
||||
inlinevar-rgx=^[a-z][a-z0-9_]*$
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma
|
||||
good-names=i,j,k,ex,main,Run,_
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma
|
||||
bad-names=foo,bar,baz,toto,tutu,tata
|
||||
|
||||
# Regular expression which should only match functions or classes name which do
|
||||
# not require a docstring
|
||||
no-docstring-rgx=(__.*__|main|test.*|.*Test)
|
||||
|
||||
# Minimum length for a docstring
|
||||
docstring-min-length=10
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,XXX,TODO
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=80
|
||||
|
||||
# Maximum number of lines in a module
|
||||
max-module-lines=99999
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# List of classes names for which member attributes should not be checked
|
||||
# (useful for classes with attributes dynamically set).
|
||||
ignored-classes=
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E0201 when accessed.
|
||||
generated-members=
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method
|
||||
max-args=5
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore
|
||||
ignored-argument-names=_.*
|
||||
|
||||
# Maximum number of locals for function / method body
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of return / yield for function / method body
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of branch for function / method body
|
||||
max-branchs=12
|
||||
|
||||
# Maximum number of statements in function / method body
|
||||
max-statements=50
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma
|
||||
deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled)
|
||||
import-graph=
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
int-import-graph=
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,__new__,setUp
|
||||
@@ -0,0 +1 @@
|
||||
.
|
||||
@@ -0,0 +1,11 @@
|
||||
[metadata]
|
||||
license-file = LICENSE
|
||||
|
||||
[wheel]
|
||||
universal = 1
|
||||
|
||||
[aliases]
|
||||
test = pytest
|
||||
|
||||
[tool:pytest]
|
||||
addopts = --ignore=fire/test_components_py3.py --ignore=fire/parser_fuzz_test.py
|
||||
@@ -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,7 +16,8 @@
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
long_description = """
|
||||
|
||||
LONG_DESCRIPTION = """
|
||||
Python Fire is a library for automatically generating command line interfaces
|
||||
(CLIs) with a single line of code.
|
||||
|
||||
@@ -25,32 +26,31 @@ component will work!) into a CLI. It's called Fire because when you call Fire(),
|
||||
it fires off your command.
|
||||
""".strip()
|
||||
|
||||
short_description = """
|
||||
A library for automatically generating commane line interfaces.""".strip()
|
||||
SHORT_DESCRIPTION = """
|
||||
A library for automatically generating command line interfaces.""".strip()
|
||||
|
||||
dependencies = [
|
||||
'ipython',
|
||||
DEPENDENCIES = [
|
||||
'six',
|
||||
]
|
||||
|
||||
test_dependencies = [
|
||||
TEST_DEPENDENCIES = [
|
||||
'hypothesis',
|
||||
'mock',
|
||||
'python-Levenshtein',
|
||||
]
|
||||
|
||||
VERSION = '0.1.3'
|
||||
URL = 'https://github.com/google/python-fire'
|
||||
|
||||
setup(
|
||||
name='fire',
|
||||
version='0.1.0',
|
||||
|
||||
description=short_description,
|
||||
long_description=long_description,
|
||||
|
||||
url='https://github.com/google/python-fire',
|
||||
version=VERSION,
|
||||
description=SHORT_DESCRIPTION,
|
||||
long_description=LONG_DESCRIPTION,
|
||||
url=URL,
|
||||
|
||||
author='David Bieber',
|
||||
author_email='dbieber@google.com',
|
||||
|
||||
license='Apache Software License',
|
||||
|
||||
classifiers=[
|
||||
@@ -65,6 +65,9 @@ setup(
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
|
||||
'Operating System :: OS Independent',
|
||||
'Operating System :: POSIX',
|
||||
@@ -76,6 +79,6 @@ setup(
|
||||
|
||||
packages=['fire'],
|
||||
|
||||
install_requires=dependencies,
|
||||
tests_require=test_dependencies,
|
||||
install_requires=DEPENDENCIES,
|
||||
tests_require=TEST_DEPENDENCIES,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user