├── .github
├── dependabot.yml
├── scripts
│ ├── build.sh
│ └── requirements.txt
└── workflows
│ └── build.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── docs
├── api.md
├── benefits.md
├── guide.md
├── index.md
├── installation.md
├── troubleshooting.md
└── using-cli.md
├── examples
├── __init__.py
├── cipher
│ ├── __init__.py
│ ├── cipher.py
│ └── cipher_test.py
├── diff
│ ├── __init__.py
│ ├── diff.py
│ ├── diff_test.py
│ └── difffull.py
├── identity
│ ├── __init__.py
│ └── identity.py
└── widget
│ ├── __init__.py
│ ├── collector.py
│ ├── collector_test.py
│ ├── widget.py
│ └── widget_test.py
├── fire
├── __init__.py
├── __main__.py
├── completion.py
├── completion_test.py
├── console
│ ├── README.md
│ ├── __init__.py
│ ├── console_attr.py
│ ├── console_attr_os.py
│ ├── console_io.py
│ ├── console_pager.py
│ ├── encoding.py
│ ├── files.py
│ ├── platforms.py
│ └── text.py
├── core.py
├── core_test.py
├── custom_descriptions.py
├── custom_descriptions_test.py
├── decorators.py
├── decorators_test.py
├── docstrings.py
├── docstrings_fuzz_test.py
├── docstrings_test.py
├── fire_import_test.py
├── fire_test.py
├── formatting.py
├── formatting_test.py
├── formatting_windows.py
├── helptext.py
├── helptext_test.py
├── inspectutils.py
├── inspectutils_test.py
├── interact.py
├── interact_test.py
├── main_test.py
├── parser.py
├── parser_fuzz_test.py
├── parser_test.py
├── test_components.py
├── test_components_bin.py
├── test_components_py3.py
├── test_components_test.py
├── testutils.py
├── testutils_test.py
├── trace.py
├── trace_test.py
└── value_types.py
├── mkdocs.yml
├── pylintrc
├── requirements.txt
├── setup.cfg
└── setup.py
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # Basic dependabot.yml file with minimum configuration for two package managers
2 |
3 | version: 2
4 | updates:
5 | # Enable version updates for python
6 | - package-ecosystem: "pip"
7 | directory: ".github/scripts/"
8 | schedule:
9 | interval: "monthly"
10 | labels: ["dependabot"]
11 | pull-request-branch-name:
12 | separator: "-"
13 | open-pull-requests-limit: 5
14 | reviewers:
15 | - "dbieber"
16 |
17 | # Enable version updates for GitHub Actions
18 | - package-ecosystem: "github-actions"
19 | directory: "/"
20 | schedule:
21 | interval: "monthly"
22 | groups:
23 | gh-actions:
24 | patterns:
25 | - "*" # Check all dependencies
26 | labels: ["dependabot"]
27 | pull-request-branch-name:
28 | separator: "-"
29 | open-pull-requests-limit: 5
30 | reviewers:
31 | - "dbieber"
32 |
--------------------------------------------------------------------------------
/.github/scripts/build.sh:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | #!/usr/bin/env bash
16 |
17 | # Exit when any command fails.
18 | set -e
19 |
20 | PYTHON_VERSION=${PYTHON_VERSION:-3.7}
21 |
22 | pip install -U -r .github/scripts/requirements.txt
23 | python setup.py develop
24 | python -m pytest # Run the tests without IPython.
25 | pip install ipython
26 | python -m pytest # Now run the tests with IPython.
27 | pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console
28 | if [[ ${PYTHON_VERSION} == 3.7 ]]; then
29 | # Run type-checking.
30 | pip install pytype;
31 | pytype -x fire/test_components_py3.py;
32 | fi
33 |
--------------------------------------------------------------------------------
/.github/scripts/requirements.txt:
--------------------------------------------------------------------------------
1 | setuptools <=78.1.0
2 | pip
3 | pylint <3.3.7
4 | pytest <=8.3.3
5 | pytest-pylint <=1.1.2
6 | pytest-runner <7.0.0
7 | termcolor <2.6.0
8 | hypothesis <6.133.0
9 | levenshtein <=0.26.1
10 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Python Fire
2 |
3 | on:
4 | push:
5 | branches: ["master"]
6 | pull_request:
7 | branches: ["master"]
8 |
9 | defaults:
10 | run:
11 | shell: bash
12 |
13 | jobs:
14 | build:
15 | runs-on: ${{ matrix.os }}
16 | strategy:
17 | matrix:
18 | os: ["macos-latest", "ubuntu-latest"]
19 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-rc.2"]
20 | include:
21 | - {os: "ubuntu-22.04", python-version: "3.7"}
22 |
23 | steps:
24 | # Checkout the repo.
25 | - name: Checkout Python Fire repository
26 | uses: actions/checkout@v4
27 |
28 | # Set up Python environment.
29 | - name: Set up Python ${{ matrix.python-version }}
30 | uses: actions/setup-python@v5
31 | with:
32 | python-version: ${{ matrix.python-version }}
33 |
34 | # Build Python Fire using the build.sh script.
35 | - name: Run build script
36 | run: ./.github/scripts/build.sh
37 | env:
38 | PYTHON_VERSION: ${{ matrix.python-version }}
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *,cover
47 | .hypothesis/
48 |
49 | # Translations
50 | *.mo
51 | *.pot
52 |
53 | # Django stuff:
54 | *.log
55 | local_settings.py
56 |
57 | # Flask stuff:
58 | instance/
59 | .webassets-cache
60 |
61 | # Scrapy stuff:
62 | .scrapy
63 |
64 | # Sphinx documentation
65 | docs/_build/
66 |
67 | # PyBuilder
68 | target/
69 |
70 | # Jupyter Notebook
71 | .ipynb_checkpoints
72 |
73 | # pyenv
74 | .python-version
75 |
76 | # celery beat schedule file
77 | celerybeat-schedule
78 |
79 | # SageMath parsed files
80 | *.sage.py
81 |
82 | # dotenv
83 | .env
84 |
85 | # virtualenv
86 | .venv
87 | venv/
88 | ENV/
89 |
90 | # Spyder project settings
91 | .spyderproject
92 |
93 | # Rope project settings
94 | .ropeproject
95 |
96 | # mkdocs documentation
97 | /site
98 |
99 | # PyCharm IDE
100 | .idea/
101 |
102 | # Type-checking
103 | .pytype/
104 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | First, read these guidelines.
7 | Before you begin making changes, state your intent to do so in an Issue.
8 | Then, fork the project. Make changes in your copy of the repository.
9 | Then open a pull request once your changes are ready.
10 | If this is your first contribution, sign the Contributor License Agreement.
11 | A discussion about your change will follow, and if accepted your contribution
12 | will be incorporated into the Python Fire codebase.
13 |
14 | ## Contributor License Agreement
15 |
16 | Contributions to this project must be accompanied by a Contributor License
17 | Agreement. You (or your employer) retain the copyright to your contribution,
18 | this simply gives us permission to use and redistribute your contributions as
19 | part of the project. Head over to to see
20 | your current agreements on file or to sign a new one.
21 |
22 | You generally only need to submit a CLA once, so if you've already submitted one
23 | (even if it was for a different project), you probably don't need to do it
24 | again.
25 |
26 | ## Code reviews
27 |
28 | All submissions, including submissions by project members, require review.
29 | For changes introduced by non-Googlers, we use GitHub pull requests for this
30 | purpose. Consult [GitHub Help] for more information on using pull requests.
31 |
32 | [GitHub Help]: https://help.github.com/articles/about-pull-requests/
33 |
34 | ## Code style
35 |
36 | In general, Python Fire follows the guidelines in the
37 | [Google Python Style Guide].
38 |
39 | In addition, the project follows a convention of:
40 | - Maximum line length: 80 characters
41 | - Indentation: 2 spaces (4 for line continuation)
42 | - PascalCase for function and method names.
43 | - Single quotes around strings, three double quotes around docstrings.
44 |
45 | [Google Python Style Guide]: http://google.github.io/styleguide/pyguide.html
46 |
47 | ## Testing
48 |
49 | Python Fire uses [GitHub Actions](https://github.com/google/python-fire/actions) to run tests on each pull request. You can run
50 | these tests yourself as well. To do this, first install the test dependencies
51 | listed in setup.py (e.g. pytest, mock, termcolor, and hypothesis).
52 | Then run the tests by running `pytest` in the root directory of the repository.
53 |
54 | ## Linting
55 |
56 | Please run lint on your pull requests to make accepting the requests easier.
57 | To do this, run `pylint fire` in the root directory of the repository.
58 | Note that even if lint is passing, additional style changes to your submission
59 | may be made during merging.
60 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2017 Google Inc. All rights reserved.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Python Fire [](https://github.com/google/python-fire)
2 |
3 | _Python Fire is a library for automatically generating command line interfaces
4 | (CLIs) from absolutely any Python object._
5 |
6 | - Python Fire is a simple way to create a CLI in Python.
7 | [[1]](docs/benefits.md#simple-cli)
8 | - Python Fire is a helpful tool for developing and debugging Python code.
9 | [[2]](docs/benefits.md#debugging)
10 | - Python Fire helps with exploring existing code or turning other people's
11 | code into a CLI. [[3]](docs/benefits.md#exploring)
12 | - Python Fire makes transitioning between Bash and Python easier.
13 | [[4]](docs/benefits.md#bash)
14 | - Python Fire makes using a Python REPL easier by setting up the REPL with the
15 | modules and variables you'll need already imported and created.
16 | [[5]](docs/benefits.md#repl)
17 |
18 | ## Installation
19 |
20 | To install Python Fire with pip, run: `pip install fire`
21 |
22 | To install Python Fire with conda, run: `conda install fire -c conda-forge`
23 |
24 | To install Python Fire from source, first clone the repository and then run:
25 | `python setup.py install`
26 |
27 | ## Basic Usage
28 |
29 | You can call `Fire` on any Python object:
30 | functions, classes, modules, objects, dictionaries, lists, tuples, etc.
31 | They all work!
32 |
33 | Here's an example of calling Fire on a function.
34 |
35 | ```python
36 | import fire
37 |
38 | def hello(name="World"):
39 | return "Hello %s!" % name
40 |
41 | if __name__ == '__main__':
42 | fire.Fire(hello)
43 | ```
44 |
45 | Then, from the command line, you can run:
46 |
47 | ```bash
48 | python hello.py # Hello World!
49 | python hello.py --name=David # Hello David!
50 | python hello.py --help # Shows usage information.
51 | ```
52 |
53 | Here's an example of calling Fire on a class.
54 |
55 | ```python
56 | import fire
57 |
58 | class Calculator(object):
59 | """A simple calculator class."""
60 |
61 | def double(self, number):
62 | return 2 * number
63 |
64 | if __name__ == '__main__':
65 | fire.Fire(Calculator)
66 | ```
67 |
68 | Then, from the command line, you can run:
69 |
70 | ```bash
71 | python calculator.py double 10 # 20
72 | python calculator.py double --number=15 # 30
73 | ```
74 |
75 | To learn how Fire behaves on functions, objects, dicts, lists, etc, and to learn
76 | about Fire's other features, see the [Using a Fire CLI page](docs/using-cli.md).
77 |
78 | For additional examples, see [The Python Fire Guide](docs/guide.md).
79 |
80 | ## Why is it called Fire?
81 |
82 | When you call `Fire`, it fires off (executes) your command.
83 |
84 | ## Where can I learn more?
85 |
86 | Please see [The Python Fire Guide](docs/guide.md).
87 |
88 | ## Reference
89 |
90 | | Setup | Command | Notes
91 | | :------ | :------------------ | :---------
92 | | install | `pip install fire` |
93 |
94 | | Creating a CLI | Command | Notes
95 | | :--------------| :--------------------- | :---------
96 | | import | `import fire` |
97 | | Call | `fire.Fire()` | Turns the current module into a Fire CLI.
98 | | Call | `fire.Fire(component)` | Turns `component` into a Fire CLI.
99 |
100 | | Using a CLI | Command | Notes
101 | | :---------------------------------------------- | :-------------------------------------- | :----
102 | | [Help](docs/using-cli.md#help-flag) | `command --help` or `command -- --help` |
103 | | [REPL](docs/using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
104 | | [Separator](docs/using-cli.md#separator-flag) | `command -- --separator=X` | Sets the separator to `X`. The default separator is `-`.
105 | | [Completion](docs/using-cli.md#completion-flag) | `command -- --completion [shell]` | Generates a completion script for the CLI.
106 | | [Trace](docs/using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
107 | | [Verbose](docs/using-cli.md#verbose-flag) | `command -- --verbose` |
108 |
109 | _Note that these flags are separated from the Fire command by an isolated `--`._
110 |
111 | ## License
112 |
113 | Licensed under the
114 | [Apache 2.0](https://github.com/google/python-fire/blob/master/LICENSE) License.
115 |
116 | ## Disclaimer
117 |
118 | This is not an official Google product.
119 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | ## Python Fire Quick Reference
2 |
3 | | Setup | Command | Notes
4 | | ------- | ------------------- | ----------
5 | | install | `pip install fire` | Installs fire from pypi
6 |
7 | | Creating a CLI | Command | Notes
8 | | ---------------| ---------------------- | ----------
9 | | import | `import fire` |
10 | | Call | `fire.Fire()` | Turns the current module into a Fire CLI.
11 | | Call | `fire.Fire(component)` | Turns `component` into a Fire CLI.
12 |
13 | | Using a CLI | Command | Notes |
14 | | ------------------------------------------ | ----------------- | -------------- |
15 | | [Help](using-cli.md#help-flag) | `command --help` | Show the help screen. |
16 | | [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode. |
17 | | [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`. |
18 | | [Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI. |
19 | | [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command. |
20 | | [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` | |
21 |
22 | _Note that flags are separated from the Fire command by an isolated `--` arg.
23 | Help is an exception; the isolated `--` is optional for getting help._
24 |
25 | ## Arguments for Calling fire.Fire()
26 |
27 | | Argument | Usage | Notes |
28 | | --------- | ------------------------- | ------------------------------------ |
29 | | component | `fire.Fire(component)` | If omitted, defaults to a dict of all locals and globals. |
30 | | command | `fire.Fire(command='hello --name=5')` | Either a string or a list of arguments. If a string is provided, it is split to determine the arguments. If a list or tuple is provided, they are the arguments. If `command` is omitted, then `sys.argv[1:]` (the arguments from the command line) are used by default. |
31 | | name | `fire.Fire(name='tool')` | The name of the CLI, ideally the name users will enter to run the CLI. This name will be used in the CLI's help screens. If the argument is omitted, it will be inferred automatically.|
32 | | serialize | `fire.Fire(serialize=custom_serializer)` | If omitted, simple types are serialized via their builtin str method, and any objects that define a custom `__str__` method are serialized with that. If specified, all objects are serialized to text via the provided method. |
33 |
34 | ## Using a Fire CLI without modifying any code
35 |
36 | You can use Python Fire on a module without modifying the code of the module.
37 | The syntax for this is:
38 |
39 | `python -m fire `
40 |
41 | or
42 |
43 | `python -m fire `
44 |
45 | For example, `python -m fire calendar -h` will treat the built in `calendar`
46 | module as a CLI and provide its help.
47 |
--------------------------------------------------------------------------------
/docs/benefits.md:
--------------------------------------------------------------------------------
1 | # Benefits of Python Fire
2 |
3 |
4 | ## Create CLIs in Python
5 |
6 | It's dead simple. Simply write the functionality you want exposed at the command
7 | line as a function / module / class, and then call Fire. With this addition of a
8 | single-line call to Fire, your CLI is ready to go.
9 |
10 |
11 | ## Develop and debug Python code
12 |
13 | When you're writing a Python library, you probably want to try it out as you go.
14 | You could write a main method to check the functionality you're interested in,
15 | but then you have to change the main method with every new experiment you're
16 | interested in testing, and constantly updating the main method is a hassle.
17 | You could also open an IPython REPL and import your library there and test it,
18 | but then you have to deal with reloading your imports every time you change
19 | something.
20 |
21 | If you simply call Fire in your library, then you can run all of it's
22 | functionality from the command line without having to keep making changes to
23 | a main method. And if you use the `--interactive` flag to enter an IPython REPL
24 | then you don't need to load the imports or create your variables; they'll
25 | already be ready for use as soon as you start the REPL.
26 |
27 |
28 | ## Explore existing code; turn other people's code into a CLI
29 |
30 | You can take an existing module, maybe even one that you don't have access to
31 | the source code for, and call `Fire` on it. This lets you easily see what
32 | functionality this code exposes, without you having to read through all the
33 | code.
34 |
35 | This technique can be a very simple way to create very powerful CLIs. Call
36 | `Fire` on the difflib library and you get a powerful diffing tool. Call `Fire`
37 | on the Python Imaging Library (PIL) module and you get a powerful image
38 | manipulation command line tool, very similar in nature to ImageMagick.
39 |
40 | The auto-generated help strings that Fire provides when you run a Fire CLI
41 | allow you to see all the functionality these modules provide in a concise
42 | manner.
43 |
44 |
45 | ## Transition between Bash and Python
46 |
47 | Using Fire lets you call Python directly from Bash. So you can mix your Python
48 | functions with the unix tools you know and love, like `grep`, `xargs`, `wc`,
49 | etc.
50 |
51 | Additionally since writing CLIs in Python requires only a single call to Fire,
52 | it is now easy to write even one-off scripts that would previously have been in
53 | Bash, in Python.
54 |
55 |
56 | ## Explore code in a Python REPL
57 |
58 | When you use the `--interactive` flag to enter an IPython REPL, it starts with
59 | variables and modules already defined for you. You don't need to waste time
60 | importing the modules you care about or defining the variables you're going to
61 | use, since Fire has already done so for you.
62 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Python Fire [](https://github.com/google/python-fire)
2 |
3 | _Python Fire is a library for automatically generating command line interfaces
4 | (CLIs) from absolutely any Python object._
5 |
6 | - Python Fire is a simple way to create a CLI in Python.
7 | [[1]](benefits.md#simple-cli)
8 | - Python Fire is a helpful tool for developing and debugging Python code.
9 | [[2]](benefits.md#debugging)
10 | - Python Fire helps with exploring existing code or turning other people's
11 | code into a CLI. [[3]](benefits.md#exploring)
12 | - Python Fire makes transitioning between Bash and Python easier.
13 | [[4]](benefits.md#bash)
14 | - Python Fire makes using a Python REPL easier by setting up the REPL with the
15 | modules and variables you'll need already imported and created.
16 | [[5]](benefits.md#repl)
17 |
18 | ## Installation
19 |
20 | To install Python Fire with pip, run: `pip install fire`
21 |
22 | To install Python Fire with conda, run: `conda install fire -c conda-forge`
23 |
24 | To install Python Fire from source, first clone the repository and then run:
25 | `python setup.py install`
26 |
27 | ## Basic Usage
28 |
29 | You can call `Fire` on any Python object:
30 | functions, classes, modules, objects, dictionaries, lists, tuples, etc.
31 | They all work!
32 |
33 | Here's an example of calling Fire on a function.
34 |
35 | ```python
36 | import fire
37 |
38 | def hello(name="World"):
39 | return "Hello %s!" % name
40 |
41 | if __name__ == '__main__':
42 | fire.Fire(hello)
43 | ```
44 |
45 | Then, from the command line, you can run:
46 |
47 | ```bash
48 | python hello.py # Hello World!
49 | python hello.py --name=David # Hello David!
50 | python hello.py --help # Shows usage information.
51 | ```
52 |
53 | Here's an example of calling Fire on a class.
54 |
55 | ```python
56 | import fire
57 |
58 | class Calculator(object):
59 | """A simple calculator class."""
60 |
61 | def double(self, number):
62 | return 2 * number
63 |
64 | if __name__ == '__main__':
65 | fire.Fire(Calculator)
66 | ```
67 |
68 | Then, from the command line, you can run:
69 |
70 | ```bash
71 | python calculator.py double 10 # 20
72 | python calculator.py double --number=15 # 30
73 | ```
74 |
75 | To learn how Fire behaves on functions, objects, dicts, lists, etc, and to learn
76 | about Fire's other features, see the [Using a Fire CLI page](using-cli.md).
77 |
78 | For additional examples, see [The Python Fire Guide](guide.md).
79 |
80 | ## Why is it called Fire?
81 |
82 | When you call `Fire`, it fires off (executes) your command.
83 |
84 | ## Where can I learn more?
85 |
86 | Please see [The Python Fire Guide](guide.md).
87 |
88 | ## Reference
89 |
90 | | Setup | Command | Notes
91 | | :------ | :------------------ | :---------
92 | | install | `pip install fire` |
93 |
94 | | Creating a CLI | Command | Notes
95 | | :--------------| :--------------------- | :---------
96 | | import | `import fire` |
97 | | Call | `fire.Fire()` | Turns the current module into a Fire CLI.
98 | | Call | `fire.Fire(component)` | Turns `component` into a Fire CLI.
99 |
100 | | Using a CLI | Command | Notes
101 | | :---------------------------------------------- | :-------------------------------------- | :----
102 | | [Help](using-cli.md#help-flag) | `command --help` or `command -- --help` |
103 | | [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
104 | | [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | Sets the separator to `X`. The default separator is `-`.
105 | | [Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generates a completion script for the CLI.
106 | | [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
107 | | [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` |
108 |
109 | _Note that flags are separated from the Fire command by an isolated `--` arg.
110 | Help is an exception; the isolated `--` is optional for getting help._
111 |
112 | ## License
113 |
114 | Licensed under the
115 | [Apache 2.0](https://github.com/google/python-fire/blob/master/LICENSE) License.
116 |
117 | ## Disclaimer
118 |
119 | This is not an official Google product.
120 |
--------------------------------------------------------------------------------
/docs/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | To install Python Fire with pip, run: `pip install fire`
4 |
5 | To install Python Fire with conda, run: `conda install fire -c conda-forge`
6 |
7 | To install Python Fire from source, first clone the repository and then run
8 | `python setup.py install`. To install from source for development, instead run `python setup.py develop`.
9 |
--------------------------------------------------------------------------------
/docs/troubleshooting.md:
--------------------------------------------------------------------------------
1 | # Troubleshooting
2 |
3 | This page describes known issues that users of Python Fire have run into. If you
4 | have an issue not resolved here, consider opening a
5 | [GitHub Issue](https://github.com/google/python-fire/issues).
6 |
7 | ### Issue [#19](https://github.com/google/python-fire/issues/19): Don't name your module "cmd"
8 |
9 | If you have a module name that conflicts with the name of a builtin module, then
10 | when Fire goes to import the builtin module, it will import your module instead.
11 | This will result in an error, possibly an `AttributeError`. Specifically, do not
12 | name your module any of the following:
13 | sys, linecache, cmd, bdb, repr, os, re, pprint, traceback
14 |
--------------------------------------------------------------------------------
/docs/using-cli.md:
--------------------------------------------------------------------------------
1 | # Using a Fire CLI
2 |
3 | ## Basic usage
4 |
5 | Every Fire command corresponds to a Python component.
6 |
7 | The simplest Fire command consists of running your program with no additional
8 | arguments. This command corresponds to the Python component you called the
9 | `Fire` function on. If you did not supply an object in the call to `Fire`, then
10 | the context in which `Fire` was called will be used as the Python component.
11 |
12 | You can append `--help` or `-h` to a command to see what Python component it
13 | corresponds to, as well as the various ways in which you can extend the command.
14 |
15 | Flags to Fire should be separated from the Fire command by an isolated `--` in
16 | order to distinguish between flags and named arguments. So, for example, to
17 | enter interactive mode append `-- -i` or `-- --interactive` to any command. To
18 | use Fire in verbose mode, append `-- --verbose`.
19 |
20 | Given a Fire command that corresponds to a Python object, you can extend that
21 | command to access a member of that object, call it with arguments if it is a
22 | function, instantiate it if it is a class, or index into it if it is a list.
23 |
24 | Read on to learn about how you can write a Fire command corresponding to
25 | whatever Python component you're looking for.
26 |
27 |
28 | ### Accessing members of an object
29 |
30 | If your command corresponds to an object, you can extend your command by adding
31 | the name of a member of that object as a new argument to the command. The
32 | resulting command will correspond to that member.
33 |
34 | For example, if the object your command corresponds to has a method defined on
35 | it named 'whack', then you can add the argument 'whack' to your command, and the
36 | resulting new command corresponds to the whack method.
37 |
38 | As another example, if the object your command corresponds to has a property
39 | named high_score, then you can add the argument 'high-score' to your command,
40 | and the resulting new command corresponds to the value of the high_score
41 | property.
42 |
43 |
44 | ### Accessing members of a dict
45 |
46 | If your command corresponds to a dict, you can extend your command by adding
47 | the name of one of the dict's keys as an argument.
48 |
49 | For example, `widget function-that-returns-dict key` will correspond to the
50 | value of the item with key `key` in the dict returned by
51 | `function_that_returns_dict`.
52 |
53 |
54 | ### Accessing members of a list or tuple
55 |
56 | If your command corresponds to a list or tuple, you can extend your command by
57 | adding the index of an element of the component to your command as an argument.
58 |
59 | For example, `widget function-that-returns-list 2` will correspond to item 2 of
60 | the result of `function_that_returns_list`.
61 |
62 |
63 | ### Calling a function
64 |
65 | If your command corresponds to a function, you can extend your command by adding
66 | the arguments of this function. Arguments can be specified positionally, or by
67 | name. To specify an argument by name, use flag syntax.
68 |
69 | For example, suppose your `command` corresponds to the function `double`:
70 |
71 | ```python
72 | def double(value=0):
73 | return 2 * value
74 | ```
75 |
76 | Then you can extend your command using named arguments as `command --value 5`,
77 | or using positional arguments as `command 5`. In both cases, the new command
78 | corresponds to the result of the function, in this case the number 10.
79 |
80 | You can force a function that takes a variable number of arguments to be
81 | evaluated by adding a separator (the default separator is the hyphen, "-"). This
82 | will prevent arguments to the right of the separator from being consumed for
83 | calling the function. This is useful if the function has arguments with default
84 | values, or if the function accepts \*varargs, or if the function accepts
85 | \*\*kwargs.
86 |
87 | See also the section on [Changing the Separator](#separator-flag).
88 |
89 |
90 | ### Instantiating a class
91 |
92 | If your command corresponds to a class, you can extend your command by adding
93 | the arguments of the class's `__init__` function. Arguments must be specified
94 | by name, using the flags syntax. See the section on
95 | [calling a function](#calling-a-function) for more details.
96 |
97 | Similarly, when passing arguments to a callable object (an object with a custom
98 | `__call__` function), those arguments must be passed using flags syntax.
99 |
100 | ## Using Flags with Fire CLIs
101 |
102 | Command line arguments to a Fire CLI are normally consumed by Fire, as described
103 | in the [Basic Usage](#basic-usage) section. In order to set Flags, put the flags
104 | after the final standalone `--` argument. (If there is no `--` argument, then no
105 | arguments are used for flags.)
106 |
107 | For example, to set the alsologtostderr flag, you could run the command:
108 | `widget bang --noise=boom -- --alsologtostderr`. The `--noise` argument is
109 | consumed by Fire, but the `--alsologtostderr` argument is treated as a normal
110 | Flag.
111 |
112 | All CLIs built with Python Fire share some flags, as described in the next
113 | sections.
114 |
115 |
116 | ## Python Fire's Flags
117 |
118 | As described in the [Using Flags](#using-flags) section, you must add an
119 | isolated `--` argument in order to have arguments treated as Flags rather than
120 | be consumed by Python Fire. All arguments to a Fire CLI after the final
121 | standalone `--` argument are treated as Flags.
122 |
123 | The following flags are accepted by all Fire CLIs:
124 | [`--interactive`/`-i`](#interactive-flag),
125 | [`--help`/`-h`](#help-flag),
126 | [`--separator`](#separator-flag),
127 | [`--completion`](#completion-flag),
128 | [`--trace`](#trace-flag),
129 | and [`--verbose`/`-v`](#verbose-flag),
130 | as described in the following sections.
131 |
132 | ### `--interactive`: Interactive mode
133 |
134 | Call `widget -- --interactive` or `widget -- -i` to enter interactive mode. This
135 | will put you in an IPython REPL, with the variable `widget` already defined.
136 |
137 | You can then explore the Python object that `widget` corresponds to
138 | interactively using Python.
139 |
140 | Note: if you want fire to start the IPython REPL instead of the regular Python one,
141 | the `ipython` package needs to be installed in your environment.
142 |
143 |
144 | ### `--completion`: Generating a completion script
145 |
146 | Call `widget -- --completion` to generate a completion script for the Fire CLI
147 | `widget`. To save the completion script to your home directory, you could e.g.
148 | run `widget -- --completion > ~/.widget-completion`. You should then source this
149 | file; to get permanent completion, source this file from your `.bashrc` file.
150 |
151 | Call `widget -- --completion fish` to generate a completion script for the Fish
152 | shell. Source this file from your fish.config.
153 |
154 | If the commands available in the Fire CLI change, you'll have to regenerate the
155 | completion script and source it again.
156 |
157 |
158 | ### `--help`: Getting help
159 |
160 | Let say you have a command line tool named `widget` that was made with Fire. How
161 | do you use this Fire CLI?
162 |
163 | The simplest way to get started is to run `widget -- --help`. This will give you
164 | usage information for your CLI. You can always append `-- --help` to any Fire
165 | command in order to get usage information for that command and any subcommands.
166 |
167 | Additionally, help will be displayed if you hit an error using Fire. For
168 | example, if you try to pass too many or too few arguments to a function, then
169 | help will be displayed. Similarly, if you try to access a member that does not
170 | exist, or if you index into a list with too high an index, then help will be
171 | displayed.
172 |
173 | The displayed help shows information about which Python component your command
174 | corresponds to, as well as usage information for how to extend that command.
175 |
176 |
177 | ### `--trace`: Getting a Fire trace
178 |
179 | In order to understand what is happening when you call Python Fire, it can be
180 | useful to request a trace. This is done via the `--trace` flag, e.g.
181 | `widget whack 5 -- --trace`.
182 |
183 | A trace provides step by step information about how the Fire command was
184 | executed. In includes which actions were taken, starting with the initial
185 | component, leading to the final component represented by the command.
186 |
187 | A trace is also shown alongside the help if your Fire command reaches an error.
188 |
189 |
190 | ### `--separator`: Changing the separator
191 |
192 | As described in [Calling a Function](#calling-a-function), you can use a
193 | separator argument when writing a command that corresponds to calling a
194 | function. The separator will cause the function to be evaluated or the class to
195 | be instantiated using only the arguments left of the separator. Arguments right
196 | of the separator will then be applied to the result of the function call or to
197 | the instantiated object.
198 |
199 | The default separator is `-`.
200 |
201 | If you want to supply the string "-" as an argument, then you will have to
202 | change the separator. You can choose a new separator by supplying the
203 | `--separator` flag to Fire.
204 |
205 | Here's an example to demonstrate separator usage. Let's say you have a function
206 | that takes a variable number of args, and you want to call that function, and
207 | then upper case the result. Here's how to do it:
208 |
209 | ```python
210 | # Here's the Python function
211 | def display(arg1, arg2='!'):
212 | return arg1 + arg2
213 | ```
214 |
215 | ```bash
216 | # Here's what you can do from Bash (Note: the default separator is the hyphen -)
217 | display hello # hello!
218 | display hello upper # helloupper
219 | display hello - upper # HELLO!
220 | display - SEP upper -- --separator SEP # -!
221 | ```
222 | Notice how in the third and fourth lines, the separator caused the display
223 | function to be called with the default value for arg2. In the fourth example,
224 | we change the separator to the string "SEP" so that we can pass '-' as an
225 | argument.
226 |
227 | ### `--verbose`: Verbose usage
228 |
229 | Adding the `-v` or `--verbose` flag turns on verbose mode. This will eg
230 | reveal private members in the usage string. Often these members will not
231 | actually be usable from the command line tool. As such, verbose mode should be
232 | considered a debugging tool, but not fully supported yet.
233 |
--------------------------------------------------------------------------------
/examples/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/python-fire/dba7e1d0da014e555d174225fdf5ab4c4574b18b/examples/__init__.py
--------------------------------------------------------------------------------
/examples/cipher/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/python-fire/dba7e1d0da014e555d174225fdf5ab4c4574b18b/examples/cipher/__init__.py
--------------------------------------------------------------------------------
/examples/cipher/cipher.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """The Caesar Shift Cipher example Fire CLI.
16 |
17 | This module demonstrates the use of Fire without specifying a target component.
18 | Notice how the call to Fire() in the main method doesn't indicate a component.
19 | So, all local and global variables (including all functions defined in the
20 | module) are made available as part of the Fire CLI.
21 |
22 | Example usage:
23 | cipher rot13 'Hello world!' # Uryyb jbeyq!
24 | cipher rot13 'Uryyb jbeyq!' # Hello world!
25 | cipher caesar-encode 1 'Hello world!' # Ifmmp xpsme!
26 | cipher caesar-decode 1 'Ifmmp xpsme!' # Hello world!
27 | """
28 |
29 | import fire
30 |
31 |
32 | def caesar_encode(n=0, text=''):
33 | return ''.join(
34 | _caesar_shift_char(n, char)
35 | for char in text
36 | )
37 |
38 |
39 | def caesar_decode(n=0, text=''):
40 | return caesar_encode(-n, text)
41 |
42 |
43 | def rot13(text):
44 | return caesar_encode(13, text)
45 |
46 |
47 | def _caesar_shift_char(n=0, char=' '):
48 | if not char.isalpha():
49 | return char
50 | if char.isupper():
51 | return chr((ord(char) - ord('A') + n) % 26 + ord('A'))
52 | return chr((ord(char) - ord('a') + n) % 26 + ord('a'))
53 |
54 |
55 | def main():
56 | fire.Fire(name='cipher')
57 |
58 | if __name__ == '__main__':
59 | main()
60 |
--------------------------------------------------------------------------------
/examples/cipher/cipher_test.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Tests for the cipher module."""
16 |
17 | from fire import testutils
18 |
19 | from examples.cipher import cipher
20 |
21 |
22 | class CipherTest(testutils.BaseTestCase):
23 |
24 | def testCipher(self):
25 | self.assertEqual(cipher.rot13('Hello world!'), 'Uryyb jbeyq!')
26 | self.assertEqual(cipher.caesar_encode(13, 'Hello world!'), 'Uryyb jbeyq!')
27 | self.assertEqual(cipher.caesar_decode(13, 'Uryyb jbeyq!'), 'Hello world!')
28 |
29 | self.assertEqual(cipher.caesar_encode(1, 'Hello world!'), 'Ifmmp xpsme!')
30 | self.assertEqual(cipher.caesar_decode(1, 'Ifmmp xpsme!'), 'Hello world!')
31 |
32 |
33 | if __name__ == '__main__':
34 | testutils.main()
35 |
--------------------------------------------------------------------------------
/examples/diff/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/python-fire/dba7e1d0da014e555d174225fdf5ab4c4574b18b/examples/diff/__init__.py
--------------------------------------------------------------------------------
/examples/diff/diff.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | r"""A command line tool for diffing files.
16 |
17 | The Python 2.7 documentation demonstrates how to make a command line interface
18 | for the difflib library using optparse:
19 | https://docs.python.org/2/library/difflib.html#a-command-line-interface-to-difflib
20 |
21 | This file demonstrates how to create a command line interface providing the same
22 | functionality using Python Fire.
23 |
24 | Usage:
25 |
26 | diff FROMFILE TOFILE COMMAND [LINES]
27 |
28 | Arguments can be passed positionally or via the Flag syntax.
29 | Using positional arguments, the usage is:
30 |
31 | diff FROMFILE TOFILE
32 | diff FROMFILE TOFILE context-diff [LINES]
33 | diff FROMFILE TOFILE unified-diff [LINES]
34 | diff FROMFILE TOFILE ndiff
35 | diff FROMFILE TOFILE make-file [CONTEXT] [LINES]
36 |
37 | Using the Flag syntax, the usage is:
38 |
39 | diff --fromfile=FROMFILE --tofile=TOFILE
40 | diff --fromfile=FROMFILE --tofile=TOFILE context-diff [--lines=LINES]
41 | diff --fromfile=FROMFILE --tofile=TOFILE unified-diff [--lines=LINES]
42 | diff --fromfile=FROMFILE --tofile=TOFILE ndiff
43 | diff --fromfile=FROMFILE --tofile=TOFILE make-file \
44 | [--context=CONTEXT] [--lines LINES]
45 |
46 | As with any Fire CLI, you can append '--' followed by any Flags to any command.
47 |
48 | The Flags available for all Fire CLIs are:
49 | --help
50 | --interactive
51 | --trace
52 | --separator=SEPARATOR
53 | --completion
54 | --verbose
55 | """
56 |
57 | import difflib
58 | import os
59 | import time
60 |
61 | import fire
62 |
63 |
64 | class DiffLibWrapper(object):
65 | """Provides a simple interface to the difflib module.
66 |
67 | The purpose of this simple interface is to offer a limited subset of the
68 | difflib functionality as a command line interface.
69 | """
70 |
71 | def __init__(self, fromfile, tofile):
72 | self._fromfile = fromfile
73 | self._tofile = tofile
74 |
75 | self.fromdate = time.ctime(os.stat(fromfile).st_mtime)
76 | self.todate = time.ctime(os.stat(tofile).st_mtime)
77 | with open(fromfile) as f:
78 | self.fromlines = f.readlines()
79 | with open(tofile) as f:
80 | self.tolines = f.readlines()
81 |
82 | def unified_diff(self, lines=3):
83 | return difflib.unified_diff(
84 | self.fromlines, self.tolines, self._fromfile,
85 | self._tofile, self.fromdate, self.todate, n=lines)
86 |
87 | def ndiff(self):
88 | return difflib.ndiff(self.fromlines, self.tolines)
89 |
90 | def make_file(self, context=False, lines=3):
91 | return difflib.HtmlDiff().make_file(
92 | self.fromlines, self.tolines, self._fromfile, self._tofile,
93 | context=context, numlines=lines)
94 |
95 | def context_diff(self, lines=3):
96 | return difflib.context_diff(
97 | self.fromlines, self.tolines, self._fromfile,
98 | self._tofile, self.fromdate, self.todate, n=lines)
99 |
100 |
101 | def main():
102 | fire.Fire(DiffLibWrapper, name='diff')
103 |
104 | if __name__ == '__main__':
105 | main()
106 |
--------------------------------------------------------------------------------
/examples/diff/diff_test.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Tests for the diff and difffull modules."""
16 |
17 | import tempfile
18 |
19 | from fire import testutils
20 |
21 | from examples.diff import diff
22 | from examples.diff import difffull
23 |
24 |
25 | class DiffTest(testutils.BaseTestCase):
26 | """The purpose of these tests is to ensure the difflib wrappers works.
27 |
28 | It is not the goal of these tests to exhaustively test difflib functionality.
29 | """
30 |
31 | def setUp(self):
32 | self.file1 = file1 = tempfile.NamedTemporaryFile()
33 | self.file2 = file2 = tempfile.NamedTemporaryFile()
34 |
35 | file1.write(b'test\ntest1\n')
36 | file2.write(b'test\ntest2\nextraline\n')
37 |
38 | file1.flush()
39 | file2.flush()
40 |
41 | self.diff = diff.DiffLibWrapper(file1.name, file2.name)
42 |
43 | def testSetUp(self):
44 | self.assertEqual(self.diff.fromlines, ['test\n', 'test1\n'])
45 | self.assertEqual(self.diff.tolines, ['test\n', 'test2\n', 'extraline\n'])
46 |
47 | def testUnifiedDiff(self):
48 | results = list(self.diff.unified_diff())
49 | self.assertTrue(results[0].startswith('--- ' + self.file1.name))
50 | self.assertTrue(results[1].startswith('+++ ' + self.file2.name))
51 | self.assertEqual(
52 | results[2:],
53 | [
54 | '@@ -1,2 +1,3 @@\n',
55 | ' test\n',
56 | '-test1\n',
57 | '+test2\n',
58 | '+extraline\n',
59 | ]
60 | )
61 |
62 | def testContextDiff(self):
63 | expected_lines = [
64 | '***************\n',
65 | '*** 1,2 ****\n',
66 | ' test\n',
67 | '! test1\n',
68 | '--- 1,3 ----\n',
69 | ' test\n',
70 | '! test2\n',
71 | '! extraline\n']
72 | results = list(self.diff.context_diff())
73 | self.assertEqual(results[2:], expected_lines)
74 |
75 | def testNDiff(self):
76 | expected_lines = [
77 | ' test\n',
78 | '- test1\n',
79 | '? ^\n',
80 | '+ test2\n',
81 | '? ^\n',
82 | '+ extraline\n']
83 | results = list(self.diff.ndiff())
84 | self.assertEqual(results, expected_lines)
85 |
86 | def testMakeDiff(self):
87 | self.assertTrue(''.join(self.diff.make_file()).startswith('\n for function keys, otherwise a
173 | character.
174 | """
175 | ansi_to_key = {
176 | 'A': '',
177 | 'B': '',
178 | 'D': '',
179 | 'C': '',
180 | '5': '',
181 | '6': '',
182 | 'H': '',
183 | 'F': '',
184 | 'M': '',
185 | 'S': '',
186 | 'T': '',
187 | }
188 |
189 | # Flush pending output. sys.stdin.read() would do this, but it's explicitly
190 | # bypassed in _GetKeyChar().
191 | sys.stdout.flush()
192 |
193 | fd = sys.stdin.fileno()
194 |
195 | def _GetKeyChar():
196 | return encoding.Decode(os.read(fd, 1))
197 |
198 | old_settings = termios.tcgetattr(fd)
199 | try:
200 | tty.setraw(fd)
201 | c = _GetKeyChar()
202 | if c == _ANSI_CSI:
203 | c = _GetKeyChar()
204 | while True:
205 | if c == _ANSI_CSI:
206 | return c
207 | if c.isalpha():
208 | break
209 | prev_c = c
210 | c = _GetKeyChar()
211 | if c == '~':
212 | c = prev_c
213 | break
214 | return ansi_to_key.get(c, '')
215 | except: # pylint:disable=bare-except
216 | c = None
217 | finally:
218 | termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
219 | return None if c in (_CONTROL_D, _CONTROL_Z) else c
220 |
221 | return _GetRawKeyPosix
222 |
223 |
224 | def _GetRawKeyFunctionWindows():
225 | """_GetRawKeyFunction helper using Windows APIs."""
226 | # pylint: disable=g-import-not-at-top
227 | import msvcrt
228 |
229 | def _GetRawKeyWindows():
230 | """Reads and returns one keypress from stdin, no echo, using Windows APIs.
231 |
232 | Returns:
233 | The key name, None for EOF, <*> for function keys, otherwise a
234 | character.
235 | """
236 | windows_to_key = {
237 | 'H': '',
238 | 'P': '',
239 | 'K': '',
240 | 'M': '',
241 | 'I': '',
242 | 'Q': '',
243 | 'G': '',
244 | 'O': '',
245 | }
246 |
247 | # Flush pending output. sys.stdin.read() would do this it's explicitly
248 | # bypassed in _GetKeyChar().
249 | sys.stdout.flush()
250 |
251 | def _GetKeyChar():
252 | return encoding.Decode(msvcrt.getch())
253 |
254 | c = _GetKeyChar()
255 | # Special function key is a two character sequence; return the second char.
256 | if c in (_WINDOWS_CSI_1, _WINDOWS_CSI_2):
257 | return windows_to_key.get(_GetKeyChar(), '')
258 | return None if c in (_CONTROL_D, _CONTROL_Z) else c
259 |
260 | return _GetRawKeyWindows
261 |
--------------------------------------------------------------------------------
/fire/console/console_io.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*- #
2 | # Copyright 2013 Google LLC. All Rights Reserved.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | """General console printing utilities used by the Cloud SDK."""
17 |
18 | import os
19 | import signal
20 | import subprocess
21 | import sys
22 |
23 | from fire.console import console_attr
24 | from fire.console import console_pager
25 | from fire.console import encoding
26 | from fire.console import files
27 |
28 |
29 | def IsInteractive(output=False, error=False, heuristic=False):
30 | """Determines if the current terminal session is interactive.
31 |
32 | sys.stdin must be a terminal input stream.
33 |
34 | Args:
35 | output: If True then sys.stdout must also be a terminal output stream.
36 | error: If True then sys.stderr must also be a terminal output stream.
37 | heuristic: If True then we also do some additional heuristics to check if
38 | we are in an interactive context. Checking home path for example.
39 |
40 | Returns:
41 | True if the current terminal session is interactive.
42 | """
43 | if not sys.stdin.isatty():
44 | return False
45 | if output and not sys.stdout.isatty():
46 | return False
47 | if error and not sys.stderr.isatty():
48 | return False
49 |
50 | if heuristic:
51 | # Check the home path. Most startup scripts for example are executed by
52 | # users that don't have a home path set. Home is OS dependent though, so
53 | # check everything.
54 | # *NIX OS usually sets the HOME env variable. It is usually '/home/user',
55 | # but can also be '/root'. If it's just '/' we are most likely in an init
56 | # script.
57 | # Windows usually sets HOMEDRIVE and HOMEPATH. If they don't exist we are
58 | # probably being run from a task scheduler context. HOMEPATH can be '\'
59 | # when a user has a network mapped home directory.
60 | # Cygwin has it all! Both Windows and Linux. Checking both is perfect.
61 | home = os.getenv('HOME')
62 | homepath = os.getenv('HOMEPATH')
63 | if not homepath and (not home or home == '/'):
64 | return False
65 | return True
66 |
67 |
68 | def More(contents, out, prompt=None, check_pager=True):
69 | """Run a user specified pager or fall back to the internal pager.
70 |
71 | Args:
72 | contents: The entire contents of the text lines to page.
73 | out: The output stream.
74 | prompt: The page break prompt.
75 | check_pager: Checks the PAGER env var and uses it if True.
76 | """
77 | if not IsInteractive(output=True):
78 | out.write(contents)
79 | return
80 | if check_pager:
81 | pager = encoding.GetEncodedValue(os.environ, 'PAGER', None)
82 | if pager == '-':
83 | # Use the fallback Pager.
84 | pager = None
85 | elif not pager:
86 | # Search for a pager that handles ANSI escapes.
87 | for command in ('less', 'pager'):
88 | if files.FindExecutableOnPath(command):
89 | pager = command
90 | break
91 | if pager:
92 | # If the pager is less(1) then instruct it to display raw ANSI escape
93 | # sequences to enable colors and font embellishments.
94 | less_orig = encoding.GetEncodedValue(os.environ, 'LESS', None)
95 | less = '-R' + (less_orig or '')
96 | encoding.SetEncodedValue(os.environ, 'LESS', less)
97 | # Ignore SIGINT while the pager is running.
98 | # We don't want to terminate the parent while the child is still alive.
99 | signal.signal(signal.SIGINT, signal.SIG_IGN)
100 | p = subprocess.Popen(pager, stdin=subprocess.PIPE, shell=True)
101 | enc = console_attr.GetConsoleAttr().GetEncoding()
102 | p.communicate(input=contents.encode(enc))
103 | p.wait()
104 | # Start using default signal handling for SIGINT again.
105 | signal.signal(signal.SIGINT, signal.SIG_DFL)
106 | if less_orig is None:
107 | encoding.SetEncodedValue(os.environ, 'LESS', None)
108 | return
109 | # Fall back to the internal pager.
110 | console_pager.Pager(contents, out, prompt).Run()
111 |
--------------------------------------------------------------------------------
/fire/console/console_pager.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*- #
2 | # Copyright 2015 Google LLC. All Rights Reserved.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | """Simple console pager."""
17 |
18 | from __future__ import absolute_import
19 | from __future__ import division
20 | from __future__ import unicode_literals
21 |
22 | import re
23 | import sys
24 |
25 | from fire.console import console_attr
26 |
27 |
28 | class Pager(object):
29 | """A simple console text pager.
30 |
31 | This pager requires the entire contents to be available. The contents are
32 | written one page of lines at a time. The prompt is written after each page of
33 | lines. A one character response is expected. See HELP_TEXT below for more
34 | info.
35 |
36 | The contents are written as is. For example, ANSI control codes will be in
37 | effect. This is different from pagers like more(1) which is ANSI control code
38 | agnostic and miscalculates line lengths, and less(1) which displays control
39 | character names by default.
40 |
41 | Attributes:
42 | _attr: The current ConsoleAttr handle.
43 | _clear: A string that clears the prompt when written to _out.
44 | _contents: The entire contents of the text lines to page.
45 | _height: The terminal height in characters.
46 | _out: The output stream, log.out (effectively) if None.
47 | _prompt: The page break prompt.
48 | _search_direction: The search direction command, n:forward, N:reverse.
49 | _search_pattern: The current forward/reverse search compiled RE.
50 | _width: The termonal width in characters.
51 | """
52 |
53 | HELP_TEXT = """
54 | Simple pager commands:
55 |
56 | b, ^B, ,
57 | Back one page.
58 | f, ^F, , ,
59 | Forward one page. Does not quit if there are no more lines.
60 | g,
61 | Back to the first page.
62 | g
63 | Go to lines from the top.
64 | G,
65 | Forward to the last page.
66 | G
67 | Go to lines from the bottom.
68 | h
69 | Print pager command help.
70 | j, +,
71 | Forward one line.
72 | k, -,
73 | Back one line.
74 | /pattern
75 | Forward search for pattern.
76 | ?pattern
77 | Backward search for pattern.
78 | n
79 | Repeat current search.
80 | N
81 | Repeat current search in the opposite direction.
82 | q, Q, ^C, ^D, ^Z
83 | Quit return to the caller.
84 | any other character
85 | Prompt again.
86 |
87 | Hit any key to continue:"""
88 |
89 | PREV_POS_NXT_REPRINT = -1, -1
90 |
91 | def __init__(self, contents, out=None, prompt=None):
92 | """Constructor.
93 |
94 | Args:
95 | contents: The entire contents of the text lines to page.
96 | out: The output stream, log.out (effectively) if None.
97 | prompt: The page break prompt, a default prompt is used if None..
98 | """
99 | self._contents = contents
100 | self._out = out or sys.stdout
101 | self._search_pattern = None
102 | self._search_direction = None
103 |
104 | # prev_pos, prev_next values to force reprint
105 | self.prev_pos, self.prev_nxt = self.PREV_POS_NXT_REPRINT
106 | # Initialize the console attributes.
107 | self._attr = console_attr.GetConsoleAttr()
108 | self._width, self._height = self._attr.GetTermSize()
109 |
110 | # Initialize the prompt and the prompt clear string.
111 | if not prompt:
112 | prompt = '{bold}--({{percent}}%)--{normal}'.format(
113 | bold=self._attr.GetFontCode(bold=True),
114 | normal=self._attr.GetFontCode())
115 | self._clear = '\r{0}\r'.format(' ' * (self._attr.DisplayWidth(prompt) - 6))
116 | self._prompt = prompt
117 |
118 | # Initialize a list of lines with long lines split into separate display
119 | # lines.
120 | self._lines = []
121 | for line in contents.splitlines():
122 | self._lines += self._attr.SplitLine(line, self._width)
123 |
124 | def _Write(self, s):
125 | """Mockable helper that writes s to self._out."""
126 | self._out.write(s)
127 |
128 | def _GetSearchCommand(self, c):
129 | """Consumes a search command and returns the equivalent pager command.
130 |
131 | The search pattern is an RE that is pre-compiled and cached for subsequent
132 | /, ?, n, or N commands.
133 |
134 | Args:
135 | c: The search command char.
136 |
137 | Returns:
138 | The pager command char.
139 | """
140 | self._Write(c)
141 | buf = ''
142 | while True:
143 | p = self._attr.GetRawKey()
144 | if p in (None, '\n', '\r') or len(p) != 1:
145 | break
146 | self._Write(p)
147 | buf += p
148 | self._Write('\r' + ' ' * len(buf) + '\r')
149 | if buf:
150 | try:
151 | self._search_pattern = re.compile(buf)
152 | except re.error:
153 | # Silently ignore pattern errors.
154 | self._search_pattern = None
155 | return ''
156 | self._search_direction = 'n' if c == '/' else 'N'
157 | return 'n'
158 |
159 | def _Help(self):
160 | """Print command help and wait for any character to continue."""
161 | clear = self._height - (len(self.HELP_TEXT) -
162 | len(self.HELP_TEXT.replace('\n', '')))
163 | if clear > 0:
164 | self._Write('\n' * clear)
165 | self._Write(self.HELP_TEXT)
166 | self._attr.GetRawKey()
167 | self._Write('\n')
168 |
169 | def Run(self):
170 | """Run the pager."""
171 | # No paging if the contents are small enough.
172 | if len(self._lines) <= self._height:
173 | self._Write(self._contents)
174 | return
175 |
176 | # We will not always reset previous values.
177 | reset_prev_values = True
178 | # Save room for the prompt at the bottom of the page.
179 | self._height -= 1
180 |
181 | # Loop over all the pages.
182 | pos = 0
183 | while pos < len(self._lines):
184 | # Write a page of lines.
185 | nxt = pos + self._height
186 | if nxt > len(self._lines):
187 | nxt = len(self._lines)
188 | pos = nxt - self._height
189 | # Checks if the starting position is in between the current printed lines
190 | # so we don't need to reprint all the lines.
191 | if self.prev_pos < pos < self.prev_nxt:
192 | # we start where the previous page ended.
193 | self._Write('\n'.join(self._lines[self.prev_nxt:nxt]) + '\n')
194 | elif pos != self.prev_pos and nxt != self.prev_nxt:
195 | self._Write('\n'.join(self._lines[pos:nxt]) + '\n')
196 |
197 | # Handle the prompt response.
198 | percent = self._prompt.format(percent=100 * nxt // len(self._lines))
199 | digits = ''
200 | while True:
201 | # We want to reset prev values if we just exited out of the while loop
202 | if reset_prev_values:
203 | self.prev_pos, self.prev_nxt = pos, nxt
204 | reset_prev_values = False
205 | self._Write(percent)
206 | c = self._attr.GetRawKey()
207 | self._Write(self._clear)
208 |
209 | # Parse the command.
210 | if c in (None, # EOF.
211 | 'q', # Quit.
212 | 'Q', # Quit.
213 | '\x03', # ^C (unix & windows terminal interrupt)
214 | '\x1b', # ESC.
215 | ):
216 | # Quit.
217 | return
218 | elif c in ('/', '?'):
219 | c = self._GetSearchCommand(c)
220 | elif c.isdigit():
221 | # Collect digits for operation count.
222 | digits += c
223 | continue
224 |
225 | # Set the optional command count.
226 | if digits:
227 | count = int(digits)
228 | digits = ''
229 | else:
230 | count = 0
231 |
232 | # Finally commit to command c.
233 | if c in ('', '', 'b', '\x02'):
234 | # Previous page.
235 | nxt = pos - self._height
236 | if nxt < 0:
237 | nxt = 0
238 | elif c in ('', '', 'f', '\x06', ' '):
239 | # Next page.
240 | if nxt >= len(self._lines):
241 | continue
242 | nxt = pos + self._height
243 | if nxt >= len(self._lines):
244 | nxt = pos
245 | elif c in ('', 'g'):
246 | # First page.
247 | nxt = count - 1
248 | if nxt > len(self._lines) - self._height:
249 | nxt = len(self._lines) - self._height
250 | if nxt < 0:
251 | nxt = 0
252 | elif c in ('', 'G'):
253 | # Last page.
254 | nxt = len(self._lines) - count
255 | if nxt > len(self._lines) - self._height:
256 | nxt = len(self._lines) - self._height
257 | if nxt < 0:
258 | nxt = 0
259 | elif c == 'h':
260 | self._Help()
261 | # Special case when we want to reprint the previous display.
262 | self.prev_pos, self.prev_nxt = self.PREV_POS_NXT_REPRINT
263 | nxt = pos
264 | break
265 | elif c in ('', 'j', '+', '\n', '\r'):
266 | # Next line.
267 | if nxt >= len(self._lines):
268 | continue
269 | nxt = pos + 1
270 | if nxt >= len(self._lines):
271 | nxt = pos
272 | elif c in ('', 'k', '-'):
273 | # Previous line.
274 | nxt = pos - 1
275 | if nxt < 0:
276 | nxt = 0
277 | elif c in ('n', 'N'):
278 | # Next pattern match search.
279 | if not self._search_pattern:
280 | continue
281 | nxt = pos
282 | i = pos
283 | direction = 1 if c == self._search_direction else -1
284 | while True:
285 | i += direction
286 | if i < 0 or i >= len(self._lines):
287 | break
288 | if self._search_pattern.search(self._lines[i]):
289 | nxt = i
290 | break
291 | else:
292 | # Silently ignore everything else.
293 | continue
294 | if nxt != pos:
295 | # We will exit the while loop because position changed so we can reset
296 | # prev values.
297 | reset_prev_values = True
298 | break
299 | pos = nxt
300 |
--------------------------------------------------------------------------------
/fire/console/encoding.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*- #
2 |
3 | # Copyright 2015 Google LLC. All Rights Reserved.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | """A module for dealing with unknown string and environment encodings."""
18 |
19 | from __future__ import absolute_import
20 | from __future__ import division
21 | from __future__ import unicode_literals
22 |
23 | import sys
24 |
25 |
26 | def Encode(string, encoding=None):
27 | """Encode the text string to a byte string.
28 |
29 | Args:
30 | string: str, The text string to encode.
31 | encoding: The suggested encoding if known.
32 |
33 | Returns:
34 | str, The binary string.
35 | """
36 | del encoding # Unused.
37 | return string
38 |
39 |
40 | def Decode(data, encoding=None):
41 | """Returns string with non-ascii characters decoded to UNICODE.
42 |
43 | UTF-8, the suggested encoding, and the usual suspects will be attempted in
44 | order.
45 |
46 | Args:
47 | data: A string or object that has str() and unicode() methods that may
48 | contain an encoding incompatible with the standard output encoding.
49 | encoding: The suggested encoding if known.
50 |
51 | Returns:
52 | A text string representing the decoded byte string.
53 | """
54 | if data is None:
55 | return None
56 |
57 | # First we are going to get the data object to be a text string.
58 | if isinstance(data, str) or isinstance(data, bytes):
59 | string = data
60 | else:
61 | # Some non-string type of object.
62 | string = str(data)
63 |
64 | if isinstance(string, str):
65 | # Our work is done here.
66 | return string
67 |
68 | try:
69 | # Just return the string if its pure ASCII.
70 | return string.decode('ascii') # pytype: disable=attribute-error
71 | except UnicodeError:
72 | # The string is not ASCII encoded.
73 | pass
74 |
75 | # Try the suggested encoding if specified.
76 | if encoding:
77 | try:
78 | return string.decode(encoding) # pytype: disable=attribute-error
79 | except UnicodeError:
80 | # Bad suggestion.
81 | pass
82 |
83 | # Try UTF-8 because the other encodings could be extended ASCII. It would
84 | # be exceptional if a valid extended ascii encoding with extended chars
85 | # were also a valid UITF-8 encoding.
86 | try:
87 | return string.decode('utf8') # pytype: disable=attribute-error
88 | except UnicodeError:
89 | # Not a UTF-8 encoding.
90 | pass
91 |
92 | # Try the filesystem encoding.
93 | try:
94 | return string.decode(sys.getfilesystemencoding()) # pytype: disable=attribute-error
95 | except UnicodeError:
96 | # string is not encoded for filesystem paths.
97 | pass
98 |
99 | # Try the system default encoding.
100 | try:
101 | return string.decode(sys.getdefaultencoding()) # pytype: disable=attribute-error
102 | except UnicodeError:
103 | # string is not encoded using the default encoding.
104 | pass
105 |
106 | # We don't know the string encoding.
107 | # This works around a Python str.encode() "feature" that throws
108 | # an ASCII *decode* exception on str strings that contain 8th bit set
109 | # bytes. For example, this sequence throws an exception:
110 | # string = '\xdc' # iso-8859-1 'Ü'
111 | # string = string.encode('ascii', 'backslashreplace')
112 | # even though 'backslashreplace' is documented to handle encoding
113 | # errors. We work around the problem by first decoding the str string
114 | # from an 8-bit encoding to unicode, selecting any 8-bit encoding that
115 | # uses all 256 bytes (such as ISO-8559-1):
116 | # string = string.decode('iso-8859-1')
117 | # Using this produces a sequence that works:
118 | # string = '\xdc'
119 | # string = string.decode('iso-8859-1')
120 | # string = string.encode('ascii', 'backslashreplace')
121 | return string.decode('iso-8859-1') # pytype: disable=attribute-error
122 |
123 |
124 | def GetEncodedValue(env, name, default=None):
125 | """Returns the decoded value of the env var name.
126 |
127 | Args:
128 | env: {str: str}, The env dict.
129 | name: str, The env var name.
130 | default: The value to return if name is not in env.
131 |
132 | Returns:
133 | The decoded value of the env var name.
134 | """
135 | name = Encode(name)
136 | value = env.get(name)
137 | if value is None:
138 | return default
139 | # In Python 3, the environment sets and gets accept and return text strings
140 | # only, and it handles the encoding itself so this is not necessary.
141 | return Decode(value)
142 |
143 |
144 | def SetEncodedValue(env, name, value, encoding=None):
145 | """Sets the value of name in env to an encoded value.
146 |
147 | Args:
148 | env: {str: str}, The env dict.
149 | name: str, The env var name.
150 | value: str or unicode, The value for name. If None then name is removed from
151 | env.
152 | encoding: str, The encoding to use or None to try to infer it.
153 | """
154 | # Python 2 *and* 3 unicode support falls apart at filesystem/argv/environment
155 | # boundaries. The encoding used for filesystem paths and environment variable
156 | # names/values is under user control on most systems. With one of those values
157 | # in hand there is no way to tell exactly how the value was encoded. We get
158 | # some reasonable hints from sys.getfilesystemencoding() or
159 | # sys.getdefaultencoding() and use them to encode values that the receiving
160 | # process will have a chance at decoding. Leaving the values as unicode
161 | # strings will cause os module Unicode exceptions. What good is a language
162 | # unicode model when the module support could care less?
163 | name = Encode(name, encoding=encoding)
164 | if value is None:
165 | env.pop(name, None)
166 | return
167 | env[name] = Encode(value, encoding=encoding)
168 |
169 |
170 | def EncodeEnv(env, encoding=None):
171 | """Encodes all the key value pairs in env in preparation for subprocess.
172 |
173 | Args:
174 | env: {str: str}, The environment you are going to pass to subprocess.
175 | encoding: str, The encoding to use or None to use the default.
176 |
177 | Returns:
178 | {bytes: bytes}, The environment to pass to subprocess.
179 | """
180 | encoding = encoding or _GetEncoding()
181 | return {
182 | Encode(k, encoding=encoding): Encode(v, encoding=encoding)
183 | for k, v in env.items()
184 | }
185 |
186 |
187 | def _GetEncoding():
188 | """Gets the default encoding to use."""
189 | return sys.getfilesystemencoding() or sys.getdefaultencoding()
190 |
--------------------------------------------------------------------------------
/fire/console/files.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*- #
2 | # Copyright 2013 Google LLC. All Rights Reserved.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | """Some general file utilities used that can be used by the Cloud SDK."""
17 |
18 | from __future__ import absolute_import
19 | from __future__ import division
20 | from __future__ import unicode_literals
21 |
22 | import os
23 |
24 | from fire.console import encoding as encoding_util
25 | from fire.console import platforms
26 |
27 |
28 | def _GetSystemPath():
29 | """Returns properly encoded system PATH variable string."""
30 | return encoding_util.GetEncodedValue(os.environ, 'PATH')
31 |
32 |
33 | def _FindExecutableOnPath(executable, path, pathext):
34 | """Internal function to a find an executable.
35 |
36 | Args:
37 | executable: The name of the executable to find.
38 | path: A list of directories to search separated by 'os.pathsep'.
39 | pathext: An iterable of file name extensions to use.
40 |
41 | Returns:
42 | str, the path to a file on `path` with name `executable` + `p` for
43 | `p` in `pathext`.
44 |
45 | Raises:
46 | ValueError: invalid input.
47 | """
48 |
49 | if isinstance(pathext, str):
50 | raise ValueError('_FindExecutableOnPath(..., pathext=\'{0}\') failed '
51 | 'because pathext must be an iterable of strings, but got '
52 | 'a string.'.format(pathext))
53 |
54 | # Prioritize preferred extension over earlier in path.
55 | for ext in pathext:
56 | for directory in path.split(os.pathsep):
57 | # Windows can have paths quoted.
58 | directory = directory.strip('"')
59 | full = os.path.normpath(os.path.join(directory, executable) + ext)
60 | # On Windows os.access(full, os.X_OK) is always True.
61 | if os.path.isfile(full) and os.access(full, os.X_OK):
62 | return full
63 | return None
64 |
65 |
66 | def _PlatformExecutableExtensions(platform):
67 | if platform == platforms.OperatingSystem.WINDOWS:
68 | return ('.exe', '.cmd', '.bat', '.com', '.ps1')
69 | else:
70 | return ('', '.sh')
71 |
72 |
73 | def FindExecutableOnPath(executable, path=None, pathext=None,
74 | allow_extensions=False):
75 | """Searches for `executable` in the directories listed in `path` or $PATH.
76 |
77 | Executable must not contain a directory or an extension.
78 |
79 | Args:
80 | executable: The name of the executable to find.
81 | path: A list of directories to search separated by 'os.pathsep'. If None
82 | then the system PATH is used.
83 | pathext: An iterable of file name extensions to use. If None then
84 | platform specific extensions are used.
85 | allow_extensions: A boolean flag indicating whether extensions in the
86 | executable are allowed.
87 |
88 | Returns:
89 | The path of 'executable' (possibly with a platform-specific extension) if
90 | found and executable, None if not found.
91 |
92 | Raises:
93 | ValueError: if executable has a path or an extension, and extensions are
94 | not allowed, or if there's an internal error.
95 | """
96 |
97 | if not allow_extensions and os.path.splitext(executable)[1]:
98 | raise ValueError('FindExecutableOnPath({0},...) failed because first '
99 | 'argument must not have an extension.'.format(executable))
100 |
101 | if os.path.dirname(executable):
102 | raise ValueError('FindExecutableOnPath({0},...) failed because first '
103 | 'argument must not have a path.'.format(executable))
104 |
105 | if path is None:
106 | effective_path = _GetSystemPath()
107 | else:
108 | effective_path = path
109 | effective_pathext = (pathext if pathext is not None
110 | else _PlatformExecutableExtensions(
111 | platforms.OperatingSystem.Current()))
112 |
113 | return _FindExecutableOnPath(executable, effective_path,
114 | effective_pathext)
115 |
--------------------------------------------------------------------------------
/fire/console/text.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*- #
2 | # Copyright 2018 Google LLC. All Rights Reserved.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | """Semantic text objects that are used for styled outputting."""
16 |
17 | from __future__ import absolute_import
18 | from __future__ import division
19 | from __future__ import unicode_literals
20 |
21 | import enum
22 |
23 |
24 | class TextAttributes(object):
25 | """Attributes to use to style text with."""
26 |
27 | def __init__(self, format_str=None, color=None, attrs=None):
28 | """Defines a set of attributes for a piece of text.
29 |
30 | Args:
31 | format_str: (str), string that will be used to format the text
32 | with. For example '[{}]', to enclose text in brackets.
33 | color: (Colors), the color the text should be formatted with.
34 | attrs: (Attrs), the attributes to apply to text.
35 | """
36 | self._format_str = format_str
37 | self._color = color
38 | self._attrs = attrs or []
39 |
40 | @property
41 | def format_str(self):
42 | return self._format_str
43 |
44 | @property
45 | def color(self):
46 | return self._color
47 |
48 | @property
49 | def attrs(self):
50 | return self._attrs
51 |
52 |
53 | class TypedText(object):
54 | """Text with a semantic type that will be used for styling."""
55 |
56 | def __init__(self, texts, text_type=None):
57 | """String of text and a corresponding type to use to style that text.
58 |
59 | Args:
60 | texts: (list[str]), list of strs or TypedText objects
61 | that should be styled using text_type.
62 | text_type: (TextTypes), the semantic type of the text that
63 | will be used to style text.
64 | """
65 | self.texts = texts
66 | self.text_type = text_type
67 |
68 | def __len__(self):
69 | length = 0
70 | for text in self.texts:
71 | length += len(text)
72 | return length
73 |
74 | def __add__(self, other):
75 | texts = [self, other]
76 | return TypedText(texts)
77 |
78 | def __radd__(self, other):
79 | texts = [other, self]
80 | return TypedText(texts)
81 |
82 |
83 | class _TextTypes(enum.Enum):
84 | """Text types base class that defines base functionality."""
85 |
86 | def __call__(self, *args):
87 | """Returns a TypedText object using this style."""
88 | return TypedText(list(args), self)
89 |
90 |
91 | # TODO: Add more types.
92 | class TextTypes(_TextTypes):
93 | """Defines text types that can be used for styling text."""
94 | RESOURCE_NAME = 1
95 | URL = 2
96 | USER_INPUT = 3
97 | COMMAND = 4
98 | INFO = 5
99 | URI = 6
100 | OUTPUT = 7
101 | PT_SUCCESS = 8
102 | PT_FAILURE = 9
103 |
104 |
--------------------------------------------------------------------------------
/fire/core_test.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Tests for the core module."""
16 |
17 | from unittest import mock
18 |
19 | from fire import core
20 | from fire import test_components as tc
21 | from fire import testutils
22 | from fire import trace
23 |
24 |
25 | class CoreTest(testutils.BaseTestCase):
26 |
27 | def testOneLineResult(self):
28 | self.assertEqual(core._OneLineResult(1), '1') # pylint: disable=protected-access
29 | self.assertEqual(core._OneLineResult('hello'), 'hello') # pylint: disable=protected-access
30 | self.assertEqual(core._OneLineResult({}), '{}') # pylint: disable=protected-access
31 | self.assertEqual(core._OneLineResult({'x': 'y'}), '{"x": "y"}') # pylint: disable=protected-access
32 |
33 | def testOneLineResultCircularRef(self):
34 | circular_reference = tc.CircularReference()
35 | self.assertEqual(core._OneLineResult(circular_reference.create()), # pylint: disable=protected-access
36 | "{'y': {...}}")
37 |
38 | @mock.patch('fire.interact.Embed')
39 | def testInteractiveMode(self, mock_embed):
40 | core.Fire(tc.TypedProperties, command=['alpha'])
41 | self.assertFalse(mock_embed.called)
42 | core.Fire(tc.TypedProperties, command=['alpha', '--', '-i'])
43 | self.assertTrue(mock_embed.called)
44 |
45 | @mock.patch('fire.interact.Embed')
46 | def testInteractiveModeFullArgument(self, mock_embed):
47 | core.Fire(tc.TypedProperties, command=['alpha', '--', '--interactive'])
48 | self.assertTrue(mock_embed.called)
49 |
50 | @mock.patch('fire.interact.Embed')
51 | def testInteractiveModeVariables(self, mock_embed):
52 | core.Fire(tc.WithDefaults, command=['double', '2', '--', '-i'])
53 | self.assertTrue(mock_embed.called)
54 | (variables, verbose), unused_kwargs = mock_embed.call_args
55 | self.assertFalse(verbose)
56 | self.assertEqual(variables['result'], 4)
57 | self.assertIsInstance(variables['self'], tc.WithDefaults)
58 | self.assertIsInstance(variables['trace'], trace.FireTrace)
59 |
60 | @mock.patch('fire.interact.Embed')
61 | def testInteractiveModeVariablesWithName(self, mock_embed):
62 | core.Fire(tc.WithDefaults,
63 | command=['double', '2', '--', '-i', '-v'], name='D')
64 | self.assertTrue(mock_embed.called)
65 | (variables, verbose), unused_kwargs = mock_embed.call_args
66 | self.assertTrue(verbose)
67 | self.assertEqual(variables['result'], 4)
68 | self.assertIsInstance(variables['self'], tc.WithDefaults)
69 | self.assertEqual(variables['D'], tc.WithDefaults)
70 | self.assertIsInstance(variables['trace'], trace.FireTrace)
71 |
72 | # TODO(dbieber): Use parameterized tests to break up repetitive tests.
73 | def testHelpWithClass(self):
74 | with self.assertRaisesFireExit(0, 'SYNOPSIS.*ARG1'):
75 | core.Fire(tc.InstanceVars, command=['--', '--help'])
76 | with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*ARG1'):
77 | core.Fire(tc.InstanceVars, command=['--help'])
78 | with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*ARG1'):
79 | core.Fire(tc.InstanceVars, command=['-h'])
80 |
81 | def testHelpWithMember(self):
82 | with self.assertRaisesFireExit(0, 'SYNOPSIS.*capitalize'):
83 | core.Fire(tc.TypedProperties, command=['gamma', '--', '--help'])
84 | with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*capitalize'):
85 | core.Fire(tc.TypedProperties, command=['gamma', '--help'])
86 | with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*capitalize'):
87 | core.Fire(tc.TypedProperties, command=['gamma', '-h'])
88 | with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*delta'):
89 | core.Fire(tc.TypedProperties, command=['delta', '--help'])
90 | with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*echo'):
91 | core.Fire(tc.TypedProperties, command=['echo', '--help'])
92 |
93 | def testHelpOnErrorInConstructor(self):
94 | with self.assertRaisesFireExit(0, 'SYNOPSIS.*VALUE'):
95 | core.Fire(tc.ErrorInConstructor, command=['--', '--help'])
96 | with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*VALUE'):
97 | core.Fire(tc.ErrorInConstructor, command=['--help'])
98 |
99 | def testHelpWithNamespaceCollision(self):
100 | # Tests cases when calling the help shortcut should not show help.
101 | with self.assertOutputMatches(stdout='DESCRIPTION.*', stderr=None):
102 | core.Fire(tc.WithHelpArg, command=['--help', 'False'])
103 | with self.assertOutputMatches(stdout='help in a dict', stderr=None):
104 | core.Fire(tc.WithHelpArg, command=['dictionary', '__help'])
105 | with self.assertOutputMatches(stdout='{}', stderr=None):
106 | core.Fire(tc.WithHelpArg, command=['dictionary', '--help'])
107 | with self.assertOutputMatches(stdout='False', stderr=None):
108 | core.Fire(tc.function_with_help, command=['False'])
109 |
110 | def testInvalidParameterRaisesFireExit(self):
111 | with self.assertRaisesFireExit(2, 'runmisspelled'):
112 | core.Fire(tc.Kwargs, command=['props', '--a=1', '--b=2', 'runmisspelled'])
113 |
114 | def testErrorRaising(self):
115 | # Errors in user code should not be caught; they should surface as normal.
116 | # This will lead to exit status code 1 for the client program.
117 | with self.assertRaises(ValueError):
118 | core.Fire(tc.ErrorRaiser, command=['fail'])
119 |
120 | def testFireError(self):
121 | error = core.FireError('Example error')
122 | self.assertIsNotNone(error)
123 |
124 | def testFireErrorMultipleValues(self):
125 | error = core.FireError('Example error', 'value')
126 | self.assertIsNotNone(error)
127 |
128 | def testPrintEmptyDict(self):
129 | with self.assertOutputMatches(stdout='{}', stderr=None):
130 | core.Fire(tc.EmptyDictOutput, command=['totally_empty'])
131 | with self.assertOutputMatches(stdout='{}', stderr=None):
132 | core.Fire(tc.EmptyDictOutput, command=['nothing_printable'])
133 |
134 | def testPrintOrderedDict(self):
135 | with self.assertOutputMatches(stdout=r'A:\s+A\s+2:\s+2\s+', stderr=None):
136 | core.Fire(tc.OrderedDictionary, command=['non_empty'])
137 | with self.assertOutputMatches(stdout='{}'):
138 | core.Fire(tc.OrderedDictionary, command=['empty'])
139 |
140 | def testPrintNamedTupleField(self):
141 | with self.assertOutputMatches(stdout='11', stderr=None):
142 | core.Fire(tc.NamedTuple, command=['point', 'x'])
143 |
144 | def testPrintNamedTupleFieldNameEqualsValue(self):
145 | with self.assertOutputMatches(stdout='x', stderr=None):
146 | core.Fire(tc.NamedTuple, command=['matching_names', 'x'])
147 |
148 | def testPrintNamedTupleIndex(self):
149 | with self.assertOutputMatches(stdout='22', stderr=None):
150 | core.Fire(tc.NamedTuple, command=['point', '1'])
151 |
152 | def testPrintSet(self):
153 | with self.assertOutputMatches(stdout='.*three.*', stderr=None):
154 | core.Fire(tc.simple_set(), command=[])
155 |
156 | def testPrintFrozenSet(self):
157 | with self.assertOutputMatches(stdout='.*three.*', stderr=None):
158 | core.Fire(tc.simple_frozenset(), command=[])
159 |
160 | def testPrintNamedTupleNegativeIndex(self):
161 | with self.assertOutputMatches(stdout='11', stderr=None):
162 | core.Fire(tc.NamedTuple, command=['point', '-2'])
163 |
164 | def testCallable(self):
165 | with self.assertOutputMatches(stdout=r'foo:\s+foo\s+', stderr=None):
166 | core.Fire(tc.CallableWithKeywordArgument(), command=['--foo=foo'])
167 | with self.assertOutputMatches(stdout=r'foo\s+', stderr=None):
168 | core.Fire(tc.CallableWithKeywordArgument(), command=['print_msg', 'foo'])
169 | with self.assertOutputMatches(stdout=r'', stderr=None):
170 | core.Fire(tc.CallableWithKeywordArgument(), command=[])
171 |
172 | def testCallableWithPositionalArgs(self):
173 | with self.assertRaisesFireExit(2, ''):
174 | # This does not give 7 since positional args are disallowed for callable
175 | # objects.
176 | core.Fire(tc.CallableWithPositionalArgs(), command=['3', '4'])
177 |
178 | def testStaticMethod(self):
179 | self.assertEqual(
180 | core.Fire(tc.HasStaticAndClassMethods,
181 | command=['static_fn', 'alpha']),
182 | 'alpha',
183 | )
184 |
185 | def testClassMethod(self):
186 | self.assertEqual(
187 | core.Fire(tc.HasStaticAndClassMethods,
188 | command=['class_fn', '6']),
189 | 7,
190 | )
191 |
192 | def testCustomSerialize(self):
193 | def serialize(x):
194 | if isinstance(x, list):
195 | return ', '.join(str(xi) for xi in x)
196 | if isinstance(x, dict):
197 | return ', '.join('{}={!r}'.format(k, v) for k, v in sorted(x.items()))
198 | if x == 'special':
199 | return ['SURPRISE!!', "I'm a list!"]
200 | return x
201 |
202 | ident = lambda x: x
203 |
204 | with self.assertOutputMatches(stdout='a, b', stderr=None):
205 | _ = core.Fire(ident, command=['[a,b]'], serialize=serialize)
206 | with self.assertOutputMatches(stdout='a=5, b=6', stderr=None):
207 | _ = core.Fire(ident, command=['{a:5,b:6}'], serialize=serialize)
208 | with self.assertOutputMatches(stdout='asdf', stderr=None):
209 | _ = core.Fire(ident, command=['asdf'], serialize=serialize)
210 | with self.assertOutputMatches(
211 | stdout="SURPRISE!!\nI'm a list!\n", stderr=None):
212 | _ = core.Fire(ident, command=['special'], serialize=serialize)
213 | with self.assertRaises(core.FireError):
214 | core.Fire(ident, command=['asdf'], serialize=55)
215 |
216 | def testLruCacheDecoratorBoundArg(self):
217 | self.assertEqual(
218 | core.Fire(tc.py3.LruCacheDecoratedMethod, # pytype: disable=module-attr
219 | command=['lru_cache_in_class', 'foo']), 'foo')
220 |
221 | def testLruCacheDecorator(self):
222 | self.assertEqual(
223 | core.Fire(tc.py3.lru_cache_decorated, # pytype: disable=module-attr
224 | command=['foo']), 'foo')
225 |
226 |
227 | if __name__ == '__main__':
228 | testutils.main()
229 |
--------------------------------------------------------------------------------
/fire/custom_descriptions.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Custom descriptions and summaries for the builtin types.
16 |
17 | The docstrings for objects of primitive types reflect the type of the object,
18 | rather than the object itself. For example, the docstring for any dict is this:
19 |
20 | > print({'key': 'value'}.__doc__)
21 | dict() -> new empty dictionary
22 | dict(mapping) -> new dictionary initialized from a mapping object's
23 | (key, value) pairs
24 | dict(iterable) -> new dictionary initialized as if via:
25 | d = {}
26 | for k, v in iterable:
27 | d[k] = v
28 | dict(**kwargs) -> new dictionary initialized with the name=value pairs
29 | in the keyword argument list. For example: dict(one=1, two=2)
30 |
31 | As you can see, this docstring is more pertinent to the function `dict` and
32 | would be suitable as the result of `dict.__doc__`, but is wholely unsuitable
33 | as a description for the dict `{'key': 'value'}`.
34 |
35 | This modules aims to resolve that problem, providing custom summaries and
36 | descriptions for primitive typed values.
37 | """
38 |
39 | from fire import formatting
40 |
41 | TWO_DOUBLE_QUOTES = '""'
42 | STRING_DESC_PREFIX = 'The string '
43 |
44 |
45 | def NeedsCustomDescription(component):
46 | """Whether the component should use a custom description and summary.
47 |
48 | Components of primitive type, such as ints, floats, dicts, lists, and others
49 | have messy builtin docstrings. These are inappropriate for display as
50 | descriptions and summaries in a CLI. This function determines whether the
51 | provided component has one of these docstrings.
52 |
53 | Note that an object such as `int` has the same docstring as an int like `3`.
54 | The docstring is OK for `int`, but is inappropriate as a docstring for `3`.
55 |
56 | Args:
57 | component: The component of interest.
58 | Returns:
59 | Whether the component should use a custom description and summary.
60 | """
61 | type_ = type(component)
62 | if (
63 | type_ in (str, int, bytes)
64 | or type_ in (float, complex, bool)
65 | or type_ in (dict, tuple, list, set, frozenset)
66 | ):
67 | return True
68 | return False
69 |
70 |
71 | def GetStringTypeSummary(obj, available_space, line_length):
72 | """Returns a custom summary for string type objects.
73 |
74 | This function constructs a summary for string type objects by double quoting
75 | the string value. The double quoted string value will be potentially truncated
76 | with ellipsis depending on whether it has enough space available to show the
77 | full string value.
78 |
79 | Args:
80 | obj: The object to generate summary for.
81 | available_space: Number of character spaces available.
82 | line_length: The full width of the terminal, default is 80.
83 |
84 | Returns:
85 | A summary for the input object.
86 | """
87 | if len(obj) + len(TWO_DOUBLE_QUOTES) <= available_space:
88 | content = obj
89 | else:
90 | additional_len_needed = len(TWO_DOUBLE_QUOTES) + len(formatting.ELLIPSIS)
91 | if available_space < additional_len_needed:
92 | available_space = line_length
93 | content = formatting.EllipsisTruncate(
94 | obj, available_space - len(TWO_DOUBLE_QUOTES), line_length)
95 | return formatting.DoubleQuote(content)
96 |
97 |
98 | def GetStringTypeDescription(obj, available_space, line_length):
99 | """Returns the predefined description for string obj.
100 |
101 | This function constructs a description for string type objects in the format
102 | of 'The string ""'. could be potentially
103 | truncated depending on whether it has enough space available to show the full
104 | string value.
105 |
106 | Args:
107 | obj: The object to generate description for.
108 | available_space: Number of character spaces available.
109 | line_length: The full width of the terminal, default if 80.
110 |
111 | Returns:
112 | A description for input object.
113 | """
114 | additional_len_needed = len(STRING_DESC_PREFIX) + len(
115 | TWO_DOUBLE_QUOTES) + len(formatting.ELLIPSIS)
116 | if available_space < additional_len_needed:
117 | available_space = line_length
118 |
119 | return STRING_DESC_PREFIX + formatting.DoubleQuote(
120 | formatting.EllipsisTruncate(
121 | obj, available_space - len(STRING_DESC_PREFIX) -
122 | len(TWO_DOUBLE_QUOTES), line_length))
123 |
124 |
125 | CUSTOM_DESC_SUM_FN_DICT = {
126 | 'str': (GetStringTypeSummary, GetStringTypeDescription),
127 | 'unicode': (GetStringTypeSummary, GetStringTypeDescription),
128 | }
129 |
130 |
131 | def GetSummary(obj, available_space, line_length):
132 | obj_type_name = type(obj).__name__
133 | if obj_type_name in CUSTOM_DESC_SUM_FN_DICT:
134 | return CUSTOM_DESC_SUM_FN_DICT.get(obj_type_name)[0](obj, available_space,
135 | line_length)
136 | return None
137 |
138 |
139 | def GetDescription(obj, available_space, line_length):
140 | obj_type_name = type(obj).__name__
141 | if obj_type_name in CUSTOM_DESC_SUM_FN_DICT:
142 | return CUSTOM_DESC_SUM_FN_DICT.get(obj_type_name)[1](obj, available_space,
143 | line_length)
144 | return None
145 |
--------------------------------------------------------------------------------
/fire/custom_descriptions_test.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Tests for custom description module."""
16 |
17 | from fire import custom_descriptions
18 | from fire import testutils
19 |
20 | LINE_LENGTH = 80
21 |
22 |
23 | class CustomDescriptionTest(testutils.BaseTestCase):
24 |
25 | def test_string_type_summary_enough_space(self):
26 | component = 'Test'
27 | summary = custom_descriptions.GetSummary(
28 | obj=component, available_space=80, line_length=LINE_LENGTH)
29 | self.assertEqual(summary, '"Test"')
30 |
31 | def test_string_type_summary_not_enough_space_truncated(self):
32 | component = 'Test'
33 | summary = custom_descriptions.GetSummary(
34 | obj=component, available_space=5, line_length=LINE_LENGTH)
35 | self.assertEqual(summary, '"..."')
36 |
37 | def test_string_type_summary_not_enough_space_new_line(self):
38 | component = 'Test'
39 | summary = custom_descriptions.GetSummary(
40 | obj=component, available_space=4, line_length=LINE_LENGTH)
41 | self.assertEqual(summary, '"Test"')
42 |
43 | def test_string_type_summary_not_enough_space_long_truncated(self):
44 | component = 'Lorem ipsum dolor sit amet'
45 | summary = custom_descriptions.GetSummary(
46 | obj=component, available_space=10, line_length=LINE_LENGTH)
47 | self.assertEqual(summary, '"Lorem..."')
48 |
49 | def test_string_type_description_enough_space(self):
50 | component = 'Test'
51 | description = custom_descriptions.GetDescription(
52 | obj=component, available_space=80, line_length=LINE_LENGTH)
53 | self.assertEqual(description, 'The string "Test"')
54 |
55 | def test_string_type_description_not_enough_space_truncated(self):
56 | component = 'Lorem ipsum dolor sit amet'
57 | description = custom_descriptions.GetDescription(
58 | obj=component, available_space=20, line_length=LINE_LENGTH)
59 | self.assertEqual(description, 'The string "Lore..."')
60 |
61 | def test_string_type_description_not_enough_space_new_line(self):
62 | component = 'Lorem ipsum dolor sit amet'
63 | description = custom_descriptions.GetDescription(
64 | obj=component, available_space=10, line_length=LINE_LENGTH)
65 | self.assertEqual(description, 'The string "Lorem ipsum dolor sit amet"')
66 |
67 |
68 | if __name__ == '__main__':
69 | testutils.main()
70 |
--------------------------------------------------------------------------------
/fire/decorators.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """These decorators provide function metadata to Python Fire.
16 |
17 | SetParseFn and SetParseFns allow you to set the functions Fire uses for parsing
18 | command line arguments to client code.
19 | """
20 |
21 | from typing import Any, Dict
22 | import inspect
23 |
24 | FIRE_METADATA = 'FIRE_METADATA'
25 | FIRE_PARSE_FNS = 'FIRE_PARSE_FNS'
26 | ACCEPTS_POSITIONAL_ARGS = 'ACCEPTS_POSITIONAL_ARGS'
27 |
28 |
29 | def SetParseFn(fn, *arguments):
30 | """Sets the fn for Fire to use to parse args when calling the decorated fn.
31 |
32 | Args:
33 | fn: The function to be used for parsing arguments.
34 | *arguments: The arguments for which to use the parse fn. If none are listed,
35 | then this will set the default parse function.
36 | Returns:
37 | The decorated function, which now has metadata telling Fire how to perform.
38 | """
39 | def _Decorator(func):
40 | parse_fns = GetParseFns(func)
41 | if not arguments:
42 | parse_fns['default'] = fn
43 | else:
44 | for argument in arguments:
45 | parse_fns['named'][argument] = fn
46 | _SetMetadata(func, FIRE_PARSE_FNS, parse_fns)
47 | return func
48 |
49 | return _Decorator
50 |
51 |
52 | def SetParseFns(*positional, **named):
53 | """Set the fns for Fire to use to parse args when calling the decorated fn.
54 |
55 | Returns a decorator, which when applied to a function adds metadata to the
56 | function telling Fire how to turn string command line arguments into proper
57 | Python arguments with which to call the function.
58 |
59 | A parse function should accept a single string argument and return a value to
60 | be used in its place when calling the decorated function.
61 |
62 | Args:
63 | *positional: The functions to be used for parsing positional arguments.
64 | **named: The functions to be used for parsing named arguments.
65 | Returns:
66 | The decorated function, which now has metadata telling Fire how to perform.
67 | """
68 | def _Decorator(fn):
69 | parse_fns = GetParseFns(fn)
70 | parse_fns['positional'] = positional
71 | parse_fns['named'].update(named) # pytype: disable=attribute-error
72 | _SetMetadata(fn, FIRE_PARSE_FNS, parse_fns)
73 | return fn
74 |
75 | return _Decorator
76 |
77 |
78 | def _SetMetadata(fn, attribute, value):
79 | metadata = GetMetadata(fn)
80 | metadata[attribute] = value
81 | setattr(fn, FIRE_METADATA, metadata)
82 |
83 |
84 | def GetMetadata(fn) -> Dict[str, Any]:
85 | """Gets metadata attached to the function `fn` as an attribute.
86 |
87 | Args:
88 | fn: The function from which to retrieve the function metadata.
89 | Returns:
90 | A dictionary mapping property strings to their value.
91 | """
92 | # Class __init__ functions and object __call__ functions require flag style
93 | # arguments. Other methods and functions may accept positional args.
94 | default = {
95 | ACCEPTS_POSITIONAL_ARGS: inspect.isroutine(fn),
96 | }
97 | try:
98 | metadata = getattr(fn, FIRE_METADATA, default)
99 | if ACCEPTS_POSITIONAL_ARGS in metadata:
100 | return metadata
101 | else:
102 | return default
103 | except: # pylint: disable=bare-except
104 | return default
105 |
106 |
107 | def GetParseFns(fn) -> Dict[str, Any]:
108 | metadata = GetMetadata(fn)
109 | default = {'default': None, 'positional': [], 'named': {}}
110 | return metadata.get(FIRE_PARSE_FNS, default)
111 |
--------------------------------------------------------------------------------
/fire/decorators_test.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Tests for the decorators module."""
16 |
17 | from fire import core
18 | from fire import decorators
19 | from fire import testutils
20 |
21 |
22 | class NoDefaults:
23 | """A class for testing decorated functions without default values."""
24 |
25 | @decorators.SetParseFns(count=int)
26 | def double(self, count):
27 | return 2 * count
28 |
29 | @decorators.SetParseFns(count=float)
30 | def triple(self, count):
31 | return 3 * count
32 |
33 | @decorators.SetParseFns(int)
34 | def quadruple(self, count):
35 | return 4 * count
36 |
37 |
38 | @decorators.SetParseFns(int)
39 | def double(count):
40 | return 2 * count
41 |
42 |
43 | class WithDefaults:
44 |
45 | @decorators.SetParseFns(float)
46 | def example1(self, arg1=10):
47 | return arg1, type(arg1)
48 |
49 | @decorators.SetParseFns(arg1=float)
50 | def example2(self, arg1=10):
51 | return arg1, type(arg1)
52 |
53 |
54 | class MixedArguments:
55 |
56 | @decorators.SetParseFns(float, arg2=str)
57 | def example3(self, arg1, arg2):
58 | return arg1, arg2
59 |
60 |
61 | class PartialParseFn:
62 |
63 | @decorators.SetParseFns(arg1=str)
64 | def example4(self, arg1, arg2):
65 | return arg1, arg2
66 |
67 | @decorators.SetParseFns(arg2=str)
68 | def example5(self, arg1, arg2):
69 | return arg1, arg2
70 |
71 |
72 | class WithKwargs:
73 |
74 | @decorators.SetParseFns(mode=str, count=int)
75 | def example6(self, **kwargs):
76 | return (
77 | kwargs.get('mode', 'default'),
78 | kwargs.get('count', 0),
79 | )
80 |
81 |
82 | class WithVarArgs:
83 |
84 | @decorators.SetParseFn(str)
85 | def example7(self, arg1, arg2=None, *varargs, **kwargs): # pylint: disable=keyword-arg-before-vararg
86 | return arg1, arg2, varargs, kwargs
87 |
88 |
89 | class FireDecoratorsTest(testutils.BaseTestCase):
90 |
91 | def testSetParseFnsNamedArgs(self):
92 | self.assertEqual(core.Fire(NoDefaults, command=['double', '2']), 4)
93 | self.assertEqual(core.Fire(NoDefaults, command=['triple', '4']), 12.0)
94 |
95 | def testSetParseFnsPositionalArgs(self):
96 | self.assertEqual(core.Fire(NoDefaults, command=['quadruple', '5']), 20)
97 |
98 | def testSetParseFnsFnWithPositionalArgs(self):
99 | self.assertEqual(core.Fire(double, command=['5']), 10)
100 |
101 | def testSetParseFnsDefaultsFromPython(self):
102 | # When called from Python, function should behave normally.
103 | self.assertTupleEqual(WithDefaults().example1(), (10, int))
104 | self.assertEqual(WithDefaults().example1(5), (5, int))
105 | self.assertEqual(WithDefaults().example1(12.0), (12, float))
106 |
107 | def testSetParseFnsDefaultsFromFire(self):
108 | # Fire should use the decorator to know how to parse string arguments.
109 | self.assertEqual(core.Fire(WithDefaults, command=['example1']), (10, int))
110 | self.assertEqual(core.Fire(WithDefaults, command=['example1', '10']),
111 | (10, float))
112 | self.assertEqual(core.Fire(WithDefaults, command=['example1', '13']),
113 | (13, float))
114 | self.assertEqual(core.Fire(WithDefaults, command=['example1', '14.0']),
115 | (14, float))
116 |
117 | def testSetParseFnsNamedDefaultsFromPython(self):
118 | # When called from Python, function should behave normally.
119 | self.assertTupleEqual(WithDefaults().example2(), (10, int))
120 | self.assertEqual(WithDefaults().example2(5), (5, int))
121 | self.assertEqual(WithDefaults().example2(12.0), (12, float))
122 |
123 | def testSetParseFnsNamedDefaultsFromFire(self):
124 | # Fire should use the decorator to know how to parse string arguments.
125 | self.assertEqual(core.Fire(WithDefaults, command=['example2']), (10, int))
126 | self.assertEqual(core.Fire(WithDefaults, command=['example2', '10']),
127 | (10, float))
128 | self.assertEqual(core.Fire(WithDefaults, command=['example2', '13']),
129 | (13, float))
130 | self.assertEqual(core.Fire(WithDefaults, command=['example2', '14.0']),
131 | (14, float))
132 |
133 | def testSetParseFnsPositionalAndNamed(self):
134 | self.assertEqual(core.Fire(MixedArguments, ['example3', '10', '10']),
135 | (10, '10'))
136 |
137 | def testSetParseFnsOnlySomeTypes(self):
138 | self.assertEqual(
139 | core.Fire(PartialParseFn, command=['example4', '10', '10']), ('10', 10))
140 | self.assertEqual(
141 | core.Fire(PartialParseFn, command=['example5', '10', '10']), (10, '10'))
142 |
143 | def testSetParseFnsForKeywordArgs(self):
144 | self.assertEqual(
145 | core.Fire(WithKwargs, command=['example6']), ('default', 0))
146 | self.assertEqual(
147 | core.Fire(WithKwargs, command=['example6', '--herring', '"red"']),
148 | ('default', 0))
149 | self.assertEqual(
150 | core.Fire(WithKwargs, command=['example6', '--mode', 'train']),
151 | ('train', 0))
152 | self.assertEqual(core.Fire(WithKwargs, command=['example6', '--mode', '3']),
153 | ('3', 0))
154 | self.assertEqual(
155 | core.Fire(WithKwargs,
156 | command=['example6', '--mode', '-1', '--count', '10']),
157 | ('-1', 10))
158 | self.assertEqual(
159 | core.Fire(WithKwargs, command=['example6', '--count', '-2']),
160 | ('default', -2))
161 |
162 | def testSetParseFn(self):
163 | self.assertEqual(
164 | core.Fire(WithVarArgs,
165 | command=['example7', '1', '--arg2=2', '3', '4', '--kwarg=5']),
166 | ('1', '2', ('3', '4'), {'kwarg': '5'}))
167 |
168 |
169 | if __name__ == '__main__':
170 | testutils.main()
171 |
--------------------------------------------------------------------------------
/fire/docstrings_fuzz_test.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Fuzz tests for the docstring parser module."""
16 |
17 | from fire import docstrings
18 | from fire import testutils
19 |
20 | from hypothesis import example
21 | from hypothesis import given
22 | from hypothesis import settings
23 | from hypothesis import strategies as st
24 |
25 |
26 | class DocstringsFuzzTest(testutils.BaseTestCase):
27 |
28 | @settings(max_examples=1000, deadline=1000)
29 | @given(st.text(min_size=1))
30 | @example('This is a one-line docstring.')
31 | def test_fuzz_parse(self, value):
32 | docstrings.parse(value)
33 |
34 |
35 | if __name__ == '__main__':
36 | testutils.main()
37 |
--------------------------------------------------------------------------------
/fire/fire_import_test.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Tests importing the fire module."""
16 |
17 | import sys
18 | from unittest import mock
19 |
20 | import fire
21 | from fire import testutils
22 |
23 |
24 | class FireImportTest(testutils.BaseTestCase):
25 | """Tests importing Fire."""
26 |
27 | def testFire(self):
28 | with mock.patch.object(sys, 'argv', ['commandname']):
29 | fire.Fire()
30 |
31 | def testFireMethods(self):
32 | self.assertIsNotNone(fire.Fire)
33 |
34 | def testNoPrivateMethods(self):
35 | self.assertTrue(hasattr(fire, 'Fire'))
36 | self.assertFalse(hasattr(fire, '_Fire'))
37 |
38 |
39 | if __name__ == '__main__':
40 | testutils.main()
41 |
--------------------------------------------------------------------------------
/fire/formatting.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Formatting utilities for use in creating help text."""
16 |
17 | from fire import formatting_windows # pylint: disable=unused-import
18 | import termcolor
19 |
20 |
21 | ELLIPSIS = '...'
22 |
23 |
24 | def Indent(text, spaces=2):
25 | lines = text.split('\n')
26 | return '\n'.join(
27 | ' ' * spaces + line if line else line
28 | for line in lines)
29 |
30 |
31 | def Bold(text):
32 | return termcolor.colored(text, attrs=['bold'])
33 |
34 |
35 | def Underline(text):
36 | return termcolor.colored(text, attrs=['underline'])
37 |
38 |
39 | def BoldUnderline(text):
40 | return Bold(Underline(text))
41 |
42 |
43 | def WrappedJoin(items, separator=' | ', width=80):
44 | """Joins the items by the separator, wrapping lines at the given width."""
45 | lines = []
46 | current_line = ''
47 | for index, item in enumerate(items):
48 | is_final_item = index == len(items) - 1
49 | if is_final_item:
50 | if len(current_line) + len(item) <= width:
51 | current_line += item
52 | else:
53 | lines.append(current_line.rstrip())
54 | current_line = item
55 | else:
56 | if len(current_line) + len(item) + len(separator) <= width:
57 | current_line += item + separator
58 | else:
59 | lines.append(current_line.rstrip())
60 | current_line = item + separator
61 |
62 | lines.append(current_line)
63 | return lines
64 |
65 |
66 | def Error(text):
67 | return termcolor.colored(text, color='red', attrs=['bold'])
68 |
69 |
70 | def EllipsisTruncate(text, available_space, line_length):
71 | """Truncate text from the end with ellipsis."""
72 | if available_space < len(ELLIPSIS):
73 | available_space = line_length
74 | # No need to truncate
75 | if len(text) <= available_space:
76 | return text
77 | return text[:available_space - len(ELLIPSIS)] + ELLIPSIS
78 |
79 |
80 | def EllipsisMiddleTruncate(text, available_space, line_length):
81 | """Truncates text from the middle with ellipsis."""
82 | if available_space < len(ELLIPSIS):
83 | available_space = line_length
84 | if len(text) < available_space:
85 | return text
86 | available_string_len = available_space - len(ELLIPSIS)
87 | first_half_len = int(available_string_len / 2) # start from middle
88 | second_half_len = available_string_len - first_half_len
89 | return text[:first_half_len] + ELLIPSIS + text[-second_half_len:]
90 |
91 |
92 | def DoubleQuote(text):
93 | return '"%s"' % text
94 |
--------------------------------------------------------------------------------
/fire/formatting_test.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Tests for formatting.py."""
16 |
17 | from fire import formatting
18 | from fire import testutils
19 |
20 | LINE_LENGTH = 80
21 |
22 |
23 | class FormattingTest(testutils.BaseTestCase):
24 |
25 | def test_bold(self):
26 | text = formatting.Bold('hello')
27 | self.assertIn(text, ['hello', '\x1b[1mhello\x1b[0m'])
28 |
29 | def test_underline(self):
30 | text = formatting.Underline('hello')
31 | self.assertIn(text, ['hello', '\x1b[4mhello\x1b[0m'])
32 |
33 | def test_indent(self):
34 | text = formatting.Indent('hello', spaces=2)
35 | self.assertEqual(' hello', text)
36 |
37 | def test_indent_multiple_lines(self):
38 | text = formatting.Indent('hello\nworld', spaces=2)
39 | self.assertEqual(' hello\n world', text)
40 |
41 | def test_wrap_one_item(self):
42 | lines = formatting.WrappedJoin(['rice'])
43 | self.assertEqual(['rice'], lines)
44 |
45 | def test_wrap_multiple_items(self):
46 | lines = formatting.WrappedJoin(['rice', 'beans', 'chicken', 'cheese'],
47 | width=15)
48 | self.assertEqual(['rice | beans |',
49 | 'chicken |',
50 | 'cheese'], lines)
51 |
52 | def test_ellipsis_truncate(self):
53 | text = 'This is a string'
54 | truncated_text = formatting.EllipsisTruncate(
55 | text=text, available_space=10, line_length=LINE_LENGTH)
56 | self.assertEqual('This is...', truncated_text)
57 |
58 | def test_ellipsis_truncate_not_enough_space(self):
59 | text = 'This is a string'
60 | truncated_text = formatting.EllipsisTruncate(
61 | text=text, available_space=2, line_length=LINE_LENGTH)
62 | self.assertEqual('This is a string', truncated_text)
63 |
64 | def test_ellipsis_middle_truncate(self):
65 | text = '1000000000L'
66 | truncated_text = formatting.EllipsisMiddleTruncate(
67 | text=text, available_space=7, line_length=LINE_LENGTH)
68 | self.assertEqual('10...0L', truncated_text)
69 |
70 | def test_ellipsis_middle_truncate_not_enough_space(self):
71 | text = '1000000000L'
72 | truncated_text = formatting.EllipsisMiddleTruncate(
73 | text=text, available_space=2, line_length=LINE_LENGTH)
74 | self.assertEqual('1000000000L', truncated_text)
75 |
76 |
77 | if __name__ == '__main__':
78 | testutils.main()
79 |
--------------------------------------------------------------------------------
/fire/formatting_windows.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """This module is used for enabling formatting on Windows."""
16 |
17 | import ctypes
18 | import os
19 | import platform
20 | import subprocess
21 | import sys
22 |
23 | try:
24 | import colorama # pylint: disable=g-import-not-at-top, # pytype: disable=import-error
25 | HAS_COLORAMA = True
26 | except ImportError:
27 | HAS_COLORAMA = False
28 |
29 |
30 | def initialize_or_disable():
31 | """Enables ANSI processing on Windows or disables it as needed."""
32 | if HAS_COLORAMA:
33 | wrap = True
34 | if (hasattr(sys.stdout, 'isatty')
35 | and sys.stdout.isatty()
36 | and platform.release() == '10'):
37 | # Enables native ANSI sequences in console.
38 | # Windows 10, 2016, and 2019 only.
39 |
40 | wrap = False
41 | kernel32 = ctypes.windll.kernel32 # pytype: disable=module-attr
42 | enable_virtual_terminal_processing = 0x04
43 | out_handle = kernel32.GetStdHandle(subprocess.STD_OUTPUT_HANDLE) # pylint: disable=line-too-long, # pytype: disable=module-attr
44 | # GetConsoleMode fails if the terminal isn't native.
45 | mode = ctypes.wintypes.DWORD()
46 | if kernel32.GetConsoleMode(out_handle, ctypes.byref(mode)) == 0:
47 | wrap = True
48 | if not mode.value & enable_virtual_terminal_processing:
49 | if kernel32.SetConsoleMode(
50 | out_handle, mode.value | enable_virtual_terminal_processing) == 0:
51 | # kernel32.SetConsoleMode to enable ANSI sequences failed
52 | wrap = True
53 | colorama.init(wrap=wrap)
54 | else:
55 | os.environ['ANSI_COLORS_DISABLED'] = '1'
56 |
57 | if sys.platform.startswith('win'):
58 | initialize_or_disable()
59 |
--------------------------------------------------------------------------------
/fire/inspectutils_test.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Tests for the inspectutils module."""
16 |
17 | import os
18 |
19 | from fire import inspectutils
20 | from fire import test_components as tc
21 | from fire import testutils
22 |
23 |
24 | class InspectUtilsTest(testutils.BaseTestCase):
25 |
26 | def testGetFullArgSpec(self):
27 | spec = inspectutils.GetFullArgSpec(tc.identity)
28 | self.assertEqual(spec.args, ['arg1', 'arg2', 'arg3', 'arg4'])
29 | self.assertEqual(spec.defaults, (10, 20))
30 | self.assertEqual(spec.varargs, 'arg5')
31 | self.assertEqual(spec.varkw, 'arg6')
32 | self.assertEqual(spec.kwonlyargs, [])
33 | self.assertEqual(spec.kwonlydefaults, {})
34 | self.assertEqual(spec.annotations, {'arg2': int, 'arg4': int})
35 |
36 | def testGetFullArgSpecPy3(self):
37 | spec = inspectutils.GetFullArgSpec(tc.py3.identity)
38 | self.assertEqual(spec.args, ['arg1', 'arg2', 'arg3', 'arg4'])
39 | self.assertEqual(spec.defaults, (10, 20))
40 | self.assertEqual(spec.varargs, 'arg5')
41 | self.assertEqual(spec.varkw, 'arg10')
42 | self.assertEqual(spec.kwonlyargs, ['arg6', 'arg7', 'arg8', 'arg9'])
43 | self.assertEqual(spec.kwonlydefaults, {'arg8': 30, 'arg9': 40})
44 | self.assertEqual(spec.annotations,
45 | {'arg2': int, 'arg4': int, 'arg7': int, 'arg9': int})
46 |
47 | def testGetFullArgSpecFromBuiltin(self):
48 | spec = inspectutils.GetFullArgSpec('test'.upper)
49 | self.assertEqual(spec.args, [])
50 | self.assertEqual(spec.defaults, ())
51 | self.assertEqual(spec.kwonlyargs, [])
52 | self.assertEqual(spec.kwonlydefaults, {})
53 | self.assertEqual(spec.annotations, {})
54 |
55 | def testGetFullArgSpecFromSlotWrapper(self):
56 | spec = inspectutils.GetFullArgSpec(tc.NoDefaults)
57 | self.assertEqual(spec.args, [])
58 | self.assertEqual(spec.defaults, ())
59 | self.assertEqual(spec.varargs, None)
60 | self.assertEqual(spec.varkw, None)
61 | self.assertEqual(spec.kwonlyargs, [])
62 | self.assertEqual(spec.kwonlydefaults, {})
63 | self.assertEqual(spec.annotations, {})
64 |
65 | def testGetFullArgSpecFromNamedTuple(self):
66 | spec = inspectutils.GetFullArgSpec(tc.NamedTuplePoint)
67 | self.assertEqual(spec.args, ['x', 'y'])
68 | self.assertEqual(spec.defaults, ())
69 | self.assertEqual(spec.varargs, None)
70 | self.assertEqual(spec.varkw, None)
71 | self.assertEqual(spec.kwonlyargs, [])
72 | self.assertEqual(spec.kwonlydefaults, {})
73 | self.assertEqual(spec.annotations, {})
74 |
75 | def testGetFullArgSpecFromNamedTupleSubclass(self):
76 | spec = inspectutils.GetFullArgSpec(tc.SubPoint)
77 | self.assertEqual(spec.args, ['x', 'y'])
78 | self.assertEqual(spec.defaults, ())
79 | self.assertEqual(spec.varargs, None)
80 | self.assertEqual(spec.varkw, None)
81 | self.assertEqual(spec.kwonlyargs, [])
82 | self.assertEqual(spec.kwonlydefaults, {})
83 | self.assertEqual(spec.annotations, {})
84 |
85 | def testGetFullArgSpecFromClassNoInit(self):
86 | spec = inspectutils.GetFullArgSpec(tc.OldStyleEmpty)
87 | self.assertEqual(spec.args, [])
88 | self.assertEqual(spec.defaults, ())
89 | self.assertEqual(spec.varargs, None)
90 | self.assertEqual(spec.varkw, None)
91 | self.assertEqual(spec.kwonlyargs, [])
92 | self.assertEqual(spec.kwonlydefaults, {})
93 | self.assertEqual(spec.annotations, {})
94 |
95 | def testGetFullArgSpecFromMethod(self):
96 | spec = inspectutils.GetFullArgSpec(tc.NoDefaults().double)
97 | self.assertEqual(spec.args, ['count'])
98 | self.assertEqual(spec.defaults, ())
99 | self.assertEqual(spec.varargs, None)
100 | self.assertEqual(spec.varkw, None)
101 | self.assertEqual(spec.kwonlyargs, [])
102 | self.assertEqual(spec.kwonlydefaults, {})
103 | self.assertEqual(spec.annotations, {})
104 |
105 | def testInfoOne(self):
106 | info = inspectutils.Info(1)
107 | self.assertEqual(info.get('type_name'), 'int')
108 | self.assertEqual(info.get('file'), None)
109 | self.assertEqual(info.get('line'), None)
110 | self.assertEqual(info.get('string_form'), '1')
111 |
112 | def testInfoClass(self):
113 | info = inspectutils.Info(tc.NoDefaults)
114 | self.assertEqual(info.get('type_name'), 'type')
115 | self.assertIn(os.path.join('fire', 'test_components.py'), info.get('file'))
116 | self.assertGreater(info.get('line'), 0)
117 |
118 | def testInfoClassNoInit(self):
119 | info = inspectutils.Info(tc.OldStyleEmpty)
120 | self.assertEqual(info.get('type_name'), 'type')
121 | self.assertIn(os.path.join('fire', 'test_components.py'), info.get('file'))
122 | self.assertGreater(info.get('line'), 0)
123 |
124 | def testInfoNoDocstring(self):
125 | info = inspectutils.Info(tc.NoDefaults)
126 | self.assertEqual(info['docstring'], None, 'Docstring should be None')
127 |
128 |
129 | if __name__ == '__main__':
130 | testutils.main()
131 |
--------------------------------------------------------------------------------
/fire/interact.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """This module enables interactive mode in Python Fire.
16 |
17 | It uses IPython as an optional dependency. When IPython is installed, the
18 | interactive flag will use IPython's REPL. When IPython is not installed, the
19 | interactive flag will start a Python REPL with the builtin `code` module's
20 | InteractiveConsole class.
21 | """
22 |
23 | import inspect
24 |
25 |
26 | def Embed(variables, verbose=False):
27 | """Drops into a Python REPL with variables available as local variables.
28 |
29 | Args:
30 | variables: A dict of variables to make available. Keys are variable names.
31 | Values are variable values.
32 | verbose: Whether to include 'hidden' members, those keys starting with _.
33 | """
34 | print(_AvailableString(variables, verbose))
35 |
36 | try:
37 | _EmbedIPython(variables)
38 | except ImportError:
39 | _EmbedCode(variables)
40 |
41 |
42 | def _AvailableString(variables, verbose=False):
43 | """Returns a string describing what objects are available in the Python REPL.
44 |
45 | Args:
46 | variables: A dict of the object to be available in the REPL.
47 | verbose: Whether to include 'hidden' members, those keys starting with _.
48 | Returns:
49 | A string fit for printing at the start of the REPL, indicating what objects
50 | are available for the user to use.
51 | """
52 | modules = []
53 | other = []
54 | for name, value in variables.items():
55 | if not verbose and name.startswith('_'):
56 | continue
57 | if '-' in name or '/' in name:
58 | continue
59 |
60 | if inspect.ismodule(value):
61 | modules.append(name)
62 | else:
63 | other.append(name)
64 |
65 | lists = [
66 | ('Modules', modules),
67 | ('Objects', other)]
68 | list_strs = []
69 | for name, varlist in lists:
70 | if varlist:
71 | items_str = ', '.join(sorted(varlist))
72 | list_strs.append(f'{name}: {items_str}')
73 |
74 | lists_str = '\n'.join(list_strs)
75 | return (
76 | 'Fire is starting a Python REPL with the following objects:\n'
77 | f'{lists_str}\n'
78 | )
79 |
80 |
81 | def _EmbedIPython(variables, argv=None):
82 | """Drops into an IPython REPL with variables available for use.
83 |
84 | Args:
85 | variables: A dict of variables to make available. Keys are variable names.
86 | Values are variable values.
87 | argv: The argv to use for starting ipython. Defaults to an empty list.
88 | """
89 | import IPython # pylint: disable=import-outside-toplevel,g-import-not-at-top
90 | argv = argv or []
91 | IPython.start_ipython(argv=argv, user_ns=variables)
92 |
93 |
94 | def _EmbedCode(variables):
95 | import code # pylint: disable=import-outside-toplevel,g-import-not-at-top
96 | code.InteractiveConsole(variables).interact()
97 |
--------------------------------------------------------------------------------
/fire/interact_test.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Tests for the interact module."""
16 |
17 | from unittest import mock
18 |
19 | from fire import interact
20 | from fire import testutils
21 |
22 |
23 | try:
24 | import IPython # pylint: disable=unused-import, g-import-not-at-top
25 | INTERACT_METHOD = 'IPython.start_ipython'
26 | except ImportError:
27 | INTERACT_METHOD = 'code.InteractiveConsole'
28 |
29 |
30 | class InteractTest(testutils.BaseTestCase):
31 |
32 | @mock.patch(INTERACT_METHOD)
33 | def testInteract(self, mock_interact_method):
34 | self.assertFalse(mock_interact_method.called)
35 | interact.Embed({})
36 | self.assertTrue(mock_interact_method.called)
37 |
38 | @mock.patch(INTERACT_METHOD)
39 | def testInteractVariables(self, mock_interact_method):
40 | self.assertFalse(mock_interact_method.called)
41 | interact.Embed({
42 | 'count': 10,
43 | 'mock': mock,
44 | })
45 | self.assertTrue(mock_interact_method.called)
46 |
47 | if __name__ == '__main__':
48 | testutils.main()
49 |
--------------------------------------------------------------------------------
/fire/main_test.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Test using Fire via `python -m fire`."""
16 |
17 | import os
18 | import tempfile
19 |
20 | from fire import __main__
21 | from fire import testutils
22 |
23 |
24 | class MainModuleTest(testutils.BaseTestCase):
25 | """Tests to verify the behavior of __main__ (python -m fire)."""
26 |
27 | def testNameSetting(self):
28 | # Confirm one of the usage lines has the gettempdir member.
29 | with self.assertOutputMatches('gettempdir'):
30 | __main__.main(['__main__.py', 'tempfile'])
31 |
32 | def testArgPassing(self):
33 | expected = os.path.join('part1', 'part2', 'part3')
34 | with self.assertOutputMatches('%s\n' % expected):
35 | __main__.main(
36 | ['__main__.py', 'os.path', 'join', 'part1', 'part2', 'part3'])
37 | with self.assertOutputMatches('%s\n' % expected):
38 | __main__.main(
39 | ['__main__.py', 'os', 'path', '-', 'join', 'part1', 'part2', 'part3'])
40 |
41 |
42 | class MainModuleFileTest(testutils.BaseTestCase):
43 | """Tests to verify correct import behavior for file executables."""
44 |
45 | def setUp(self):
46 | super().setUp()
47 | self.file = tempfile.NamedTemporaryFile(suffix='.py') # pylint: disable=consider-using-with
48 | self.file.write(b'class Foo:\n def double(self, n):\n return 2 * n\n')
49 | self.file.flush()
50 |
51 | self.file2 = tempfile.NamedTemporaryFile() # pylint: disable=consider-using-with
52 |
53 | def testFileNameFire(self):
54 | # Confirm that the file is correctly imported and doubles the number.
55 | with self.assertOutputMatches('4'):
56 | __main__.main(
57 | ['__main__.py', self.file.name, 'Foo', 'double', '--n', '2'])
58 |
59 | def testFileNameFailure(self):
60 | # Confirm that an existing file without a .py suffix raises a ValueError.
61 | with self.assertRaises(ValueError):
62 | __main__.main(
63 | ['__main__.py', self.file2.name, 'Foo', 'double', '--n', '2'])
64 |
65 | def testFileNameModuleDuplication(self):
66 | # Confirm that a file that masks a module still loads the module.
67 | with self.assertOutputMatches('gettempdir'):
68 | dirname = os.path.dirname(self.file.name)
69 | with testutils.ChangeDirectory(dirname):
70 | with open('tempfile', 'w'):
71 | __main__.main([
72 | '__main__.py',
73 | 'tempfile',
74 | ])
75 |
76 | os.remove('tempfile')
77 |
78 | def testFileNameModuleFileFailure(self):
79 | # Confirm that an invalid file that masks a non-existent module fails.
80 | with self.assertRaisesRegex(ValueError,
81 | r'Fire can only be called on \.py files\.'): # pylint: disable=line-too-long, # pytype: disable=attribute-error
82 | dirname = os.path.dirname(self.file.name)
83 | with testutils.ChangeDirectory(dirname):
84 | with open('foobar', 'w'):
85 | __main__.main([
86 | '__main__.py',
87 | 'foobar',
88 | ])
89 |
90 | os.remove('foobar')
91 |
92 |
93 | if __name__ == '__main__':
94 | testutils.main()
95 |
--------------------------------------------------------------------------------
/fire/parser.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Provides parsing functionality used by Python Fire."""
16 |
17 | import argparse
18 | import ast
19 | import sys
20 |
21 | if sys.version_info[0:2] < (3, 8):
22 | _StrNode = ast.Str
23 | else:
24 | _StrNode = ast.Constant
25 |
26 |
27 | def CreateParser():
28 | parser = argparse.ArgumentParser(add_help=False)
29 | parser.add_argument('--verbose', '-v', action='store_true')
30 | parser.add_argument('--interactive', '-i', action='store_true')
31 | parser.add_argument('--separator', default='-')
32 | parser.add_argument('--completion', nargs='?', const='bash', type=str)
33 | parser.add_argument('--help', '-h', action='store_true')
34 | parser.add_argument('--trace', '-t', action='store_true')
35 | # TODO(dbieber): Consider allowing name to be passed as an argument.
36 | return parser
37 |
38 |
39 | def SeparateFlagArgs(args):
40 | """Splits a list of args into those for Flags and those for Fire.
41 |
42 | If an isolated '--' arg is not present in the arg list, then all of the args
43 | are for Fire. If there is an isolated '--', then the args after the final '--'
44 | are flag args, and the rest of the args are fire args.
45 |
46 | Args:
47 | args: The list of arguments received by the Fire command.
48 | Returns:
49 | A tuple with the Fire args (a list), followed by the Flag args (a list).
50 | """
51 | if '--' in args:
52 | separator_index = len(args) - 1 - args[::-1].index('--') # index of last --
53 | flag_args = args[separator_index + 1:]
54 | args = args[:separator_index]
55 | return args, flag_args
56 | return args, []
57 |
58 |
59 | def DefaultParseValue(value):
60 | """The default argument parsing function used by Fire CLIs.
61 |
62 | If the value is made of only Python literals and containers, then the value
63 | is parsed as it's Python value. Otherwise, provided the value contains no
64 | quote, escape, or parenthetical characters, the value is treated as a string.
65 |
66 | Args:
67 | value: A string from the command line to be parsed for use in a Fire CLI.
68 | Returns:
69 | The parsed value, of the type determined most appropriate.
70 | """
71 | # Note: _LiteralEval will treat '#' as the start of a comment.
72 | try:
73 | return _LiteralEval(value)
74 | except (SyntaxError, ValueError):
75 | # If _LiteralEval can't parse the value, treat it as a string.
76 | return value
77 |
78 |
79 | def _LiteralEval(value):
80 | """Parse value as a Python literal, or container of containers and literals.
81 |
82 | First the AST of the value is updated so that bare-words are turned into
83 | strings. Then the resulting AST is evaluated as a literal or container of
84 | only containers and literals.
85 |
86 | This allows for the YAML-like syntax {a: b} to represent the dict {'a': 'b'}
87 |
88 | Args:
89 | value: A string to be parsed as a literal or container of containers and
90 | literals.
91 | Returns:
92 | The Python value representing the value arg.
93 | Raises:
94 | ValueError: If the value is not an expression with only containers and
95 | literals.
96 | SyntaxError: If the value string has a syntax error.
97 | """
98 | root = ast.parse(value, mode='eval')
99 | if isinstance(root.body, ast.BinOp): # pytype: disable=attribute-error
100 | raise ValueError(value)
101 |
102 | for node in ast.walk(root):
103 | for field, child in ast.iter_fields(node):
104 | if isinstance(child, list):
105 | for index, subchild in enumerate(child):
106 | if isinstance(subchild, ast.Name):
107 | child[index] = _Replacement(subchild)
108 |
109 | elif isinstance(child, ast.Name):
110 | replacement = _Replacement(child)
111 | setattr(node, field, replacement)
112 |
113 | # ast.literal_eval supports the following types:
114 | # strings, bytes, numbers, tuples, lists, dicts, sets, booleans, and None
115 | # (bytes and set literals only starting with Python 3.2)
116 | return ast.literal_eval(root)
117 |
118 |
119 | def _Replacement(node):
120 | """Returns a node to use in place of the supplied node in the AST.
121 |
122 | Args:
123 | node: A node of type Name. Could be a variable, or builtin constant.
124 | Returns:
125 | A node to use in place of the supplied Node. Either the same node, or a
126 | String node whose value matches the Name node's id.
127 | """
128 | value = node.id
129 | # These are the only builtin constants supported by literal_eval.
130 | if value in ('True', 'False', 'None'):
131 | return node
132 | return _StrNode(value)
133 |
--------------------------------------------------------------------------------
/fire/parser_fuzz_test.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Fuzz tests for the parser module."""
16 |
17 | from fire import parser
18 | from fire import testutils
19 | from hypothesis import example
20 | from hypothesis import given
21 | from hypothesis import settings
22 | from hypothesis import strategies as st
23 | import Levenshtein
24 |
25 |
26 | class ParserFuzzTest(testutils.BaseTestCase):
27 |
28 | @settings(max_examples=10000)
29 | @given(st.text(min_size=1))
30 | @example('True')
31 | @example(r'"test\t\t\a\\a"')
32 | @example(r' "test\t\t\a\\a" ')
33 | @example('"(1, 2)"')
34 | @example('(1, 2)')
35 | @example('(1, 2)')
36 | @example('(1, 2) ')
37 | @example('a,b,c,d')
38 | @example('(a,b,c,d)')
39 | @example('[a,b,c,d]')
40 | @example('{a,b,c,d}')
41 | @example('test:(a,b,c,d)')
42 | @example('{test:(a,b,c,d)}')
43 | @example('{test:a,b,c,d}')
44 | @example('{test:a,b:(c,d)}') # Note: Edit distance may be high for dicts.
45 | @example('0,')
46 | @example('#')
47 | @example('A#00000') # Note: '#'' is treated as a comment.
48 | @example('\x80') # Note: Causes UnicodeDecodeError.
49 | @example(100 * '[' + '0') # Note: Causes MemoryError.
50 | @example('\r\r\r\r1\r\r')
51 | def testDefaultParseValueFuzz(self, value):
52 | try:
53 | result = parser.DefaultParseValue(value)
54 | except TypeError:
55 | # It's OK to get a TypeError if the string has the null character.
56 | if '\x00' in value:
57 | return
58 | raise
59 | except MemoryError:
60 | if len(value) > 100:
61 | # This is not what we're testing.
62 | return
63 | raise
64 |
65 | try:
66 | uvalue = str(value)
67 | uresult = str(result)
68 | except UnicodeDecodeError:
69 | # This is not what we're testing.
70 | return
71 |
72 | # Check that the parsed value doesn't differ too much from the input.
73 | distance = Levenshtein.distance(uresult, uvalue)
74 | max_distance = (
75 | 2 + # Quotes or parenthesis can be implicit.
76 | sum(c.isspace() for c in value) +
77 | value.count('"') + value.count("'") +
78 | 3 * (value.count(',') + 1) + # 'a,' can expand to "'a', "
79 | 3 * (value.count(':')) + # 'a:' can expand to "'a': "
80 | 2 * value.count('\\'))
81 | if '#' in value:
82 | max_distance += len(value) - value.index('#')
83 |
84 | if not isinstance(result, str):
85 | max_distance += value.count('0') # Leading 0s are stripped.
86 |
87 | # Note: We don't check distance for dicts since item order can be changed.
88 | if '{' not in value:
89 | self.assertLessEqual(distance, max_distance,
90 | (distance, max_distance, uvalue, uresult))
91 |
92 |
93 | if __name__ == '__main__':
94 | testutils.main()
95 |
--------------------------------------------------------------------------------
/fire/parser_test.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Tests for the parser module."""
16 |
17 | from fire import parser
18 | from fire import testutils
19 |
20 |
21 | class ParserTest(testutils.BaseTestCase):
22 |
23 | def testCreateParser(self):
24 | self.assertIsNotNone(parser.CreateParser())
25 |
26 | def testSeparateFlagArgs(self):
27 | self.assertEqual(parser.SeparateFlagArgs([]), ([], []))
28 | self.assertEqual(parser.SeparateFlagArgs(['a', 'b']), (['a', 'b'], []))
29 | self.assertEqual(parser.SeparateFlagArgs(['a', 'b', '--']),
30 | (['a', 'b'], []))
31 | self.assertEqual(parser.SeparateFlagArgs(['a', 'b', '--', 'c']),
32 | (['a', 'b'], ['c']))
33 | self.assertEqual(parser.SeparateFlagArgs(['--']),
34 | ([], []))
35 | self.assertEqual(parser.SeparateFlagArgs(['--', 'c', 'd']),
36 | ([], ['c', 'd']))
37 | self.assertEqual(parser.SeparateFlagArgs(['a', 'b', '--', 'c', 'd']),
38 | (['a', 'b'], ['c', 'd']))
39 | self.assertEqual(parser.SeparateFlagArgs(['a', 'b', '--', 'c', 'd', '--']),
40 | (['a', 'b', '--', 'c', 'd'], []))
41 | self.assertEqual(parser.SeparateFlagArgs(['a', 'b', '--', 'c', '--', 'd']),
42 | (['a', 'b', '--', 'c'], ['d']))
43 |
44 | def testDefaultParseValueStrings(self):
45 | self.assertEqual(parser.DefaultParseValue('hello'), 'hello')
46 | self.assertEqual(parser.DefaultParseValue('path/file.jpg'), 'path/file.jpg')
47 | self.assertEqual(parser.DefaultParseValue('hello world'), 'hello world')
48 | self.assertEqual(parser.DefaultParseValue('--flag'), '--flag')
49 |
50 | def testDefaultParseValueQuotedStrings(self):
51 | self.assertEqual(parser.DefaultParseValue("'hello'"), 'hello')
52 | self.assertEqual(parser.DefaultParseValue("'hello world'"), 'hello world')
53 | self.assertEqual(parser.DefaultParseValue("'--flag'"), '--flag')
54 | self.assertEqual(parser.DefaultParseValue('"hello"'), 'hello')
55 | self.assertEqual(parser.DefaultParseValue('"hello world"'), 'hello world')
56 | self.assertEqual(parser.DefaultParseValue('"--flag"'), '--flag')
57 |
58 | def testDefaultParseValueSpecialStrings(self):
59 | self.assertEqual(parser.DefaultParseValue('-'), '-')
60 | self.assertEqual(parser.DefaultParseValue('--'), '--')
61 | self.assertEqual(parser.DefaultParseValue('---'), '---')
62 | self.assertEqual(parser.DefaultParseValue('----'), '----')
63 | self.assertEqual(parser.DefaultParseValue('None'), None)
64 | self.assertEqual(parser.DefaultParseValue("'None'"), 'None')
65 |
66 | def testDefaultParseValueNumbers(self):
67 | self.assertEqual(parser.DefaultParseValue('23'), 23)
68 | self.assertEqual(parser.DefaultParseValue('-23'), -23)
69 | self.assertEqual(parser.DefaultParseValue('23.0'), 23.0)
70 | self.assertIsInstance(parser.DefaultParseValue('23'), int)
71 | self.assertIsInstance(parser.DefaultParseValue('23.0'), float)
72 | self.assertEqual(parser.DefaultParseValue('23.5'), 23.5)
73 | self.assertEqual(parser.DefaultParseValue('-23.5'), -23.5)
74 |
75 | def testDefaultParseValueStringNumbers(self):
76 | self.assertEqual(parser.DefaultParseValue("'23'"), '23')
77 | self.assertEqual(parser.DefaultParseValue("'23.0'"), '23.0')
78 | self.assertEqual(parser.DefaultParseValue("'23.5'"), '23.5')
79 | self.assertEqual(parser.DefaultParseValue('"23"'), '23')
80 | self.assertEqual(parser.DefaultParseValue('"23.0"'), '23.0')
81 | self.assertEqual(parser.DefaultParseValue('"23.5"'), '23.5')
82 |
83 | def testDefaultParseValueQuotedStringNumbers(self):
84 | self.assertEqual(parser.DefaultParseValue('"\'123\'"'), "'123'")
85 |
86 | def testDefaultParseValueOtherNumbers(self):
87 | self.assertEqual(parser.DefaultParseValue('1e5'), 100000.0)
88 |
89 | def testDefaultParseValueLists(self):
90 | self.assertEqual(parser.DefaultParseValue('[1, 2, 3]'), [1, 2, 3])
91 | self.assertEqual(parser.DefaultParseValue('[1, "2", 3]'), [1, '2', 3])
92 | self.assertEqual(parser.DefaultParseValue('[1, \'"2"\', 3]'), [1, '"2"', 3])
93 | self.assertEqual(parser.DefaultParseValue(
94 | '[1, "hello", 3]'), [1, 'hello', 3])
95 |
96 | def testDefaultParseValueBareWordsLists(self):
97 | self.assertEqual(parser.DefaultParseValue('[one, 2, "3"]'), ['one', 2, '3'])
98 |
99 | def testDefaultParseValueDict(self):
100 | self.assertEqual(
101 | parser.DefaultParseValue('{"abc": 5, "123": 1}'), {'abc': 5, '123': 1})
102 |
103 | def testDefaultParseValueNone(self):
104 | self.assertEqual(parser.DefaultParseValue('None'), None)
105 |
106 | def testDefaultParseValueBool(self):
107 | self.assertEqual(parser.DefaultParseValue('True'), True)
108 | self.assertEqual(parser.DefaultParseValue('False'), False)
109 |
110 | def testDefaultParseValueBareWordsTuple(self):
111 | self.assertEqual(parser.DefaultParseValue('(one, 2, "3")'), ('one', 2, '3'))
112 | self.assertEqual(parser.DefaultParseValue('one, "2", 3'), ('one', '2', 3))
113 |
114 | def testDefaultParseValueNestedContainers(self):
115 | self.assertEqual(
116 | parser.DefaultParseValue(
117 | '[(A, 2, "3"), 5, {alpha: 10.2, beta: "cat"}]'),
118 | [('A', 2, '3'), 5, {'alpha': 10.2, 'beta': 'cat'}])
119 |
120 | def testDefaultParseValueComments(self):
121 | self.assertEqual(parser.DefaultParseValue('"0#comments"'), '0#comments')
122 | # Comments are stripped. This behavior may change in the future.
123 | self.assertEqual(parser.DefaultParseValue('0#comments'), 0)
124 |
125 | def testDefaultParseValueBadLiteral(self):
126 | # If it can't be parsed, we treat it as a string. This behavior may change.
127 | self.assertEqual(
128 | parser.DefaultParseValue('[(A, 2, "3"), 5'), '[(A, 2, "3"), 5')
129 | self.assertEqual(parser.DefaultParseValue('x=10'), 'x=10')
130 |
131 | def testDefaultParseValueSyntaxError(self):
132 | # If it can't be parsed, we treat it as a string.
133 | self.assertEqual(parser.DefaultParseValue('"'), '"')
134 |
135 | def testDefaultParseValueIgnoreBinOp(self):
136 | self.assertEqual(parser.DefaultParseValue('2017-10-10'), '2017-10-10')
137 | self.assertEqual(parser.DefaultParseValue('1+1'), '1+1')
138 |
139 | if __name__ == '__main__':
140 | testutils.main()
141 |
--------------------------------------------------------------------------------
/fire/test_components_bin.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Python Fire test components Fire CLI.
16 |
17 | This file is useful for replicating test results manually.
18 | """
19 |
20 | import fire
21 | from fire import test_components
22 |
23 |
24 | def main():
25 | fire.Fire(test_components)
26 |
27 | if __name__ == '__main__':
28 | main()
29 |
--------------------------------------------------------------------------------
/fire/test_components_py3.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """This module has components that use Python 3 specific syntax."""
16 |
17 | import asyncio
18 | import functools
19 | from typing import Tuple
20 |
21 |
22 | # pylint: disable=keyword-arg-before-vararg
23 | def identity(arg1, arg2: int, arg3=10, arg4: int = 20, *arg5,
24 | arg6, arg7: int, arg8=30, arg9: int = 40, **arg10):
25 | return arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10
26 |
27 |
28 | class HelpTextComponent:
29 |
30 | def identity(self, *, alpha, beta='0'):
31 | return alpha, beta
32 |
33 |
34 | class KeywordOnly:
35 |
36 | def double(self, *, count):
37 | return count * 2
38 |
39 | def triple(self, *, count):
40 | return count * 3
41 |
42 | def with_default(self, *, x="x"):
43 | print("x: " + x)
44 |
45 |
46 | class LruCacheDecoratedMethod:
47 |
48 | @functools.lru_cache()
49 | def lru_cache_in_class(self, arg1):
50 | return arg1
51 |
52 |
53 | @functools.lru_cache()
54 | def lru_cache_decorated(arg1):
55 | return arg1
56 |
57 |
58 | class WithAsyncio:
59 |
60 | async def double(self, count=0):
61 | return 2 * count
62 |
63 |
64 | class WithTypes:
65 | """Class with functions that have default arguments and types."""
66 |
67 | def double(self, count: float) -> float:
68 | """Returns the input multiplied by 2.
69 |
70 | Args:
71 | count: Input number that you want to double.
72 |
73 | Returns:
74 | A number that is the double of count.
75 | """
76 | return 2 * count
77 |
78 | def long_type(
79 | self,
80 | long_obj: (Tuple[Tuple[Tuple[Tuple[Tuple[Tuple[Tuple[
81 | Tuple[Tuple[Tuple[Tuple[Tuple[int]]]]]]]]]]]])
82 | ):
83 | return long_obj
84 |
85 |
86 | class WithDefaultsAndTypes:
87 | """Class with functions that have default arguments and types."""
88 |
89 | def double(self, count: float = 0) -> float:
90 | """Returns the input multiplied by 2.
91 |
92 | Args:
93 | count: Input number that you want to double.
94 |
95 | Returns:
96 | A number that is the double of count.
97 | """
98 | return 2 * count
99 |
100 | def get_int(self, value: int = None):
101 | return 0 if value is None else value
102 |
--------------------------------------------------------------------------------
/fire/test_components_test.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Tests for the test_components module."""
16 |
17 | from fire import test_components as tc
18 | from fire import testutils
19 |
20 |
21 | class TestComponentsTest(testutils.BaseTestCase):
22 | """Tests to verify that the test components are importable and okay."""
23 |
24 | def testTestComponents(self):
25 | self.assertIsNotNone(tc.Empty)
26 | self.assertIsNotNone(tc.OldStyleEmpty)
27 |
28 | def testNonComparable(self):
29 | with self.assertRaises(ValueError):
30 | tc.NonComparable() != 2 # pylint: disable=expression-not-assigned
31 | with self.assertRaises(ValueError):
32 | tc.NonComparable() == 2 # pylint: disable=expression-not-assigned
33 |
34 |
35 | if __name__ == '__main__':
36 | testutils.main()
37 |
--------------------------------------------------------------------------------
/fire/testutils.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Utilities for Python Fire's tests."""
16 |
17 | import contextlib
18 | import io
19 | import os
20 | import re
21 | import sys
22 | import unittest
23 | from unittest import mock
24 |
25 | from fire import core
26 | from fire import trace
27 |
28 |
29 | class BaseTestCase(unittest.TestCase):
30 | """Shared test case for Python Fire tests."""
31 |
32 | @contextlib.contextmanager
33 | def assertOutputMatches(self, stdout='.*', stderr='.*', capture=True):
34 | """Asserts that the context generates stdout and stderr matching regexps.
35 |
36 | Note: If wrapped code raises an exception, stdout and stderr will not be
37 | checked.
38 |
39 | Args:
40 | stdout: (str) regexp to match against stdout (None will check no stdout)
41 | stderr: (str) regexp to match against stderr (None will check no stderr)
42 | capture: (bool, default True) do not bubble up stdout or stderr
43 |
44 | Yields:
45 | Yields to the wrapped context.
46 | """
47 | stdout_fp = io.StringIO()
48 | stderr_fp = io.StringIO()
49 | try:
50 | with mock.patch.object(sys, 'stdout', stdout_fp):
51 | with mock.patch.object(sys, 'stderr', stderr_fp):
52 | yield
53 | finally:
54 | if not capture:
55 | sys.stdout.write(stdout_fp.getvalue())
56 | sys.stderr.write(stderr_fp.getvalue())
57 |
58 | for name, regexp, fp in [('stdout', stdout, stdout_fp),
59 | ('stderr', stderr, stderr_fp)]:
60 | value = fp.getvalue()
61 | if regexp is None:
62 | if value:
63 | raise AssertionError('%s: Expected no output. Got: %r' %
64 | (name, value))
65 | else:
66 | if not re.search(regexp, value, re.DOTALL | re.MULTILINE):
67 | raise AssertionError('%s: Expected %r to match %r' %
68 | (name, value, regexp))
69 |
70 | @contextlib.contextmanager
71 | def assertRaisesFireExit(self, code, regexp='.*'):
72 | """Asserts that a FireExit error is raised in the context.
73 |
74 | Allows tests to check that Fire's wrapper around SystemExit is raised
75 | and that a regexp is matched in the output.
76 |
77 | Args:
78 | code: The status code that the FireExit should contain.
79 | regexp: stdout must match this regex.
80 |
81 | Yields:
82 | Yields to the wrapped context.
83 | """
84 | with self.assertOutputMatches(stderr=regexp):
85 | with self.assertRaises(core.FireExit):
86 | try:
87 | yield
88 | except core.FireExit as exc:
89 | if exc.code != code:
90 | raise AssertionError('Incorrect exit code: %r != %r' %
91 | (exc.code, code))
92 | self.assertIsInstance(exc.trace, trace.FireTrace)
93 | raise
94 |
95 |
96 | @contextlib.contextmanager
97 | def ChangeDirectory(directory):
98 | """Context manager to mock a directory change and revert on exit."""
99 | cwdir = os.getcwd()
100 | os.chdir(directory)
101 |
102 | try:
103 | yield directory
104 | finally:
105 | os.chdir(cwdir)
106 |
107 |
108 | # pylint: disable=invalid-name
109 | main = unittest.main
110 | skip = unittest.skip
111 | skipIf = unittest.skipIf
112 | # pylint: enable=invalid-name
113 |
--------------------------------------------------------------------------------
/fire/testutils_test.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Test the test utilities for Fire's tests."""
16 |
17 | import sys
18 |
19 | from fire import testutils
20 |
21 |
22 | class TestTestUtils(testutils.BaseTestCase):
23 | """Let's get meta."""
24 |
25 | def testNoCheckOnException(self):
26 | with self.assertRaises(ValueError):
27 | with self.assertOutputMatches(stdout='blah'):
28 | raise ValueError()
29 |
30 | def testCheckStdoutOrStderrNone(self):
31 | with self.assertRaisesRegex(AssertionError, 'stdout:'):
32 | with self.assertOutputMatches(stdout=None):
33 | print('blah')
34 |
35 | with self.assertRaisesRegex(AssertionError, 'stderr:'):
36 | with self.assertOutputMatches(stderr=None):
37 | print('blah', file=sys.stderr)
38 |
39 | with self.assertRaisesRegex(AssertionError, 'stderr:'):
40 | with self.assertOutputMatches(stdout='apple', stderr=None):
41 | print('apple')
42 | print('blah', file=sys.stderr)
43 |
44 | def testCorrectOrderingOfAssertRaises(self):
45 | # Check to make sure FireExit tests are correct.
46 | with self.assertOutputMatches(stdout='Yep.*first.*second'):
47 | with self.assertRaises(ValueError):
48 | print('Yep, this is the first line.\nThis is the second.')
49 | raise ValueError()
50 |
51 |
52 | if __name__ == '__main__':
53 | testutils.main()
54 |
--------------------------------------------------------------------------------
/fire/trace.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """This module has classes for tracing the execution of a Fire execution.
16 |
17 | A FireTrace consists of a sequence of FireTraceElement objects. Each element
18 | represents an action taken by Fire during a single Fire execution. An action may
19 | be instantiating a class, calling a routine, or accessing a property.
20 |
21 | Each action consumes args and results in a new component. The final component
22 | is serialized to stdout by Fire as well as returned by the Fire method. If
23 | a Fire usage error occurs, such as insufficient arguments being provided to call
24 | a function, then that error will be captured in the trace and the final
25 | component will be None.
26 | """
27 |
28 | import shlex
29 |
30 | from fire import inspectutils
31 |
32 | INITIAL_COMPONENT = 'Initial component'
33 | INSTANTIATED_CLASS = 'Instantiated class'
34 | CALLED_ROUTINE = 'Called routine'
35 | CALLED_CALLABLE = 'Called callable'
36 | ACCESSED_PROPERTY = 'Accessed property'
37 | COMPLETION_SCRIPT = 'Generated completion script'
38 | INTERACTIVE_MODE = 'Entered interactive mode'
39 |
40 |
41 | class FireTrace:
42 | """A FireTrace represents the steps taken during a single Fire execution.
43 |
44 | A FireTrace consists of a sequence of FireTraceElement objects. Each element
45 | represents an action taken by Fire during a single Fire execution. An action
46 | may be instantiating a class, calling a routine, or accessing a property.
47 | """
48 |
49 | def __init__(self, initial_component, name=None, separator='-', verbose=False,
50 | show_help=False, show_trace=False):
51 | initial_trace_element = FireTraceElement(
52 | component=initial_component,
53 | action=INITIAL_COMPONENT,
54 | )
55 |
56 | self.name = name
57 | self.separator = separator
58 | self.elements = [initial_trace_element]
59 | self.verbose = verbose
60 | self.show_help = show_help
61 | self.show_trace = show_trace
62 |
63 | def GetResult(self):
64 | """Returns the component from the last element of the trace."""
65 | # pytype: disable=attribute-error
66 | return self.GetLastHealthyElement().component
67 | # pytype: enable=attribute-error
68 |
69 | def GetLastHealthyElement(self):
70 | """Returns the last element of the trace that is not an error.
71 |
72 | This element will contain the final component indicated by the trace.
73 |
74 | Returns:
75 | The last element of the trace that is not an error.
76 | """
77 | for element in reversed(self.elements):
78 | if not element.HasError():
79 | return element
80 | return self.elements[0] # The initial element is always healthy.
81 |
82 | def HasError(self):
83 | """Returns whether the Fire execution encountered a Fire usage error."""
84 | return self.elements[-1].HasError()
85 |
86 | def AddAccessedProperty(self, component, target, args, filename, lineno):
87 | element = FireTraceElement(
88 | component=component,
89 | action=ACCESSED_PROPERTY,
90 | target=target,
91 | args=args,
92 | filename=filename,
93 | lineno=lineno,
94 | )
95 | self.elements.append(element)
96 |
97 | def AddCalledComponent(self, component, target, args, filename, lineno,
98 | capacity, action=CALLED_CALLABLE):
99 | """Adds an element to the trace indicating that a component was called.
100 |
101 | Also applies to instantiating a class.
102 |
103 | Args:
104 | component: The result of calling the callable.
105 | target: The name of the callable.
106 | args: The args consumed in order to call this callable.
107 | filename: The file in which the callable is defined, or None if N/A.
108 | lineno: The line number on which the callable is defined, or None if N/A.
109 | capacity: (bool) Whether the callable could have accepted additional args.
110 | action: The value to include as the action in the FireTraceElement.
111 | """
112 | element = FireTraceElement(
113 | component=component,
114 | action=action,
115 | target=target,
116 | args=args,
117 | filename=filename,
118 | lineno=lineno,
119 | capacity=capacity,
120 | )
121 | self.elements.append(element)
122 |
123 | def AddCompletionScript(self, script):
124 | element = FireTraceElement(
125 | component=script,
126 | action=COMPLETION_SCRIPT,
127 | )
128 | self.elements.append(element)
129 |
130 | def AddInteractiveMode(self):
131 | element = FireTraceElement(action=INTERACTIVE_MODE)
132 | self.elements.append(element)
133 |
134 | def AddError(self, error, args):
135 | element = FireTraceElement(error=error, args=args)
136 | self.elements.append(element)
137 |
138 | def AddSeparator(self):
139 | """Marks that the most recent element of the trace used a separator.
140 |
141 | A separator is an argument you can pass to a Fire CLI to separate args left
142 | of the separator from args right of the separator.
143 |
144 | Here's an example to demonstrate the separator. Let's say you have a
145 | function that takes a variable number of args, and you want to call that
146 | function, and then upper case the result. Here's how to do it:
147 |
148 | # in Python
149 | def display(arg1, arg2='!'):
150 | return arg1 + arg2
151 |
152 | # from Bash (the default separator is the hyphen -)
153 | display hello # hello!
154 | display hello upper # helloupper
155 | display hello - upper # HELLO!
156 |
157 | Note how the separator caused the display function to be called with the
158 | default value for arg2.
159 | """
160 | self.elements[-1].AddSeparator()
161 |
162 | def _Quote(self, arg):
163 | if arg.startswith('--') and '=' in arg:
164 | prefix, value = arg.split('=', 1)
165 | return shlex.quote(prefix) + '=' + shlex.quote(value)
166 | return shlex.quote(arg)
167 |
168 | def GetCommand(self, include_separators=True):
169 | """Returns the command representing the trace up to this point.
170 |
171 | Args:
172 | include_separators: Whether or not to include separators in the command.
173 |
174 | Returns:
175 | A string representing a Fire CLI command that would produce this trace.
176 | """
177 | args = []
178 | if self.name:
179 | args.append(self.name)
180 |
181 | for element in self.elements:
182 | if element.HasError():
183 | continue
184 | if element.args:
185 | args.extend(element.args)
186 | if element.HasSeparator() and include_separators:
187 | args.append(self.separator)
188 |
189 | if self.NeedsSeparator() and include_separators:
190 | args.append(self.separator)
191 |
192 | return ' '.join(self._Quote(arg) for arg in args)
193 |
194 | def NeedsSeparator(self):
195 | """Returns whether a separator should be added to the command.
196 |
197 | If the command is a function call, then adding an additional argument to the
198 | command sometimes would add an extra arg to the function call, and sometimes
199 | would add an arg acting on the result of the function call.
200 |
201 | This function tells us whether we should add a separator to the command
202 | before adding additional arguments in order to make sure the arg is applied
203 | to the result of the function call, and not the function call itself.
204 |
205 | Returns:
206 | Whether a separator should be added to the command if order to keep the
207 | component referred to by the command the same when adding additional args.
208 | """
209 | element = self.GetLastHealthyElement()
210 | return element.HasCapacity() and not element.HasSeparator()
211 |
212 | def __str__(self):
213 | lines = []
214 | for index, element in enumerate(self.elements):
215 | line = f'{index + 1}. {element}'
216 | lines.append(line)
217 | return '\n'.join(lines)
218 |
219 | def NeedsSeparatingHyphenHyphen(self, flag='help'):
220 | """Returns whether a the trace need '--' before '--help'.
221 |
222 | '--' is needed when the component takes keyword arguments, when the value of
223 | flag matches one of the argument of the component, or the component takes in
224 | keyword-only arguments(e.g. argument with default value).
225 |
226 | Args:
227 | flag: the flag available for the trace
228 |
229 | Returns:
230 | True for needed '--', False otherwise.
231 |
232 | """
233 | element = self.GetLastHealthyElement()
234 | component = element.component
235 | spec = inspectutils.GetFullArgSpec(component)
236 | return (spec.varkw is not None
237 | or flag in spec.args
238 | or flag in spec.kwonlyargs)
239 |
240 |
241 | class FireTraceElement:
242 | """A FireTraceElement represents a single step taken by a Fire execution.
243 |
244 | Examples of a FireTraceElement are the instantiation of a class or the
245 | accessing of an object member.
246 | """
247 |
248 | def __init__(self,
249 | component=None,
250 | action=None,
251 | target=None,
252 | args=None,
253 | filename=None,
254 | lineno=None,
255 | error=None,
256 | capacity=None):
257 | """Instantiates a FireTraceElement.
258 |
259 | Args:
260 | component: The result of this element of the trace.
261 | action: The type of action (e.g. instantiating a class) taking place.
262 | target: (string) The name of the component being acted upon.
263 | args: The args consumed by the represented action.
264 | filename: The file in which the action is defined, or None if N/A.
265 | lineno: The line number on which the action is defined, or None if N/A.
266 | error: The error represented by the action, or None if N/A.
267 | capacity: (bool) Whether the action could have accepted additional args.
268 | """
269 | self.component = component
270 | self._action = action
271 | self._target = target
272 | self.args = args
273 | self._filename = filename
274 | self._lineno = lineno
275 | self._error = error
276 | self._separator = False
277 | self._capacity = capacity
278 |
279 | def HasError(self):
280 | return self._error is not None
281 |
282 | def HasCapacity(self):
283 | return self._capacity
284 |
285 | def HasSeparator(self):
286 | return self._separator
287 |
288 | def AddSeparator(self):
289 | self._separator = True
290 |
291 | def ErrorAsStr(self):
292 | return ' '.join(str(arg) for arg in self._error.args)
293 |
294 | def __str__(self):
295 | if self.HasError():
296 | return self.ErrorAsStr()
297 | else:
298 | # Format is: {action} "{target}" ({filename}:{lineno})
299 | string = self._action
300 | if self._target is not None:
301 | string += f' "{self._target}"'
302 | if self._filename is not None:
303 | path = self._filename
304 | if self._lineno is not None:
305 | path += f':{self._lineno}'
306 |
307 | string += f' ({path})'
308 | return string
309 |
--------------------------------------------------------------------------------
/fire/trace_test.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Tests for the trace module."""
16 |
17 | from fire import testutils
18 | from fire import trace
19 |
20 |
21 | class FireTraceTest(testutils.BaseTestCase):
22 |
23 | def testFireTraceInitialization(self):
24 | t = trace.FireTrace(10)
25 | self.assertIsNotNone(t)
26 | self.assertIsNotNone(t.elements)
27 |
28 | def testFireTraceGetResult(self):
29 | t = trace.FireTrace('start')
30 | self.assertEqual(t.GetResult(), 'start')
31 | t.AddAccessedProperty('t', 'final', None, 'example.py', 10)
32 | self.assertEqual(t.GetResult(), 't')
33 |
34 | def testFireTraceHasError(self):
35 | t = trace.FireTrace('start')
36 | self.assertFalse(t.HasError())
37 | t.AddAccessedProperty('t', 'final', None, 'example.py', 10)
38 | self.assertFalse(t.HasError())
39 | t.AddError(ValueError('example error'), ['arg'])
40 | self.assertTrue(t.HasError())
41 |
42 | def testAddAccessedProperty(self):
43 | t = trace.FireTrace('initial object')
44 | args = ('example', 'args')
45 | t.AddAccessedProperty('new component', 'prop', args, 'sample.py', 12)
46 | self.assertEqual(
47 | str(t),
48 | '1. Initial component\n2. Accessed property "prop" (sample.py:12)')
49 |
50 | def testAddCalledCallable(self):
51 | t = trace.FireTrace('initial object')
52 | args = ('example', 'args')
53 | t.AddCalledComponent('result', 'cell', args, 'sample.py', 10, False,
54 | action=trace.CALLED_CALLABLE)
55 | self.assertEqual(
56 | str(t),
57 | '1. Initial component\n2. Called callable "cell" (sample.py:10)')
58 |
59 | def testAddCalledRoutine(self):
60 | t = trace.FireTrace('initial object')
61 | args = ('example', 'args')
62 | t.AddCalledComponent('result', 'run', args, 'sample.py', 12, False,
63 | action=trace.CALLED_ROUTINE)
64 | self.assertEqual(
65 | str(t),
66 | '1. Initial component\n2. Called routine "run" (sample.py:12)')
67 |
68 | def testAddInstantiatedClass(self):
69 | t = trace.FireTrace('initial object')
70 | args = ('example', 'args')
71 | t.AddCalledComponent(
72 | 'Classname', 'classname', args, 'sample.py', 12, False,
73 | action=trace.INSTANTIATED_CLASS)
74 | target = """1. Initial component
75 | 2. Instantiated class "classname" (sample.py:12)"""
76 | self.assertEqual(str(t), target)
77 |
78 | def testAddCompletionScript(self):
79 | t = trace.FireTrace('initial object')
80 | t.AddCompletionScript('This is the completion script string.')
81 | self.assertEqual(
82 | str(t),
83 | '1. Initial component\n2. Generated completion script')
84 |
85 | def testAddInteractiveMode(self):
86 | t = trace.FireTrace('initial object')
87 | t.AddInteractiveMode()
88 | self.assertEqual(
89 | str(t),
90 | '1. Initial component\n2. Entered interactive mode')
91 |
92 | def testGetCommand(self):
93 | t = trace.FireTrace('initial object')
94 | args = ('example', 'args')
95 | t.AddCalledComponent('result', 'run', args, 'sample.py', 12, False,
96 | action=trace.CALLED_ROUTINE)
97 | self.assertEqual(t.GetCommand(), 'example args')
98 |
99 | def testGetCommandWithQuotes(self):
100 | t = trace.FireTrace('initial object')
101 | args = ('example', 'spaced arg')
102 | t.AddCalledComponent('result', 'run', args, 'sample.py', 12, False,
103 | action=trace.CALLED_ROUTINE)
104 | self.assertEqual(t.GetCommand(), "example 'spaced arg'")
105 |
106 | def testGetCommandWithFlagQuotes(self):
107 | t = trace.FireTrace('initial object')
108 | args = ('--example=spaced arg',)
109 | t.AddCalledComponent('result', 'run', args, 'sample.py', 12, False,
110 | action=trace.CALLED_ROUTINE)
111 | self.assertEqual(t.GetCommand(), "--example='spaced arg'")
112 |
113 |
114 | class FireTraceElementTest(testutils.BaseTestCase):
115 |
116 | def testFireTraceElementHasError(self):
117 | el = trace.FireTraceElement()
118 | self.assertFalse(el.HasError())
119 |
120 | el = trace.FireTraceElement(error=ValueError('example error'))
121 | self.assertTrue(el.HasError())
122 |
123 | def testFireTraceElementAsStringNoMetadata(self):
124 | el = trace.FireTraceElement(
125 | component='Example',
126 | action='Fake action',
127 | )
128 | self.assertEqual(str(el), 'Fake action')
129 |
130 | def testFireTraceElementAsStringWithTarget(self):
131 | el = trace.FireTraceElement(
132 | component='Example',
133 | action='Created toy',
134 | target='Beaker',
135 | )
136 | self.assertEqual(str(el), 'Created toy "Beaker"')
137 |
138 | def testFireTraceElementAsStringWithTargetAndLineNo(self):
139 | el = trace.FireTraceElement(
140 | component='Example',
141 | action='Created toy',
142 | target='Beaker',
143 | filename='beaker.py',
144 | lineno=10,
145 | )
146 | self.assertEqual(str(el), 'Created toy "Beaker" (beaker.py:10)')
147 |
148 |
149 | if __name__ == '__main__':
150 | testutils.main()
151 |
--------------------------------------------------------------------------------
/fire/value_types.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Types of values."""
16 |
17 | import inspect
18 |
19 | from fire import inspectutils
20 |
21 |
22 | VALUE_TYPES = (bool, str, bytes, int, float, complex,
23 | type(Ellipsis), type(None), type(NotImplemented))
24 |
25 |
26 | def IsGroup(component):
27 | # TODO(dbieber): Check if there are any subcomponents.
28 | return not IsCommand(component) and not IsValue(component)
29 |
30 |
31 | def IsCommand(component):
32 | return inspect.isroutine(component) or inspect.isclass(component)
33 |
34 |
35 | def IsValue(component):
36 | return isinstance(component, VALUE_TYPES) or HasCustomStr(component)
37 |
38 |
39 | def IsSimpleGroup(component):
40 | """If a group is simple enough, then we treat it as a value in PrintResult.
41 |
42 | Only if a group contains all value types do we consider it simple enough to
43 | print as a value.
44 |
45 | Args:
46 | component: The group to check for value-group status.
47 | Returns:
48 | A boolean indicating if the group should be treated as a value for printing
49 | purposes.
50 | """
51 | assert isinstance(component, dict)
52 | for unused_key, value in component.items():
53 | if not IsValue(value) and not isinstance(value, (list, dict)):
54 | return False
55 | return True
56 |
57 |
58 | def HasCustomStr(component):
59 | """Determines if a component has a custom __str__ method.
60 |
61 | Uses inspect.classify_class_attrs to determine the origin of the object's
62 | __str__ method, if one is present. If it defined by `object` itself, then
63 | it is not considered custom. Otherwise it is. This means that the __str__
64 | methods of primitives like ints and floats are considered custom.
65 |
66 | Objects with custom __str__ methods are treated as values and can be
67 | serialized in places where more complex objects would have their help screen
68 | shown instead.
69 |
70 | Args:
71 | component: The object to check for a custom __str__ method.
72 | Returns:
73 | Whether `component` has a custom __str__ method.
74 | """
75 | if hasattr(component, '__str__'):
76 | class_attrs = inspectutils.GetClassAttrsDict(type(component)) or {}
77 | str_attr = class_attrs.get('__str__')
78 | if str_attr and str_attr.defining_class is not object:
79 | return True
80 | return False
81 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Python Fire
2 | theme: readthedocs
3 | markdown_extensions: [fenced_code]
4 | nav:
5 | - Overview: index.md
6 | - Installation: installation.md
7 | - Benefits: benefits.md
8 | - The Python Fire Guide: guide.md
9 | - Using a CLI: using-cli.md
10 | - Troubleshooting: troubleshooting.md
11 | - Reference: api.md
12 |
--------------------------------------------------------------------------------
/pylintrc:
--------------------------------------------------------------------------------
1 | [MASTER]
2 |
3 | # Specify a configuration file.
4 | #rcfile=
5 |
6 | # Python code to execute, usually for sys.path manipulation such as
7 | # pygtk.require().
8 | #init-hook=
9 |
10 | # Add to the black list. It should be a base name, not a
11 | # path. You may set this option multiple times.
12 | ignore=
13 |
14 | # Pickle collected data for later comparisons.
15 | persistent=yes
16 |
17 | # List of plugins (as comma separated values of python modules names) to load,
18 | # usually to register additional checkers.
19 | load-plugins=
20 |
21 |
22 | [MESSAGES CONTROL]
23 |
24 | # Enable the message, report, category or checker with the given id(s). You can
25 | # either give multiple identifier separated by comma (,) or put this option
26 | # multiple time.
27 | enable=indexing-exception,old-raise-syntax
28 |
29 | # Disable the message, report, category or checker with the given id(s). You
30 | # can either give multiple identifier separated by comma (,) or put this option
31 | # multiple time.
32 | disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return,super-with-arguments,raise-missing-from,consider-using-f-string,unspecified-encoding,unnecessary-lambda-assignment,wrong-import-position,ungrouped-imports,deprecated-module
33 |
34 |
35 | [REPORTS]
36 |
37 | # Set the output format. Available formats are text, parseable, colorized, msvs
38 | # (visual studio) and html
39 | output-format=text
40 |
41 | # Tells whether to display a full report or only the messages
42 | reports=yes
43 |
44 | # Python expression which should return a note less than 10 (10 is the highest
45 | # note). You have access to the variables errors warning, statement which
46 | # respectively contain the number of errors / warnings messages and the total
47 | # number of statements analyzed. This is used by the global evaluation report
48 | # (R0004).
49 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
50 |
51 |
52 | [VARIABLES]
53 |
54 | # Tells whether we should check for unused import in __init__ files.
55 | init-import=no
56 |
57 | # A regular expression matching names used for dummy variables (i.e. not used).
58 | dummy-variables-rgx=\*{0,2}(_$|unused_|dummy_)
59 |
60 | # List of additional names supposed to be defined in builtins. Remember that
61 | # you should avoid to define new builtins when possible.
62 | additional-builtins=
63 |
64 |
65 | [BASIC]
66 |
67 | # Regular expression which should only match correct module names
68 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
69 |
70 | # Regular expression which should only match correct module level names
71 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
72 |
73 | # Regular expression which should only match correct class names
74 | class-rgx=[A-Z_][a-zA-Z0-9]+$
75 |
76 | # Regular expression which should only match correct function names
77 | function-rgx=^(?:(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$
78 |
79 | # Regular expression which should only match correct method names
80 | method-rgx=^(?:(?P__[a-z0-9_]+__|next)|(?P_{0,2}(?:test|assert)?[A-Z][a-zA-Z0-9]*)|(?:_{0,2}[a-z][a-z0-9_]*))$
81 |
82 | # Regular expression which should only match correct instance attribute names
83 | attr-rgx=^_{0,2}[a-z][a-z0-9_]*$
84 |
85 | # Regular expression which should only match correct argument names
86 | argument-rgx=^[a-z][a-z0-9_]*$
87 |
88 | # Regular expression which should only match correct variable names
89 | variable-rgx=^[a-z][a-z0-9_]*$
90 |
91 | # Regular expression which should only match correct list comprehension /
92 | # generator expression variable names
93 | inlinevar-rgx=^[a-z][a-z0-9_]*$
94 |
95 | # Good variable names which should always be accepted, separated by a comma
96 | good-names=i,j,k,ex,main,Run,_
97 |
98 | # Bad variable names which should always be refused, separated by a comma
99 | bad-names=map,filter,apply,input,reduce,foo,bar,baz,toto,tutu,tata
100 |
101 | # Regular expression which should only match functions or classes name which do
102 | # not require a docstring
103 | no-docstring-rgx=(__.*__|main|test.*|.*Test)
104 |
105 | # Minimum length for a docstring
106 | docstring-min-length=10
107 |
108 |
109 | [MISCELLANEOUS]
110 |
111 | # List of note tags to take in consideration, separated by a comma.
112 | notes=FIXME,XXX,TODO
113 |
114 |
115 | [FORMAT]
116 |
117 | # Maximum number of characters on a single line.
118 | max-line-length=80
119 |
120 | # Maximum number of lines in a module
121 | max-module-lines=99999
122 |
123 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
124 | # tab).
125 | indent-string=' '
126 |
127 |
128 | [SIMILARITIES]
129 |
130 | # Minimum lines number of a similarity.
131 | min-similarity-lines=4
132 |
133 | # Ignore comments when computing similarities.
134 | ignore-comments=yes
135 |
136 | # Ignore docstrings when computing similarities.
137 | ignore-docstrings=yes
138 |
139 |
140 | [TYPECHECK]
141 |
142 | # Tells whether missing members accessed in mixin class should be ignored. A
143 | # mixin class is detected if its name ends with "mixin" (case insensitive).
144 | ignore-mixin-members=yes
145 |
146 | # List of classes names for which member attributes should not be checked
147 | # (useful for classes with attributes dynamically set).
148 | ignored-classes=
149 |
150 | # List of members which are set dynamically and missed by pylint inference
151 | # system, and so shouldn't trigger E0201 when accessed.
152 | generated-members=
153 |
154 |
155 | [DESIGN]
156 |
157 | # Maximum number of arguments for function / method
158 | max-args=5
159 |
160 | # Argument names that match this expression will be ignored. Default to name
161 | # with leading underscore
162 | ignored-argument-names=_.*
163 |
164 | # Maximum number of locals for function / method body
165 | max-locals=15
166 |
167 | # Maximum number of return / yield for function / method body
168 | max-returns=6
169 |
170 | # Maximum number of branch for function / method body
171 | max-branches=12
172 |
173 | # Maximum number of statements in function / method body
174 | max-statements=50
175 |
176 | # Maximum number of parents for a class (see R0901).
177 | max-parents=7
178 |
179 | # Maximum number of attributes for a class (see R0902).
180 | max-attributes=7
181 |
182 | # Minimum number of public methods for a class (see R0903).
183 | min-public-methods=2
184 |
185 | # Maximum number of public methods for a class (see R0904).
186 | max-public-methods=20
187 |
188 |
189 | [IMPORTS]
190 |
191 | # Deprecated modules which should not be used, separated by a comma
192 | deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
193 |
194 | # Create a graph of every (i.e. internal and external) dependencies in the
195 | # given file (report RP0402 must not be disabled)
196 | import-graph=
197 |
198 | # Create a graph of external dependencies in the given file (report RP0402 must
199 | # not be disabled)
200 | ext-import-graph=
201 |
202 | # Create a graph of internal dependencies in the given file (report RP0402 must
203 | # not be disabled)
204 | int-import-graph=
205 |
206 |
207 | [CLASSES]
208 |
209 | # List of method names used to declare (i.e. assign) instance attributes.
210 | defining-attr-methods=__init__,__new__,setUp
211 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | .
2 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [aliases]
2 | test = pytest
3 |
4 | [tool:pytest]
5 | addopts = --ignore=fire/test_components_py3.py
6 | --ignore=fire/parser_fuzz_test.py
7 |
8 | [pytype]
9 | inputs = .
10 | output = .pytype
11 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2018 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """The setup.py file for Python Fire."""
16 |
17 | from setuptools import setup
18 |
19 | LONG_DESCRIPTION = """
20 | Python Fire is a library for automatically generating command line interfaces
21 | (CLIs) with a single line of code.
22 |
23 | It will turn any Python module, class, object, function, etc. (any Python
24 | component will work!) into a CLI. It's called Fire because when you call Fire(),
25 | it fires off your command.
26 | """.strip()
27 |
28 | SHORT_DESCRIPTION = """
29 | A library for automatically generating command line interfaces.""".strip()
30 |
31 | DEPENDENCIES = [
32 | 'termcolor',
33 | ]
34 |
35 | TEST_DEPENDENCIES = [
36 | 'hypothesis',
37 | 'levenshtein',
38 | ]
39 |
40 | VERSION = '0.7.0'
41 | URL = 'https://github.com/google/python-fire'
42 |
43 | setup(
44 | name='fire',
45 | version=VERSION,
46 | description=SHORT_DESCRIPTION,
47 | long_description=LONG_DESCRIPTION,
48 | url=URL,
49 |
50 | author='David Bieber',
51 | author_email='dbieber@google.com',
52 | license='Apache Software License',
53 |
54 | classifiers=[
55 | 'Development Status :: 4 - Beta',
56 |
57 | 'Intended Audience :: Developers',
58 | 'Topic :: Software Development :: Libraries :: Python Modules',
59 |
60 | 'License :: OSI Approved :: Apache Software License',
61 |
62 | 'Programming Language :: Python',
63 | 'Programming Language :: Python :: 3',
64 | 'Programming Language :: Python :: 3.7',
65 | 'Programming Language :: Python :: 3.8',
66 | 'Programming Language :: Python :: 3.9',
67 | 'Programming Language :: Python :: 3.10',
68 | 'Programming Language :: Python :: 3.11',
69 | 'Programming Language :: Python :: 3.12',
70 | 'Programming Language :: Python :: 3.13',
71 |
72 | 'Operating System :: OS Independent',
73 | 'Operating System :: POSIX',
74 | 'Operating System :: MacOS',
75 | 'Operating System :: Unix',
76 | ],
77 |
78 | keywords='command line interface cli python fire interactive bash tool',
79 |
80 | requires_python='>=3.7',
81 | packages=['fire', 'fire.console'],
82 |
83 | install_requires=DEPENDENCIES,
84 | tests_require=TEST_DEPENDENCIES,
85 | )
86 |
--------------------------------------------------------------------------------