├── .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 [![PyPI](https://img.shields.io/pypi/pyversions/fire.svg?style=plastic)](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 [![PyPI](https://img.shields.io/pypi/pyversions/fire.svg?style=plastic)](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 | --------------------------------------------------------------------------------