56 Commits
Author SHA1 Message Date
David Bieber 669cf281a5 Split helputils into helputils (old, still used) and helptext (new module, scheduled to replace helputils)
PiperOrigin-RevId: 240429364
Change-Id: Ifddbb6a4aff20407df9a84931898239d0b26649c
2019-03-26 14:36:22 -07:00
Rebecca Chen c507c093fa Fix or ignore type errors generated by the next release of pytype.
PiperOrigin-RevId: 239662678
Change-Id: Iab3c34c8c5b59e0de2b0ff887699428fe28c68d5
2019-03-21 13:40:51 -07:00
Joe Chen 2f80522f6d Fixed naming of test cases broke travis build
PiperOrigin-RevId: 239256432
Change-Id: Idb349ed14613fbb3d65082eac6e0d7dc1f30555c
2019-03-19 13:27:31 -07:00
Joe Chen 665c497693 Generating flags information for function objects
PiperOrigin-RevId: 239247774
Change-Id: I2dc83a9006f94239cfc5b267c434304e4317f39a
2019-03-19 12:41:36 -07:00
David Bieber 36fcb1995b Resolve no-else-raise linter errors.
PiperOrigin-RevId: 236714504
Change-Id: Ifaeee426ee05a5e5deb3039639da6bd6ab8de7d8
2019-03-04 13:33:17 -08:00
Rebecca Chen 1c1f6dd110 Add type-checking to Python Fire via pytype.
PiperOrigin-RevId: 236693773
Change-Id: I8eb393b69283445b952780c288704e4b8e262628
2019-03-04 11:39:00 -08:00
David Bieber 2363b5f2b7 Mark Python 3.7 support in setup.py
PiperOrigin-RevId: 235782693
Change-Id: I55f0bfd76f6d1750223ca7378581985d48fae14d
2019-02-26 13:35:17 -08:00
David Bieber 3578817f16 Enable pylint for all Python versions
PiperOrigin-RevId: 235739402
Change-Id: Ia95401b3983546c6b392789b12ed5e7754cf5c8f
2019-02-26 09:52:44 -08:00
David Bieber 2bfb82ef23 Support Python 3.7 in travis tests for Python Fire
PiperOrigin-RevId: 235738586
Change-Id: I85b79f073b2db8b95e98d38bb3b3e7bcce4b8ab6
2019-02-26 09:47:38 -08:00
Andrew Au 668007ae41 initial support for Python 3.7 and additional builtins
PiperOrigin-RevId: 235730005
Change-Id: Idcbbd79d074aeb0db0133bc6d72f7d61df555f81
2019-02-26 08:56:29 -08:00
David Bieber 6dc6531695 disable g-bad-exception-name for FireExit Exception.
PiperOrigin-RevId: 235251946
Change-Id: Id6ff561b840e415bdf564b1ef01e8434e13ddf33
2019-02-22 13:50:12 -08:00
David Bieber 9104daf7ae clean up of completion enhancement
PiperOrigin-RevId: 235251580
Change-Id: Ia8b4c9cfb2688f74be1a2ee7e5a088ae42246409
2019-02-22 13:48:17 -08:00
David Bieber 7538c3dd82 Decrease max_examples by 10x
PiperOrigin-RevId: 235229117
Change-Id: I6a50ff559a738cced52779e52515ca09d819f3b4
2019-02-22 16:01:31 -05:00
David Bieber 7651785f96 Completion enhancements cleanup (ought to be nearly a no-op). Completion enhancements author: meshde
PiperOrigin-RevId: 235075422
Change-Id: I56ad1357b7ba9a021110a760e7095a6fb36e8ab8
2019-02-22 16:01:09 -05:00
David Bieber cb311f16a8 Reduce number of examples for docstrings fuzzing by 10x.
PiperOrigin-RevId: 235063811
Change-Id: I525b64a43fd78e96730efd19c572d56859ab2c97
2019-02-22 16:01:00 -05:00
David Bieber 8afe885e6d Adds copybara workflow for Python Fire for creating a CL from a PR.
Usage:
copybara third_party/py/fire/copy.bara.sky github_pr_to_piper 144
PiperOrigin-RevId: 235040108
Change-Id: I2af5727890cdce30116b3e783a7a05b16b91db86
2019-02-22 16:00:50 -05:00
Mehmood Deshmukh 5f2fa98518 Completion Enhancements (#144)
Completion Enhancements:
* Makes option flags available in completion even if prev word is another option
* Ensure module flags appear before a func or after its args
2019-02-21 17:03:16 -05:00
David Bieber 00608beef2 don't run pylint against the console package.
PiperOrigin-RevId: 235017763
Change-Id: I54b3d1a46525fea184795ff7fbe56b6e1edc78dd
2019-02-21 10:16:36 -08:00
David Bieber e35e650f72 Add __init__.py to console package.
PiperOrigin-RevId: 235013192
Change-Id: I47e7da8685b58dae44527da971c9dbd88fcfd92b
2019-02-21 09:52:42 -08:00
David Bieber f47c938b8e quick lint fixes for helputils
PiperOrigin-RevId: 235010954
Change-Id: I1db851071dfe7d0f6c8e6715dd0248ed3ad97d93
2019-02-21 09:39:04 -08:00
David Bieber 42ff6cfa53 Use the console package in Python Fire for help text
PiperOrigin-RevId: 235010314
Change-Id: I7095c36bfb28d4c4a059654656334322185f3d3b
2019-02-21 09:35:40 -08:00
Joe Chen c11a3fa793 HelpTextForFunction: detailed help text for functions
Implemented HelpTextForFunction to generate detail help text for function component.

PiperOrigin-RevId: 235009112
Change-Id: Ia627ff59c70c48c27d3d1afdc46de1ac209459cd
2019-02-21 09:29:16 -08:00
David Bieber c13d87beee Release console package as part of fire.
PiperOrigin-RevId: 234196877
Change-Id: I17ff52e9330e2f009e8f86323662cf2abeda4389
2019-02-15 12:57:06 -08:00
David Bieber c99a034998 Add deadline to hypothesis docstring test.
PiperOrigin-RevId: 231674992
Change-Id: If476a6d13976881539e61807efded5855e5321d6
2019-01-30 15:05:00 -08:00
David Bieber b9aafbdf34 Add docstring about helputils.
PiperOrigin-RevId: 231648600
Change-Id: Iac178bf1b1ea8099f51f8288041517c23fd445d5
2019-01-30 12:43:06 -08:00
David Bieber 855f317c31 Shorten line too long in helputils 2019-01-30 15:26:23 -05:00
David Bieber a81d1d2f13 Add docstring about interactive mode to interact.py.
PiperOrigin-RevId: 231638941
Change-Id: If574662dceaf7b219b9124cb9c33ee76ec104f97
2019-01-30 15:17:27 -05:00
David Bieber 105930e3d9 Remove deprecated hypothesis setting.
PiperOrigin-RevId: 230545402
Change-Id: Ib1ead3e70bfec9a7fe1aa803200c307fd5d84375
2019-01-30 15:17:21 -05:00
Python Fire Team e58c858dbf Implemented HelpTextForObject
-Generates detailed help screen for objects.
-Added basic test case to test help text generation.

PiperOrigin-RevId: 229994423
2019-01-30 15:17:16 -05:00
Florian Schäfer 57c777316d Make namedtuple fields accessible by their name (#161)
* Make namedtuple fields accessible by their name (in addition to their indexes)
2019-01-03 11:22:59 -08:00
David Bieber 871ffabb00 Docs and tests
- License in README
- _IncludeMember docstring
- testUsageOutputVerbose

PiperOrigin-RevId: 225603056
Change-Id: Iceed29fa261026f3f41e6e29ddeb76147e2051fe
Reviewed-on: https://team-review.git.corp.google.com/c/378384
Reviewed-by: David Bieber <dbieber@google.com>
2018-12-14 15:07:43 -08:00
Bruno P. Kinoshita 69f1ef0417 Fix minor typos (#154) 2018-12-13 22:03:09 -08:00
David Bieber 1d0465e8c1 docstring parser and initial commit for new usage screens.
PiperOrigin-RevId: 224036127
Change-Id: Ia0cd2d79b743f31ea7b6fc00686338a79fcca79b
Reviewed-on: https://team-review.git.corp.google.com/c/370510
Reviewed-by: David Bieber <dbieber@google.com>
2018-12-04 14:30:42 -08:00
James George 5867c55439 include license information in readme (#147)
Add license information
2018-11-30 16:30:46 -05:00
David Bieber 687bf37aa7 Inline _CallCallable into _CallAndUpdateTrace.
PiperOrigin-RevId: 216367260
Change-Id: I5ab2e522be81655ba412bf534d3493a36bcd181f
Reviewed-on: https://team-review.git.corp.google.com/c/332673
Reviewed-by: David Bieber <dbieber@google.com>
2018-10-09 10:26:05 -07:00
David Bieber 5f83beecda Adds test for 1-letter arg names.
PiperOrigin-RevId: 216285688
Change-Id: I5ab05bc08432562e3c5e30f7bad43d7765ba2f70
Reviewed-on: https://team-review.git.corp.google.com/c/332036
Reviewed-by: David Bieber <dbieber@google.com>
2018-10-08 18:30:27 -07:00
David Bieber fe1fda46d9 Single-character flag improvements:
Gives exact matches priority for shortcut flags (-a)
and supports equal signs with single-character args (-a=10 should be OK)

PiperOrigin-RevId: 216056777
Change-Id: I2fdf96842b59b2b90c15b384a13997b800c538c4
Reviewed-on: https://team-review.git.corp.google.com/c/332035
Reviewed-by: David Bieber <dbieber@google.com>
2018-10-08 18:15:05 -07:00
David Bieber 53a3498fd2 Support single-hyphen flags.
PiperOrigin-RevId: 215983601
Change-Id: I7d7d1323b786bc93f3f311a609e7b8f836011ac4
Reviewed-on: https://team-review.git.corp.google.com/c/330493
Reviewed-by: David Bieber <dbieber@google.com>
2018-10-05 16:30:19 -07:00
David Bieber 5347858957 Support capital letters for single-char flags
PiperOrigin-RevId: 215968361
Change-Id: I60cbabf5c74bdba78127c664dcf629436ec5d390
Reviewed-on: https://team-review.git.corp.google.com/c/330541
Reviewed-by: David Bieber <dbieber@google.com>
2018-10-05 16:08:56 -07:00
David Bieber f37dd2b8a5 Refactoring of some helputils, lint clean up, and a spelling fix.
Copybara generated commit for Python Fire.

PiperOrigin-RevId: 215941316
Change-Id: I32a36103b5f4de2dc396a11465baf3a3a95734e0
Reviewed-on: https://team-review.git.corp.google.com/c/330453
Reviewed-by: David Bieber <dbieber@google.com>
2018-10-05 13:42:01 -07:00
Alex Shadley 0a6bb9d754 Issue #108: Shortcuts for boolean arguments (#141)
* adds support for parsing shortcuts for single-character arguments, along with tests
2018-10-05 11:29:54 -07:00
Mickaël Schoentgen 54f91a21c0 Fix warnings in the diff example (#135)
Fix ResourceWarning (unclosed file) and DeprecationWarning ('U' mode is deprecated)
2018-08-28 16:21:50 -07:00
David Bieber 6e0ca736a9 Consolidate fire trace methods into AddCalledComponent
PiperOrigin-RevId: 206324964
Change-Id: I691b0882fc216221be122181eb80d3526e81a15c
Reviewed-on: https://team-review.git.corp.google.com/293492
Reviewed-by: David Bieber <dbieber@google.com>
2018-07-27 09:09:53 -07:00
David Bieber 625b305d9d Adds useless-object-inheritance and no-else-return to pylintrc disable
PiperOrigin-RevId: 205717397
Change-Id: I0aa6fd6be26b6629db5949515e984873175d861d
Reviewed-on: https://team-review.git.corp.google.com/291456
Reviewed-by: David Bieber <dbieber@google.com>
2018-07-23 13:59:55 -07:00
Neal Fultz 80ee2564ac Fixes bug setting stories (#130)
The example wass slightly broken, and always sets stories=1. Also changed the phew to be after a whole floor instead of every single step.
2018-07-02 11:05:12 -07:00
Joe Chen b55cebd963 Copybara generated commit for Python Fire.
- Added support of calling callable to Python Fire.
  - Use six.text_type instead of unicode for Python 2/3 compatibility in parser_fuzz_test.

PiperOrigin-RevId: 202218864
Change-Id: I87a1a719c16c2162722f4bbee749eafb485b3f99
Reviewed-on: https://team-review.git.corp.google.com/280130
Reviewed-by: Joe Chen <zuhaochen@google.com>
2018-06-28 15:48:44 -07:00
cclauss eb8f053cee Change unicode() —> six.text_type() for Python 3 (#128)
See #111
2018-06-21 21:05:15 -07:00
Joe Chen 021d627c92 Copybara generated commit for Python Fire.
- Formatting for completion.
  - Preserving order of keys when printing result which is an OrderedDict.
  - Allow use of --help without --.

PiperOrigin-RevId: 201601732
Change-Id: Icba7e663634f5153283095d8b177d834af480b15
Reviewed-on: https://team-review.git.corp.google.com/278010
Reviewed-by: Joe Chen <zuhaochen@google.com>
2018-06-21 16:09:42 -07:00
Isaac Ellmen 83a8036ea9 Fish completion support (#122)
- Fish completion support (--completion fish)
2018-05-21 13:41:04 -07:00
David Bieber 6912ccd56f - Prepare Fire for v0.1.3 release. by dbieber <dbieber@google.com>
PiperOrigin-RevId: 186785089
Change-Id: I9c0d87c78fc37c73768c72f9630a5f6d40023cf6
Reviewed-on: https://team-review.git.corp.google.com/221474
Reviewed-by: David Bieber <dbieber@google.com>
2018-02-23 10:18:14 -08:00
cclauss 9bff9d01ce unicode() --> six.u() for Python 3 compatibility (#111)
__unicode()__ was removed from Python 3 because all strs are unicode.

Executing these lines in Python 3 would raise a NameError.

* https://pythonhosted.org/six/#six.u
2018-01-10 16:20:54 -08:00
David Bieber 7f166e9089 Consistent return statements, keyword-arg-before-vararg lint disables, docstrings, and import order pylintrc.
PiperOrigin-RevId: 181191467
Change-Id: I1e7670270b7ff94abaeec1294bdb57fdfcaf8e7e
Reviewed-on: https://team-review.git.corp.google.com/192187
Reviewed-by: David Bieber <dbieber@google.com>
2018-01-08 19:28:38 +00:00
David Bieber 72604f4031 Lint for continued line, fire.svg badge, and spelling fix.
PiperOrigin-RevId: 177869077
Change-Id: I2feed4959ce0842ef6152b7bfcf3539e9141b176
Reviewed-on: https://team-review.git.corp.google.com/172708
Reviewed-by: David Bieber <dbieber@google.com>
2017-12-04 22:40:34 +00:00
greenmoon55 fa57593896 Do not treat arguments that start with '--' as string. (#99)
* Do not treat arguments that start with '--' as strings / values.
2017-12-04 14:01:10 -08:00
David Bieber aff56bb0dc Use proper shields.io image. 2017-10-11 11:07:03 -07:00
mbarkhau 7400ca845e Fix for BinOp in args (#96)
* Always treat binary op args as string literals
* add test for binop
* add test for negative numbers
* raise ValueError for BinOp expressions
Fixes #95
2017-10-11 10:15:08 -07:00
57 changed files with 4497 additions and 308 deletions
+3
View File
@@ -98,3 +98,6 @@ ENV/
# PyCharm IDE
.idea/
# Type-checking
.pytype/
+18 -1
View File
@@ -4,13 +4,30 @@ python:
- "3.4"
- "3.5"
- "3.6"
# Workaround for testing Python 3.7:
# https://github.com/travis-ci/travis-ci/issues/9815
matrix:
include:
- python: 3.7
dist: xenial
sudo: yes
before_install:
- pip install --upgrade setuptools pip
- pip install --upgrade pylint pytest pytest-pylint pytest-runner
install:
- pip install hypothesis python-Levenshtein
- python setup.py develop
script:
- python -m pytest # Run the tests without IPython.
- pip install ipython
- python -m pytest # Now run the tests with IPython.
- if [[ $TRAVIS_PYTHON_VERSION != 3.6 ]]; then pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py; fi
- pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console
- pip install pytype
# Run type-checking, excluding files that define or use py3 features in py2.
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then
pytype -x
fire/fire_test.py
fire/inspectutils_test.py
fire/test_components_py3.py;
else
pytype; fi
+6 -2
View File
@@ -1,4 +1,4 @@
# Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/Django.svg?style=plastic)](https://github.com/google/python-fire)
# Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/fire.svg?style=plastic)](https://github.com/google/python-fire)
_Python Fire is a library for automatically generating command line interfaces
(CLIs) from absolutely any Python object._
@@ -82,12 +82,16 @@ Please see [The Python Fire Guide](docs/guide.md).
| [Help](docs/using-cli.md#help-flag) | `command -- --help` |
| [REPL](docs/using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
| [Separator](docs/using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
| [Completion](docs/using-cli.md#completion-flag) | `command -- --completion` | Generate a completion script for the CLI.
| [Completion](docs/using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI.
| [Trace](docs/using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
| [Verbose](docs/using-cli.md#verbose-flag) | `command -- --verbose` |
_Note that flags are separated from the Fire command by an isolated `--` arg._
## License
Licensed under the
[Apache 2.0](https://github.com/google/python-fire/blob/master/LICENSE) License.
## Disclaimer
+1 -1
View File
@@ -13,7 +13,7 @@
| [Help](using-cli.md#help-flag) | `command -- --help` |
| [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
| [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
| [Completion](using-cli.md#completion-flag) | `command -- --completion` | Generate a completion script for the CLI.
| [Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI.
| [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
| [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` |
+3 -3
View File
@@ -461,13 +461,13 @@ class Building(object):
def __init__(self, name, stories=1):
self.name = name
self.stories = 1
self.stories = stories
def climb_stairs(self, stairs_per_story=10):
for story in range(self.stories):
for stair in range(1, stairs_per_story):
yield stair
yield 'Phew!'
yield 'Phew!'
yield 'Done!'
if __name__ == '__main__':
@@ -702,7 +702,7 @@ The complete set of flags available is shown below, in the reference section.
| [Help](using-cli.md#help-flag) | `command -- --help` | Show help and usage information for the command.
| [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enter interactive mode.
| [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
| [Completion](using-cli.md#completion-flag) | `command -- --completion` | Generate a completion script for the CLI.
| [Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI.
| [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
| [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` | Include private members in the output.
+6 -2
View File
@@ -1,4 +1,4 @@
# Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/Django.svg?style=plastic)](https://github.com/google/python-fire)
# Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/fire.svg?style=plastic)](https://github.com/google/python-fire)
_Python Fire is a library for automatically generating command line interfaces
(CLIs) from absolutely any Python object._
@@ -82,12 +82,16 @@ Please see [The Python Fire Guide](guide.md).
| [Help](using-cli.md#help-flag) | `command -- --help` |
| [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
| [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
| [Completion](using-cli.md#completion-flag) | `command -- --completion` | Generate a completion script for the CLI.
| [Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI.
| [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
| [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` |
_Note that flags are separated from the Fire command by an isolated `--` arg._
## License
Licensed under the
[Apache 2.0](https://github.com/google/python-fire/blob/master/LICENSE) License.
## Disclaimer
+3
View File
@@ -140,6 +140,9 @@ Call `widget -- --completion` to generate a completion script for the Fire CLI
run `widget -- --completion > ~/.widget-completion`. You should then source this
file; to get permanent completion, source this file from your .bashrc file.
Call `widget -- --completion fish` to generate a completion script for the Fish
shell. Source this file from your fish.config.
If the commands available in the Fire CLI change, you'll have to regenerate the
completion script and source it again.
+1 -1
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
+1 -1
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
+5 -3
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -74,8 +74,10 @@ class DiffLibWrapper(object):
self.fromdate = time.ctime(os.stat(fromfile).st_mtime)
self.todate = time.ctime(os.stat(tofile).st_mtime)
self.fromlines = open(fromfile, 'U').readlines()
self.tolines = open(tofile, 'U').readlines()
with open(fromfile) as f:
self.fromlines = f.readlines()
with open(tofile) as f:
self.tolines = f.readlines()
def unified_diff(self, lines=3):
return difflib.unified_diff(
+1 -1
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
+1 -1
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
+1 -1
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
+1 -1
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
+1 -1
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
+1 -1
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
+1 -1
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
+1 -1
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
+278 -29
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -18,19 +18,21 @@ from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from collections import defaultdict
from copy import copy
import collections
import copy
import inspect
from fire import inspectutils
import six
def Script(name, component, default_options=None):
return _Script(name, _Commands(component), default_options)
def Script(name, component, default_options=None, shell='bash'):
if shell == 'fish':
return _FishScript(name, _Commands(component), default_options)
return _BashScript(name, _Commands(component), default_options)
def _Script(name, commands, default_options=None):
def _BashScript(name, commands, default_options=None):
"""Returns a Bash script registering a completion function for the commands.
Args:
@@ -45,12 +47,9 @@ def _Script(name, commands, default_options=None):
completion in Bash.
"""
default_options = default_options or set()
options_map = defaultdict(lambda: copy(default_options))
for command in commands:
start = (name + ' ' + ' '.join(command[:-1])).strip()
completion = _FormatForCommand(command[-1])
options_map[start].add(completion)
options_map[start.replace('_', '-')].add(completion)
global_options, options_map, subcommands_map = _GetMaps(
name, commands, default_options
)
bash_completion_template = """# bash completion support for {name}
# DO NOT EDIT.
@@ -58,46 +57,250 @@ def _Script(name, commands, default_options=None):
_complete-{identifier}()
{{
local start cur opts
local cur prev opts lastcommand
COMPREPLY=()
start="${{COMP_WORDS[@]:0:COMP_CWORD}}"
prev="${{COMP_WORDS[COMP_CWORD-1]}}"
cur="${{COMP_WORDS[COMP_CWORD]}}"
lastcommand=$(get_lastcommand)
opts="{default_options}"
GLOBAL_OPTIONS="{global_options}"
{start_checks}
{checks}
COMPREPLY=( $(compgen -W "${{opts}}" -- ${{cur}}) )
return 0
}}
get_lastcommand()
{{
local lastcommand i
lastcommand=
for ((i=0; i < ${{#COMP_WORDS[@]}}; ++i)); do
if [[ ${{COMP_WORDS[i]}} != -* ]] && [[ -n ${{COMP_WORDS[i]}} ]] && [[
${{COMP_WORDS[i]}} != $cur ]]; then
lastcommand=${{COMP_WORDS[i]}}
fi
done
echo $lastcommand
}}
filter_options()
{{
local opts
opts=""
for opt in "$@"
do
if ! option_already_entered $opt; then
opts="$opts $opt"
fi
done
echo $opts
}}
option_already_entered()
{{
local opt
for opt in ${{COMP_WORDS[@]:0:COMP_CWORD}}
do
if [ $1 == $opt ]; then
return 0
fi
done
return 1
}}
is_prev_global()
{{
local opt
for opt in $GLOBAL_OPTIONS
do
if [ $opt == $prev ]; then
return 0
fi
done
return 1
}}
complete -F _complete-{identifier} {command}
"""
start_check_template = """
if [[ "$start" == "{start}" ]] ; then
opts="{completions}"
fi"""
start_checks = '\n'.join(
start_check_template.format(
start=start,
completions=' '.join(sorted(options_map[start]))
)
for start in options_map
check_wrapper = """
case "${{lastcommand}}" in
{lastcommand_checks}
esac"""
lastcommand_check_template = """
{command})
{opts_assignment}
opts=$(filter_options $opts)
;;"""
opts_assignment_subcommand_template = """
if is_prev_global; then
opts="${{GLOBAL_OPTIONS}}"
else
opts="{options} ${{GLOBAL_OPTIONS}}"
fi"""
opts_assignment_main_command_template = """
opts="{options} ${{GLOBAL_OPTIONS}}" """
def _GetOptsAssignmentTemplate(command):
if command == name:
return opts_assignment_main_command_template
else:
return opts_assignment_subcommand_template
lines = []
for command in set(subcommands_map.keys()).union(set(options_map.keys())):
opts_assignment = _GetOptsAssignmentTemplate(command).format(
options=' '.join(
sorted(options_map[command].union(subcommands_map[command]))
),
)
lines.append(
lastcommand_check_template.format(
command=command,
opts_assignment=opts_assignment)
)
lastcommand_checks = '\n'.join(lines)
checks = check_wrapper.format(
lastcommand_checks=lastcommand_checks,
)
return (
bash_completion_template.format(
name=name,
command=name,
start_checks=start_checks,
checks=checks,
default_options=' '.join(default_options),
identifier=name.replace('/', '').replace('.', '').replace(',', '')
identifier=name.replace('/', '').replace('.', '').replace(',', ''),
global_options=' '.join(global_options),
)
)
def _FishScript(name, commands, default_options=None):
"""Returns a Fish script registering a completion function for the commands.
Args:
name: The first token in the commands, also the name of the command.
commands: A list of all possible commands that tab completion can complete
to. Each command is a list or tuple of the string tokens that make up
that command.
default_options: A dict of options that can be used with any command. Use
this if there are flags that can always be appended to a command.
Returns:
A string which is the Fish script. Source the fish script to enable tab
completion in Fish.
"""
default_options = default_options or set()
global_options, options_map, subcommands_map = _GetMaps(
name, commands, default_options
)
fish_source = """function __fish_using_command
set cmd (commandline -opc)
for i in (seq (count $cmd) 1)
switch $cmd[$i]
case "-*"
case "*"
if [ $cmd[$i] = $argv[1] ]
return 0
else
return 1
end
end
end
return 1
end
function __option_entered_check
set cmd (commandline -opc)
for i in (seq (count $cmd))
switch $cmd[$i]
case "-*"
if [ $cmd[$i] = $argv[1] ]
return 1
end
end
end
return 0
end
function __is_prev_global
set cmd (commandline -opc)
set global_options {global_options}
set prev (count $cmd)
for opt in $global_options
if [ "--$opt" = $cmd[$prev] ]
echo $prev
return 0
end
end
return 1
end
"""
subcommand_template = ("complete -c {name} -n '__fish_using_command "
"{command}' -f -a {subcommand}\n")
flag_template = ("complete -c {name} -n "
"'__fish_using_command {command};{prev_global_check} and "
"__option_entered_check --{option}' -l {option}\n")
prev_global_check = ' and __is_prev_global;'
for command in set(subcommands_map.keys()).union(set(options_map.keys())):
for subcommand in subcommands_map[command]:
fish_source += subcommand_template.format(
name=name,
command=command,
subcommand=subcommand,
)
for option in options_map[command].union(global_options):
check_needed = command != name
fish_source += flag_template.format(
name=name,
command=command,
prev_global_check=prev_global_check if check_needed else '',
option=option.lstrip('--'),
)
return fish_source.format(
global_options=' '.join(
'"{option}"'.format(option=option)
for option in global_options
)
)
def _IncludeMember(name, verbose):
"""Returns whether a member should be included in auto-completion or help.
Determines whether a member of an object with the specified name should be
included in auto-completion or help text(both usage and detailed help).
If the member starts with '__', it will always be excluded. If the member
starts with only one '_', it will be included for all non-string types. If
verbose is True, the members, including the private members, are always
included.
Args:
name: The name of the member.
verbose: Whether to include private members.
Returns
A boolean value indicating whether the member should be included.
"""
if isinstance(name, six.string_types) and name[:2] == '__':
return False
if verbose:
return True
if isinstance(name, six.string_types):
@@ -164,7 +367,7 @@ def Completions(component, verbose=False):
return [str(index) for index in range(len(component))]
if inspect.isgenerator(component):
# TODO: There are currently no commands available for generators.
# TODO(dbieber): There are currently no commands available for generators.
return []
return [
@@ -219,10 +422,56 @@ def _Commands(component, depth=3):
return
for member_name, member in _Members(component):
# TODO: Also skip components we've already seen.
# TODO(dbieber): Also skip components we've already seen.
member_name = _FormatForCommand(member_name)
yield (member_name,)
for command in _Commands(member, depth - 1):
yield (member_name,) + command
def _IsOption(arg):
return arg.startswith('-')
def _GetMaps(name, commands, default_options):
"""Returns sets of subcommands and options for each command.
Args:
name: The first token in the commands, also the name of the command.
commands: A list of all possible commands that tab completion can complete
to. Each command is a list or tuple of the string tokens that make up
that command.
default_options: A dict of options that can be used with any command. Use
this if there are flags that can always be appended to a command.
Returns:
global_options: A set of all options of the first token of the command.
subcommands_map: A dict storing set of subcommands for each
command/subcommand.
options_map: A dict storing set of options for each subcommand.
"""
global_options = copy.copy(default_options)
options_map = collections.defaultdict(lambda: copy.copy(default_options))
subcommands_map = collections.defaultdict(set)
for command in commands:
if len(command) == 1:
if _IsOption(command[0]):
global_options.add(command[0])
else:
subcommands_map[name].add(command[0])
elif command:
subcommand = command[-2]
arg = _FormatForCommand(command[-1])
if _IsOption(arg):
args_map = options_map
else:
args_map = subcommands_map
args_map[subcommand].add(arg)
args_map[subcommand.replace('_', '-')].add(arg)
return global_options, options_map, subcommands_map
+45 -6
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -25,18 +25,34 @@ from fire import testutils
class TabCompletionTest(testutils.BaseTestCase):
def testCompletionScript(self):
# A sanity check test to make sure the completion script satisfies some
# basic assumptions.
def testCompletionBashScript(self):
# A sanity check test to make sure the bash completion script satisfies
# some basic assumptions.
commands = [
['run'],
['halt'],
['halt', '--now'],
]
script = completion._Script(name='command', commands=commands) # pylint: disable=protected-access
script = completion._BashScript(name='command', commands=commands) # pylint: disable=protected-access
self.assertIn('command', script)
self.assertIn('halt', script)
self.assertIn('"$start" == "command"', script)
assert_template = '{command})'
for last_command in ['command', 'halt']:
self.assertIn(assert_template.format(command=last_command), script)
def testCompletionFishScript(self):
# A sanity check test to make sure the fish completion script satisfies
# some basic assumptions.
commands = [
['run'],
['halt'],
['halt', '--now'],
]
script = completion._FishScript(name='command', commands=commands) # pylint: disable=protected-access
self.assertIn('command', script)
self.assertIn('halt', script)
self.assertIn('-l now', script)
def testFnCompletions(self):
def example(one, two, three):
@@ -113,6 +129,29 @@ class TabCompletionTest(testutils.BaseTestCase):
self.assertIn('--alpha', script)
self.assertIn('--beta', script)
def testDeepDictFishScript(self):
deepdict = {'level1': {'level2': {'level3': {'level4': {}}}}}
script = completion.Script('deepdict', deepdict, shell='fish')
self.assertIn('level1', script)
self.assertIn('level2', script)
self.assertIn('level3', script)
self.assertNotIn('level4', script) # The default depth is 3.
def testFnFishScript(self):
script = completion.Script('identity', tc.identity, shell='fish')
self.assertIn('arg1', script)
self.assertIn('arg2', script)
self.assertIn('arg3', script)
self.assertIn('arg4', script)
def testClassFishScript(self):
script = completion.Script('', tc.MixedDefaults, shell='fish')
self.assertIn('ten', script)
self.assertIn('sum', script)
self.assertIn('identity', script)
self.assertIn('alpha', script)
self.assertIn('beta', script)
def testNonStringDictCompletions(self):
completions = completion.Completions({
10: 'green',
+3
View File
@@ -0,0 +1,3 @@
This is the console package from googlecloudsdk, as used by Python Fire.
Python Fire does not accept pull requests modifying the console package; rather,
changes to console should go through the upstream project googlecloudsdk.
View File
+784
View File
@@ -0,0 +1,784 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
r"""A module for console attributes, special characters and functions.
The target architectures {linux, macos, windows} support inline encoding for
all attributes except color. Windows requires win32 calls to manipulate the
console color state.
Usage:
# Get the console attribute state.
out = log.out
con = console_attr.GetConsoleAttr(out=out)
# Get the ISO 8879:1986//ENTITIES Box and Line Drawing characters.
box = con.GetBoxLineCharacters()
# Print an X inside a box.
out.write(box.dr)
out.write(box.h)
out.write(box.dl)
out.write('\n')
out.write(box.v)
out.write('X')
out.write(box.v)
out.write('\n')
out.write(box.ur)
out.write(box.h)
out.write(box.ul)
out.write('\n')
# Print the bullet characters.
for c in con.GetBullets():
out.write(c)
out.write('\n')
# Print FAIL in red.
out.write('Epic ')
con.Colorize('FAIL', 'red')
out.write(', my first.')
# Print italic and bold text.
bold = con.GetFontCode(bold=True)
italic = con.GetFontCode(italic=True)
normal = con.GetFontCode()
out.write('This is {bold}bold{normal}, this is {italic}italic{normal},'
' and this is normal.\n'.format(bold=bold, italic=italic,
normal=normal))
# Read one character from stdin with echo disabled.
c = con.GetRawKey()
if c is None:
print 'EOF\n'
# Return the display width of a string that may contain FontCode() chars.
display_width = con.DisplayWidth(string)
# Reset the memoized state.
con = console_attr.ResetConsoleAttr()
# Print the console width and height in characters.
width, height = con.GetTermSize()
print 'width={width}, height={height}'.format(width=width, height=height)
# Colorize table data cells.
fail = console_attr.Colorizer('FAIL', 'red')
pass = console_attr.Colorizer('PASS', 'green')
cells = ['label', fail, 'more text', pass, 'end']
for cell in cells;
if isinstance(cell, console_attr.Colorizer):
cell.Render()
else:
out.write(cell)
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import os
import sys
import unicodedata
from fire.console import console_attr_os
from fire.console import encoding as encoding_util
import six
class BoxLineCharacters(object):
"""Box/line drawing characters.
The element names are from ISO 8879:1986//ENTITIES Box and Line Drawing//EN:
http://www.w3.org/2003/entities/iso8879doc/isobox.html
"""
class BoxLineCharactersUnicode(BoxLineCharacters):
"""unicode Box/line drawing characters (cp437 compatible unicode)."""
dl = ''
dr = ''
h = ''
hd = ''
hu = ''
ul = ''
ur = ''
v = ''
vh = ''
vl = ''
vr = ''
d_dl = ''
d_dr = ''
d_h = ''
d_hd = ''
d_hu = ''
d_ul = ''
d_ur = ''
d_v = ''
d_vh = ''
d_vl = ''
d_vr = ''
class BoxLineCharactersAscii(BoxLineCharacters):
"""ASCII Box/line drawing characters."""
dl = '+'
dr = '+'
h = '-'
hd = '+'
hu = '+'
ul = '+'
ur = '+'
v = '|'
vh = '+'
vl = '+'
vr = '+'
d_dl = '#'
d_dr = '#'
d_h = '='
d_hd = '#'
d_hu = '#'
d_ul = '#'
d_ur = '#'
d_v = '#'
d_vh = '#'
d_vl = '#'
d_vr = '#'
class ProgressTrackerSymbols(object):
"""Characters used by progress trackers."""
class ProgressTrackerSymbolsUnicode(ProgressTrackerSymbols):
"""Characters used by progress trackers."""
@property
def spin_marks(self):
return ['', '', '', '', '', '']
success = ''
failed = 'X'
interrupted = '-'
not_started = '.'
prefix_length = 2
class ProgressTrackerSymbolsAscii(ProgressTrackerSymbols):
"""Characters used by progress trackers."""
@property
def spin_marks(self):
return ['|', '/', '-', '\\',]
success = 'OK'
failed = 'X'
interrupted = '-'
not_started = '.'
prefix_length = 3
class ConsoleAttr(object):
"""Console attribute and special drawing characters and functions accessor.
Use GetConsoleAttr() to get a global ConsoleAttr object shared by all callers.
Use ConsoleAttr() for abstracting multiple consoles.
If _out is not associated with a console, or if the console properties cannot
be determined, the default behavior is ASCII art with no attributes.
Attributes:
_ANSI_COLOR: The ANSI color control sequence dict.
_ANSI_COLOR_RESET: The ANSI color reset control sequence string.
_csi: The ANSI Control Sequence indicator string, '' if not supported.
_encoding: The character encoding.
ascii: ASCII art. This is the default.
utf8: UTF-8 unicode.
win: Windows code page 437.
_font_bold: The ANSI bold font embellishment code string.
_font_italic: The ANSI italic font embellishment code string.
_get_raw_key: A function that reads one keypress from stdin with no echo.
_out: The console output file stream.
_term: TERM environment variable value.
_term_size: The terminal (x, y) dimensions in characters.
"""
_CONSOLE_ATTR_STATE = None
_ANSI_COLOR = {
'red': '31;1m',
'yellow': '33;1m',
'green': '32m',
'blue': '34;1m'
}
_ANSI_COLOR_RESET = '39;0m'
_BULLETS_UNICODE = ('', '', '', '', '', '')
_BULLETS_WINDOWS = ('', '', '', 'Φ', '·') # cp437 compatible unicode
_BULLETS_ASCII = ('o', '*', '+', '-')
def __init__(self, encoding=None):
"""Constructor.
Args:
encoding: Encoding override.
ascii -- ASCII art. This is the default.
utf8 -- UTF-8 unicode.
win -- Windows code page 437.
"""
# Normalize the encoding name.
if not encoding:
encoding = self._GetConsoleEncoding()
elif encoding == 'win':
encoding = 'cp437'
self._encoding = encoding or 'ascii'
self._term = os.getenv('TERM', '').lower()
# ANSI "standard" attributes.
if self.SupportsAnsi():
# Select Graphic Rendition paramaters from
# http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
# Italic '3' would be nice here but its not widely supported.
self._csi = '\x1b['
self._font_bold = '1'
self._font_italic = '4'
else:
self._csi = None
self._font_bold = ''
self._font_italic = ''
# Encoded character attributes.
if self._encoding == 'utf8':
self._box_line_characters = BoxLineCharactersUnicode()
self._bullets = self._BULLETS_UNICODE
self._progress_tracker_symbols = ProgressTrackerSymbolsUnicode()
elif self._encoding == 'cp437':
self._box_line_characters = BoxLineCharactersUnicode()
self._bullets = self._BULLETS_WINDOWS
# Windows does not suport the unicode characters used for the spinner.
self._progress_tracker_symbols = ProgressTrackerSymbolsAscii()
else:
self._box_line_characters = BoxLineCharactersAscii()
self._bullets = self._BULLETS_ASCII
self._progress_tracker_symbols = ProgressTrackerSymbolsAscii()
# OS specific attributes.
self._get_raw_key = [console_attr_os.GetRawKeyFunction()]
self._term_size = console_attr_os.GetTermSize()
self._display_width_cache = {}
def _GetConsoleEncoding(self):
"""Gets the encoding as declared by the stdout stream.
Returns:
str, The encoding name or None if it could not be determined.
"""
console_encoding = getattr(sys.stdout, 'encoding', None)
if not console_encoding:
return None
console_encoding = console_encoding.lower()
if 'utf-8' in console_encoding:
return 'utf8'
elif 'cp437' in console_encoding:
return 'cp437'
return None
def Colorize(self, string, color, justify=None):
"""Generates a colorized string, optionally justified.
Args:
string: The string to write.
color: The color name -- must be in _ANSI_COLOR.
justify: The justification function, no justification if None. For
example, justify=lambda s: s.center(10)
Returns:
str, The colorized string that can be printed to the console.
"""
if justify:
string = justify(string)
if self._csi and color in self._ANSI_COLOR:
return '{csi}{color_code}{string}{csi}{reset_code}'.format(
csi=self._csi,
color_code=self._ANSI_COLOR[color],
reset_code=self._ANSI_COLOR_RESET,
string=string)
# TODO: Add elif self._encoding == 'cp437': code here.
return string
def ConvertOutputToUnicode(self, buf):
"""Converts a console output string buf to unicode.
Mainly used for testing. Allows test comparisons in unicode while ensuring
that unicode => encoding => unicode works.
Args:
buf: The console output string to convert.
Returns:
The console output string buf converted to unicode.
"""
if isinstance(buf, six.text_type):
buf = buf.encode(self._encoding)
return six.text_type(buf, self._encoding, 'replace')
def GetBoxLineCharacters(self):
"""Returns the box/line drawing characters object.
The element names are from ISO 8879:1986//ENTITIES Box and Line Drawing//EN:
http://www.w3.org/2003/entities/iso8879doc/isobox.html
Returns:
A BoxLineCharacters object for the console output device.
"""
return self._box_line_characters
def GetBullets(self):
"""Returns the bullet characters list.
Use the list elements in order for best appearance in nested bullet lists,
wrapping back to the first element for deep nesting. The list size depends
on the console implementation.
Returns:
A tuple of bullet characters.
"""
return self._bullets
def GetProgressTrackerSymbols(self):
"""Returns the progress tracker characters object.
Returns:
A ProgressTrackerSymbols object for the console output device.
"""
return self._progress_tracker_symbols
def GetControlSequenceIndicator(self):
"""Returns the control sequence indicator string.
Returns:
The conrol sequence indicator string or None if control sequences are not
supported.
"""
return self._csi
def GetControlSequenceLen(self, buf):
"""Returns the control sequence length at the beginning of buf.
Used in display width computations. Control sequences have display width 0.
Args:
buf: The string to check for a control sequence.
Returns:
The conrol sequence length at the beginning of buf or 0 if buf does not
start with a control sequence.
"""
if not self._csi or not buf.startswith(self._csi):
return 0
n = 0
for c in buf:
n += 1
if c.isalpha():
break
return n
def GetEncoding(self):
"""Returns the current encoding."""
return self._encoding
def GetFontCode(self, bold=False, italic=False):
"""Returns a font code string for 0 or more embellishments.
GetFontCode() with no args returns the default font code string.
Args:
bold: True for bold embellishment.
italic: True for italic embellishment.
Returns:
The font code string for the requested embellishments. Write this string
to the console output to control the font settings.
"""
if not self._csi:
return ''
codes = []
if bold:
codes.append(self._font_bold)
if italic:
codes.append(self._font_italic)
return '{csi}{codes}m'.format(csi=self._csi, codes=';'.join(codes))
def GetRawKey(self):
"""Reads one key press from stdin with no echo.
Returns:
The key name, None for EOF, <KEY-*> for function keys, otherwise a
character.
"""
return self._get_raw_key[0]()
def GetTermSize(self):
"""Returns the terminal (x, y) dimensions in characters.
Returns:
(x, y): A tuple of the terminal x and y dimensions.
"""
return self._term_size
def DisplayWidth(self, buf):
"""Returns the display width of buf, handling unicode and ANSI controls.
Args:
buf: The string to count from.
Returns:
The display width of buf, handling unicode and ANSI controls.
"""
if not isinstance(buf, six.string_types):
# Handle non-string objects like Colorizer().
return len(buf)
cached = self._display_width_cache.get(buf, None)
if cached is not None:
return cached
width = 0
max_width = 0
i = 0
while i < len(buf):
if self._csi and buf[i:].startswith(self._csi):
i += self.GetControlSequenceLen(buf[i:])
elif buf[i] == '\n':
# A newline incidates the start of a new line.
# Newline characters have 0 width.
max_width = max(width, max_width)
width = 0
i += 1
else:
width += GetCharacterDisplayWidth(buf[i])
i += 1
max_width = max(width, max_width)
self._display_width_cache[buf] = max_width
return max_width
def SplitIntoNormalAndControl(self, buf):
"""Returns a list of (normal_string, control_sequence) tuples from buf.
Args:
buf: The input string containing one or more control sequences
interspersed with normal strings.
Returns:
A list of (normal_string, control_sequence) tuples.
"""
if not self._csi or not buf:
return [(buf, '')]
seq = []
i = 0
while i < len(buf):
c = buf.find(self._csi, i)
if c < 0:
seq.append((buf[i:], ''))
break
normal = buf[i:c]
i = c + self.GetControlSequenceLen(buf[c:])
seq.append((normal, buf[c:i]))
return seq
def SplitLine(self, line, width):
"""Splits line into width length chunks.
Args:
line: The line to split.
width: The width of each chunk except the last which could be smaller than
width.
Returns:
A list of chunks, all but the last with display width == width.
"""
lines = []
chunk = ''
w = 0
keep = False
for normal, control in self.SplitIntoNormalAndControl(line):
keep = True
while True:
n = width - w
w += len(normal)
if w <= width:
break
lines.append(chunk + normal[:n])
chunk = ''
keep = False
w = 0
normal = normal[n:]
chunk += normal + control
if chunk or keep:
lines.append(chunk)
return lines
def SupportsAnsi(self):
return (self._encoding != 'ascii' and
('screen' in self._term or 'xterm' in self._term))
class Colorizer(object):
"""Resource string colorizer.
Attributes:
_con: ConsoleAttr object.
_color: Color name.
_string: The string to colorize.
_justify: The justification function, no justification if None. For example,
justify=lambda s: s.center(10)
"""
def __init__(self, string, color, justify=None):
"""Constructor.
Args:
string: The string to colorize.
color: Color name used to index ConsoleAttr._ANSI_COLOR.
justify: The justification function, no justification if None. For
example, justify=lambda s: s.center(10)
"""
self._con = GetConsoleAttr()
self._color = color
self._string = string
self._justify = justify
def __eq__(self, other):
return self._string == six.text_type(other)
def __ne__(self, other):
return not self == other
def __gt__(self, other):
return self._string > six.text_type(other)
def __lt__(self, other):
return self._string < six.text_type(other)
def __ge__(self, other):
return not self < other
def __le__(self, other):
return not self > other
def __len__(self):
return self._con.DisplayWidth(self._string)
def __str__(self):
return self._string
def Render(self, stream, justify=None):
"""Renders the string as self._color on the console.
Args:
stream: The stream to render the string to. The stream given here *must*
have the same encoding as sys.stdout for this to work properly.
justify: The justification function, self._justify if None.
"""
stream.write(
self._con.Colorize(self._string, self._color, justify or self._justify))
def GetConsoleAttr(encoding=None, reset=False):
"""Gets the console attribute state.
If this is the first call or reset is True or encoding is not None and does
not match the current encoding or out is not None and does not match the
current out then the state is (re)initialized. Otherwise the current state
is returned.
This call associates the out file stream with the console. All console related
output should go to the same stream.
Args:
encoding: Encoding override.
ascii -- ASCII. This is the default.
utf8 -- UTF-8 unicode.
win -- Windows code page 437.
reset: Force re-initialization if True.
Returns:
The global ConsoleAttr state object.
"""
attr = ConsoleAttr._CONSOLE_ATTR_STATE # pylint: disable=protected-access
if not reset:
if not attr:
reset = True
elif encoding and encoding != attr.GetEncoding():
reset = True
if reset:
attr = ConsoleAttr(encoding=encoding)
ConsoleAttr._CONSOLE_ATTR_STATE = attr # pylint: disable=protected-access
return attr
def ResetConsoleAttr(encoding=None):
"""Resets the console attribute state to the console default.
Args:
encoding: Reset to this encoding instead of the default.
ascii -- ASCII. This is the default.
utf8 -- UTF-8 unicode.
win -- Windows code page 437.
Returns:
The global ConsoleAttr state object.
"""
return GetConsoleAttr(encoding=encoding, reset=True)
def GetCharacterDisplayWidth(char):
"""Returns the monospaced terminal display width of char.
Assumptions:
- monospaced display
- ambiguous or unknown chars default to width 1
- ASCII control char width is 1 => don't use this for control chars
Args:
char: The character to determine the display width of.
Returns:
The monospaced terminal display width of char: either 0, 1, or 2.
"""
if not isinstance(char, six.text_type):
# Non-unicode chars have width 1. Don't use this function on control chars.
return 1
# Normalize to avoid special cases.
char = unicodedata.normalize('NFC', char)
if unicodedata.combining(char) != 0:
# Modifies the previous character and does not move the cursor.
return 0
elif unicodedata.category(char) == 'Cf':
# Unprintable formatting char.
return 0
elif unicodedata.east_asian_width(char) in 'FW':
# Fullwidth or Wide chars take 2 character positions.
return 2
else:
# Don't use this function on control chars.
return 1
def SafeText(data, encoding=None, escape=True):
br"""Converts the data to a text string compatible with the given encoding.
This works the same way as Decode() below except it guarantees that any
characters in the resulting text string can be re-encoded using the given
encoding (or GetConsoleAttr().GetEncoding() if None is given). This means
that the string will be safe to print to sys.stdout (for example) without
getting codec exceptions if the user's terminal doesn't support the encoding
used by the source of the text.
Args:
data: Any bytes, string, or object that has str() or unicode() methods.
encoding: The encoding name to ensure compatibility with. Defaults to
GetConsoleAttr().GetEncoding().
escape: Replace unencodable characters with a \uXXXX or \xXX equivalent if
True. Otherwise replace unencodable characters with an appropriate unknown
character, '?' for ASCII, and the unicode unknown replacement character
\uFFFE for unicode.
Returns:
A text string representation of the data, but modified to remove any
characters that would result in an encoding exception with the target
encoding. In the worst case, with escape=False, it will contain only ?
characters.
"""
if data is None:
return 'None'
encoding = encoding or GetConsoleAttr().GetEncoding()
string = encoding_util.Decode(data, encoding=encoding)
try:
# No change needed if the string encodes to the output encoding.
string.encode(encoding)
return string
except UnicodeError:
# The string does not encode to the output encoding. Encode it with error
# handling then convert it back into a text string (which will be
# guaranteed to only contain characters that can be encoded later.
return (string
.encode(encoding, 'backslashreplace' if escape else 'replace')
.decode(encoding))
def EncodeToBytes(data):
r"""Encode data to bytes.
The primary use case is for base64/mime style 7-bit ascii encoding where the
encoder input must be bytes. "safe" means that the conversion always returns
bytes and will not raise codec exceptions.
If data is text then an 8-bit ascii encoding is attempted, then the console
encoding, and finally utf-8.
Args:
data: Any bytes, string, or object that has str() or unicode() methods.
Returns:
A bytes string representation of the data.
"""
if data is None:
return b''
if isinstance(data, bytes):
# Already bytes - our work is done.
return data
# Coerce to text that will be converted to bytes.
s = six.text_type(data)
try:
# Assume the text can be directly converted to bytes (8-bit ascii).
return s.encode('iso-8859-1')
except UnicodeEncodeError:
pass
try:
# Try the output encoding.
return s.encode(GetConsoleAttr().GetEncoding())
except UnicodeEncodeError:
pass
# Punt to utf-8.
return s.encode('utf-8')
def Decode(data, encoding=None):
"""Converts the given string, bytes, or object to a text string.
Args:
data: Any bytes, string, or object that has str() or unicode() methods.
encoding: A suggesting encoding used to decode. If this encoding doesn't
work, other defaults are tried. Defaults to
GetConsoleAttr().GetEncoding().
Returns:
A text string representation of the data.
"""
encoding = encoding or GetConsoleAttr().GetEncoding()
return encoding_util.Decode(data, encoding=encoding)
+260
View File
@@ -0,0 +1,260 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""OS specific console_attr helper functions."""
# This file contains platform specific code which is not currently handled
# by pytype.
# pytype: skip-file
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import os
import sys
from fire.console import encoding
def GetTermSize():
"""Gets the terminal x and y dimensions in characters.
_GetTermSize*() helper functions taken from:
http://stackoverflow.com/questions/263890/
Returns:
(columns, lines): A tuple containing the terminal x and y dimensions.
"""
xy = None
# Believe the first helper that doesn't bail.
for get_terminal_size in (_GetTermSizePosix,
_GetTermSizeWindows,
_GetTermSizeEnvironment,
_GetTermSizeTput):
try:
xy = get_terminal_size()
if xy:
break
except: # pylint: disable=bare-except
pass
return xy or (80, 24)
def _GetTermSizePosix():
"""Returns the Posix terminal x and y dimensions."""
# pylint: disable=g-import-not-at-top
import fcntl
# pylint: disable=g-import-not-at-top
import struct
# pylint: disable=g-import-not-at-top
import termios
def _GetXY(fd):
"""Returns the terminal (x,y) size for fd.
Args:
fd: The terminal file descriptor.
Returns:
The terminal (x,y) size for fd or None on error.
"""
try:
# This magic incantation converts a struct from ioctl(2) containing two
# binary shorts to a (rows, columns) int tuple.
rc = struct.unpack(b'hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, 'junk'))
return (rc[1], rc[0]) if rc else None
except: # pylint: disable=bare-except
return None
xy = _GetXY(0) or _GetXY(1) or _GetXY(2)
if not xy:
fd = None
try:
fd = os.open(os.ctermid(), os.O_RDONLY)
xy = _GetXY(fd)
except: # pylint: disable=bare-except
xy = None
finally:
if fd is not None:
os.close(fd)
return xy
def _GetTermSizeWindows():
"""Returns the Windows terminal x and y dimensions."""
# pylint:disable=g-import-not-at-top
import struct
# pylint: disable=g-import-not-at-top
from ctypes import create_string_buffer
# pylint:disable=g-import-not-at-top
from ctypes import windll
# stdin handle is -10
# stdout handle is -11
# stderr handle is -12
h = windll.kernel32.GetStdHandle(-12)
csbi = create_string_buffer(22)
if not windll.kernel32.GetConsoleScreenBufferInfo(h, csbi):
return None
(unused_bufx, unused_bufy, unused_curx, unused_cury, unused_wattr,
left, top, right, bottom,
unused_maxx, unused_maxy) = struct.unpack(b'hhhhHhhhhhh', csbi.raw)
x = right - left + 1
y = bottom - top + 1
return (x, y)
def _GetTermSizeEnvironment():
"""Returns the terminal x and y dimensions from the environment."""
return (int(os.environ['COLUMNS']), int(os.environ['LINES']))
def _GetTermSizeTput():
"""Returns the terminal x and y dimemsions from tput(1)."""
import subprocess # pylint: disable=g-import-not-at-top
output = encoding.Decode(subprocess.check_output(['tput', 'cols'],
stderr=subprocess.STDOUT))
cols = int(output)
output = encoding.Decode(subprocess.check_output(['tput', 'lines'],
stderr=subprocess.STDOUT))
rows = int(output)
return (cols, rows)
_ANSI_CSI = '\x1b' # ANSI control sequence indicator (ESC)
_CONTROL_D = '\x04' # unix EOF (^D)
_CONTROL_Z = '\x1a' # Windows EOF (^Z)
_WINDOWS_CSI_1 = '\x00' # Windows control sequence indicator #1
_WINDOWS_CSI_2 = '\xe0' # Windows control sequence indicator #2
def GetRawKeyFunction():
"""Returns a function that reads one keypress from stdin with no echo.
Returns:
A function that reads one keypress from stdin with no echo or a function
that always returns None if stdin does not support it.
"""
# Believe the first helper that doesn't bail.
for get_raw_key_function in (_GetRawKeyFunctionPosix,
_GetRawKeyFunctionWindows):
try:
return get_raw_key_function()
except: # pylint: disable=bare-except
pass
return lambda: None
def _GetRawKeyFunctionPosix():
"""_GetRawKeyFunction helper using Posix APIs."""
# pylint: disable=g-import-not-at-top
import tty
# pylint: disable=g-import-not-at-top
import termios
def _GetRawKeyPosix():
"""Reads and returns one keypress from stdin, no echo, using Posix APIs.
Returns:
The key name, None for EOF, <*> for function keys, otherwise a
character.
"""
ansi_to_key = {
'A': '<UP-ARROW>',
'B': '<DOWN-ARROW>',
'D': '<LEFT-ARROW>',
'C': '<RIGHT-ARROW>',
'5': '<PAGE-UP>',
'6': '<PAGE-DOWN>',
'H': '<HOME>',
'F': '<END>',
'M': '<DOWN-ARROW>',
'S': '<PAGE-UP>',
'T': '<PAGE-DOWN>',
}
# Flush pending output. sys.stdin.read() would do this, but it's explicitly
# bypassed in _GetKeyChar().
sys.stdout.flush()
fd = sys.stdin.fileno()
def _GetKeyChar():
return encoding.Decode(os.read(fd, 1))
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(fd)
c = _GetKeyChar()
if c == _ANSI_CSI:
c = _GetKeyChar()
while True:
if c == _ANSI_CSI:
return c
if c.isalpha():
break
prev_c = c
c = _GetKeyChar()
if c == '~':
c = prev_c
break
return ansi_to_key.get(c, '')
except: # pylint:disable=bare-except
c = None
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return None if c in (_CONTROL_D, _CONTROL_Z) else c
return _GetRawKeyPosix
def _GetRawKeyFunctionWindows():
"""_GetRawKeyFunction helper using Windows APIs."""
# pylint: disable=g-import-not-at-top
import msvcrt
def _GetRawKeyWindows():
"""Reads and returns one keypress from stdin, no echo, using Windows APIs.
Returns:
The key name, None for EOF, <*> for function keys, otherwise a
character.
"""
windows_to_key = {
'H': '<UP-ARROW>',
'P': '<DOWN-ARROW>',
'K': '<LEFT-ARROW>',
'M': '<RIGHT-ARROW>',
'I': '<PAGE-UP>',
'Q': '<PAGE-DOWN>',
'G': '<HOME>',
'O': '<END>',
}
# Flush pending output. sys.stdin.read() would do this it's explicitly
# bypassed in _GetKeyChar().
sys.stdout.flush()
def _GetKeyChar():
return encoding.Decode(msvcrt.getch())
c = _GetKeyChar()
# Special function key is a two character sequence; return the second char.
if c in (_WINDOWS_CSI_1, _WINDOWS_CSI_2):
return windows_to_key.get(_GetKeyChar(), '')
return None if c in (_CONTROL_D, _CONTROL_Z) else c
return _GetRawKeyWindows
+299
View File
@@ -0,0 +1,299 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Simple console pager."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import re
import sys
from fire.console import console_attr
class Pager(object):
"""A simple console text pager.
This pager requires the entire contents to be available. The contents are
written one page of lines at a time. The prompt is written after each page of
lines. A one character response is expected. See HELP_TEXT below for more
info.
The contents are written as is. For example, ANSI control codes will be in
effect. This is different from pagers like more(1) which is ANSI control code
agnostic and miscalculates line lengths, and less(1) which displays control
character names by default.
Attributes:
_attr: The current ConsoleAttr handle.
_clear: A string that clears the prompt when written to _out.
_contents: The entire contents of the text lines to page.
_height: The terminal height in characters.
_out: The output stream, log.out (effectively) if None.
_prompt: The page break prompt.
_search_direction: The search direction command, n:forward, N:reverse.
_search_pattern: The current forward/reverse search compiled RE.
_width: The termonal width in characters.
"""
HELP_TEXT = """
Simple pager commands:
b, ^B, <PAGE-UP>, <LEFT-ARROW>
Back one page.
f, ^F, <SPACE>, <PAGE-DOWN>, <RIGHT-ARROW>
Forward one page. Does not quit if there are no more lines.
g, <HOME>
Back to the first page.
<number>g
Go to <number> lines from the top.
G, <END>
Forward to the last page.
<number>G
Go to <number> lines from the bottom.
h
Print pager command help.
j, +, <DOWN-ARROW>
Forward one line.
k, -, <UP-ARROW>
Back one line.
/pattern
Forward search for pattern.
?pattern
Backward search for pattern.
n
Repeat current search.
N
Repeat current search in the opposite direction.
q, Q, ^C, ^D, ^Z
Quit return to the caller.
any other character
Prompt again.
Hit any key to continue:"""
PREV_POS_NXT_REPRINT = -1, -1
def __init__(self, contents, out=None, prompt=None):
"""Constructor.
Args:
contents: The entire contents of the text lines to page.
out: The output stream, log.out (effectively) if None.
prompt: The page break prompt, a defalt prompt is used if None..
"""
self._contents = contents
self._out = out or sys.stdout
self._search_pattern = None
self._search_direction = None
# prev_pos, prev_next values to force reprint
self.prev_pos, self.prev_nxt = self.PREV_POS_NXT_REPRINT
# Initialize the console attributes.
self._attr = console_attr.GetConsoleAttr()
self._width, self._height = self._attr.GetTermSize()
# Initialize the prompt and the prompt clear string.
if not prompt:
prompt = '{bold}--({{percent}}%)--{normal}'.format(
bold=self._attr.GetFontCode(bold=True),
normal=self._attr.GetFontCode())
self._clear = '\r{0}\r'.format(' ' * (self._attr.DisplayWidth(prompt) - 6))
self._prompt = prompt
# Initialize a list of lines with long lines split into separate display
# lines.
self._lines = []
for line in contents.splitlines():
self._lines += self._attr.SplitLine(line, self._width)
def _Write(self, s):
"""Mockable helper that writes s to self._out."""
self._out.write(s)
def _GetSearchCommand(self, c):
"""Consumes a search command and returns the equivalent pager command.
The search pattern is an RE that is pre-compiled and cached for subsequent
/<newline>, ?<newline>, n, or N commands.
Args:
c: The search command char.
Returns:
The pager command char.
"""
self._Write(c)
buf = ''
while True:
p = self._attr.GetRawKey()
if p in (None, '\n', '\r') or len(p) != 1:
break
self._Write(p)
buf += p
self._Write('\r' + ' ' * len(buf) + '\r')
if buf:
try:
self._search_pattern = re.compile(buf)
except re.error:
# Silently ignore pattern errors.
self._search_pattern = None
return ''
self._search_direction = 'n' if c == '/' else 'N'
return 'n'
def _Help(self):
"""Print command help and wait for any character to continue."""
clear = self._height - (len(self.HELP_TEXT) -
len(self.HELP_TEXT.replace('\n', '')))
if clear > 0:
self._Write('\n' * clear)
self._Write(self.HELP_TEXT)
self._attr.GetRawKey()
self._Write('\n')
def Run(self):
"""Run the pager."""
# No paging if the contents are small enough.
if len(self._lines) <= self._height:
self._Write(self._contents)
return
# We will not always reset previous values.
reset_prev_values = True
# Save room for the prompt at the bottom of the page.
self._height -= 1
# Loop over all the pages.
pos = 0
while pos < len(self._lines):
# Write a page of lines.
nxt = pos + self._height
if nxt > len(self._lines):
nxt = len(self._lines)
pos = nxt - self._height
# Checks if the starting position is in between the current printed lines
# so we don't need to reprint all the lines.
if self.prev_pos < pos < self.prev_nxt:
# we start where the previous page ended.
self._Write('\n'.join(self._lines[self.prev_nxt:nxt]) + '\n')
elif pos != self.prev_pos and nxt != self.prev_nxt:
self._Write('\n'.join(self._lines[pos:nxt]) + '\n')
# Handle the prompt response.
percent = self._prompt.format(percent=100 * nxt // len(self._lines))
digits = ''
while True:
# We want to reset prev values if we just exited out of the while loop
if reset_prev_values:
self.prev_pos, self.prev_nxt = pos, nxt
reset_prev_values = False
self._Write(percent)
c = self._attr.GetRawKey()
self._Write(self._clear)
# Parse the command.
if c in (None, # EOF.
'q', # Quit.
'Q', # Quit.
'\x03', # ^C (unix & windows terminal interrupt)
'\x1b', # ESC.
):
# Quit.
return
elif c in ('/', '?'):
c = self._GetSearchCommand(c)
elif c.isdigit():
# Collect digits for operation count.
digits += c
continue
# Set the optional command count.
if digits:
count = int(digits)
digits = ''
else:
count = 0
# Finally commit to command c.
if c in ('<PAGE-UP>', '<LEFT-ARROW>', 'b', '\x02'):
# Previous page.
nxt = pos - self._height
if nxt < 0:
nxt = 0
elif c in ('<PAGE-DOWN>', '<RIGHT-ARROW>', 'f', '\x06', ' '):
# Next page.
if nxt >= len(self._lines):
continue
nxt = pos + self._height
if nxt >= len(self._lines):
nxt = pos
elif c in ('<HOME>', 'g'):
# First page.
nxt = count - 1
if nxt > len(self._lines) - self._height:
nxt = len(self._lines) - self._height
if nxt < 0:
nxt = 0
elif c in ('<END>', 'G'):
# Last page.
nxt = len(self._lines) - count
if nxt > len(self._lines) - self._height:
nxt = len(self._lines) - self._height
if nxt < 0:
nxt = 0
elif c == 'h':
self._Help()
# Special case when we want to reprint the previous display.
self.prev_pos, self.prev_nxt = self.PREV_POS_NXT_REPRINT
nxt = pos
break
elif c in ('<DOWN-ARROW>', 'j', '+', '\n', '\r'):
# Next line.
if nxt >= len(self._lines):
continue
nxt = pos + 1
if nxt >= len(self._lines):
nxt = pos
elif c in ('<UP-ARROW>', 'k', '-'):
# Previous line.
nxt = pos - 1
if nxt < 0:
nxt = 0
elif c in ('n', 'N'):
# Next pattern match search.
if not self._search_pattern:
continue
nxt = pos
i = pos
direction = 1 if c == self._search_direction else -1
while True:
i += direction
if i < 0 or i >= len(self._lines):
break
if self._search_pattern.search(self._lines[i]):
nxt = i
break
else:
# Silently ignore everything else.
continue
if nxt != pos:
# We will exit the while loop because position changed so we can reset
# prev values.
reset_prev_values = True
break
pos = nxt
+207
View File
@@ -0,0 +1,207 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A module for dealing with unknown string and environment encodings."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import sys
import six
def Encode(string, encoding=None):
"""Encode the text string to a byte string.
Args:
string: str, The text string to encode.
encoding: The suggested encoding if known.
Returns:
str, The binary string.
"""
if string is None:
return None
if not six.PY2:
# In Python 3, the environment sets and gets accept and return text strings
# only, and it handles the encoding itself so this is not necessary.
return string
if isinstance(string, six.binary_type):
# Already an encoded byte string, we are done
return string
encoding = encoding or _GetEncoding()
return string.encode(encoding)
def Decode(data, encoding=None):
"""Returns string with non-ascii characters decoded to UNICODE.
UTF-8, the suggested encoding, and the usual suspects will be attempted in
order.
Args:
data: A string or object that has str() and unicode() methods that may
contain an encoding incompatible with the standard output encoding.
encoding: The suggested encoding if known.
Returns:
A text string representing the decoded byte string.
"""
if data is None:
return None
# First we are going to get the data object to be a text string.
# Don't use six.string_types here because on Python 3 bytes is not considered
# a string type and we want to include that.
if isinstance(data, six.text_type) or isinstance(data, six.binary_type):
string = data
else:
# Some non-string type of object.
try:
string = six.text_type(data)
except (TypeError, UnicodeError):
# The string cannot be converted to unicode -- default to str() which will
# catch objects with special __str__ methods.
string = str(data)
if isinstance(string, six.text_type):
# Our work is done here.
return string
try:
# Just return the string if its pure ASCII.
return string.decode('ascii')
except UnicodeError:
# The string is not ASCII encoded.
pass
# Try the suggested encoding if specified.
if encoding:
try:
return string.decode(encoding)
except UnicodeError:
# Bad suggestion.
pass
# Try UTF-8 because the other encodings could be extended ASCII. It would
# be exceptional if a valid extended ascii encoding with extended chars
# were also a valid UITF-8 encoding.
try:
return string.decode('utf8')
except UnicodeError:
# Not a UTF-8 encoding.
pass
# Try the filesystem encoding.
try:
return string.decode(sys.getfilesystemencoding())
except UnicodeError:
# string is not encoded for filesystem paths.
pass
# Try the system default encoding.
try:
return string.decode(sys.getdefaultencoding())
except UnicodeError:
# string is not encoded using the default encoding.
pass
# We don't know the string encoding.
# This works around a Python str.encode() "feature" that throws
# an ASCII *decode* exception on str strings that contain 8th bit set
# bytes. For example, this sequence throws an exception:
# string = '\xdc' # iso-8859-1 'Ü'
# string = string.encode('ascii', 'backslashreplace')
# even though 'backslashreplace' is documented to handle encoding
# errors. We work around the problem by first decoding the str string
# from an 8-bit encoding to unicode, selecting any 8-bit encoding that
# uses all 256 bytes (such as ISO-8559-1):
# string = string.decode('iso-8859-1')
# Using this produces a sequence that works:
# string = '\xdc'
# string = string.decode('iso-8859-1')
# string = string.encode('ascii', 'backslashreplace')
return string.decode('iso-8859-1')
def GetEncodedValue(env, name, default=None):
"""Returns the decoded value of the env var name.
Args:
env: {str: str}, The env dict.
name: str, The env var name.
default: The value to return if name is not in env.
Returns:
The decoded value of the env var name.
"""
name = Encode(name)
value = env.get(name)
if value is None:
return default
# In Python 3, the environment sets and gets accept and return text strings
# only, and it handles the encoding itself so this is not necessary.
return Decode(value)
def SetEncodedValue(env, name, value, encoding=None):
"""Sets the value of name in env to an encoded value.
Args:
env: {str: str}, The env dict.
name: str, The env var name.
value: str or unicode, The value for name. If None then name is removed from
env.
encoding: str, The encoding to use or None to try to infer it.
"""
# Python 2 *and* 3 unicode support falls apart at filesystem/argv/environment
# boundaries. The encoding used for filesystem paths and environment variable
# names/values is under user control on most systems. With one of those values
# in hand there is no way to tell exactly how the value was encoded. We get
# some reasonable hints from sys.getfilesystemencoding() or
# sys.getdefaultencoding() and use them to encode values that the receiving
# process will have a chance at decoding. Leaving the values as unicode
# strings will cause os module Unicode exceptions. What good is a language
# unicode model when the module support could care less?
name = Encode(name, encoding=encoding)
if value is None:
env.pop(name, None)
return
env[name] = Encode(value, encoding=encoding)
def EncodeEnv(env, encoding=None):
"""Encodes all the key value pairs in env in preparation for subprocess.
Args:
env: {str: str}, The environment you are going to pass to subprocess.
encoding: str, The encoding to use or None to use the default.
Returns:
{bytes: bytes}, The environment to pass to subprocess.
"""
encoding = encoding or _GetEncoding()
return {
Encode(k, encoding=encoding): Encode(v, encoding=encoding)
for k, v in six.iteritems(env)}
def _GetEncoding():
"""Gets the default encoding to use."""
return sys.getfilesystemencoding() or sys.getdefaultencoding()
+305 -136
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -30,7 +30,7 @@ A Fire CLI command is run by consuming the arguments in the command in order to
access a member of current component, call the current component (if it's a
function), or instantiate the current component (if it's a class). The target
component begins as Component, and at each operation the component becomes the
result of the preceeding operation.
result of the preceding operation.
For example "command fn arg1 arg2" might access the "fn" property of the initial
target component, and then call that function with arguments 'arg1' and 'arg2'.
@@ -44,6 +44,7 @@ The available flags for all Fire CLIs are:
-h --help: Provide help and usage information for the command.
-i --interactive: Drop into a Python REPL after running the command.
--completion: Write the Bash completion script for the tool to stdout.
--completion fish: Write the Fish completion script for the tool to stdout.
--separator SEPARATOR: Use SEPARATOR in place of the default separator, '-'.
--trace: Get the Fire Trace for the command.
"""
@@ -56,6 +57,7 @@ import inspect
import json
import os
import pipes
import re
import shlex
import sys
import types
@@ -67,6 +69,8 @@ from fire import inspectutils
from fire import interact
from fire import parser
from fire import trace
from fire import value_types
from fire.console import console_pager
import six
@@ -127,47 +131,48 @@ def Fire(component=None, command=None, name=None):
component_trace = _Fire(component, args, context, name)
if component_trace.HasError():
for help_flag in ['-h', '--help']:
if help_flag in component_trace.elements[-1].args:
command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
print(('WARNING: The proper way to show help is {cmd}.\n'
'Showing help anyway.\n').format(cmd=pipes.quote(command)),
file=sys.stderr)
print('Fire trace:\n{trace}\n'.format(trace=component_trace),
file=sys.stderr)
result = component_trace.GetResult()
print(
helputils.HelpString(result, component_trace, component_trace.verbose),
file=sys.stderr)
_DisplayError(component_trace)
raise FireExit(2, component_trace)
elif component_trace.show_trace and component_trace.show_help:
print('Fire trace:\n{trace}\n'.format(trace=component_trace),
file=sys.stderr)
if component_trace.show_trace and component_trace.show_help:
output = ['Fire trace:\n{trace}\n'.format(trace=component_trace)]
result = component_trace.GetResult()
print(
helputils.HelpString(result, component_trace, component_trace.verbose),
file=sys.stderr)
help_string = helputils.HelpString(
result, component_trace, component_trace.verbose)
output.append(help_string)
Display(output)
raise FireExit(0, component_trace)
elif component_trace.show_trace:
print('Fire trace:\n{trace}'.format(trace=component_trace),
file=sys.stderr)
if component_trace.show_trace:
output = ['Fire trace:\n{trace}'.format(trace=component_trace)]
Display(output)
raise FireExit(0, component_trace)
elif component_trace.show_help:
if component_trace.show_help:
result = component_trace.GetResult()
print(
helputils.HelpString(result, component_trace, component_trace.verbose),
file=sys.stderr)
help_string = helputils.HelpString(
result, component_trace, component_trace.verbose)
output = [help_string]
Display(output)
raise FireExit(0, component_trace)
else:
_PrintResult(component_trace, verbose=component_trace.verbose)
result = component_trace.GetResult()
return result
# The command succeeded normally; print the result.
_PrintResult(component_trace, verbose=component_trace.verbose)
result = component_trace.GetResult()
return result
def CompletionScript(name, component):
"""Returns the text of the Bash completion script for a Fire CLI."""
return completion.Script(name, component)
def Display(lines):
text = '\n'.join(lines) + '\n'
pager = console_pager.Pager(text, out=sys.stderr)
try:
pager.Run()
except: # pylint: disable=bare-except
# pager.Run() fails with termios.error(25, 'Inappropriate ioctl for device')
# for outputs that don't fit on a single screen in our test environment.
pass
def CompletionScript(name, component, shell):
"""Returns the text of the completion script for a Fire CLI."""
return completion.Script(name, component, shell=shell)
class FireError(Exception):
@@ -178,7 +183,7 @@ class FireError(Exception):
"""
class FireExit(SystemExit):
class FireExit(SystemExit): # pylint: disable=g-bad-exception-name
"""An exception raised by Fire to the client in the case of a FireError.
The trace of the Fire program is available on the `trace` property.
@@ -199,9 +204,42 @@ class FireExit(SystemExit):
self.trace = component_trace
def _IsHelpShortcut(component_trace, remaining_args):
"""Determines if the user is trying to access help without '--' separator.
For example, mycmd.py --help instead of mycmd.py -- --help.
Args:
component_trace: (FireTrace) The trace for the Fire command.
remaining_args: List of remaining args that haven't been consumed yet.
Returns:
True if help is requested, False otherwise.
"""
show_help = False
if remaining_args:
target = remaining_args[0]
if target in ('-h', '--help'):
# Check if --help would be consumed as a keyword argument, or is a member.
component = component_trace.GetResult()
if inspect.isclass(component) or inspect.isroutine(component):
fn_spec = inspectutils.GetFullArgSpec(component)
_, remaining_kwargs, _ = _ParseKeywordArgs(remaining_args, fn_spec)
show_help = target in remaining_kwargs
else:
members = dict(inspect.getmembers(component))
show_help = target not in members
if show_help:
component_trace.show_help = True
command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
print('INFO: Showing help with the command {cmd}.\n'.format(
cmd=pipes.quote(command)), file=sys.stderr)
return show_help
def _PrintResult(component_trace, verbose=False):
"""Prints the result of the Fire call to stdout in a human readable way."""
# TODO: Design human readable deserializable serialization method
# TODO(dbieber): Design human readable deserializable serialization method
# and move serialization to it's own module.
result = component_trace.GetResult()
@@ -214,13 +252,29 @@ def _PrintResult(component_trace, verbose=False):
print(_DictAsString(result, verbose))
elif isinstance(result, tuple):
print(_OneLineResult(result))
elif isinstance(result,
(bool, six.string_types, six.integer_types, float, complex)):
elif isinstance(result, value_types.VALUE_TYPES):
print(result)
elif result is not None:
print(helputils.HelpString(result, component_trace, verbose))
def _DisplayError(component_trace):
"""Prints the Fire trace and the error to stdout."""
output = []
for help_flag in ('-h', '--help'):
if help_flag in component_trace.elements[-1].args:
command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
message = 'INFO: Showing help with the command {cmd}.\n'.format(
cmd=pipes.quote(command))
output.append(message)
output.append('Fire trace:\n{trace}\n'.format(trace=component_trace))
result = component_trace.GetResult()
help_string = helputils.HelpString(result, component_trace,
component_trace.verbose)
output.append(help_string)
Display(output)
def _DictAsString(result, verbose=False):
"""Returns a dict as a string.
@@ -230,20 +284,25 @@ def _DictAsString(result, verbose=False):
Returns:
A string representing the dict
"""
result = {key: value for key, value in result.items()
if _ComponentVisible(key, verbose)}
if not result:
# We need to do 2 iterations over the items in the result dict
# 1) Getting visible items and the longest key for output formatting
# 2) Actually construct the output lines
result_visible = {key: value for key, value in result.items()
if _ComponentVisible(key, verbose)}
if not result_visible:
return '{}'
longest_key = max(len(str(key)) for key in result.keys())
longest_key = max(len(str(key)) for key in result_visible.keys())
format_string = '{{key:{padding}s}} {{value}}'.format(padding=longest_key + 1)
lines = []
for key, value in result.items():
line = format_string.format(key=str(key) + ':',
value=_OneLineResult(value))
lines.append(line)
if _ComponentVisible(key, verbose):
line = format_string.format(key=str(key) + ':',
value=_OneLineResult(value))
lines.append(line)
return '\n'.join(lines)
@@ -257,7 +316,7 @@ def _ComponentVisible(component, verbose=False):
def _OneLineResult(result):
"""Returns result serialized to a single line string."""
# TODO: Ensure line is fewer than eg 120 characters.
# TODO(dbieber): Ensure line is fewer than eg 120 characters.
if isinstance(result, six.string_types):
return str(result).replace('\n', ' ')
@@ -338,11 +397,15 @@ def _Fire(component, args, context, name=None):
initial_args = remaining_args
if not remaining_args and (show_help or interactive or show_trace
or show_completion):
or show_completion is not None):
# Don't initialize the final class or call the final function unless
# there's a separator after it, and instead process the current component.
break
if _IsHelpShortcut(component_trace, remaining_args):
remaining_args = []
break
saved_args = []
used_separator = False
if separator in remaining_args:
@@ -359,20 +422,12 @@ def _Fire(component, args, context, name=None):
isclass = inspect.isclass(component)
try:
target = component.__name__
filename, lineno = inspectutils.GetFileAndLine(component)
component, consumed_args, remaining_args, capacity = _CallCallable(
component, remaining_args)
# Update the trace.
if isclass:
component_trace.AddInstantiatedClass(
component, target, consumed_args, filename, lineno, capacity)
else:
component_trace.AddCalledRoutine(
component, target, consumed_args, filename, lineno, capacity)
component, remaining_args = _CallAndUpdateTrace(
component,
remaining_args,
component_trace,
treatment='class' if isclass else 'routine',
target=component.__name__)
except FireError as error:
component_trace.AddError(error, initial_args)
return component_trace
@@ -381,7 +436,8 @@ def _Fire(component, args, context, name=None):
# If the initial component is a class, keep an instance for use with -i.
instance = component
elif isinstance(component, (list, tuple)) and remaining_args:
elif (isinstance(component, (list, tuple)) and remaining_args
and not inspectutils.IsNamedTuple(component)):
# The component is a tuple or list; we'll try to access a member.
arg = remaining_args[0]
try:
@@ -399,18 +455,39 @@ def _Fire(component, args, context, name=None):
component_trace.AddAccessedProperty(
component, index, [arg], filename, lineno)
elif isinstance(component, dict) and remaining_args:
elif ((isinstance(component, dict) or inspectutils.IsNamedTuple(component))
and remaining_args):
# The component is a dict; we'll try to access a member.
target = remaining_args[0]
if target in component:
# Allow indexing for namedtuples.
try:
index = int(target)
is_target_int = True
except ValueError:
is_target_int = False
if inspectutils.IsNamedTuple(component) and is_target_int:
try:
component = component[index]
except (ValueError, IndexError):
error = FireError(
'Unable to index into component with argument:', target)
component_trace.AddError(error, initial_args)
return component_trace
elif target in component:
component = component[target]
elif target.replace('-', '_') in component:
component = component[target.replace('-', '_')]
else:
# The target isn't present in the dict as a string, but maybe it is as
# another type.
# TODO: Consider alternatives for accessing non-string keys.
# TODO(dbieber): Consider alternatives for accessing non-string keys.
found_target = False
# If the component is a namedtuple, we need to convert it to dict to
# be able to use the .items() method.
if inspectutils.IsNamedTuple(component):
component = component._asdict() # pytype: disable=attribute-error
for key, value in component.items():
if target == str(key):
component = value
@@ -442,8 +519,19 @@ def _Fire(component, args, context, name=None):
component, target, consumed_args, filename, lineno)
except FireError as error:
component_trace.AddError(error, initial_args)
return component_trace
if not callable(component):
component_trace.AddError(error, initial_args)
return component_trace
# If we can't access the member, try to treat component as a callable.
try:
component, remaining_args = _CallAndUpdateTrace(component,
remaining_args,
component_trace,
treatment='callable')
except FireError as error:
component_trace.AddError(error, initial_args)
return component_trace
if used_separator:
# Add back in the arguments from after the separator.
@@ -469,10 +557,10 @@ def _Fire(component, args, context, name=None):
initial_args)
return component_trace
if show_completion:
if show_completion is not None:
if name is None:
raise ValueError('Cannot make completion script without command name')
script = CompletionScript(name, initial_component)
script = CompletionScript(name, initial_component, shell=show_completion)
component_trace.AddCompletionScript(script)
if interactive:
@@ -524,23 +612,44 @@ def _GetMember(component, args):
raise FireError('Could not consume arg:', arg)
def _CallCallable(fn, args):
"""Calls the function fn by consuming args from args.
def _CallAndUpdateTrace(component, args, component_trace, treatment='class',
target=None):
"""Call the component by consuming args from args, and update the FireTrace.
The component could be a class, a routine, or a callable object. This function
calls the component and adds the appropriate action to component_trace.
Args:
fn: The function to call or class to instantiate.
args: Args from which to consume for calling the function.
component: The component to call
args: Args for calling the component
component_trace: FireTrace object that contains action trace
treatment: Type of treatment used. Indicating whether we treat the component
as a class, a routine, or a callable.
target: Target in FireTrace element, default is None. If the value is None,
the component itself will be used as target.
Returns:
component: The object that is the result of the function call.
consumed_args: The args that were consumed for the function call.
component: The object that is the result of the callable call.
remaining_args: The remaining args that haven't been consumed yet.
capacity: Whether the call could have taken additional args.
"""
if not target:
target = component
filename, lineno = inspectutils.GetFileAndLine(component)
fn = component.__call__ if treatment == 'callable' else component
parse = _MakeParseFn(fn)
(varargs, kwargs), consumed_args, remaining_args, capacity = parse(args)
component = fn(*varargs, **kwargs)
result = fn(*varargs, **kwargs)
return result, consumed_args, remaining_args, capacity
if treatment == 'class':
action = trace.INSTANTIATED_CLASS
elif treatment == 'routine':
action = trace.CALLED_ROUTINE
else:
action = trace.CALLED_CALLABLE
component_trace.AddCalledComponent(
component, target, consumed_args, filename, lineno, capacity,
action=action)
return component, remaining_args
def _MakeParseFn(fn):
@@ -555,7 +664,6 @@ def _MakeParseFn(fn):
the leftover args from the arguments to the parse function.
"""
fn_spec = inspectutils.GetFullArgSpec(fn)
all_args = fn_spec.args + fn_spec.kwonlyargs
metadata = decorators.GetMetadata(fn)
# Note: num_required_args is the number of positional arguments without
@@ -565,7 +673,7 @@ def _MakeParseFn(fn):
def _ParseFn(args):
"""Parses the list of `args` into (varargs, kwargs), remaining_args."""
kwargs, remaining_args = _ParseKeywordArgs(args, all_args, fn_spec.varkw)
kwargs, remaining_kwargs, remaining_args = _ParseKeywordArgs(args, fn_spec)
# Note: _ParseArgs modifies kwargs.
parsed_args, kwargs, remaining_args, capacity = _ParseArgs(
@@ -594,6 +702,7 @@ def _MakeParseFn(fn):
varargs[index] = _ParseValue(value, None, None, metadata)
varargs = parsed_args + varargs
remaining_args += remaining_kwargs
consumed_args = args[:len(args) - len(remaining_args)]
return (varargs, kwargs), consumed_args, remaining_args, capacity
@@ -625,7 +734,7 @@ def _ParseArgs(fn_args, fn_defaults, num_required_args, kwargs,
remaining_args: A list of the supplied args that have not been used yet.
capacity: Whether the call could have taken args in place of defaults.
Raises:
FireError: if additional positional arguments are expected, but none are
FireError: If additional positional arguments are expected, but none are
available.
"""
accepts_positional_args = metadata.get(decorators.ACCEPTS_POSITIONAL_ARGS)
@@ -660,10 +769,10 @@ def _ParseArgs(fn_args, fn_defaults, num_required_args, kwargs,
return parsed_args, kwargs, remaining_args, capacity
def _ParseKeywordArgs(args, fn_args, fn_keywords):
def _ParseKeywordArgs(args, fn_spec):
"""Parses the supplied arguments for keyword arguments.
Given a list of arguments, finds occurences of --name value, and uses 'name'
Given a list of arguments, finds occurrences of --name value, and uses 'name'
as the keyword and 'value' as the value. Constructs and returns a dictionary
of these keyword arguments, and returns a list of the remaining arguments.
@@ -674,71 +783,131 @@ def _ParseKeywordArgs(args, fn_args, fn_keywords):
_ParseArgs, which converts them to the appropriate type.
Args:
args: A list of arguments
fn_args: A list of argument names that the target function accepts,
including positional and named arguments, but not the varargs or kwargs
names.
fn_keywords: The argument name for **kwargs, or None if **kwargs not used
args: A list of arguments.
fn_spec: The inspectutils.FullArgSpec describing the given callable.
Returns:
kwargs: A dictionary mapping keywords to values.
remaining_kwargs: A list of the unused kwargs from the original args.
remaining_args: A list of the unused arguments from the original args.
Raises:
FireError: If a single-character flag is passed that could refer to multiple
possible args.
"""
kwargs = {}
if args:
remaining_args = []
skip_argument = False
remaining_kwargs = []
remaining_args = []
fn_keywords = fn_spec.varkw
fn_args = fn_spec.args + fn_spec.kwonlyargs
for index, argument in enumerate(args):
if skip_argument:
skip_argument = False
continue
if not args:
return kwargs, remaining_kwargs, remaining_args
arg_consumed = False
if argument.startswith('--'):
# This is a named argument; get its value from this arg or the next.
skip_argument = False
for index, argument in enumerate(args):
if skip_argument:
skip_argument = False
continue
if _IsFlag(argument):
# This is a named argument. We get its value from this arg or the next.
# Terminology:
# argument: A full token from the command line, e.g. '--alpha=10'
# stripped_argument: An argument without leading hyphens.
# key: The contents of the stripped argument up to the first equal sign.
# "shortcut flag": refers to an argument where the key is just the first
# letter of a longer keyword.
# keyword: The Python function argument being set by this argument.
# value: The unparsed value for that Python function argument.
contains_equals = '=' in argument
stripped_argument = argument.lstrip('-')
if contains_equals:
key, value = stripped_argument.split('=', 1)
else:
key = stripped_argument
key = key.replace('-', '_')
is_bool_syntax = (not contains_equals and
(index + 1 == len(args) or _IsFlag(args[index + 1])))
# Determine the keyword.
keyword = '' # Indicates no valid keyword has been found yet.
if (key in fn_args
or (is_bool_syntax and key.startswith('no') and key[2:] in fn_args)
or fn_keywords):
keyword = key
elif len(key) == 1:
# This may be a shortcut flag.
matching_fn_args = [arg for arg in fn_args if arg[0] == key]
if len(matching_fn_args) == 1:
keyword = matching_fn_args[0]
elif len(matching_fn_args) > 1:
raise FireError("The argument '{}' is ambiguous as it could "
"refer to any of the following arguments: {}".format(
argument, matching_fn_args))
# Determine the value.
if not keyword:
got_argument = False
keyword = argument[2:]
contains_equals = '=' in keyword
is_bool_syntax = (
not contains_equals and
(index + 1 == len(args) or args[index + 1].startswith('--')))
if contains_equals:
keyword, value = keyword.split('=', 1)
got_argument = True
elif is_bool_syntax:
# Since there's no next arg or the next arg is a Flag, we consider
# this flag to be a boolean.
got_argument = True
if keyword in fn_args:
value = 'True'
elif keyword.startswith('no'):
keyword = keyword[2:]
value = 'False'
else:
value = 'True'
elif contains_equals:
# Already got the value above.
got_argument = True
elif is_bool_syntax:
# There's no next arg or the next arg is a Flag, so we consider this
# flag to be a boolean.
got_argument = True
if keyword in fn_args:
value = 'True'
elif keyword.startswith('no'):
keyword = keyword[2:]
value = 'False'
else:
if index + 1 < len(args):
value = args[index + 1]
got_argument = True
value = 'True'
else:
# The assert should pass. Otherwise either contains_equals or
# is_bool_syntax would have been True.
assert index + 1 < len(args)
value = args[index + 1]
got_argument = True
keyword = keyword.replace('-', '_')
# In order for us to consume the argument as a keyword arg, we either:
# Need to be explicitly expecting the keyword, or we need to be
# accepting **kwargs.
skip_argument = not contains_equals and not is_bool_syntax
if got_argument:
kwargs[keyword] = value
else:
remaining_kwargs.append(argument)
if skip_argument:
remaining_kwargs.append(args[index + 1])
else: # not _IsFlag(argument)
remaining_args.append(argument)
# In order for us to consume the argument as a keyword arg, we either:
# Need to be explicitly expecting the keyword, or we need to be
# accepting **kwargs.
if got_argument and (keyword in fn_args or fn_keywords):
kwargs[keyword] = value
skip_argument = not contains_equals and not is_bool_syntax
arg_consumed = True
return kwargs, remaining_kwargs, remaining_args
if not arg_consumed:
# The argument was not consumed, so it is still a remaining argument.
remaining_args.append(argument)
else:
remaining_args = args
return kwargs, remaining_args
def _IsFlag(argument):
"""Determines if the argument is a flag argument.
If it starts with a hyphen and isn't a negative number, it's a flag.
Args:
argument: A command line argument that may or may not be a flag.
Returns:
A boolean indicating whether the argument is a flag.
"""
return _IsSingleCharFlag(argument) or _IsMultiCharFlag(argument)
def _IsSingleCharFlag(argument):
"""Determines if the argument is a single char flag (e.g. '-a')."""
return re.match('^-[a-zA-Z]$', argument) or re.match('^-[a-zA-Z]=', argument)
def _IsMultiCharFlag(argument):
"""Determines if the argument is a multi char flag (e.g. '--alpha')."""
return argument.startswith('--') or re.match('^-[a-zA-Z]', argument)
def _ParseValue(value, index, arg, metadata):
+62 -7
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -72,14 +72,43 @@ class CoreTest(testutils.BaseTestCase):
self.assertEqual(variables['D'], tc.WithDefaults)
self.assertIsInstance(variables['trace'], trace.FireTrace)
def testImproperUseOfHelp(self):
# This should produce a warning explaining the proper use of help.
with self.assertRaisesFireExit(2, 'The proper way to show help.*Usage:'):
core.Fire(tc.TypedProperties, command=['alpha', '--help'])
# TODO(dbieber): Use parameterized tests to break up repetitive tests.
def testHelpWithClass(self):
with self.assertRaisesFireExit(0, 'Usage:.*ARG1'):
core.Fire(tc.InstanceVars, command=['--', '--help'])
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*ARG1'):
core.Fire(tc.InstanceVars, command=['--help'])
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*ARG1'):
core.Fire(tc.InstanceVars, command=['-h'])
def testProperUseOfHelp(self):
with self.assertRaisesFireExit(0, 'Usage:.*upper'):
def testHelpWithMember(self):
with self.assertRaisesFireExit(0, 'Usage:.*capitalize'):
core.Fire(tc.TypedProperties, command=['gamma', '--', '--help'])
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*capitalize'):
core.Fire(tc.TypedProperties, command=['gamma', '--help'])
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*capitalize'):
core.Fire(tc.TypedProperties, command=['gamma', '-h'])
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*delta'):
core.Fire(tc.TypedProperties, command=['delta', '--help'])
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*echo'):
core.Fire(tc.TypedProperties, command=['echo', '--help'])
def testHelpOnErrorInConstructor(self):
with self.assertRaisesFireExit(0, 'Usage:.*[VALUE]'):
core.Fire(tc.ErrorInConstructor, command=['--', '--help'])
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*[VALUE]'):
core.Fire(tc.ErrorInConstructor, command=['--help'])
def testHelpWithNamespaceCollision(self):
# Tests cases when calling the help shortcut should not show help.
with self.assertOutputMatches(stdout='Docstring.*', stderr=None):
core.Fire(tc.WithHelpArg, command=['--help', 'False'])
with self.assertOutputMatches(stdout='help in a dict', stderr=None):
core.Fire(tc.WithHelpArg, command=['dictionary', '__help'])
with self.assertOutputMatches(stdout='{}', stderr=None):
core.Fire(tc.WithHelpArg, command=['dictionary', '--help'])
with self.assertOutputMatches(stdout='False', stderr=None):
core.Fire(tc.function_with_help, command=['False'])
def testInvalidParameterRaisesFireExit(self):
with self.assertRaisesFireExit(2, 'runmisspelled'):
@@ -105,6 +134,32 @@ class CoreTest(testutils.BaseTestCase):
with self.assertOutputMatches(stdout='{}', stderr=None):
core.Fire(tc.EmptyDictOutput, command=['nothing_printable'])
def testPrintOrderedDict(self):
with self.assertOutputMatches(stdout=r'A:\s+A\s+2:\s+2\s+', stderr=None):
core.Fire(tc.OrderedDictionary, command=['non_empty'])
with self.assertOutputMatches(stdout='{}'):
core.Fire(tc.OrderedDictionary, command=['empty'])
def testPrintNamedTupleField(self):
with self.assertOutputMatches(stdout='11', stderr=None):
core.Fire(tc.NamedTuple, command=['point', 'x'])
def testPrintNamedTupleIndex(self):
with self.assertOutputMatches(stdout='22', stderr=None):
core.Fire(tc.NamedTuple, command=['point', '1'])
def testPrintNamedTupleNegativeIndex(self):
with self.assertOutputMatches(stdout='11', stderr=None):
core.Fire(tc.NamedTuple, command=['point', '-2'])
def testCallable(self):
with self.assertOutputMatches(stdout=r'foo:\s+foo\s+', stderr=None):
core.Fire(tc.CallableWithKeywordArgument(), command=['--foo=foo'])
with self.assertOutputMatches(stdout=r'foo\s+', stderr=None):
core.Fire(tc.CallableWithKeywordArgument(), command=['print_msg', 'foo'])
with self.assertOutputMatches(stdout=r'', stderr=None):
core.Fire(tc.CallableWithKeywordArgument(), command=[])
if __name__ == '__main__':
testutils.main()
+1 -1
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
+3 -2
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@ from fire import testutils
class NoDefaults(object):
"""A class for testing decorated functions without default values."""
@decorators.SetParseFns(count=int)
def double(self, count):
@@ -85,7 +86,7 @@ class WithKwargs(object):
class WithVarArgs(object):
@decorators.SetParseFn(str)
def example7(self, arg1, arg2=None, *varargs, **kwargs):
def example7(self, arg1, arg2=None, *varargs, **kwargs): # pylint: disable=keyword-arg-before-vararg
return arg1, arg2, varargs, kwargs
+695
View File
@@ -0,0 +1,695 @@
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Docstring parsing module for Python Fire.
The following features of docstrings are not supported.
TODO(dbieber): Support these features.
- numpy docstrings may begin with the function signature.
- whitespace may be important for proper structuring of a docstring
- I've seen `argname` (with single backticks) as a style of documenting
arguments. The `argname` appears on one line, and the description on the next.
- .. Sphinx directives such as .. note:: are not understood.
- After a section ends, future contents may be included in the section. E.g.
:returns: This is what is returned.
Example: An example goes here.
- @param is sometimes used. E.g.
@param argname (type) Description
@return (type) Description
- The true signature of a function is not used by the docstring parser. It could
be useful for determining whether something is a section header or an argument
for example.
- This example confuses types as part of the docstrings.
Parameters
argname : argtype
Arg description
- If there's no blank line after the summary, the description will be slurped
up into the summary.
- "Examples" should be its own section type. aka "Usage".
- "Notes" should be a section type.
- Some people put parenthesis around their types in RST format, e.g.
:param (type) paramname:
- :rtype: directive (return type)
- Also ":rtype str" with no closing ":" has come up.
- Return types are not supported.
- "# Returns" as a section title style
- ":raises ExceptionType: Description" ignores the ExceptionType currently.
- "Defaults to X" occurs sometimes.
- "True | False" indicates bool type.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import collections
import re
import enum
class DocstringInfo(
collections.namedtuple(
'DocstringInfo',
('summary', 'description', 'args', 'returns', 'yields', 'raises'))):
pass
DocstringInfo.__new__.__defaults__ = (None,) * len(DocstringInfo._fields)
class ArgInfo(
collections.namedtuple(
'ArgInfo',
('name', 'type', 'description'))):
pass
ArgInfo.__new__.__defaults__ = (None,) * len(ArgInfo._fields)
class Namespace(dict):
"""A dict with attribute (dot-notation) access enabled."""
def __getattr__(self, key):
if key not in self:
self[key] = Namespace()
return self[key]
def __setattr__(self, key, value):
self[key] = value
def __delattr__(self, key):
if key in self:
del self[key]
class Sections(enum.Enum):
ARGS = 0
RETURNS = 1
YIELDS = 2
RAISES = 3
TYPE = 4
class Formats(enum.Enum):
GOOGLE = 0
NUMPY = 1
RST = 2
SECTION_TITLES = {
Sections.ARGS: ('argument', 'arg', 'parameter', 'param'),
Sections.RETURNS: ('return',),
Sections.YIELDS: ('yield',),
Sections.RAISES: ('raise', 'except', 'exception', 'throw', 'error', 'warn'),
Sections.TYPE: ('type',), # rst-only
}
def parse(docstring):
"""Returns DocstringInfo about the given docstring.
This parser aims to parse Google, numpy, and rst formatted docstrings. These
are the three most common docstring styles at the time of this writing.
This parser aims to be permissive, working even when the docstring deviates
from the strict recommendations of these styles.
This parser does not aim to fully extract all structured information from a
docstring, since there are simply too many ways to structure information in a
docstring. Sometimes content will remain as unstructured text and simply gets
included in the description.
The Google docstring style guide is available at:
https://github.com/google/styleguide/blob/gh-pages/pyguide.md
The numpy docstring style guide is available at:
https://numpydoc.readthedocs.io/en/latest/format.html
Information about the rST docstring format is available at:
https://www.python.org/dev/peps/pep-0287/
The full set of directives such as param and type for rST docstrings are at:
http://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html
Note: This function does not claim to handle all docstrings well. A list of
limitations is available at the top of the file. It does aim to run without
crashing in O(n) time on all strings on length n. If you find a string that
causes this to crash or run unacceptably slowly, please consider submitting
a pull request.
Args:
docstring: The docstring to parse.
Returns:
A DocstringInfo containing information about the docstring.
"""
if docstring is None:
return DocstringInfo()
lines = docstring.strip().split('\n')
lines_len = len(lines)
state = Namespace() # TODO(dbieber): Switch to an explicit class.
# Variables in state include:
state.section.title = None
state.section.indentation = None
state.section.line1_indentation = None
state.section.format = None
state.summary.permitted = True
state.summary.lines = []
state.description.lines = []
state.args = []
state.current_arg = None
state.returns.lines = []
state.yields.lines = []
state.raises.lines = []
for index, line in enumerate(lines):
has_next = index + 1 < lines_len
next_line = lines[index + 1] if has_next else None
line_info = _create_line_info(line, next_line)
_consume_line(line_info, state)
summary = ' '.join(state.summary.lines) if state.summary.lines else None
description = _join_lines(state.description.lines)
returns = _join_lines(state.returns.lines)
yields = _join_lines(state.yields.lines)
raises = _join_lines(state.raises.lines)
args = [
ArgInfo(
name=arg.name,
type=_cast_to_known_type(_join_lines(arg.type.lines)),
description=_join_lines(arg.description.lines),
)
for arg in state.args
]
return DocstringInfo(
summary=summary,
description=description,
args=args or None,
returns=returns,
raises=raises,
yields=yields,
)
def _join_lines(lines):
"""Joins lines with the appropriate connective whitespace.
This puts a single space between consecutive lines, unless there's a blank
line, in which case a full blank line is included.
Args:
lines: A list of lines to join.
Returns:
A string, the lines joined together.
"""
# TODO(dbieber): Add parameters for variations in whitespace handling.
if not lines:
return None
started = False
group_texts = [] # Full text of each section.
group_lines = [] # Lines within the current section.
for line in lines:
stripped_line = line.strip()
if stripped_line:
started = True
group_lines.append(stripped_line)
else:
if started:
group_text = ' '.join(group_lines)
group_texts.append(group_text)
group_lines = []
if group_lines: # Process the final group.
group_text = ' '.join(group_lines)
group_texts.append(group_text)
return '\n\n'.join(group_texts)
def _get_or_create_arg_by_name(state, name):
"""Gets or creates a new Arg.
These Arg objects (Namespaces) are turned into the ArgInfo namedtuples
returned by parse. Each Arg object is used to collect the name, type, and
description of a single argument to the docstring's function.
Args:
state: The state of the parser.
name: The name of the arg to create.
Returns:
The new Arg.
"""
for arg in state.args:
if arg.name == name:
return arg
arg = Namespace() # TODO(dbieber): Switch to an explicit class.
arg.name = name
arg.type.lines = []
arg.description.lines = []
state.args.append(arg)
return arg
def _is_arg_name(name):
"""Returns whether name is a valid arg name.
This is used to prevent multiple words (plaintext) from being misinterpreted
as an argument name. So if ":" appears in the middle of a line in a docstring,
we don't accidentally interpret the first half of that line as a single arg
name.
Args:
name: The name of the potential arg.
Returns:
True if name looks like an arg name, False otherwise.
"""
name = name.strip()
return (name
and ' ' not in name
and ':' not in name)
def _as_arg_name_and_type(text):
"""Returns text as a name and type, if text looks like an arg name and type.
Example:
_as_arg_name_and_type("foo (int)") == "foo", "int"
Args:
text: The text, which may or may not be an arg name and type.
Returns:
The arg name and type, if text looks like an arg name and type.
None otherwise.
"""
tokens = text.split()
if len(tokens) < 2:
return None
if _is_arg_name(tokens[0]):
type_token = ' '.join(tokens[1:])
type_token = type_token.lstrip('{([').rstrip('])}')
return tokens[0], type_token
else:
return None
def _as_arg_names(names_str):
"""Converts names_str to a list of arg names.
Example:
_as_arg_names("a, b, c") == ["a", "b", "c"]
Args:
names_str: A string with multiple space or comma separated arg names.
Returns:
A list of arg names, or None if names_str doesn't look like a list of arg
names.
"""
names = re.split(',| ', names_str)
names = [name.strip() for name in names if name.strip()]
for name in names:
if not _is_arg_name(name):
return None
if not names:
return None
return names
def _cast_to_known_type(name):
"""Canonicalizes a string representing a type if possible.
# TODO(dbieber): Support additional canonicalization, such as string/str, and
# boolean/bool.
Example:
_cast_to_known_type("str.") == "str"
Args:
name: A string representing a type, or None.
Returns:
A canonicalized version of the type string.
"""
if name is None:
return None
return name.rstrip('.')
def _consume_google_args_line(line_info, state):
"""Consume a single line from a Google args section."""
split_line = line_info.remaining.split(':', 1)
if len(split_line) > 1:
first, second = split_line # first is either the "arg" or "arg (type)"
if _is_arg_name(first.strip()):
arg = _get_or_create_arg_by_name(state, first.strip())
arg.description.lines.append(second.strip())
state.current_arg = arg
else:
arg_name_and_type = _as_arg_name_and_type(first)
if arg_name_and_type:
arg_name, type_str = arg_name_and_type
arg = _get_or_create_arg_by_name(state, arg_name)
arg.type.lines.append(type_str)
arg.description.lines.append(second.strip())
else:
if state.current_arg:
state.current_arg.description.lines.append(split_line[0])
else:
if state.current_arg:
state.current_arg.description.lines.append(split_line[0])
def _consume_line(line_info, state):
"""Consumes one line of text, updating the state accordingly.
When _consume_line is called, part of the line may already have been processed
for header information.
Args:
line_info: Information about the current and next line of the docstring.
state: The state of the docstring parser.
"""
_update_section_state(line_info, state)
if state.section.title is None:
if state.summary.permitted:
if line_info.remaining:
state.summary.lines.append(line_info.remaining)
elif state.summary.lines:
state.summary.permitted = False
else:
# We're past the end of the summary.
# Additions now contribute to the description.
state.description.lines.append(line_info.remaining)
else:
state.summary.permitted = False
if state.section.new and state.section.format == Formats.RST:
# The current line starts with an RST directive, e.g. ":param arg:".
directive = _get_directive(line_info)
directive_tokens = directive.split() # pytype: disable=attribute-error
if state.section.title == Sections.ARGS:
name = directive_tokens[-1]
arg = _get_or_create_arg_by_name(state, name)
if len(directive_tokens) == 3:
# A param directive of the form ":param type arg:".
arg.type.lines.append(directive_tokens[1])
state.current_arg = arg
elif state.section.title == Sections.TYPE:
name = directive_tokens[-1]
arg = _get_or_create_arg_by_name(state, name)
state.current_arg = arg
if (state.section.format == Formats.NUMPY and
_line_is_hyphens(line_info.remaining)):
# Skip this all-hyphens line, which is part of the numpy section header.
return
if state.section.title == Sections.ARGS:
if state.section.format == Formats.GOOGLE:
_consume_google_args_line(line_info, state)
elif state.section.format == Formats.RST:
state.current_arg.description.lines.append(line_info.remaining.strip())
elif state.section.format == Formats.NUMPY:
line_stripped = line_info.remaining.strip()
if _is_arg_name(line_stripped):
# Token on it's own line can either be the last word of the description
# of the previous arg, or a new arg. TODO: Whitespace can distinguish.
arg = _get_or_create_arg_by_name(state, line_stripped)
state.current_arg = arg
elif ':' in line_stripped:
possible_args, type_data = line_stripped.split(':', 1)
arg_names = _as_arg_names(possible_args) # re.split(' |,', s)
if arg_names:
for arg_name in arg_names:
arg = _get_or_create_arg_by_name(state, arg_name)
arg.type.lines.append(type_data)
state.current_arg = arg # TODO(dbieber): Multiple current args.
else: # Just an ordinary line.
if state.current_arg:
state.current_arg.description.lines.append(
line_info.remaining.strip())
else:
# TODO(dbieber): If not a blank line, add it to the description.
pass
else: # Just an ordinary line.
if state.current_arg:
state.current_arg.description.lines.append(
line_info.remaining.strip())
else:
# TODO(dbieber): If not a blank line, add it to the description.
pass
elif state.section.title == Sections.RETURNS:
state.returns.lines.append(line_info.remaining.strip())
elif state.section.title == Sections.YIELDS:
state.yields.lines.append(line_info.remaining.strip())
elif state.section.title == Sections.RAISES:
state.raises.lines.append(line_info.remaining.strip())
elif state.section.title == Sections.TYPE:
if state.section.format == Formats.RST:
assert state.current_arg is not None
state.current_arg.type.lines.append(line_info.remaining.strip())
else:
pass
def _create_line_info(line, next_line):
"""Returns information about the current and next line of the docstring."""
line_info = Namespace() # TODO(dbieber): Switch to an explicit class.
line_info.line = line
line_info.stripped = line.strip()
line_info.remaining = line_info.stripped
line_info.indentation = len(line) - len(line.lstrip())
line_info.next.line = next_line
line_info.next.stripped = next_line.strip() if next_line else None
line_info.next.indentation = (
len(next_line) - len(next_line.lstrip()) if next_line else None)
# Note: This counts all whitespace equally.
return line_info
def _update_section_state(line_info, state):
"""Uses line_info to determine the current section of the docstring.
Updates state and line_info.remaining.
Args:
line_info: Information about the current line.
state: The state of the parser.
"""
section_updated = False
google_section_permitted = _google_section_permitted(line_info, state)
google_section = google_section_permitted and _google_section(line_info)
if google_section:
state.section.format = Formats.GOOGLE
state.section.title = google_section
line_info.remaining = _get_after_google_header(line_info)
section_updated = True
rst_section = _rst_section(line_info)
if rst_section:
state.section.format = Formats.RST
state.section.title = rst_section
line_info.remaining = _get_after_directive(line_info)
section_updated = True
numpy_section = _numpy_section(line_info)
if numpy_section:
state.section.format = Formats.NUMPY
state.section.title = numpy_section
line_info.remaining = ''
section_updated = True
if section_updated:
state.section.new = True
state.section.indentation = line_info.indentation
state.section.line1_indentation = line_info.next.indentation
else:
state.section.new = False
def _google_section_permitted(line_info, state):
"""Returns whether a new google section is permitted to start here.
Q: Why might a new Google section not be allowed?
A: If we're in the middle of a Google "Args" section, then lines that start
"param:" will usually be a new arg, rather than a new section.
We use whitespace to determine when the Args section has actually ended.
A Google section ends when either:
- A new google section begins at either
- indentation less than indentation of line 1 of the previous section
- or <= indentation of the previous section
- Or the docstring terminates.
Args:
line_info: Information about the current line.
state: The state of the parser.
Returns:
True or False, indicating whether a new Google section is permitted at the
current line.
"""
if state.section.indentation is None: # We're not in a section yet.
return True
return (line_info.indentation <= state.section.indentation
or line_info.indentation < state.section.line1_indentation)
def _matches_section_title(title, section_title):
"""Returns whether title is a match for a specific section_title.
Example:
_matches_section_title('Yields', 'yield') == True
Args:
title: The title to check for matching.
section_title: A specific known section title to check against.
"""
title = title.lower()
section_title = section_title.lower()
return section_title in (title, title[:-1]) # Supports plurals / some typos.
def _matches_section(title, section):
"""Returns whether title is a match any known title for a specific section.
Example:
_matches_section_title('Yields', Sections.YIELDS) == True
_matches_section_title('param', Sections.Args) == True
Args:
title: The title to check for matching.
section: A specific section to check all possible titles for.
Returns:
True or False, indicating whether title is a match for the specified
section.
"""
for section_title in SECTION_TITLES[section]:
if _matches_section_title(title, section_title):
return True
return False
def _section_from_possible_title(possible_title):
"""Returns a section matched by the possible title, or None if none match.
Args:
possible_title: A string that may be the title of a new section.
Returns:
A Section type if one matches, or None if no section type matches.
"""
for section in SECTION_TITLES:
if _matches_section(possible_title, section):
return section
return None
def _google_section(line_info):
"""Checks whether the current line is the start of a new Google-style section.
This docstring is a Google-style docstring. Google-style sections look like
this:
Section Name:
section body goes here
Args:
line_info: Information about the current line.
Returns:
A Section type if one matches, or None if no section type matches.
"""
colon_index = line_info.remaining.find(':')
possible_title = line_info.remaining[:colon_index]
return _section_from_possible_title(possible_title)
def _get_after_google_header(line_info):
"""Gets the remainder of the line, after a Google header."""
colon_index = line_info.remaining.find(':')
return line_info.remaining[colon_index + 1:]
def _get_directive(line_info):
"""Gets a directive from the start of the line.
If the line is ":param str foo: Description of foo", then
_get_directive(line_info) returns "param str foo".
Args:
line_info: Information about the current line.
Returns:
The contents of a directive, or None if the line doesn't start with a
directive.
"""
if line_info.stripped.startswith(':'):
return line_info.stripped.split(':', 2)[1]
else:
return None
def _get_after_directive(line_info):
"""Gets the remainder of the line, after a directive."""
sections = line_info.stripped.split(':', 2)
if len(sections) > 2:
return sections[-1]
else:
return ''
def _rst_section(line_info):
"""Checks whether the current line is the start of a new RST-style section.
RST uses directives to specify information. An RST directive, which we refer
to as a section here, are surrounded with colons. For example, :param name:.
Args:
line_info: Information about the current line.
Returns:
A Section type if one matches, or None if no section type matches.
"""
directive = _get_directive(line_info)
if directive:
possible_title = directive.split()[0]
return _section_from_possible_title(possible_title)
else:
return None
def _line_is_hyphens(line):
"""Returns whether the line is entirely hyphens (and not blank)."""
return line and not line.strip('-')
def _numpy_section(line_info):
"""Checks whether the current line is the start of a new numpy-style section.
Numpy style sections are followed by a full line of hyphens, for example:
Section Name
------------
Section body goes here.
Args:
line_info: Information about the current line.
Returns:
A Section type if one matches, or None if no section type matches.
"""
next_line_is_hyphens = _line_is_hyphens(line_info.next.stripped)
if next_line_is_hyphens:
possible_title = line_info.remaining
return _section_from_possible_title(possible_title)
else:
return None
+40
View File
@@ -0,0 +1,40 @@
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Fuzz tests for the docstring parser module."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from fire import docstrings
from fire import testutils
from hypothesis import example
from hypothesis import given
from hypothesis import settings
from hypothesis import strategies as st
class DocstringsFuzzTest(testutils.BaseTestCase):
@settings(max_examples=1000, deadline=1000)
@given(st.text(min_size=1))
@example('This is a one-line docstring.')
def test_fuzz_parse(self, value):
docstrings.parse(value)
if __name__ == '__main__':
testutils.main()
+237
View File
@@ -0,0 +1,237 @@
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for fire docstrings module."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from fire import docstrings
from fire import testutils
DocstringInfo = docstrings.DocstringInfo # pylint: disable=invalid-name
ArgInfo = docstrings.ArgInfo # pylint: disable=invalid-name
class DocstringsTest(testutils.BaseTestCase):
def test_one_line_simple(self):
docstring = """A simple one line docstring."""
docstring_info = docstrings.parse(docstring)
expected_docstring_info = DocstringInfo(
summary='A simple one line docstring.',
)
self.assertEqual(docstring_info, expected_docstring_info)
def test_one_line_simple_whitespace(self):
docstring = """
A simple one line docstring.
"""
docstring_info = docstrings.parse(docstring)
expected_docstring_info = DocstringInfo(
summary='A simple one line docstring.',
)
self.assertEqual(docstring_info, expected_docstring_info)
def test_one_line_too_long(self):
# pylint: disable=line-too-long
docstring = """A one line docstring thats both a little too verbose and a little too long so it keeps going well beyond a reasonable length for a one-liner.
"""
# pylint: enable=line-too-long
docstring_info = docstrings.parse(docstring)
expected_docstring_info = DocstringInfo(
summary='A one line docstring thats both a little too verbose and '
'a little too long so it keeps going well beyond a reasonable length '
'for a one-liner.',
)
self.assertEqual(docstring_info, expected_docstring_info)
def test_one_line_runs_over(self):
# pylint: disable=line-too-long
docstring = """A one line docstring thats both a little too verbose and a little too long
so it runs onto a second line.
"""
# pylint: enable=line-too-long
docstring_info = docstrings.parse(docstring)
expected_docstring_info = DocstringInfo(
summary='A one line docstring thats both a little too verbose and '
'a little too long so it runs onto a second line.',
)
self.assertEqual(docstring_info, expected_docstring_info)
def test_one_line_runs_over_whitespace(self):
docstring = """
A one line docstring thats both a little too verbose and a little too long
so it runs onto a second line.
"""
docstring_info = docstrings.parse(docstring)
expected_docstring_info = DocstringInfo(
summary='A one line docstring thats both a little too verbose and '
'a little too long so it runs onto a second line.',
)
self.assertEqual(docstring_info, expected_docstring_info)
def test_google_format_args_only(self):
docstring = """One line description.
Args:
arg1: arg1_description
arg2: arg2_description
"""
docstring_info = docstrings.parse(docstring)
expected_docstring_info = DocstringInfo(
summary='One line description.',
args=[
ArgInfo(name='arg1', description='arg1_description'),
ArgInfo(name='arg2', description='arg2_description'),
]
)
self.assertEqual(docstring_info, expected_docstring_info)
def test_google_format_arg_named_args(self):
docstring = """
Args:
args: arg_description
"""
docstring_info = docstrings.parse(docstring)
expected_docstring_info = DocstringInfo(
args=[
ArgInfo(name='args', description='arg_description'),
]
)
self.assertEqual(docstring_info, expected_docstring_info)
def test_google_format_typed_args_and_returns(self):
docstring = """Docstring summary.
This is a longer description of the docstring. It spans multiple lines, as
is allowed.
Args:
param1 (int): The first parameter.
param2 (str): The second parameter.
Returns:
bool: The return value. True for success, False otherwise.
"""
docstring_info = docstrings.parse(docstring)
expected_docstring_info = DocstringInfo(
summary='Docstring summary.',
description='This is a longer description of the docstring. It spans '
'multiple lines, as is allowed.',
args=[
ArgInfo(name='param1', type='int',
description='The first parameter.'),
ArgInfo(name='param2', type='str',
description='The second parameter.'),
],
returns='bool: The return value. True for success, False otherwise.'
)
self.assertEqual(docstring_info, expected_docstring_info)
def test_rst_format_typed_args_and_returns(self):
docstring = """Docstring summary.
This is a longer description of the docstring. It spans across multiple
lines.
:param arg1: Description of arg1.
:type arg1: str.
:param arg2: Description of arg2.
:type arg2: bool.
:returns: int -- description of the return value.
:raises: AttributeError, KeyError
"""
docstring_info = docstrings.parse(docstring)
expected_docstring_info = DocstringInfo(
summary='Docstring summary.',
description='This is a longer description of the docstring. It spans '
'across multiple lines.',
args=[
ArgInfo(name='arg1', type='str',
description='Description of arg1.'),
ArgInfo(name='arg2', type='bool',
description='Description of arg2.'),
],
returns='int -- description of the return value.',
raises='AttributeError, KeyError',
)
self.assertEqual(docstring_info, expected_docstring_info)
def test_numpy_format_typed_args_and_returns(self):
docstring = """Docstring summary.
This is a longer description of the docstring. It spans across multiple
lines.
Parameters
----------
param1 : int
The first parameter.
param2 : str
The second parameter.
Returns
-------
bool
True if successful, False otherwise.
"""
docstring_info = docstrings.parse(docstring)
expected_docstring_info = DocstringInfo(
summary='Docstring summary.',
description='This is a longer description of the docstring. It spans '
'across multiple lines.',
args=[
ArgInfo(name='param1', type='int',
description='The first parameter.'),
ArgInfo(name='param2', type='str',
description='The second parameter.'),
],
# TODO(dbieber): Support return type.
returns='bool True if successful, False otherwise.',
)
self.assertEqual(docstring_info, expected_docstring_info)
def test_multisection_docstring(self):
docstring = """Docstring summary.
This is the first section of a docstring description.
This is the second section of a docstring description. This docstring
description has just two sections.
"""
docstring_info = docstrings.parse(docstring)
expected_docstring_info = DocstringInfo(
summary='Docstring summary.',
description='This is the first section of a docstring description.\n\n'
'This is the second section of a docstring description. This docstring '
'description has just two sections.',
)
self.assertEqual(docstring_info, expected_docstring_info)
def test_ill_formed_docstring(self):
docstring = """Docstring summary.
args: raises ::
:
pathological docstrings should not fail, and ideally should behave
reasonably.
"""
docstrings.parse(docstring)
if __name__ == '__main__':
testutils.main()
+1 -1
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
+115 -6
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -92,12 +92,30 @@ class FireTest(testutils.BaseTestCase):
fire.Fire(tc.OldStyleWithDefaults, command=['triple', '--count', '5']),
15)
def testFireNamedArgsSingleHyphen(self):
self.assertEqual(fire.Fire(tc.WithDefaults,
command=['double', '-count', '5']), 10)
self.assertEqual(fire.Fire(tc.WithDefaults,
command=['triple', '-count', '5']), 15)
self.assertEqual(
fire.Fire(tc.OldStyleWithDefaults, command=['double', '-count', '5']),
10)
self.assertEqual(
fire.Fire(tc.OldStyleWithDefaults, command=['triple', '-count', '5']),
15)
def testFireNamedArgsWithEquals(self):
self.assertEqual(fire.Fire(tc.WithDefaults,
command=['double', '--count=5']), 10)
self.assertEqual(fire.Fire(tc.WithDefaults,
command=['triple', '--count=5']), 15)
def testFireNamedArgsWithEqualsSingleHyphen(self):
self.assertEqual(fire.Fire(tc.WithDefaults,
command=['double', '-count=5']), 10)
self.assertEqual(fire.Fire(tc.WithDefaults,
command=['triple', '-count=5']), 15)
def testFireAllNamedArgs(self):
self.assertEqual(fire.Fire(tc.MixedDefaults, command=['sum', '1', '2']), 5)
self.assertEqual(fire.Fire(tc.MixedDefaults,
@@ -354,6 +372,23 @@ class FireTest(testutils.BaseTestCase):
fire.Fire(tc.MixedDefaults, command=['identity', '10', '--beta']),
(10, True))
def testBoolParsingSingleHyphen(self):
self.assertEqual(
fire.Fire(tc.MixedDefaults,
command=['identity', '-alpha=False', '10']), (False, 10))
self.assertEqual(
fire.Fire(tc.MixedDefaults,
command=['identity', '-alpha', '-beta', '10']), (True, 10))
self.assertEqual(
fire.Fire(tc.MixedDefaults,
command=['identity', '-alpha', '-beta=10']), (True, 10))
self.assertEqual(
fire.Fire(tc.MixedDefaults,
command=['identity', '-noalpha', '-beta']), (False, True))
self.assertEqual(
fire.Fire(tc.MixedDefaults,
command=['identity', '-alpha', '-10', '-beta']), (-10, True))
def testBoolParsingLessExpectedCases(self):
# Note: Does not return (True, 10).
self.assertEqual(
@@ -368,10 +403,15 @@ class FireTest(testutils.BaseTestCase):
fire.Fire(tc.MixedDefaults,
command=['identity', 'True', '10']), (True, 10))
# Note: Does not return ('--test', '0').
self.assertEqual(fire.Fire(tc.MixedDefaults,
command=['identity', '--alpha', '--test']),
(True, '--test'))
# Note: Does not return (True, '--test') or ('--test', 0).
with self.assertRaisesFireExit(2):
fire.Fire(tc.MixedDefaults, command=['identity', '--alpha', '--test'])
self.assertEqual(
fire.Fire(
tc.MixedDefaults,
command=['identity', '--alpha', 'True', '"--test"']),
(True, '--test'))
# To get ('--test', '0'), use one of the following:
self.assertEqual(fire.Fire(tc.MixedDefaults,
command=['identity', '--alpha=--test']),
@@ -380,6 +420,70 @@ class FireTest(testutils.BaseTestCase):
fire.Fire(tc.MixedDefaults, command=r'identity --alpha \"--test\"'),
('--test', '0'))
def testSingleCharFlagParsing(self):
self.assertEqual(
fire.Fire(tc.MixedDefaults,
command=['identity', '-a']), (True, '0'))
self.assertEqual(
fire.Fire(tc.MixedDefaults,
command=['identity', '-a', '--beta=10']), (True, 10))
self.assertEqual(
fire.Fire(tc.MixedDefaults,
command=['identity', '-a', '-b']), (True, True))
self.assertEqual(
fire.Fire(tc.MixedDefaults,
command=['identity', '-a', '42', '-b']), (42, True))
self.assertEqual(
fire.Fire(tc.MixedDefaults,
command=['identity', '-a', '42', '-b', '10']), (42, 10))
self.assertEqual(
fire.Fire(tc.MixedDefaults,
command=['identity', '--alpha', 'True', '-b', '10']),
(True, 10))
with self.assertRaisesFireExit(2):
# This test attempts to use an ambiguous shortcut flag on a function with
# a naming conflict for the shortcut, triggering a FireError.
fire.Fire(tc.SimilarArgNames, command=['identity', '-b'])
def testSingleCharFlagParsingEqualSign(self):
self.assertEqual(
fire.Fire(tc.MixedDefaults,
command=['identity', '-a=True']), (True, '0'))
self.assertEqual(
fire.Fire(tc.MixedDefaults,
command=['identity', '-a=3', '--beta=10']), (3, 10))
self.assertEqual(
fire.Fire(tc.MixedDefaults,
command=['identity', '-a=False', '-b=15']), (False, 15))
self.assertEqual(
fire.Fire(tc.MixedDefaults,
command=['identity', '-a', '42', '-b=12']), (42, 12))
self.assertEqual(
fire.Fire(tc.MixedDefaults,
command=['identity', '-a=42', '-b', '10']), (42, 10))
def testSingleCharFlagParsingExactMatch(self):
self.assertEqual(
fire.Fire(tc.SimilarArgNames,
command=['identity2', '-a']), (True, None))
self.assertEqual(
fire.Fire(tc.SimilarArgNames,
command=['identity2', '-a=10']), (10, None))
self.assertEqual(
fire.Fire(tc.SimilarArgNames,
command=['identity2', '--a']), (True, None))
self.assertEqual(
fire.Fire(tc.SimilarArgNames,
command=['identity2', '-alpha']), (None, True))
self.assertEqual(
fire.Fire(tc.SimilarArgNames,
command=['identity2', '-a', '-alpha']), (True, True))
def testSingleCharFlagParsingCapitalLetter(self):
self.assertEqual(
fire.Fire(tc.CapitalizedArgNames,
command=['sum', '-D', '5', '-G', '10']), 15)
def testBoolParsingWithNo(self):
# In these examples --nothing always refers to the nothing argument:
def fn1(thing, nothing):
@@ -509,7 +613,7 @@ class FireTest(testutils.BaseTestCase):
fire.Fire(tc.ReturnsObj,
command=['get-obj', 'arg1', 'arg2', 'as-bool', 'True']),
tc.BoolConverter)
# With a separator only the preceeding args are consumed by get_obj.
# With a separator only the preceding args are consumed by get_obj.
self.assertEqual(
fire.Fire(
tc.ReturnsObj,
@@ -525,6 +629,11 @@ class FireTest(testutils.BaseTestCase):
'--separator', '$$']),
True)
def testNegativeNumbers(self):
self.assertEqual(
fire.Fire(tc.MixedDefaults,
command=['sum', '--alpha', '-3', '--beta', '-4']), -11)
def testFloatForExpectedInt(self):
self.assertEqual(
fire.Fire(tc.MixedDefaults,
+473
View File
@@ -0,0 +1,473 @@
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""helptext is the new, work in progress, help text module for Fire.
This is a fork of, and is intended to replace, helputils.
Utility for producing help strings for use in Fire CLIs.
Can produce help strings suitable for display in Fire CLIs for any type of
Python object, module, class, or function.
There are two types of informative strings: Usage and Help screens.
Usage screens are shown when the user accesses a group or accesses a command
without calling it. A Usage screen shows information about how to use that group
or command. Usage screens are typically short and show the minimal information
necessary for the user to determine how to proceed.
Help screens are shown when the user requests help with the help flag (--help).
Help screens are shown in a less-style console view, and contain detailed help
information.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import inspect
from fire import completion
from fire import docstrings
from fire import inspectutils
from fire import value_types
def Text(component, trace=None, verbose=False):
"""Returns the text to show for a supplied component.
The component can be any Python class, object, function, module, etc.
Args:
component: The component to determine the help string for.
trace: The Fire trace leading to this component.
verbose: Whether to include private members in the help string.
Returns:
String suitable for display giving information about the component.
"""
info = inspectutils.Info(component)
info['docstring_info'] = docstrings.parse(info['docstring'])
is_error_screen = False
if trace:
is_error_screen = trace.HasError()
if is_error_screen:
return UsageText(info, trace, verbose=verbose)
else:
return HelpText(info, trace, verbose=verbose)
def GetArgsAngFlags(component):
"""Returns all types of arguments and flags of a component."""
spec = inspectutils.GetFullArgSpec(component)
args = spec.args
if spec.defaults is None:
num_defaults = 0
else:
num_defaults = len(spec.defaults)
args_with_no_defaults = args[:len(args) - num_defaults]
args_with_defaults = args[len(args) - num_defaults:]
flags = args_with_defaults + spec.kwonlyargs
return args_with_no_defaults, args_with_defaults, flags
def GetSummaryAndDescription(docstring_info):
"""Retrieves summary and description for help text generation."""
# To handle both empty string and None
summary = docstring_info.summary if docstring_info.summary else None
description = (
docstring_info.description if docstring_info.description else None)
return summary, description
def GetCurrentCommand(trace=None):
"""Returns current command for the purpose of generating help text."""
if trace:
current_command = trace.GetCommand()
else:
current_command = ''
return current_command
def HelpText(component, info, trace=None, verbose=False):
if inspect.isroutine(component) or inspect.isclass(component):
return HelpTextForFunction(component, info, trace)
else:
return HelpTextForObject(component, info, trace, verbose)
def HelpTextForFunction(component, info, trace=None, verbose=False):
"""Returns detail help text for a function component.
Args:
component: Current component to generate help text for.
info: Info containing metadata of component.
trace: FireTrace object that leads to current component.
verbose: Whether to display help text in verbose mode.
Returns:
Formatted help text for display.
"""
# TODO(joejoevictor): Implement verbose related output
del verbose
current_command = GetCurrentCommand(trace)
summary, description = GetSummaryAndDescription(info['docstring_info'])
spec = inspectutils.GetFullArgSpec(component)
args = spec.args
args_with_no_defaults, args_with_defaults, flags = GetArgsAngFlags(component)
del args_with_defaults
output_template = """NAME
{name_section}
SYNOPSIS
{synopsis_section}
DESCRIPTION
{description_section}
{args_and_flags_section}
NOTES
You could also use flags syntax for POSITIONAL ARGUMENTS
"""
# Name section
name_section_template = '{current_command}{command_summary}'
command_summary_str = ' - ' + summary if summary else ''
name_section = name_section_template.format(
current_command=current_command, command_summary=command_summary_str)
args_and_flags = ''
if args_with_no_defaults:
items = [arg.upper() for arg in args_with_no_defaults]
args_and_flags = ' '.join(items)
synopsis_flag_template = '[--{flag_name}={flag_name_upper}]'
if flags:
items = [
synopsis_flag_template.format(
flag_name=flag, flag_name_upper=flag.upper()) for flag in flags
]
args_and_flags = args_and_flags + ' '.join(items)
# Synopsis section
synopsis_section_template = '{current_command} {args_and_flags}'
positional_arguments = '|'.join(args)
if positional_arguments:
positional_arguments = ' ' + positional_arguments
synopsis_section = synopsis_section_template.format(
current_command=current_command, args_and_flags=args_and_flags)
# Description section
description_section = description if description else summary
args_and_flags_section = ''
# Positional arguments and flags section
pos_arg_template = """
POSITIONAL ARGUMENTS
{items}
"""
pos_arg_items = []
for arg in args_with_no_defaults:
item_template = ' {arg_name}\n {arg_description}\n'
arg_description = None
for arg_in_docstring in info['docstring_info'].args:
if arg_in_docstring.name == arg:
arg_description = arg_in_docstring.description
item = item_template.format(
arg_name=arg.upper(), arg_description=arg_description)
pos_arg_items.append(item)
if pos_arg_items:
args_and_flags_section += pos_arg_template.format(
items='\n'.join(pos_arg_items).rstrip('\n'))
flags_template = """
FLAGS
{items}
"""
flag_items = []
for flag in flags:
item_template = ' --{flag_name}\n {flag_description}\n'
flag_description = None
for arg_in_docstring in info['docstring_info'].args:
if arg_in_docstring.name == flag:
flag_description = arg_in_docstring.description
item = item_template.format(
flag_name=flag, flag_description=flag_description)
flag_items.append(item)
if flag_items:
args_and_flags_section += flags_template.format(
items='\n'.join(flag_items).rstrip('\n'))
return output_template.format(
name_section=name_section,
synopsis_section=synopsis_section,
description_section=description_section,
args_and_flags_section=args_and_flags_section)
def HelpTextForObject(component, info, trace=None, verbose=False):
"""Generates help text for python objects.
Args:
component: Current component to generate help text for.
info: Info containing metadata of component.
trace: FireTrace object that leads to current component.
verbose: Whether to display help text in verbose mode.
Returns:
Formatted help text for display.
"""
output_template = """NAME
{current_command} - {command_summary}
SYNOPSIS
{synopsis}
DESCRIPTION
{command_description}
{detail_section}
"""
current_command = GetCurrentCommand(trace)
docstring_info = info['docstring_info']
command_summary = docstring_info.summary if docstring_info.summary else ''
if docstring_info.description:
command_description = docstring_info.description
else:
command_description = ''
groups = []
commands = []
values = []
members = completion._Members(component, verbose) # pylint: disable=protected-access
for member_name, member in members:
if value_types.IsGroup(member):
groups.append((member_name, member))
if value_types.IsCommand(member):
commands.append((member_name, member))
if value_types.IsValue(member):
values.append((member_name, member))
possible_actions = []
# TODO(joejoevictor): Add global flags to here. Also, if it's a callable,
# there will be additional flags.
possible_flags = ''
detail_section_string = ''
item_template = """
{name}
{command_summary}
"""
if groups:
# TODO(joejoevictor): Add missing GROUPS section handling
possible_actions.append('GROUP')
if commands:
possible_actions.append('COMMAND')
commands_str_template = """
COMMANDS
COMMAND is one of the followings:
{items}
"""
command_item_strings = []
for command_name, command in commands:
command_docstring_info = docstrings.parse(
inspectutils.Info(command)['docstring'])
command_item_strings.append(
item_template.format(
name=command_name,
command_summary=command_docstring_info.summary))
detail_section_string += commands_str_template.format(
items=('\n'.join(command_item_strings)).rstrip('\n'))
if values:
possible_actions.append('VALUES')
values_str_template = """
VALUES
VALUE is one of the followings:
{items}
"""
value_item_strings = []
for value_name, value in values:
del value
init_docstring_info = docstrings.parse(
inspectutils.Info(component.__class__.__init__)['docstring'])
for arg_info in init_docstring_info.args:
if arg_info.name == value_name:
value_item_strings.append(
item_template.format(
name=value_name, command_summary=arg_info.description))
detail_section_string += values_str_template.format(
items=('\n'.join(value_item_strings)).rstrip('\n'))
possible_actions_string = ' ' + (' | '.join(possible_actions))
synopsis_template = '{current_command}{possible_actions}{possible_flags}'
synopsis_string = synopsis_template.format(
current_command=current_command,
possible_actions=possible_actions_string,
possible_flags=possible_flags)
return output_template.format(
current_command=current_command,
command_summary=command_summary,
synopsis=synopsis_string,
command_description=command_description,
detail_section=detail_section_string)
def UsageText(component, trace=None, verbose=False):
if inspect.isroutine(component) or inspect.isclass(component):
return UsageTextForFunction(component, trace)
else:
return UsageTextForObject(component, trace, verbose)
def UsageTextForFunction(component, trace=None):
"""Returns usage text for function objects.
Args:
component: The component to determine the usage text for.
trace: The Fire trace object containing all metadata of current execution.
Returns:
String suitable for display in error screen.
"""
output_template = """Usage: {current_command} {args_and_flags}
{availability_lines}
For detailed information on this command, run:
{current_command}{hyphen_hyphen} --help
"""
if trace:
command = trace.GetCommand()
is_help_an_arg = trace.NeedsSeparatingHyphenHyphen()
else:
command = None
is_help_an_arg = False
if not command:
command = ''
spec = inspectutils.GetFullArgSpec(component)
args = spec.args
if spec.defaults is None:
num_defaults = 0
else:
num_defaults = len(spec.defaults)
args_with_no_defaults = args[:len(args) - num_defaults]
args_with_defaults = args[len(args) - num_defaults:]
flags = args_with_defaults + spec.kwonlyargs
items = [arg.upper() for arg in args_with_no_defaults]
if flags:
items.append('<flags>')
availability_lines = (
'\nAvailable flags: '
+ ' | '.join('--' + flag for flag in flags) + '\n')
else:
availability_lines = ''
args_and_flags = ' '.join(items)
hyphen_hyphen = ' --' if is_help_an_arg else ''
return output_template.format(
current_command=command,
args_and_flags=args_and_flags,
availability_lines=availability_lines,
hyphen_hyphen=hyphen_hyphen)
def UsageTextForObject(component, trace=None, verbose=False):
"""Returns help text for usage screen for objects.
Construct help text for usage screen to inform the user about error occurred
and correct syntax for invoking the object.
Args:
component: The component to determine the usage text for.
trace: The Fire trace object containing all metadata of current execution.
verbose: Whether to include private members in the usage text.
Returns:
String suitable for display in error screen.
"""
output_template = """Usage: {current_command} <{possible_actions}>
{availability_lines}
For detailed information on this command, run:
{current_command} --help
"""
if trace:
command = trace.GetCommand()
else:
command = None
if not command:
command = ''
groups = []
commands = []
values = []
members = completion._Members(component, verbose) # pylint: disable=protected-access
for member_name, member in members:
if value_types.IsGroup(member):
groups.append(member_name)
if value_types.IsCommand(member):
commands.append(member_name)
if value_types.IsValue(member):
values.append(member_name)
possible_actions = []
availability_lines = []
availability_lint_format = '{header:20s}{choices}'
if groups:
possible_actions.append('groups')
groups_string = ' | '.join(groups)
groups_text = availability_lint_format.format(
header='available groups:',
choices=groups_string)
availability_lines.append(groups_text)
if commands:
possible_actions.append('commands')
commands_string = ' | '.join(commands)
commands_text = availability_lint_format.format(
header='available commands:',
choices=commands_string)
availability_lines.append(commands_text)
if values:
possible_actions.append('values')
values_string = ' | '.join(values)
values_text = availability_lint_format.format(
header='available values:',
choices=values_string)
availability_lines.append(values_text)
possible_actions_string = '|'.join(possible_actions)
availability_lines_string = '\n'.join(availability_lines)
return output_template.format(
current_command=command,
possible_actions=possible_actions_string,
availability_lines=availability_lines_string)
+231
View File
@@ -0,0 +1,231 @@
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for the helptext module."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import textwrap
from fire import docstrings
from fire import helptext
from fire import inspectutils
from fire import test_components as tc
from fire import testutils
from fire import trace
class HelpScreenTest(testutils.BaseTestCase):
def testHelpScreen(self):
component = tc.ClassWithDocstring()
t = trace.FireTrace(component, name='ClassWithDocstring')
info = inspectutils.Info(component)
info['docstring_info'] = docstrings.parse(info['docstring'])
help_output = helptext.HelpText(component, info, t)
expected_output = """
NAME
ClassWithDocstring - Test class for testing help text output.
SYNOPSIS
ClassWithDocstring COMMAND | VALUES
DESCRIPTION
This is some detail description of this test class.
COMMANDS
COMMAND is one of the followings:
print_msg
Prints a message.
VALUES
VALUE is one of the followings:
message
The default message to print.
"""
self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
def testHelpScreenForFunctionDocstringWithLineBreak(self):
component = tc.ClassWithMultilineDocstring.example_generator
t = trace.FireTrace(component, name='example_generator')
info = inspectutils.Info(component)
info['docstring_info'] = docstrings.parse(info['docstring'])
help_output = helptext.HelpText(component, info, t)
expected_output = """
NAME
example_generator - Generators have a ``Yields`` section instead of a ``Returns`` section.
SYNOPSIS
example_generator N
DESCRIPTION
Generators have a ``Yields`` section instead of a ``Returns`` section.
POSITIONAL ARGUMENTS
N
The upper limit of the range to generate, from 0 to `n` - 1.
NOTES
You could also use flags syntax for POSITIONAL ARGUMENTS
"""
self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
def testHelpScreenForFunctionFunctionWithDefaultArgs(self):
component = tc.WithDefaults().double
t = trace.FireTrace(component, name='double')
info = inspectutils.Info(component)
info['docstring_info'] = docstrings.parse(info['docstring'])
help_output = helptext.HelpText(component, info, t)
expected_output = """
NAME
double - Returns the input multiplied by 2.
SYNOPSIS
double [--count=COUNT]
DESCRIPTION
Returns the input multiplied by 2.
FLAGS
--count
Input number that you want to double.
NOTES
You could also use flags syntax for POSITIONAL ARGUMENTS
"""
self.assertEqual(textwrap.dedent(expected_output).lstrip('\n'), help_output)
class UsageTest(testutils.BaseTestCase):
def testUsageOutput(self):
component = tc.NoDefaults()
t = trace.FireTrace(component, name='NoDefaults')
usage_output = helptext.UsageText(component, trace=t, verbose=False)
expected_output = '''
Usage: NoDefaults <commands>
available commands: double | triple
For detailed information on this command, run:
NoDefaults --help
'''
self.assertEqual(
usage_output,
textwrap.dedent(expected_output).lstrip('\n'))
def testUsageOutputVerbose(self):
component = tc.NoDefaults()
t = trace.FireTrace(component, name='NoDefaults')
usage_output = helptext.UsageText(component, trace=t, verbose=True)
expected_output = '''
Usage: NoDefaults <commands>
available commands: double | triple
For detailed information on this command, run:
NoDefaults --help
'''
self.assertEqual(
usage_output,
textwrap.dedent(expected_output).lstrip('\n'))
def testUsageOutputMethod(self):
component = tc.NoDefaults().double
t = trace.FireTrace(component, name='NoDefaults')
t.AddAccessedProperty(component, 'double', ['double'], None, None)
usage_output = helptext.UsageText(component, trace=t, verbose=True)
expected_output = '''
Usage: NoDefaults double COUNT
For detailed information on this command, run:
NoDefaults double --help
'''
self.assertEqual(
usage_output,
textwrap.dedent(expected_output).lstrip('\n'))
def testUsageOutputFunctionWithHelp(self):
component = tc.function_with_help
t = trace.FireTrace(component, name='function_with_help')
usage_output = helptext.UsageText(component, trace=t, verbose=True)
expected_output = '''
Usage: function_with_help <flags>
Available flags: --help
For detailed information on this command, run:
function_with_help -- --help
'''
self.assertEqual(
usage_output,
textwrap.dedent(expected_output).lstrip('\n'))
def testUsageOutputFunctionWithDocstring(self):
component = tc.multiplier_with_docstring
t = trace.FireTrace(component, name='multiplier_with_docstring')
usage_output = helptext.UsageText(component, trace=t, verbose=True)
expected_output = '''
Usage: multiplier_with_docstring NUM <flags>
Available flags: --rate
For detailed information on this command, run:
multiplier_with_docstring --help
'''
self.assertEqual(
usage_output,
textwrap.dedent(expected_output).lstrip('\n'))
@testutils.skip('The functionality is not implemented yet')
def testUsageOutputCallable(self):
# This is both a group and a command!
component = tc.CallableWithKeywordArgument
t = trace.FireTrace(component, name='CallableWithKeywordArgument')
usage_output = helptext.UsageText(component, trace=t, verbose=True)
# TODO(zuhaohen): We need to handle the case for keyword args as well
# i.e. __call__ method of CallableWithKeywordArgument
expected_output = '''
Usage: CallableWithKeywordArgument <commands>
Available commands: print_msg
For detailed information on this command, run:
CallableWithKeywordArgument -- --help
'''
self.assertEqual(
usage_output,
textwrap.dedent(expected_output).lstrip('\n'))
def testUsageOutputConstructorWithParameter(self):
component = tc.InstanceVars
t = trace.FireTrace(component, name='InstanceVars')
usage_output = helptext.UsageText(component, trace=t, verbose=True)
expected_output = '''
Usage: InstanceVars ARG1 ARG2
For detailed information on this command, run:
InstanceVars --help
'''
self.assertEqual(
usage_output,
textwrap.dedent(expected_output).lstrip('\n'))
if __name__ == '__main__':
testutils.main()
+83 -20
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -16,6 +16,17 @@
Can produce help strings suitable for display in Fire CLIs for any type of
Python object, module, class, or function.
There are two types of informative strings: Usage and Help screens.
Usage screens are shown when the user accesses a group or accesses a command
without calling it. A Usage screen shows information about how to use that group
or command. Usage screens are typically short and show the minimal information
necessary for the user to determine how to proceed.
Help screens are shown when the user requests help with the help flag (--help).
Help screens are shown in a less-style console view, and contain detailed help
information.
"""
from __future__ import absolute_import
@@ -25,6 +36,7 @@ from __future__ import print_function
import inspect
from fire import completion
from fire import docstrings
from fire import inspectutils
@@ -54,7 +66,7 @@ def _DisplayValue(info, field, padding):
value = info.get(field)
if value is None:
return
return None
skip_doc_types = ('dict', 'list', 'unicode', 'int', 'float', 'bool')
@@ -76,6 +88,23 @@ def _DisplayValue(info, field, padding):
return value
def _GetFields(trace=None):
"""Returns the field names to include in the help text for a component."""
del trace # Unused.
return [
'type_name',
'string_form',
'file',
'line',
'docstring',
'init_docstring',
'class_docstring',
'call_docstring',
'length',
'usage',
]
def HelpString(component, trace=None, verbose=False):
"""Returns a help string for a supplied component.
@@ -90,27 +119,33 @@ def HelpString(component, trace=None, verbose=False):
"""
info = inspectutils.Info(component)
info['usage'] = UsageString(component, trace, verbose)
info['docstring_info'] = docstrings.parse(info['docstring'])
fields = [
'type_name',
'string_form',
'file',
'line',
return _HelpText(info, trace)
'docstring',
'init_docstring',
'class_docstring',
'call_docstring',
'length',
'usage',
]
def _HelpText(info, trace=None):
"""Returns help text.
max_size = max(
len(_NormalizeField(field)) + 1
for field in fields
if field in info and info[field])
format_string = '{{field:{max_size}s}} {{value}}'.format(max_size=max_size)
This was a copy of previous HelpString function and will be removed once the
correct text formatters are implemented.
Args:
info: The IR object containing metadata of an object.
trace: The Fire trace object containing all metadata of current execution.
Returns:
String suitable for display giving information about the component.
"""
fields = _GetFields(trace)
try:
max_size = max(
len(_NormalizeField(field)) + 1
for field in fields
if field in info and info[field])
format_string = '{{field:{max_size}s}} {{value}}'.format(max_size=max_size)
except ValueError:
return ''
lines = []
for field in fields:
@@ -126,6 +161,26 @@ def HelpString(component, trace=None, verbose=False):
return '\n'.join(lines)
def GetSummaryAndDescription(docstring_info):
"""Retrieves summary and description for help text generation."""
# To handle both empty string and None
summary = docstring_info.summary if docstring_info.summary else None
description = (
docstring_info.description if docstring_info.description else None)
return summary, description
def GetCurrentCommand(trace=None):
"""Returns current command for the purpose of generating help text."""
if trace:
current_command = trace.GetCommand()
else:
current_command = ''
return current_command
def _UsageStringFromFullArgSpec(command, spec):
"""Get a usage string from the FullArgSpec for the given command.
@@ -180,7 +235,15 @@ def _UsageStringFromFullArgSpec(command, spec):
def UsageString(component, trace=None, verbose=False):
"""Returns a string showing how to use the component as a Fire command."""
command = trace.GetCommand() + ' ' if trace else ''
if trace:
command = trace.GetCommand()
else:
command = None
if command:
command += ' '
else:
command = ''
if inspect.isroutine(component) or inspect.isclass(component):
spec = inspectutils.GetFullArgSpec(component)
+2 -3
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -43,7 +43,7 @@ class HelpUtilsTest(testutils.BaseTestCase):
self.assertIn('Type: NoDefaults', helpstring)
self.assertIn('String form: <fire.test_components.NoDefaults object at ',
helpstring)
# TODO: We comment this out since it only works with IPython:
# TODO(dbieber): We comment this out since it only works with IPython:
# self.assertIn('test_components.py', helpstring)
self.assertIn('Usage: double\n'
' triple', helpstring)
@@ -84,7 +84,6 @@ class HelpUtilsTest(testutils.BaseTestCase):
helpstring = helputils.HelpString('test'.upper)
self.assertIn('Type: builtin_function_or_method', helpstring)
self.assertIn('String form: <built-in method upper of', helpstring)
self.assertIn('Usage: [VARS ...] [--KWARGS ...]', helpstring)
def testHelpStringIntType(self):
helpstring = helputils.HelpString(int)
+29 -6
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -63,7 +63,7 @@ def _GetArgSpecInfo(fn):
fn: The function or class of interest.
Returns:
A tuple with the following two items:
fn: The function to use for determing the arg spec of this function.
fn: The function to use for determining the arg spec of this function.
skip_arg: Whether the first argument will be supplied automatically, and
hence should be skipped when supplying args from a Fire command.
"""
@@ -73,11 +73,12 @@ def _GetArgSpecInfo(fn):
skip_arg = True
if six.PY2 and hasattr(fn, '__init__'):
fn = fn.__init__
else:
elif inspect.ismethod(fn):
# If the function is a bound method, we skip the `self` argument.
is_method = inspect.ismethod(fn)
skip_arg = is_method and fn.__self__ is not None
skip_arg = fn.__self__ is not None
elif inspect.isbuiltin(fn):
# If the function is a bound builtin, we skip the `self` argument.
skip_arg = fn.__self__ is not None
return fn, skip_arg
@@ -202,3 +203,25 @@ def _InfoBackup(component):
pass
return info
def IsNamedTuple(component):
"""Return true if the component is a namedtuple.
Unfortunately, Python offers no native way to check for a namedtuple type.
Instead, we need to use a simple hack which should suffice for our case.
namedtuples are internally implemented as tuples, therefore we need to:
1. Check if the component is an instance of tuple.
2. Check if the component has a _fields attribute which regular tuples do
not have.
Args:
component: The component to analyze.
Returns:
True if the component is a namedtuple or False otherwise.
"""
if not isinstance(component, tuple):
return False
has_fields = bool(getattr(component, '_fields', None))
return has_fields
+1 -3
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -56,8 +56,6 @@ class InspectUtilsTest(testutils.BaseTestCase):
spec = inspectutils.GetFullArgSpec('test'.upper)
self.assertEqual(spec.args, [])
self.assertEqual(spec.defaults, ())
self.assertEqual(spec.varargs, 'vars')
self.assertEqual(spec.varkw, 'kwargs')
self.assertEqual(spec.kwonlyargs, [])
self.assertEqual(spec.kwonlydefaults, {})
self.assertEqual(spec.annotations, {})
+8 -2
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -12,7 +12,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""This module enables interactive mode in Python Fire."""
"""This module enables interactive mode in Python Fire.
It uses IPython as an optional dependency. When IPython is installed, the
interactive flag will use IPython's REPL. When IPython is not installed, the
interactive flag will start a Python REPL with the builtin `code` module's
InteractiveConsole class.
"""
from __future__ import absolute_import
from __future__ import division
+1 -1
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
+6 -3
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -27,10 +27,10 @@ def CreateParser():
parser.add_argument('--verbose', '-v', action='store_true')
parser.add_argument('--interactive', '-i', action='store_true')
parser.add_argument('--separator', default='-')
parser.add_argument('--completion', action='store_true')
parser.add_argument('--completion', nargs='?', const='bash', type=str)
parser.add_argument('--help', '-h', action='store_true')
parser.add_argument('--trace', '-t', action='store_true')
# TODO: Consider allowing name to be passed as an argument.
# TODO(dbieber): Consider allowing name to be passed as an argument.
return parser
@@ -94,6 +94,9 @@ def _LiteralEval(value):
SyntaxError: If the value string has a syntax error.
"""
root = ast.parse(value, mode='eval')
if isinstance(root.body, ast.BinOp): # pytype: disable=attribute-error
raise ValueError(value)
for node in ast.walk(root):
for field, child in ast.iter_fields(node):
if isinstance(child, list):
+5 -4
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -30,7 +30,8 @@ import six
class ParserFuzzTest(testutils.BaseTestCase):
@given(st.text(min_size=1), settings=settings.Settings(max_examples=10000))
@settings(max_examples=10000)
@given(st.text(min_size=1))
@example('True')
@example(r'"test\t\t\a\\a"')
@example(r' "test\t\t\a\\a" ')
@@ -67,8 +68,8 @@ class ParserFuzzTest(testutils.BaseTestCase):
raise
try:
uvalue = unicode(value)
uresult = unicode(result)
uvalue = six.text_type(value)
uresult = six.text_type(result)
except UnicodeDecodeError:
# This is not what we're testing.
return
+6 -2
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -69,10 +69,12 @@ class ParserTest(testutils.BaseTestCase):
def testDefaultParseValueNumbers(self):
self.assertEqual(parser.DefaultParseValue('23'), 23)
self.assertEqual(parser.DefaultParseValue('-23'), -23)
self.assertEqual(parser.DefaultParseValue('23.0'), 23.0)
self.assertIsInstance(parser.DefaultParseValue('23'), int)
self.assertIsInstance(parser.DefaultParseValue('23.0'), float)
self.assertEqual(parser.DefaultParseValue('23.5'), 23.5)
self.assertEqual(parser.DefaultParseValue('-23.5'), -23.5)
def testDefaultParseValueStringNumbers(self):
self.assertEqual(parser.DefaultParseValue("'23'"), '23')
@@ -127,13 +129,15 @@ class ParserTest(testutils.BaseTestCase):
# If it can't be parsed, we treat it as a string. This behavior may change.
self.assertEqual(
parser.DefaultParseValue('[(A, 2, "3"), 5'), '[(A, 2, "3"), 5')
self.assertEqual(parser.DefaultParseValue('x=10'), 'x=10')
def testDefaultParseValueSyntaxError(self):
# If it can't be parsed, we treat it as a string.
self.assertEqual(parser.DefaultParseValue('"'), '"')
def testDefaultParseValueIgnoreBinOp(self):
self.assertEqual(parser.DefaultParseValue('2017-10-10'), '2017-10-10')
self.assertEqual(parser.DefaultParseValue('1+1'), '1+1')
if __name__ == '__main__':
testutils.main()
+146 -4
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -18,18 +18,36 @@ from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import collections
import six
if six.PY3:
from fire import test_components_py3 as py3 # pylint: disable=unused-import,no-name-in-module,g-import-not-at-top
def identity(arg1, arg2, arg3=10, arg4=20, *arg5, **arg6):
def identity(arg1, arg2, arg3=10, arg4=20, *arg5, **arg6): # pylint: disable=keyword-arg-before-vararg
return arg1, arg2, arg3, arg4, arg5, arg6
identity.__annotations__ = {'arg2': int, 'arg4': int}
def multiplier_with_docstring(num, rate=2):
"""Multiplies num by rate.
Args:
num (int): the num you want to multiply
rate (int): the rate for multiplication
Returns:
Multiplication of num by rate
"""
return num * rate
def function_with_help(help=True): # pylint: disable=redefined-builtin
return help
class Empty(object):
pass
@@ -44,6 +62,21 @@ class WithInit(object):
pass
class ErrorInConstructor(object):
def __init__(self, value='value'):
self.value = value
raise ValueError('Error in constructor')
class WithHelpArg(object):
"""Test class for testing when class has a help= arg."""
def __init__(self, help=True): # pylint: disable=redefined-builtin
self.has_help = help
self.dictionary = {'__help': 'help in a dict'}
class NoDefaults(object):
def double(self, count):
@@ -54,8 +87,17 @@ class NoDefaults(object):
class WithDefaults(object):
"""Class with functions that have default arguments."""
def double(self, count=0):
"""Returns the input multiplied by 2.
Args:
count: Input number that you want to double.
Returns:
A number that is the double of count.s
"""
return 2 * count
def triple(self, count=0):
@@ -83,6 +125,21 @@ class MixedDefaults(object):
return alpha, beta
class SimilarArgNames(object):
def identity(self, bool_one=False, bool_two=False):
return bool_one, bool_two
def identity2(self, a=None, alpha=None):
return a, alpha
class CapitalizedArgNames(object):
def sum(self, Delta=1.0, Gamma=2.0): # pylint: disable=invalid-name
return Delta + Gamma
class Annotations(object):
def double(self, count=0):
@@ -96,6 +153,7 @@ class Annotations(object):
class TypedProperties(object):
"""Test class for testing Python Fire with properties of various types."""
def __init__(self):
self.alpha = True
@@ -114,7 +172,7 @@ class TypedProperties(object):
class VarArgs(object):
"""Test class G for testing Python Fire."""
"""Test class for testing Python Fire with a property with varargs."""
def cumsums(self, *items):
total = None
@@ -127,7 +185,7 @@ class VarArgs(object):
sums.append(total)
return sums
def varchars(self, alpha=0, beta=0, *chars):
def varchars(self, alpha=0, beta=0, *chars): # pylint: disable=keyword-arg-before-vararg
return alpha, beta, ''.join(chars)
@@ -214,3 +272,87 @@ class CircularReference(object):
x = {}
x['y'] = x
return x
class OrderedDictionary(object):
def empty(self):
return collections.OrderedDict()
def non_empty(self):
ordered_dict = collections.OrderedDict()
ordered_dict['A'] = 'A'
ordered_dict[2] = 2
return ordered_dict
class NamedTuple(object):
def point(self):
"""Point example straight from Python docs."""
# pylint: disable=invalid-name
Point = collections.namedtuple('Point', ['x', 'y'])
return Point(11, y=22)
class CallableWithKeywordArgument(object):
"""Test class for supporting callable."""
def __call__(self, **kwargs):
for key, value in kwargs.items():
print('%s: %s' % (key, value))
def print_msg(self, msg):
print(msg)
class ClassWithDocstring(object):
"""Test class for testing help text output.
This is some detail description of this test class.
"""
def __init__(self, message='Hello!'):
"""Constructor of the test class.
Constructs a new ClassWithDocstring object.
Args:
message: The default message to print.
"""
self.message = message
def print_msg(self, msg=None):
"""Prints a message."""
if msg is None:
msg = self.message
print(msg)
class ClassWithMultilineDocstring(object):
"""Test class for testing help text output with multiline docstring.
This is a test class that has a long docstring description that spans across
multiple lines for testing line breaking in help text.
"""
@staticmethod
def example_generator(n):
"""Generators have a ``Yields`` section instead of a ``Returns`` section.
Args:
n (int): The upper limit of the range to generate, from 0 to `n` - 1.
Yields:
int: The next number in the range of 0 to `n` - 1.
Examples:
Examples should be written in doctest format, and should illustrate how
to use the function.
>>> print([i for i in example_generator(4)])
[0, 1, 2, 3]
"""
for i in range(n):
yield i
+1 -1
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
+1 -1
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
+5 -3
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -95,5 +95,7 @@ class BaseTestCase(unittest.TestCase):
raise
def main():
unittest.main()
# pylint: disable=invalid-name
main = unittest.main
skip = unittest.skip
# pylint: enable=invalid-name
+1 -1
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
+40 -34
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -30,10 +30,12 @@ from __future__ import division
from __future__ import print_function
import pipes
from fire import inspectutils
INITIAL_COMPONENT = 'Initial component'
INSTANTIATED_CLASS = 'Instantiated class'
CALLED_ROUTINE = 'Called routine'
CALLED_CALLABLE = 'Called callable'
ACCESSED_PROPERTY = 'Accessed property'
COMPLETION_SCRIPT = 'Generated completion script'
INTERACTIVE_MODE = 'Entered interactive mode'
@@ -63,7 +65,9 @@ class FireTrace(object):
def GetResult(self):
"""Returns the component from the last element of the trace."""
# pytype: disable=attribute-error
return self.GetLastHealthyElement().component
# pytype: enable=attribute-error
def GetLastHealthyElement(self):
"""Returns the last element of the trace that is not an error.
@@ -76,6 +80,7 @@ class FireTrace(object):
for element in reversed(self.elements):
if not element.HasError():
return element
return None
def HasError(self):
"""Returns whether the Fire execution encountered a Fire usage error."""
@@ -92,44 +97,24 @@ class FireTrace(object):
)
self.elements.append(element)
def AddCalledRoutine(self, component, target, args, filename, lineno,
capacity):
"""Adds an element to the trace indicating that a routine was called.
def AddCalledComponent(self, component, target, args, filename, lineno,
capacity, action=CALLED_CALLABLE):
"""Adds an element to the trace indicating that a component was called.
Also applies to instantiating a class.
Args:
component: The result of calling the routine.
target: The name of the routine.
args: The args consumed in order to call this routine.
filename: The file in which the routine is defined, or None if N/A.
lineno: The line number on which the routine is defined, or None if N/A.
capacity: (bool) Whether the routine could have accepted additional args.
component: The result of calling the callable.
target: The name of the callable.
args: The args consumed in order to call this callable.
filename: The file in which the callable is defined, or None if N/A.
lineno: The line number on which the callable is defined, or None if N/A.
capacity: (bool) Whether the callable could have accepted additional args.
action: The value to include as the action in the FireTraceElement.
"""
element = FireTraceElement(
component=component,
action=CALLED_ROUTINE,
target=target,
args=args,
filename=filename,
lineno=lineno,
capacity=capacity,
)
self.elements.append(element)
def AddInstantiatedClass(self, component, target, args, filename, lineno,
capacity):
"""Adds an element to the trace indicating that a class was instantiated.
Args:
component: The result of instantiating the class.
target: The name of the class.
args: The args consumed in order to instantiate the class.
filename: The file in which the class is defined, or None if N/A.
lineno: The line number on which the class is defined, or None if N/A.
capacity: (bool) Whether cls.__init__ could have accepted additional args.
"""
element = FireTraceElement(
component=component,
action=INSTANTIATED_CLASS,
action=action,
target=target,
args=args,
filename=filename,
@@ -233,6 +218,27 @@ class FireTrace(object):
for index, element in enumerate(self.elements)
)
def NeedsSeparatingHyphenHyphen(self, flag='help'):
"""Returns whether a the trace need '--' before '--help'.
'--' is needed when the component takes keyword arguments, when the value of
flag matches one of the argument of the component, or the component takes in
keyword-only arguments(e.g. argument with default value).
Args:
flag: the flag available for the trace
Returns:
True for needed '--', False otherwise.
"""
element = self.GetLastHealthyElement()
component = element.component
spec = inspectutils.GetFullArgSpec(component)
return (spec.varkw is not None
or flag in spec.args
or flag in spec.kwonlyargs)
class FireTraceElement(object):
"""A FireTraceElement represents a single step taken by a Fire execution.
+21 -7
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -51,10 +51,20 @@ class FireTraceTest(testutils.BaseTestCase):
str(t),
'1. Initial component\n2. Accessed property "prop" (sample.py:12)')
def testAddCalledCallable(self):
t = trace.FireTrace('initial object')
args = ('example', 'args')
t.AddCalledComponent('result', 'cell', args, 'sample.py', 10, False,
action=trace.CALLED_CALLABLE)
self.assertEqual(
str(t),
'1. Initial component\n2. Called callable "cell" (sample.py:10)')
def testAddCalledRoutine(self):
t = trace.FireTrace('initial object')
args = ('example', 'args')
t.AddCalledRoutine('result', 'run', args, 'sample.py', 12, False)
t.AddCalledComponent('result', 'run', args, 'sample.py', 12, False,
action=trace.CALLED_ROUTINE)
self.assertEqual(
str(t),
'1. Initial component\n2. Called routine "run" (sample.py:12)')
@@ -62,8 +72,9 @@ class FireTraceTest(testutils.BaseTestCase):
def testAddInstantiatedClass(self):
t = trace.FireTrace('initial object')
args = ('example', 'args')
t.AddInstantiatedClass(
'Classname', 'classname', args, 'sample.py', 12, False)
t.AddCalledComponent(
'Classname', 'classname', args, 'sample.py', 12, False,
action=trace.INSTANTIATED_CLASS)
target = """1. Initial component
2. Instantiated class "classname" (sample.py:12)"""
self.assertEqual(str(t), target)
@@ -85,19 +96,22 @@ class FireTraceTest(testutils.BaseTestCase):
def testGetCommand(self):
t = trace.FireTrace('initial object')
args = ('example', 'args')
t.AddCalledRoutine('result', 'run', args, 'sample.py', 12, False)
t.AddCalledComponent('result', 'run', args, 'sample.py', 12, False,
action=trace.CALLED_ROUTINE)
self.assertEqual(t.GetCommand(), 'example args')
def testGetCommandWithQuotes(self):
t = trace.FireTrace('initial object')
args = ('example', 'spaced arg')
t.AddCalledRoutine('result', 'run', args, 'sample.py', 12, False)
t.AddCalledComponent('result', 'run', args, 'sample.py', 12, False,
action=trace.CALLED_ROUTINE)
self.assertEqual(t.GetCommand(), "example 'spaced arg'")
def testGetCommandWithFlagQuotes(self):
t = trace.FireTrace('initial object')
args = ('--example=spaced arg',)
t.AddCalledRoutine('result', 'run', args, 'sample.py', 12, False)
t.AddCalledComponent('result', 'run', args, 'sample.py', 12, False,
action=trace.CALLED_ROUTINE)
self.assertEqual(t.GetCommand(), "--example='spaced arg'")
+39
View File
@@ -0,0 +1,39 @@
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Types of values."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import inspect
import six
VALUE_TYPES = (bool, six.string_types, six.integer_types, float, complex)
def IsGroup(component):
# TODO(dbieber): Check if there are any subcomponents.
return not IsCommand(component) and not IsValue(component)
def IsCommand(component):
return inspect.isroutine(component) or inspect.isclass(component)
def IsValue(component):
return isinstance(component, VALUE_TYPES)
+1 -1
View File
@@ -32,7 +32,7 @@ enable=indexing-exception,old-raise-syntax
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifier separated by comma (,) or put this option
# multiple time.
disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored
disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return
[REPORTS]
+4
View File
@@ -9,3 +9,7 @@ test = pytest
[tool:pytest]
addopts = --ignore=fire/test_components_py3.py --ignore=fire/parser_fuzz_test.py
[pytype]
inputs = .
output = .pytype
+3 -2
View File
@@ -1,4 +1,4 @@
# Copyright (C) 2017 Google Inc.
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -39,7 +39,7 @@ TEST_DEPENDENCIES = [
'python-Levenshtein',
]
VERSION = '0.1.2'
VERSION = '0.1.3'
URL = 'https://github.com/google/python-fire'
setup(
@@ -68,6 +68,7 @@ setup(
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Operating System :: OS Independent',
'Operating System :: POSIX',