diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..ba1b7f19 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,31 @@ +# Basic dependabot.yml file with minimum configuration for two package managers + +version: 2 +updates: + # Enable version updates for python + - package-ecosystem: "pip" + directory: ".github/scripts/" + schedule: + interval: "monthly" + labels: ["dependabot"] + pull-request-branch-name: + separator: "-" + open-pull-requests-limit: 5 + reviewers: + - "dbieber" + + # Enable version updates for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + gh-actions: + patterns: + - "*" # Check all dependencies + labels: ["dependabot"] + pull-request-branch-name: + separator: "-" + open-pull-requests-limit: 5 + reviewers: + - "dbieber" diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh index 2d5fcd10..111257ae 100755 --- a/.github/scripts/build.sh +++ b/.github/scripts/build.sh @@ -17,28 +17,16 @@ # Exit when any command fails. set -e -PYTHON_VERSION=${PYTHON_VERSION:-2.7} +PYTHON_VERSION=${PYTHON_VERSION:-3.7} -pip install --upgrade setuptools pip -pip install --upgrade pylint pytest pytest-pylint pytest-runner -pip install termcolor -pip install hypothesis python-Levenshtein -pip install mock +pip install -U -r .github/scripts/requirements.txt python setup.py develop python -m pytest # Run the tests without IPython. pip install ipython python -m pytest # Now run the tests with IPython. pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console -if [[ ${PYTHON_VERSION} == 2.7 || ${PYTHON_VERSION} == 3.7 ]]; then +if [[ ${PYTHON_VERSION} == 3.7 ]]; then + # Run type-checking. pip install pytype; + pytype -x fire/test_components_py3.py; fi -# Run type-checking, excluding files that define or use py3 features in py2. -if [[ ${PYTHON_VERSION} == 2.7 ]]; then - pytype -x \ - fire/fire_test.py \ - fire/inspectutils_test.py \ - fire/test_components_py3.py; -elif [[ ${PYTHON_VERSION} == 3.7 ]]; then - pytype; -fi - diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt new file mode 100644 index 00000000..613c4da0 --- /dev/null +++ b/.github/scripts/requirements.txt @@ -0,0 +1,9 @@ +setuptools <=78.1.0 +pip +pylint <3.3.7 +pytest <=8.3.3 +pytest-pylint <=1.1.2 +pytest-runner <7.0.0 +termcolor <2.6.0 +hypothesis <6.133.0 +levenshtein <=0.26.1 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b87d1f8a..75a687f3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,28 +1,38 @@ name: Python Fire -on: [push] +on: + push: + branches: ["master"] + pull_request: + branches: ["master"] + +defaults: + run: + shell: bash jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: matrix: - python-version: [2.7, 3.4, 3.5, 3.7, 3.8, 3.9] + os: ["macos-latest", "ubuntu-latest"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-rc.2"] + include: + - {os: "ubuntu-22.04", python-version: "3.7"} steps: # Checkout the repo. - name: Checkout Python Fire repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 # Set up Python environment. - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} # Build Python Fire using the build.sh script. - name: Run build script - shell: bash run: ./.github/scripts/build.sh env: PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b3c758e1..b5d67c96 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,16 +40,13 @@ In addition, the project follows a convention of: - Maximum line length: 80 characters - Indentation: 2 spaces (4 for line continuation) - PascalCase for function and method names. -- No type hints, as described in [PEP 484], to maintain compatibility with -Python versions < 3.5. - Single quotes around strings, three double quotes around docstrings. [Google Python Style Guide]: http://google.github.io/styleguide/pyguide.html -[PEP 484]: https://www.python.org/dev/peps/pep-0484 ## Testing -Python Fire uses [Github Actions](https://github.com/google/python-fire/actions) to run tests on each pull request. You can run +Python Fire uses [GitHub Actions](https://github.com/google/python-fire/actions) to run tests on each pull request. You can run these tests yourself as well. To do this, first install the test dependencies listed in setup.py (e.g. pytest, mock, termcolor, and hypothesis). Then run the tests by running `pytest` in the root directory of the repository. diff --git a/docs/api.md b/docs/api.md index 66451071..aae92cd6 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,37 +1,23 @@ ## Python Fire Quick Reference | Setup | Command | Notes -| :------ | :------------------ | :--------- -| install | `pip install fire` | +| ------- | ------------------- | ---------- +| install | `pip install fire` | Installs fire from pypi | Creating a CLI | Command | Notes -| :--------------| :--------------------- | :--------- +| ---------------| ---------------------- | ---------- | import | `import fire` | | Call | `fire.Fire()` | Turns the current module into a Fire CLI. | Call | `fire.Fire(component)` | Turns `component` into a Fire CLI. -| Using a CLI | Command | Notes | -| :----------------------------------------- | :------------- | :------------- | -| [Help](using-cli.md#help-flag) | `command | Show the help | -: : --help` : screen. : -| [REPL](using-cli.md#interactive-flag) | `command -- | Enters | -: : --interactive` : interactive : -: : : mode. : -| [Separator](using-cli.md#separator-flag) | `command -- | This sets the | -: : --separator=X` : separator to : -: : : `X`. The : -: : : default : -: : : separator is : -: : : `-`. : -| [Completion](using-cli.md#completion-flag) | `command -- | Generate a | -: : --completion : completion : -: : [shell]` : script for the : -: : : CLI. : -| [Trace](using-cli.md#trace-flag) | `command -- | Gets a Fire | -: : --trace` : trace for the : -: : : command. : -| [Verbose](using-cli.md#verbose-flag) | `command -- | | -: : --verbose` : : +| Using a CLI | Command | Notes | +| ------------------------------------------ | ----------------- | -------------- | +| [Help](using-cli.md#help-flag) | `command --help` | Show the help screen. | +| [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode. | +| [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`. | +| [Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI. | +| [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command. | +| [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` | | _Note that flags are separated from the Fire command by an isolated `--` arg. Help is an exception; the isolated `--` is optional for getting help._ @@ -39,24 +25,11 @@ Help is an exception; the isolated `--` is optional for getting help._ ## Arguments for Calling fire.Fire() | Argument | Usage | Notes | -| :-------- | :------------------------ | :----------------------------------- | -| component | `fire.Fire(component)` | If omitted, defaults to a dict of | -: : : all locals and globals. : -| command | `fire.Fire(command='hello | Either a string or a list of | -: : --name=5')` : arguments. If a string is provided, : -: : : it is split to determine the : -: : : arguments. If a list or tuple is : -: : : provided, they are the arguments. If : -: : : `command` is omitted, then : -: : : `sys.argv[1\:]` (the arguments from : -: : : the command line) are used by : -: : : default. : -| name | `fire.Fire(name='tool')` | The name of the CLI, ideally the | -: : : name users will enter to run the : -: : : CLI. This name will be used in the : -: : : CLI's help screens. If the argument : -: : : is omitted, it will be inferred : -: : : automatically. : +| --------- | ------------------------- | ------------------------------------ | +| component | `fire.Fire(component)` | If omitted, defaults to a dict of all locals and globals. | +| command | `fire.Fire(command='hello --name=5')` | Either a string or a list of arguments. If a string is provided, it is split to determine the arguments. If a list or tuple is provided, they are the arguments. If `command` is omitted, then `sys.argv[1:]` (the arguments from the command line) are used by default. | +| name | `fire.Fire(name='tool')` | The name of the CLI, ideally the name users will enter to run the CLI. This name will be used in the CLI's help screens. If the argument is omitted, it will be inferred automatically.| +| serialize | `fire.Fire(serialize=custom_serializer)` | If omitted, simple types are serialized via their builtin str method, and any objects that define a custom `__str__` method are serialized with that. If specified, all objects are serialized to text via the provided method. | ## Using a Fire CLI without modifying any code diff --git a/docs/guide.md b/docs/guide.md index d5da3212..444a76ff 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -30,7 +30,7 @@ the program to the command line. import fire def hello(name): - return 'Hello {name}!'.format(name=name) + return f'Hello {name}!' if __name__ == '__main__': fire.Fire() @@ -52,7 +52,7 @@ command line. import fire def hello(name): - return 'Hello {name}!'.format(name=name) + return f'Hello {name}!' if __name__ == '__main__': fire.Fire(hello) @@ -76,7 +76,7 @@ We can alternatively write this program like this: import fire def hello(name): - return 'Hello {name}!'.format(name=name) + return f'Hello {name}!' def main(): fire.Fire(hello) @@ -93,7 +93,7 @@ then simply this: import fire def hello(name): - return 'Hello {name}!'.format(name=name) + return f'Hello {name}!' def main(): fire.Fire(hello) @@ -105,7 +105,7 @@ If you have a file `example.py` that doesn't even import fire: ```python def hello(name): - return 'Hello {name}!'.format(name=name) + return f'Hello {name}!' ``` Then you can use it with Fire like this: @@ -318,9 +318,9 @@ class Pipeline(object): self.digestion = DigestionStage() def run(self): - self.ingestion.run() - self.digestion.run() - return 'Pipeline complete' + ingestion_output = self.ingestion.run() + digestion_output = self.digestion.run() + return [ingestion_output, digestion_output] if __name__ == '__main__': fire.Fire(Pipeline) @@ -589,6 +589,25 @@ default values that you don't want to specify. It is also important to remember to change the separator if you want to pass `-` as an argument. +##### Async Functions + +Fire supports calling async functions too. Here's a simple example. + +```python +import asyncio + +async def count_to_ten(): + for i in range(1, 11): + await asyncio.sleep(1) + print(i) + +if __name__ == '__main__': + fire.Fire(count_to_ten) +``` + +Whenever fire encounters a coroutine function, it runs it, blocking until it completes. + + ### Argument Parsing The types of the arguments are determined by their values, rather than by the @@ -742,7 +761,18 @@ The complete set of flags available is shown below, in the reference section. | [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command. | [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` | Include private members in the output. -_Note that flags are separated from the Fire command by an isolated `--` arg._ +_Note that flags are separated from the Fire command by an isolated `--` arg. +Help is an exception; the isolated `--` is optional for getting help._ + + +##### Arguments for Calling fire.Fire() + +| Argument | Usage | Notes | +| --------- | ------------------------- | ------------------------------------ | +| component | `fire.Fire(component)` | If omitted, defaults to a dict of all locals and globals. | +| command | `fire.Fire(command='hello --name=5')` | Either a string or a list of arguments. If a string is provided, it is split to determine the arguments. If a list or tuple is provided, they are the arguments. If `command` is omitted, then `sys.argv[1:]` (the arguments from the command line) are used by default. | +| name | `fire.Fire(name='tool')` | The name of the CLI, ideally the name users will enter to run the CLI. This name will be used in the CLI's help screens. If the argument is omitted, it will be inferred automatically.| +| serialize | `fire.Fire(serialize=custom_serializer)` | If omitted, simple types are serialized via their builtin str method, and any objects that define a custom `__str__` method are serialized with that. If specified, all objects are serialized to text via the provided method. | ### Disclaimer diff --git a/docs/index.md b/docs/index.md index 4eb114b1..8dcc5db6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -106,7 +106,8 @@ Please see [The Python Fire Guide](guide.md). | [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 these flags are separated from the Fire command by an isolated `--`._ +_Note that flags are separated from the Fire command by an isolated `--` arg. +Help is an exception; the isolated `--` is optional for getting help._ ## License diff --git a/docs/installation.md b/docs/installation.md index 614243af..7e4cccb8 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -4,5 +4,5 @@ To install Python Fire with pip, run: `pip install fire` To install Python Fire with conda, run: `conda install fire -c conda-forge` -To install Python Fire from source, first clone the repository and then run: -`python setup.py install` +To install Python Fire from source, first clone the repository and then run +`python setup.py install`. To install from source for development, instead run `python setup.py develop`. diff --git a/docs/using-cli.md b/docs/using-cli.md index 0f369a9a..bdfcb7db 100644 --- a/docs/using-cli.md +++ b/docs/using-cli.md @@ -57,7 +57,7 @@ If your command corresponds to a list or tuple, you can extend your command by adding the index of an element of the component to your command as an argument. For example, `widget function-that-returns-list 2` will correspond to item 2 of -the result of function_that_returns_list. +the result of `function_that_returns_list`. ### Calling a function @@ -90,7 +90,7 @@ See also the section on [Changing the Separator](#separator-flag). ### Instantiating a class If your command corresponds to a class, you can extend your command by adding -the arguments of the class's \_\_init\_\_ function. Arguments must be specified +the arguments of the class's `__init__` function. Arguments must be specified by name, using the flags syntax. See the section on [calling a function](#calling-a-function) for more details. @@ -105,8 +105,8 @@ after the final standalone `--` argument. (If there is no `--` argument, then no arguments are used for flags.) For example, to set the alsologtostderr flag, you could run the command: -`widget bang --noise=boom -- --alsologtostderr`. The --noise argument is -consumed by Fire, but the --alsologtostderr argument is treated as a normal +`widget bang --noise=boom -- --alsologtostderr`. The `--noise` argument is +consumed by Fire, but the `--alsologtostderr` argument is treated as a normal Flag. All CLIs built with Python Fire share some flags, as described in the next @@ -137,13 +137,16 @@ will put you in an IPython REPL, with the variable `widget` already defined. You can then explore the Python object that `widget` corresponds to interactively using Python. +Note: if you want fire to start the IPython REPL instead of the regular Python one, +the `ipython` package needs to be installed in your environment. + ### `--completion`: Generating a completion script Call `widget -- --completion` to generate a completion script for the Fire CLI `widget`. To save the completion script to your home directory, you could e.g. run `widget -- --completion > ~/.widget-completion`. You should then source this -file; to get permanent completion, source this file from your .bashrc file. +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. @@ -174,7 +177,7 @@ corresponds to, as well as usage information for how to extend that command. ### `--trace`: Getting a Fire trace In order to understand what is happening when you call Python Fire, it can be -useful to request a trace. This is done via the --trace flag, e.g. +useful to request a trace. This is done via the `--trace` flag, e.g. `widget whack 5 -- --trace`. A trace provides step by step information about how the Fire command was diff --git a/examples/widget/widget.py b/examples/widget/widget.py index bf1cbeb2..9092ad75 100644 --- a/examples/widget/widget.py +++ b/examples/widget/widget.py @@ -25,7 +25,7 @@ def whack(self, n=1): def bang(self, noise='bang'): """Makes a loud noise.""" - return '{noise} bang!'.format(noise=noise) + return f'{noise} bang!' def main(): diff --git a/fire/__init__.py b/fire/__init__.py index 9c34a5af..9ff696d3 100644 --- a/fire/__init__.py +++ b/fire/__init__.py @@ -14,11 +14,7 @@ """The Python Fire module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire.core import Fire __all__ = ['Fire'] -__version__ = '0.4.0' +__version__ = '0.7.0' diff --git a/fire/__main__.py b/fire/__main__.py index 30d556e4..140b4a76 100644 --- a/fire/__main__.py +++ b/fire/__main__.py @@ -12,18 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Lint as: python2, python3 # pylint: disable=invalid-name """Enables use of Python Fire as a "main" function (i.e. "python -m fire"). This allows using Fire with third-party libraries without modifying their code. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import importlib +from importlib import util import os import sys @@ -58,31 +54,17 @@ def import_from_file_path(path): """ if not os.path.exists(path): - raise IOError('Given file path does not exist.') + raise OSError('Given file path does not exist.') module_name = os.path.basename(path) - if sys.version_info.major == 3 and sys.version_info.minor < 5: - loader = importlib.machinery.SourceFileLoader( # pylint: disable=no-member - fullname=module_name, - path=path, - ) - - module = loader.load_module(module_name) # pylint: disable=deprecated-method - - elif sys.version_info.major == 3: - from importlib import util # pylint: disable=g-import-not-at-top,import-outside-toplevel,no-name-in-module - spec = util.spec_from_file_location(module_name, path) - - if spec is None: - raise IOError('Unable to load module from specified path.') + spec = util.spec_from_file_location(module_name, path) - module = util.module_from_spec(spec) # pylint: disable=no-member - spec.loader.exec_module(module) # pytype: disable=attribute-error + if spec is None: + raise OSError('Unable to load module from specified path.') - else: - import imp # pylint: disable=g-import-not-at-top,import-outside-toplevel - module = imp.load_source(module_name, path) + module = util.module_from_spec(spec) # pylint: disable=no-member + spec.loader.exec_module(module) # pytype: disable=attribute-error return module, module_name @@ -122,7 +104,7 @@ def import_module(module_or_filename): return import_from_file_path(module_or_filename) if os.path.sep in module_or_filename: # Use / to detect if it was a filename. - raise IOError('Fire was passed a filename which could not be found.') + raise OSError('Fire was passed a filename which could not be found.') return import_from_module_name(module_or_filename) # Assume it's a module. diff --git a/fire/completion.py b/fire/completion.py index ed7a1b61..1597d464 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -23,7 +23,6 @@ import inspect from fire import inspectutils -import six def Script(name, component, default_options=None, shell='bash'): @@ -104,7 +103,7 @@ def _BashScript(name, commands, default_options=None): option_already_entered() {{ local opt - for opt in ${{COMP_WORDS[@]:0:COMP_CWORD}} + for opt in ${{COMP_WORDS[@]:0:$COMP_CWORD}} do if [ $1 == $opt ]; then return 0 @@ -156,7 +155,11 @@ def _GetOptsAssignmentTemplate(command): return opts_assignment_subcommand_template lines = [] - for command in set(subcommands_map.keys()).union(set(options_map.keys())): + commands_set = set() + commands_set.add(name) + commands_set = commands_set.union(set(subcommands_map.keys())) + commands_set = commands_set.union(set(options_map.keys())) + for command in commands_set: opts_assignment = _GetOptsAssignmentTemplate(command).format( options=' '.join( sorted(options_map[command].union(subcommands_map[command])) @@ -274,10 +277,7 @@ def _FishScript(name, commands, default_options=None): ) return fish_source.format( - global_options=' '.join( - '"{option}"'.format(option=option) - for option in global_options - ) + global_options=' '.join(f'"{option}"' for option in global_options) ) @@ -304,7 +304,7 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False): Returns A boolean value indicating whether the member should be included. """ - if isinstance(name, six.string_types) and name.startswith('__'): + if isinstance(name, str) and name.startswith('__'): return False if verbose: return True @@ -312,15 +312,16 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False): or member is division or member is print_function): return False - if isinstance(member, type(absolute_import)) and six.PY34: + if isinstance(member, type(absolute_import)): return False - if inspect.ismodule(member) and member is six: - # TODO(dbieber): Determine more generally which modules to hide. + # TODO(dbieber): Determine more generally which modules to hide. + modules_to_hide = [] + if inspect.ismodule(member) and member in modules_to_hide: return False if inspect.isclass(component): # If class_attrs has not been provided, compute it. if class_attrs is None: - class_attrs = inspectutils.GetClassAttrsDict(class_attrs) or {} + class_attrs = inspectutils.GetClassAttrsDict(component) or {} class_attr = class_attrs.get(name) if class_attr: # Methods and properties should only be accessible on instantiated @@ -332,14 +333,7 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False): tuplegetter = getattr(collections, '_tuplegetter', type(None)) if isinstance(class_attr.object, tuplegetter): return False - if (six.PY2 and inspect.isfunction(component) - and name in ('func_closure', 'func_code', 'func_defaults', - 'func_dict', 'func_doc', 'func_globals', 'func_name')): - return False - if (six.PY2 and inspect.ismethod(component) - and name in ('im_class', 'im_func', 'im_self')): - return False - if isinstance(name, six.string_types): + if isinstance(name, str): return not name.startswith('_') return True # Default to including the member @@ -388,7 +382,7 @@ def _CompletionsFromArgs(fn_args): completions = [] for arg in fn_args: arg = arg.replace('_', '-') - completions.append('--{arg}'.format(arg=arg)) + completions.append(f'--{arg}') return completions @@ -434,7 +428,7 @@ def _FormatForCommand(token): Returns: The transformed token. """ - if not isinstance(token, six.string_types): + if not isinstance(token, str): token = str(token) if token.startswith('_'): diff --git a/fire/completion_test.py b/fire/completion_test.py index 582e5bbc..c0d5d24f 100644 --- a/fire/completion_test.py +++ b/fire/completion_test.py @@ -14,10 +14,6 @@ """Tests for the completion module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import completion from fire import test_components as tc from fire import testutils @@ -37,9 +33,8 @@ def testCompletionBashScript(self): self.assertIn('command', script) self.assertIn('halt', script) - assert_template = '{command})' for last_command in ['command', 'halt']: - self.assertIn(assert_template.format(command=last_command), script) + self.assertIn(f'{last_command})', script) def testCompletionFishScript(self): # A sanity check test to make sure the fish completion script satisfies diff --git a/fire/console/console_attr.py b/fire/console/console_attr.py index 35c10fba..c0a3d784 100644 --- a/fire/console/console_attr.py +++ b/fire/console/console_attr.py @@ -100,8 +100,6 @@ from fire.console import encoding as encoding_util from fire.console import text -import six - # TODO: Unify this logic with console.style.mappings class BoxLineCharacters(object): @@ -268,7 +266,7 @@ def __init__(self, encoding=None, suppress_output=False): # ANSI "standard" attributes. if self.SupportsAnsi(): - # Select Graphic Rendition paramaters from + # Select Graphic Rendition parameters from # http://en.wikipedia.org/wiki/ANSI_escape_code#graphics # Italic '3' would be nice here but its not widely supported. self._csi = '\x1b[' @@ -288,7 +286,7 @@ def __init__(self, encoding=None, suppress_output=False): elif self._encoding == 'cp437' and not is_screen_reader: self._box_line_characters = BoxLineCharactersUnicode() self._bullets = self._BULLETS_WINDOWS - # Windows does not suport the unicode characters used for the spinner. + # Windows does not support the unicode characters used for the spinner. self._progress_tracker_symbols = ProgressTrackerSymbolsAscii() else: self._box_line_characters = BoxLineCharactersAscii() @@ -355,9 +353,9 @@ def ConvertOutputToUnicode(self, buf): Returns: The console output string buf converted to unicode. """ - if isinstance(buf, six.text_type): + if isinstance(buf, str): buf = buf.encode(self._encoding) - return six.text_type(buf, self._encoding, 'replace') + return str(buf, self._encoding, 'replace') def GetBoxLineCharacters(self): """Returns the box/line drawing characters object. @@ -394,7 +392,7 @@ def GetControlSequenceIndicator(self): """Returns the control sequence indicator string. Returns: - The conrol sequence indicator string or None if control sequences are not + The control sequence indicator string or None if control sequences are not supported. """ return self._csi @@ -408,7 +406,7 @@ def GetControlSequenceLen(self, buf): 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 + The control 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): @@ -456,7 +454,7 @@ def GetRawKey(self): return self._get_raw_key[0]() def GetTermIdentifier(self): - """Returns the TERM envrionment variable for the console. + """Returns the TERM environment variable for the console. Returns: str: A str that describes the console's text capabilities @@ -480,7 +478,7 @@ def DisplayWidth(self, buf): Returns: The display width of buf, handling unicode and ANSI controls. """ - if not isinstance(buf, six.string_types): + if not isinstance(buf, str): # Handle non-string objects like Colorizer(). return len(buf) @@ -595,16 +593,16 @@ def __init__(self, string, color, justify=None): self._justify = justify def __eq__(self, other): - return self._string == six.text_type(other) + return self._string == str(other) def __ne__(self, other): return not self == other def __gt__(self, other): - return self._string > six.text_type(other) + return self._string > str(other) def __lt__(self, other): - return self._string < six.text_type(other) + return self._string < str(other) def __ge__(self, other): return not self < other @@ -692,7 +690,7 @@ def GetCharacterDisplayWidth(char): Returns: The monospaced terminal display width of char: either 0, 1, or 2. """ - if not isinstance(char, six.text_type): + if not isinstance(char, str): # Non-unicode chars have width 1. Don't use this function on control chars. return 1 @@ -779,7 +777,7 @@ def EncodeToBytes(data): return data # Coerce to text that will be converted to bytes. - s = six.text_type(data) + s = str(data) try: # Assume the text can be directly converted to bytes (8-bit ascii). diff --git a/fire/console/console_attr_os.py b/fire/console/console_attr_os.py index 8482c7bc..869c5949 100644 --- a/fire/console/console_attr_os.py +++ b/fire/console/console_attr_os.py @@ -123,7 +123,7 @@ def _GetTermSizeEnvironment(): def _GetTermSizeTput(): - """Returns the terminal x and y dimemsions from tput(1).""" + """Returns the terminal x and y dimensions from tput(1).""" import subprocess # pylint: disable=g-import-not-at-top output = encoding.Decode(subprocess.check_output(['tput', 'cols'], stderr=subprocess.STDOUT)) diff --git a/fire/console/console_io.py b/fire/console/console_io.py index 3d3b9f81..ec0858d9 100644 --- a/fire/console/console_io.py +++ b/fire/console/console_io.py @@ -15,10 +15,6 @@ """General console printing utilities used by the Cloud SDK.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import os import signal import subprocess diff --git a/fire/console/console_pager.py b/fire/console/console_pager.py index 044fcb37..565c7e1e 100644 --- a/fire/console/console_pager.py +++ b/fire/console/console_pager.py @@ -94,7 +94,7 @@ def __init__(self, contents, out=None, prompt=None): 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.. + prompt: The page break prompt, a default prompt is used if None.. """ self._contents = contents self._out = out or sys.stdout diff --git a/fire/console/encoding.py b/fire/console/encoding.py index 780e5a28..3ce30cb5 100644 --- a/fire/console/encoding.py +++ b/fire/console/encoding.py @@ -22,8 +22,6 @@ import sys -import six - def Encode(string, encoding=None): """Encode the text string to a byte string. @@ -35,18 +33,8 @@ def Encode(string, encoding=None): 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) + del encoding # Unused. + return string def Decode(data, encoding=None): @@ -67,26 +55,19 @@ def Decode(data, encoding=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): + if isinstance(data, str) or isinstance(data, bytes): 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) + string = str(data) - if isinstance(string, six.text_type): + if isinstance(string, str): # Our work is done here. return string try: # Just return the string if its pure ASCII. - return string.decode('ascii') + return string.decode('ascii') # pytype: disable=attribute-error except UnicodeError: # The string is not ASCII encoded. pass @@ -94,7 +75,7 @@ def Decode(data, encoding=None): # Try the suggested encoding if specified. if encoding: try: - return string.decode(encoding) + return string.decode(encoding) # pytype: disable=attribute-error except UnicodeError: # Bad suggestion. pass @@ -103,21 +84,21 @@ def Decode(data, encoding=None): # be exceptional if a valid extended ascii encoding with extended chars # were also a valid UITF-8 encoding. try: - return string.decode('utf8') + return string.decode('utf8') # pytype: disable=attribute-error except UnicodeError: # Not a UTF-8 encoding. pass # Try the filesystem encoding. try: - return string.decode(sys.getfilesystemencoding()) + return string.decode(sys.getfilesystemencoding()) # pytype: disable=attribute-error except UnicodeError: # string is not encoded for filesystem paths. pass # Try the system default encoding. try: - return string.decode(sys.getdefaultencoding()) + return string.decode(sys.getdefaultencoding()) # pytype: disable=attribute-error except UnicodeError: # string is not encoded using the default encoding. pass @@ -137,7 +118,7 @@ def Decode(data, encoding=None): # string = '\xdc' # string = string.decode('iso-8859-1') # string = string.encode('ascii', 'backslashreplace') - return string.decode('iso-8859-1') + return string.decode('iso-8859-1') # pytype: disable=attribute-error def GetEncodedValue(env, name, default=None): @@ -199,7 +180,8 @@ def EncodeEnv(env, encoding=None): encoding = encoding or _GetEncoding() return { Encode(k, encoding=encoding): Encode(v, encoding=encoding) - for k, v in six.iteritems(env)} + for k, v in env.items() + } def _GetEncoding(): diff --git a/fire/console/files.py b/fire/console/files.py index 69970f43..97222c3d 100644 --- a/fire/console/files.py +++ b/fire/console/files.py @@ -24,8 +24,6 @@ from fire.console import encoding as encoding_util from fire.console import platforms -import six - def _GetSystemPath(): """Returns properly encoded system PATH variable string.""" @@ -48,7 +46,7 @@ def _FindExecutableOnPath(executable, path, pathext): ValueError: invalid input. """ - if isinstance(pathext, six.string_types): + if isinstance(pathext, str): raise ValueError('_FindExecutableOnPath(..., pathext=\'{0}\') failed ' 'because pathext must be an iterable of strings, but got ' 'a string.'.format(pathext)) diff --git a/fire/console/platforms.py b/fire/console/platforms.py index 018eb89e..13fd8204 100644 --- a/fire/console/platforms.py +++ b/fire/console/platforms.py @@ -153,6 +153,8 @@ def Current(): return OperatingSystem.MACOSX elif 'cygwin' in sys.platform: return OperatingSystem.CYGWIN + elif 'msys' in sys.platform: + return OperatingSystem.MSYS return None @staticmethod diff --git a/fire/core.py b/fire/core.py index 8ca142c7..26a25753 100644 --- a/fire/core.py +++ b/fire/core.py @@ -49,14 +49,10 @@ def main(argv): --trace: Get the Fire Trace for the command. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - +import asyncio import inspect import json import os -import pipes import re import shlex import sys @@ -72,13 +68,9 @@ def main(argv): from fire import trace from fire import value_types from fire.console import console_io -import six - -if six.PY34: - import asyncio # pylint: disable=import-error,g-import-not-at-top # pytype: disable=import-error -def Fire(component=None, command=None, name=None): +def Fire(component=None, command=None, name=None, serialize=None): """This function, Fire, is the main entrypoint for Python Fire. Executes a command either from the `command` argument or from sys.argv by @@ -94,6 +86,8 @@ def Fire(component=None, command=None, name=None): a string or a list of strings; a list of strings is preferred. name: Optional. The name of the command as entered at the command line. Used in interactive mode and for generating the completion script. + serialize: Optional. If supplied, all objects are serialized to text via + the provided callable. Returns: The result of executing the Fire command. Execution begins with the initial target component. The component is updated by using the command arguments @@ -112,7 +106,7 @@ def Fire(component=None, command=None, name=None): name = name or os.path.basename(sys.argv[0]) # Get args as a list. - if isinstance(command, six.string_types): + if isinstance(command, str): args = shlex.split(command) elif isinstance(command, (list, tuple)): args = command @@ -144,7 +138,7 @@ def Fire(component=None, command=None, name=None): _DisplayError(component_trace) raise FireExit(2, component_trace) if component_trace.show_trace and component_trace.show_help: - output = ['Fire trace:\n{trace}\n'.format(trace=component_trace)] + output = [f'Fire trace:\n{component_trace}\n'] result = component_trace.GetResult() help_text = helptext.HelpText( result, trace=component_trace, verbose=component_trace.verbose) @@ -152,7 +146,7 @@ def Fire(component=None, command=None, name=None): Display(output, out=sys.stderr) raise FireExit(0, component_trace) if component_trace.show_trace: - output = ['Fire trace:\n{trace}'.format(trace=component_trace)] + output = [f'Fire trace:\n{component_trace}'] Display(output, out=sys.stderr) raise FireExit(0, component_trace) if component_trace.show_help: @@ -164,7 +158,8 @@ def Fire(component=None, command=None, name=None): raise FireExit(0, component_trace) # The command succeeded normally; print the result. - _PrintResult(component_trace, verbose=component_trace.verbose) + _PrintResult( + component_trace, verbose=component_trace.verbose, serialize=serialize) result = component_trace.GetResult() return result @@ -204,7 +199,7 @@ def __init__(self, code, component_trace): code: (int) Exit code for the Fire CLI. component_trace: (FireTrace) The trace for the Fire command. """ - super(FireExit, self).__init__(code) + super().__init__(code) self.trace = component_trace @@ -235,18 +230,26 @@ def _IsHelpShortcut(component_trace, remaining_args): 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) + command = f'{component_trace.GetCommand()} -- --help' + print(f'INFO: Showing help with the command {shlex.quote(command)}.\n', + file=sys.stderr) return show_help -def _PrintResult(component_trace, verbose=False): +def _PrintResult(component_trace, verbose=False, serialize=None): """Prints the result of the Fire call to stdout in a human readable way.""" # TODO(dbieber): Design human readable deserializable serialization method # and move serialization to its own module. result = component_trace.GetResult() + # Allow users to modify the return value of the component and provide + # custom formatting. + if serialize: + if not callable(serialize): + raise FireError( + 'The argument `serialize` must be empty or callable:', serialize) + result = serialize(result) + if value_types.HasCustomStr(result): # If the object has a custom __str__ method, rather than one inherited from # object, then we use that to serialize the object. @@ -283,9 +286,9 @@ def _DisplayError(component_trace): show_help = True if show_help: - 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) + command = f'{component_trace.GetCommand()} -- --help' + print(f'INFO: Showing help with the command {shlex.quote(command)}.\n', + file=sys.stderr) help_text = helptext.HelpText(result, trace=component_trace, verbose=component_trace.verbose) output.append(help_text) @@ -323,14 +326,13 @@ def _DictAsString(result, verbose=False): return '{}' longest_key = max(len(str(key)) for key in result_visible.keys()) - format_string = '{{key:{padding}s}} {{value}}'.format(padding=longest_key + 1) + format_string = f'{{key:{longest_key + 1}s}} {{value}}' lines = [] for key, value in result.items(): if completion.MemberVisible(result, key, value, class_attrs=class_attrs, verbose=verbose): - line = format_string.format(key=str(key) + ':', - value=_OneLineResult(value)) + line = format_string.format(key=f'{key}:', value=_OneLineResult(value)) lines.append(line) return '\n'.join(lines) @@ -338,16 +340,16 @@ def _DictAsString(result, verbose=False): def _OneLineResult(result): """Returns result serialized to a single line string.""" # TODO(dbieber): Ensure line is fewer than eg 120 characters. - if isinstance(result, six.string_types): + if isinstance(result, str): return str(result).replace('\n', ' ') # TODO(dbieber): Show a small amount of usage information about the function # or module if it fits cleanly on the line. if inspect.isfunction(result): - return ''.format(name=result.__name__) + return f'' if inspect.ismodule(result): - return ''.format(name=result.__name__) + return f'' try: # Don't force conversion to ascii. @@ -516,7 +518,8 @@ def _Fire(component, args, parsed_flag_args, context, name=None): # The target isn't present in the dict as a string key, but maybe it is # a key as another type. # TODO(dbieber): Consider alternatives for accessing non-string keys. - for key, value in component_dict.items(): + for key, value in ( + component_dict.items()): # pytype: disable=attribute-error if target == str(key): component = value handled = True @@ -867,6 +870,7 @@ def _ParseKeywordArgs(args, fn_spec): key, value = stripped_argument.split('=', 1) else: key = stripped_argument + value = None # value will be set later on. key = key.replace('-', '_') is_bool_syntax = (not contains_equals and @@ -884,9 +888,10 @@ def _ParseKeywordArgs(args, fn_spec): 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)) + raise FireError( + f"The argument '{argument}' is ambiguous as it could " + f"refer to any of the following arguments: {matching_fn_args}" + ) # Determine the value. if not keyword: diff --git a/fire/core_test.py b/fire/core_test.py index 27c9f418..90b7f466 100644 --- a/fire/core_test.py +++ b/fire/core_test.py @@ -14,17 +14,12 @@ """Tests for the core module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function +from unittest import mock from fire import core from fire import test_components as tc from fire import testutils from fire import trace -import mock - -import six class CoreTest(testutils.BaseTestCase): @@ -194,13 +189,35 @@ def testClassMethod(self): 7, ) - @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.') + def testCustomSerialize(self): + def serialize(x): + if isinstance(x, list): + return ', '.join(str(xi) for xi in x) + if isinstance(x, dict): + return ', '.join('{}={!r}'.format(k, v) for k, v in sorted(x.items())) + if x == 'special': + return ['SURPRISE!!', "I'm a list!"] + return x + + ident = lambda x: x + + with self.assertOutputMatches(stdout='a, b', stderr=None): + _ = core.Fire(ident, command=['[a,b]'], serialize=serialize) + with self.assertOutputMatches(stdout='a=5, b=6', stderr=None): + _ = core.Fire(ident, command=['{a:5,b:6}'], serialize=serialize) + with self.assertOutputMatches(stdout='asdf', stderr=None): + _ = core.Fire(ident, command=['asdf'], serialize=serialize) + with self.assertOutputMatches( + stdout="SURPRISE!!\nI'm a list!\n", stderr=None): + _ = core.Fire(ident, command=['special'], serialize=serialize) + with self.assertRaises(core.FireError): + core.Fire(ident, command=['asdf'], serialize=55) + def testLruCacheDecoratorBoundArg(self): self.assertEqual( core.Fire(tc.py3.LruCacheDecoratedMethod, # pytype: disable=module-attr command=['lru_cache_in_class', 'foo']), 'foo') - @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.') def testLruCacheDecorator(self): self.assertEqual( core.Fire(tc.py3.lru_cache_decorated, # pytype: disable=module-attr diff --git a/fire/custom_descriptions.py b/fire/custom_descriptions.py index 191e8b29..768f0e23 100644 --- a/fire/custom_descriptions.py +++ b/fire/custom_descriptions.py @@ -28,7 +28,7 @@ dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2) -As you can see, this docstring is more pertinant to the function `dict` and +As you can see, this docstring is more pertinent to the function `dict` and would be suitable as the result of `dict.__doc__`, but is wholely unsuitable as a description for the dict `{'key': 'value'}`. @@ -36,12 +36,7 @@ descriptions for primitive typed values. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import formatting -import six TWO_DOUBLE_QUOTES = '""' STRING_DESC_PREFIX = 'The string ' @@ -64,13 +59,11 @@ def NeedsCustomDescription(component): Whether the component should use a custom description and summary. """ type_ = type(component) - if (type_ in six.string_types - or type_ in six.integer_types - or type_ is six.text_type - or type_ is six.binary_type + if ( + type_ in (str, int, bytes) or type_ in (float, complex, bool) or type_ in (dict, tuple, list, set, frozenset) - ): + ): return True return False @@ -137,7 +130,7 @@ def GetStringTypeDescription(obj, available_space, line_length): def GetSummary(obj, available_space, line_length): obj_type_name = type(obj).__name__ - if obj_type_name in CUSTOM_DESC_SUM_FN_DICT.keys(): + if obj_type_name in CUSTOM_DESC_SUM_FN_DICT: return CUSTOM_DESC_SUM_FN_DICT.get(obj_type_name)[0](obj, available_space, line_length) return None @@ -145,7 +138,7 @@ def GetSummary(obj, available_space, line_length): def GetDescription(obj, available_space, line_length): obj_type_name = type(obj).__name__ - if obj_type_name in CUSTOM_DESC_SUM_FN_DICT.keys(): + if obj_type_name in CUSTOM_DESC_SUM_FN_DICT: return CUSTOM_DESC_SUM_FN_DICT.get(obj_type_name)[1](obj, available_space, line_length) return None diff --git a/fire/custom_descriptions_test.py b/fire/custom_descriptions_test.py index 79d7c7a1..6cff2d5d 100644 --- a/fire/custom_descriptions_test.py +++ b/fire/custom_descriptions_test.py @@ -14,10 +14,6 @@ """Tests for custom description module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import custom_descriptions from fire import testutils diff --git a/fire/decorators.py b/fire/decorators.py index 9e56d6df..914b1de6 100644 --- a/fire/decorators.py +++ b/fire/decorators.py @@ -18,10 +18,7 @@ command line arguments to client code. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - +from typing import Any, Dict import inspect FIRE_METADATA = 'FIRE_METADATA' @@ -84,8 +81,7 @@ def _SetMetadata(fn, attribute, value): setattr(fn, FIRE_METADATA, metadata) -def GetMetadata(fn): - # type: (...) -> dict +def GetMetadata(fn) -> Dict[str, Any]: """Gets metadata attached to the function `fn` as an attribute. Args: @@ -108,8 +104,7 @@ def GetMetadata(fn): return default -def GetParseFns(fn): - # type: (...) -> dict +def GetParseFns(fn) -> Dict[str, Any]: metadata = GetMetadata(fn) - default = dict(default=None, positional=[], named={}) + default = {'default': None, 'positional': [], 'named': {}} return metadata.get(FIRE_PARSE_FNS, default) diff --git a/fire/decorators_test.py b/fire/decorators_test.py index cc7d6203..9988743c 100644 --- a/fire/decorators_test.py +++ b/fire/decorators_test.py @@ -14,16 +14,12 @@ """Tests for the decorators module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import core from fire import decorators from fire import testutils -class NoDefaults(object): +class NoDefaults: """A class for testing decorated functions without default values.""" @decorators.SetParseFns(count=int) @@ -44,7 +40,7 @@ def double(count): return 2 * count -class WithDefaults(object): +class WithDefaults: @decorators.SetParseFns(float) def example1(self, arg1=10): @@ -55,14 +51,14 @@ def example2(self, arg1=10): return arg1, type(arg1) -class MixedArguments(object): +class MixedArguments: @decorators.SetParseFns(float, arg2=str) def example3(self, arg1, arg2): return arg1, arg2 -class PartialParseFn(object): +class PartialParseFn: @decorators.SetParseFns(arg1=str) def example4(self, arg1, arg2): @@ -73,7 +69,7 @@ def example5(self, arg1, arg2): return arg1, arg2 -class WithKwargs(object): +class WithKwargs: @decorators.SetParseFns(mode=str, count=int) def example6(self, **kwargs): @@ -83,7 +79,7 @@ def example6(self, **kwargs): ) -class WithVarArgs(object): +class WithVarArgs: @decorators.SetParseFn(str) def example7(self, arg1, arg2=None, *varargs, **kwargs): # pylint: disable=keyword-arg-before-vararg diff --git a/fire/docstrings.py b/fire/docstrings.py index 1cfadea9..2d7c7e63 100644 --- a/fire/docstrings.py +++ b/fire/docstrings.py @@ -49,10 +49,6 @@ - "True | False" indicates bool type. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import collections import enum import re diff --git a/fire/docstrings_fuzz_test.py b/fire/docstrings_fuzz_test.py index 7609f4f8..66be8006 100644 --- a/fire/docstrings_fuzz_test.py +++ b/fire/docstrings_fuzz_test.py @@ -14,10 +14,6 @@ """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 diff --git a/fire/docstrings_test.py b/fire/docstrings_test.py index 2328ef16..ce516944 100644 --- a/fire/docstrings_test.py +++ b/fire/docstrings_test.py @@ -14,10 +14,6 @@ """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 @@ -50,12 +46,12 @@ def test_one_line_simple_whitespace(self): 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. + docstring = """A one line docstring that is 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 ' + summary='A one line docstring that is both a little too verbose and ' 'a little too long so it keeps going well beyond a reasonable length ' 'for a one-liner.', ) @@ -63,25 +59,25 @@ def test_one_line_too_long(self): 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 + docstring = """A one line docstring that is 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 ' + summary='A one line docstring that is both a little too verbose and ' 'a little too long so it runs onto a second line.', ) self.assertEqual(expected_docstring_info, 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 + A one line docstring that is 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 ' + summary='A one line docstring that is both a little too verbose and ' 'a little too long so it runs onto a second line.', ) self.assertEqual(expected_docstring_info, docstring_info) diff --git a/fire/fire_import_test.py b/fire/fire_import_test.py index c5975681..a6b4acc3 100644 --- a/fire/fire_import_test.py +++ b/fire/fire_import_test.py @@ -15,10 +15,10 @@ """Tests importing the fire module.""" import sys +from unittest import mock import fire from fire import testutils -import mock class FireImportTest(testutils.BaseTestCase): diff --git a/fire/fire_test.py b/fire/fire_test.py index 8b904c29..99b4a7c6 100644 --- a/fire/fire_test.py +++ b/fire/fire_test.py @@ -14,20 +14,14 @@ """Tests for the fire module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import os import sys +from unittest import mock import fire from fire import test_components as tc from fire import testutils -import mock -import six - class FireTest(testutils.BaseTestCase): @@ -184,7 +178,6 @@ def testFireAnnotatedArgs(self): self.assertEqual(fire.Fire(tc.Annotations, command=['double', '5']), 10) self.assertEqual(fire.Fire(tc.Annotations, command=['triple', '5']), 15) - @testutils.skipIf(six.PY2, 'Keyword-only arguments not in Python 2.') def testFireKeywordOnlyArgs(self): with self.assertRaisesFireExit(2): # Keyword arguments must be passed with flag syntax. @@ -712,8 +705,6 @@ def testClassWithInvalidProperty(self): fire.Fire(tc.InvalidProperty, command=['double', '10']), 20 ) - @testutils.skipIf(sys.version_info[0:2] <= (3, 4), - 'Cannot inspect wrapped signatures in Python 2 or 3.4.') def testHelpKwargsDecorator(self): # Issue #190, follow the wrapped method instead of crashing. with self.assertRaisesFireExit(0): @@ -721,7 +712,6 @@ def testHelpKwargsDecorator(self): with self.assertRaisesFireExit(0): fire.Fire(tc.decorated_method, command=['--help']) - @testutils.skipIf(six.PY2, 'Asyncio not available in Python 2.') def testFireAsyncio(self): self.assertEqual(fire.Fire(tc.py3.WithAsyncio, command=['double', '--count', '10']), 20) diff --git a/fire/formatting.py b/fire/formatting.py index faef8047..68484c27 100644 --- a/fire/formatting.py +++ b/fire/formatting.py @@ -14,10 +14,6 @@ """Formatting utilities for use in creating help text.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import formatting_windows # pylint: disable=unused-import import termcolor diff --git a/fire/formatting_test.py b/fire/formatting_test.py index 61cce0e8..e0f6699d 100644 --- a/fire/formatting_test.py +++ b/fire/formatting_test.py @@ -14,10 +14,6 @@ """Tests for formatting.py.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import formatting from fire import testutils @@ -28,11 +24,11 @@ class FormattingTest(testutils.BaseTestCase): def test_bold(self): text = formatting.Bold('hello') - self.assertEqual('\x1b[1mhello\x1b[0m', text) + self.assertIn(text, ['hello', '\x1b[1mhello\x1b[0m']) def test_underline(self): text = formatting.Underline('hello') - self.assertEqual('\x1b[4mhello\x1b[0m', text) + self.assertIn(text, ['hello', '\x1b[4mhello\x1b[0m']) def test_indent(self): text = formatting.Indent('hello', spaces=2) diff --git a/fire/formatting_windows.py b/fire/formatting_windows.py index 2b85820d..cee6f393 100644 --- a/fire/formatting_windows.py +++ b/fire/formatting_windows.py @@ -14,10 +14,6 @@ """This module is used for enabling formatting on Windows.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import ctypes import os import platform @@ -35,7 +31,9 @@ def initialize_or_disable(): """Enables ANSI processing on Windows or disables it as needed.""" if HAS_COLORAMA: wrap = True - if sys.stdout.isatty() and platform.release() == '10': + if (hasattr(sys.stdout, 'isatty') + and sys.stdout.isatty() + and platform.release() == '10'): # Enables native ANSI sequences in console. # Windows 10, 2016, and 2019 only. diff --git a/fire/helptext.py b/fire/helptext.py index b1d10b44..318d6276 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -29,12 +29,8 @@ information. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - +import collections import itertools -import sys from fire import completion from fire import custom_descriptions @@ -110,7 +106,7 @@ def _NameSection(component, info, trace=None, verbose=False): LINE_LENGTH) if summary: - text = current_command + ' - ' + summary + text = f'{current_command} - {summary}' else: text = current_command return ('NAME', text) @@ -136,11 +132,7 @@ def _SynopsisSection(component, actions_grouped_by_kind, spec, metadata, continuations.append(trace.separator) continuation = ' | '.join(continuations) - synopsis_template = '{current_command} {continuation}' - text = synopsis_template.format( - current_command=current_command, - continuation=continuation) - + text = f'{current_command} {continuation}' return ('SYNOPSIS', text) @@ -172,9 +164,25 @@ def _DescriptionSection(component, info): return None -def _CreateKeywordOnlyFlagItem(flag, docstring_info, spec): +def _CreateKeywordOnlyFlagItem(flag, docstring_info, spec, short_arg): return _CreateFlagItem( - flag, docstring_info, spec, required=flag not in spec.kwonlydefaults) + flag, docstring_info, spec, required=flag not in spec.kwonlydefaults, + short_arg=short_arg) + + +def _GetShortFlags(flags): + """Gets a list of single-character flags that uniquely identify a flag. + + Args: + flags: list of strings representing flags + + Returns: + List of single character short flags, + where the character occurred at the start of a flag once. + """ + short_flags = [f[0] for f in flags] + short_flag_counts = collections.Counter(short_flags) + return [v for v in short_flags if short_flag_counts[v] == 1] def _ArgsAndFlagsSections(info, spec, metadata): @@ -209,25 +217,44 @@ def _ArgsAndFlagsSections(info, spec, metadata): ('NOTES', 'You can also use flags syntax for POSITIONAL ARGUMENTS') ) + unique_short_args = _GetShortFlags(args_with_defaults) positional_flag_items = [ - _CreateFlagItem(flag, docstring_info, spec, required=False) + _CreateFlagItem( + flag, docstring_info, spec, required=False, + short_arg=flag[0] in unique_short_args + ) for flag in args_with_defaults ] + + unique_short_kwonly_flags = _GetShortFlags(spec.kwonlyargs) kwonly_flag_items = [ - _CreateKeywordOnlyFlagItem(flag, docstring_info, spec) + _CreateKeywordOnlyFlagItem( + flag, docstring_info, spec, + short_arg=flag[0] in unique_short_kwonly_flags + ) for flag in spec.kwonlyargs ] flag_items = positional_flag_items + kwonly_flag_items if spec.varkw: # Include kwargs documented via :key param: - flag_string = '--{name}' documented_kwargs = [] - for flag in docstring_info.args or []: + + # add short flags if possible + flags = docstring_info.args or [] + flag_names = [f.name for f in flags] + unique_short_flags = _GetShortFlags(flag_names) + for flag in flags: if isinstance(flag, docstrings.KwargInfo): + if flag.name[0] in unique_short_flags: + short_name = flag.name[0] + flag_string = f'-{short_name}, --{flag.name}' + else: + flag_string = f'--{flag.name}' + flag_item = _CreateFlagItem( flag.name, docstring_info, spec, - flag_string=flag_string.format(name=flag.name)) + flag_string=flag_string) documented_kwargs.append(flag_item) if documented_kwargs: # Separate documented kwargs from other flags using a message @@ -313,9 +340,9 @@ def _GetArgsAndFlagsString(spec, metadata): for arg in args_with_no_defaults] else: arg_strings = [ - '--{arg}={arg_upper}'.format( - arg=arg, arg_upper=formatting.Underline(arg.upper())) - for arg in args_with_no_defaults] + f'--{arg}={formatting.Underline(arg.upper())}' + for arg in args_with_no_defaults + ] arg_and_flag_strings.extend(arg_strings) # If there are any arguments that are treated as flags: @@ -323,8 +350,8 @@ def _GetArgsAndFlagsString(spec, metadata): arg_and_flag_strings.append('') if spec.varargs: - varargs_string = '[{varargs}]...'.format( - varargs=formatting.Underline(spec.varargs.upper())) + varargs_underlined = formatting.Underline(spec.varargs.upper()) + varargs_string = f'[{varargs_underlined}]...' arg_and_flag_strings.append(varargs_string) return ' '.join(arg_and_flag_strings) @@ -367,7 +394,7 @@ def _GetActionsGroupedByKind(component, verbose=False): if component_len < 10: indexes.Add(name=', '.join(str(x) for x in range(component_len))) else: - indexes.Add(name='0..{max}'.format(max=component_len-1)) + indexes.Add(name=f'0..{component_len-1}') return [groups, commands, values, indexes] @@ -382,10 +409,8 @@ def _GetCurrentCommand(trace=None, include_separators=True): def _CreateOutputSection(name, content): - return """{name} -{content}""".format( - name=formatting.Bold(name), - content=formatting.Indent(content, SECTION_INDENTATION)) + return f"""{formatting.Bold(name)} +{formatting.Indent(content, SECTION_INDENTATION)}""" def _CreateArgItem(arg, docstring_info, spec): @@ -396,7 +421,7 @@ def _CreateArgItem(arg, docstring_info, spec): docstring_info: A docstrings.DocstringInfo namedtuple with information about the containing function's docstring. spec: An instance of fire.inspectutils.FullArgSpec, containing type and - default information about the arguments to a callable. + default information about the arguments to a callable. Returns: A string to be used in constructing the help screen for the function. @@ -411,7 +436,7 @@ def _CreateArgItem(arg, docstring_info, spec): arg_string = formatting.BoldUnderline(arg.upper()) arg_type = _GetArgType(arg, spec) - arg_type = 'Type: {}'.format(arg_type) if arg_type else '' + arg_type = f'Type: {arg_type}' if arg_type else '' available_space = max_str_length - len(arg_type) arg_type = ( formatting.EllipsisTruncate(arg_type, available_space, max_str_length)) @@ -422,7 +447,7 @@ def _CreateArgItem(arg, docstring_info, spec): def _CreateFlagItem(flag, docstring_info, spec, required=False, - flag_string=None): + flag_string=None, short_arg=False): """Returns a string describing a flag using docstring and FullArgSpec info. Args: @@ -434,6 +459,7 @@ def _CreateFlagItem(flag, docstring_info, spec, required=False, required: Whether the flag is required. flag_string: If provided, use this string for the flag, rather than constructing one from the flag name. + short_arg: Whether the flag has a short variation or not. Returns: A string to be used in constructing the help screen for the function. """ @@ -449,12 +475,13 @@ def _CreateFlagItem(flag, docstring_info, spec, required=False, description = _GetArgDescription(flag, docstring_info) if not flag_string: - flag_string_template = '--{flag_name}={flag_name_upper}' - flag_string = flag_string_template.format( - flag_name=flag, - flag_name_upper=formatting.Underline(flag.upper())) + flag_name_upper = formatting.Underline(flag.upper()) + flag_string = f'--{flag}={flag_name_upper}' if required: flag_string += ' (required)' + if short_arg: + short_flag = flag[0] + flag_string = f'-{short_flag}, {flag_string}' arg_type = _GetArgType(flag, spec) arg_default = _GetArgDefault(flag, spec) @@ -462,14 +489,14 @@ def _CreateFlagItem(flag, docstring_info, spec, required=False, # We need to handle the case where there is a default of None, but otherwise # the argument has another type. if arg_default == 'None': - arg_type = 'Optional[{}]'.format(arg_type) + arg_type = f'Optional[{arg_type}]' - arg_type = 'Type: {}'.format(arg_type) if arg_type else '' + arg_type = f'Type: {arg_type}' if arg_type else '' available_space = max_str_length - len(arg_type) arg_type = ( formatting.EllipsisTruncate(arg_type, available_space, max_str_length)) - arg_default = 'Default: {}'.format(arg_default) if arg_default else '' + arg_default = f'Default: {arg_default}' if arg_default else '' available_space = max_str_length - len(arg_default) arg_default = ( formatting.EllipsisTruncate(arg_default, available_space, max_str_length)) @@ -495,9 +522,7 @@ def _GetArgType(arg, spec): if arg in spec.annotations: arg_type = spec.annotations[arg] try: - if sys.version_info[0:2] >= (3, 3): - return arg_type.__qualname__ - return arg_type.__name__ + return arg_type.__qualname__ except AttributeError: # Some typing objects, such as typing.Union do not have either a __name__ # or __qualname__ attribute. @@ -524,21 +549,23 @@ def _GetArgDefault(flag, spec): for arg, default in zip(args_with_defaults, spec.defaults): if arg == flag: return repr(default) + if flag in spec.kwonlydefaults: + return repr(spec.kwonlydefaults[flag]) return '' def _CreateItem(name, description, indent=2): if not description: return name - return """{name} -{description}""".format(name=name, - description=formatting.Indent(description, indent)) + description = formatting.Indent(description, indent) + return f"""{name} +{description}""" def _GetArgDescription(name, docstring_info): if docstring_info.args: for arg_in_docstring in docstring_info.args: - if arg_in_docstring.name in (name, '*' + name, '**' + name): + if arg_in_docstring.name in (name, f'*{name}', f'**{name}'): return arg_in_docstring.description return None @@ -584,9 +611,9 @@ def _ValuesUsageDetailsSection(component, values): def _NewChoicesSection(name, choices): + name_formatted = formatting.Bold(formatting.Underline(name)) return _CreateItem( - '{name} is one of the following:'.format( - name=formatting.Bold(formatting.Underline(name))), + f'{name_formatted} is one of the following:', '\n' + '\n\n'.join(choices), indent=1) @@ -602,11 +629,6 @@ def UsageText(component, trace=None, verbose=False): Returns: String suitable for display in an error screen. """ - output_template = """Usage: {continued_command} -{availability_lines} -For detailed information on this command, run: - {help_command}""" - # Get the command so far: if trace: command = trace.GetCommand() @@ -650,15 +672,16 @@ def UsageText(component, trace=None, verbose=False): + '--help' ) - return output_template.format( - continued_command=continued_command, - availability_lines=''.join(availability_lines), - help_command=help_command) + return f"""Usage: {continued_command} +{''.join(availability_lines)} +For detailed information on this command, run: + {help_command}""" def _GetPossibleActionsUsageString(possible_actions): if possible_actions: - return '<{actions}>'.format(actions='|'.join(possible_actions)) + actions_str = '|'.join(possible_actions) + return f'<{actions_str}>' return None @@ -667,7 +690,7 @@ def _UsageAvailabilityLines(actions_grouped_by_kind): for action_group in actions_grouped_by_kind: if action_group.members: availability_line = _CreateAvailabilityLine( - header='available {plural}:'.format(plural=action_group.plural), + header=f'available {action_group.plural}:', items=action_group.names ) availability_lines.append(availability_line) @@ -683,7 +706,7 @@ def _GetCallableUsageItems(spec, metadata): accepts_positional_args = metadata.get(decorators.ACCEPTS_POSITIONAL_ARGS) if not accepts_positional_args: - items = ['--{arg}={upper}'.format(arg=arg, upper=arg.upper()) + items = [f'--{arg}={arg.upper()}' for arg in args_with_no_defaults] else: items = [arg.upper() for arg in args_with_no_defaults] @@ -693,14 +716,14 @@ def _GetCallableUsageItems(spec, metadata): items.append('') if spec.varargs: - items.append('[{varargs}]...'.format(varargs=spec.varargs.upper())) + items.append(f'[{spec.varargs.upper()}]...') return items def _KeywordOnlyArguments(spec, required=True): return (flag for flag in spec.kwonlyargs - if required == (flag in spec.kwonlydefaults)) + if required != (flag in spec.kwonlydefaults)) def _GetCallableAvailabilityLines(spec): @@ -708,10 +731,10 @@ def _GetCallableAvailabilityLines(spec): args_with_defaults = spec.args[len(spec.args) - len(spec.defaults):] # TODO(dbieber): Handle args_with_no_defaults if not accepts_positional_args. - optional_flags = [('--' + flag) for flag in itertools.chain( + optional_flags = [f'--{flag}' for flag in itertools.chain( args_with_defaults, _KeywordOnlyArguments(spec, required=False))] required_flags = [ - ('--' + flag) for flag in _KeywordOnlyArguments(spec, required=True) + f'--{flag}' for flag in _KeywordOnlyArguments(spec, required=True) ] # Flags section: @@ -744,7 +767,7 @@ def _CreateAvailabilityLine(header, items, return indented_header + indented_items_text[len(indented_header):] + '\n' -class ActionGroup(object): +class ActionGroup: """A group of actions of the same kind.""" def __init__(self, name, plural): diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 29250cec..aeff5240 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -14,12 +14,7 @@ """Tests for the helptext module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import os -import sys import textwrap from fire import formatting @@ -27,13 +22,12 @@ from fire import test_components as tc from fire import testutils from fire import trace -import six class HelpTest(testutils.BaseTestCase): def setUp(self): - super(HelpTest, self).setUp() + super().setUp() os.environ['ANSI_COLORS_DISABLED'] = '1' def testHelpTextNoDefaults(self): @@ -81,7 +75,9 @@ def testHelpTextFunctionWithDefaults(self): self.assertIn('NAME\n triple', help_screen) self.assertIn('SYNOPSIS\n triple ', help_screen) self.assertNotIn('DESCRIPTION', help_screen) - self.assertIn('FLAGS\n --count=COUNT\n Default: 0', help_screen) + self.assertIn( + 'FLAGS\n -c, --count=COUNT\n Default: 0', + help_screen) self.assertNotIn('NOTES', help_screen) def testHelpTextFunctionWithLongDefaults(self): @@ -93,7 +89,7 @@ def testHelpTextFunctionWithLongDefaults(self): self.assertIn('SYNOPSIS\n text ', help_screen) self.assertNotIn('DESCRIPTION', help_screen) self.assertIn( - 'FLAGS\n --string=STRING\n' + 'FLAGS\n -s, --string=STRING\n' ' Default: \'0001020304050607080910' '1112131415161718192021222324252627282...', help_screen) @@ -121,15 +117,12 @@ def testHelpTextFunctionWithKwargsAndDefaults(self): self.assertIn('SYNOPSIS\n text ARG1 ARG2 ', help_screen) self.assertIn('DESCRIPTION\n Function with kwarg', help_screen) self.assertIn( - 'FLAGS\n --opt=OPT\n Default: True\n' + 'FLAGS\n -o, --opt=OPT\n Default: True\n' ' The following flags are also accepted.' '\n --arg3\n Description of arg3.\n ' 'Additional undocumented flags may also be accepted.', help_screen) - @testutils.skipIf( - sys.version_info[0:2] < (3, 5), - 'Python < 3.5 does not support type hints.') def testHelpTextFunctionWithDefaultsAndTypes(self): component = ( tc.py3.WithDefaultsAndTypes().double) # pytype: disable=module-attr @@ -140,13 +133,10 @@ def testHelpTextFunctionWithDefaultsAndTypes(self): self.assertIn('SYNOPSIS\n double ', help_screen) self.assertIn('DESCRIPTION', help_screen) self.assertIn( - 'FLAGS\n --count=COUNT\n Type: float\n Default: 0', + 'FLAGS\n -c, --count=COUNT\n Type: float\n Default: 0', help_screen) self.assertNotIn('NOTES', help_screen) - @testutils.skipIf( - sys.version_info[0:2] < (3, 5), - 'Python < 3.5 does not support type hints.') def testHelpTextFunctionWithTypesAndDefaultNone(self): component = ( tc.py3.WithDefaultsAndTypes().get_int) # pytype: disable=module-attr @@ -157,14 +147,11 @@ def testHelpTextFunctionWithTypesAndDefaultNone(self): self.assertIn('SYNOPSIS\n get_int ', help_screen) self.assertNotIn('DESCRIPTION', help_screen) self.assertIn( - 'FLAGS\n --value=VALUE\n' + 'FLAGS\n -v, --value=VALUE\n' ' Type: Optional[int]\n Default: None', help_screen) self.assertNotIn('NOTES', help_screen) - @testutils.skipIf( - sys.version_info[0:2] < (3, 5), - 'Python < 3.5 does not support type hints.') def testHelpTextFunctionWithTypes(self): component = tc.py3.WithTypes().double # pytype: disable=module-attr help_screen = helptext.HelpText( @@ -180,9 +167,6 @@ def testHelpTextFunctionWithTypes(self): 'NOTES\n You can also use flags syntax for POSITIONAL ARGUMENTS', help_screen) - @testutils.skipIf( - sys.version_info[0:2] < (3, 5), - 'Python < 3.5 does not support type hints.') def testHelpTextFunctionWithLongTypes(self): component = tc.py3.WithTypes().long_type # pytype: disable=module-attr help_screen = helptext.HelpText( @@ -278,23 +262,28 @@ def testHelpTextNoInit(self): self.assertIn('NAME\n OldStyleEmpty', help_screen) self.assertIn('SYNOPSIS\n OldStyleEmpty', help_screen) - @testutils.skipIf( - six.PY2, 'Python 2 does not support keyword-only arguments.') def testHelpTextKeywordOnlyArgumentsWithDefault(self): component = tc.py3.KeywordOnly.with_default # pytype: disable=module-attr output = helptext.HelpText( component=component, trace=trace.FireTrace(component, 'with_default')) self.assertIn('NAME\n with_default', output) - self.assertIn('FLAGS\n --x=X', output) + self.assertIn('FLAGS\n -x, --x=X', output) - @testutils.skipIf( - six.PY2, 'Python 2 does not support keyword-only arguments.') def testHelpTextKeywordOnlyArgumentsWithoutDefault(self): component = tc.py3.KeywordOnly.double # pytype: disable=module-attr output = helptext.HelpText( component=component, trace=trace.FireTrace(component, 'double')) self.assertIn('NAME\n double', output) - self.assertIn('FLAGS\n --count=COUNT (required)', output) + self.assertIn('FLAGS\n -c, --count=COUNT (required)', output) + + def testHelpTextFunctionMixedDefaults(self): + component = tc.py3.HelpTextComponent().identity + t = trace.FireTrace(component, name='FunctionMixedDefaults') + output = helptext.HelpText(component, trace=t) + self.assertIn('NAME\n FunctionMixedDefaults', output) + self.assertIn('FunctionMixedDefaults ', output) + self.assertIn('--alpha=ALPHA (required)', output) + self.assertIn('--beta=BETA\n Default: \'0\'', output) def testHelpScreen(self): component = tc.ClassWithDocstring() @@ -362,7 +351,7 @@ def testHelpScreenForFunctionFunctionWithDefaultArgs(self): Returns the input multiplied by 2. FLAGS - --count=COUNT + -c, --count=COUNT Default: 0 Input number that you want to double.""" self.assertEqual(textwrap.dedent(expected_output).strip(), @@ -377,7 +366,8 @@ def testHelpTextUnderlineFlag(self): formatting.Bold('SYNOPSIS') + '\n triple ', help_screen) self.assertIn( - formatting.Bold('FLAGS') + '\n --' + formatting.Underline('count'), + formatting.Bold('FLAGS') + '\n -c, --' + + formatting.Underline('count'), help_screen) def testHelpTextBoldCommandName(self): @@ -423,6 +413,20 @@ def testHelpTextNameSectionCommandWithSeparatorVerbose(self): self.assertIn('double -', help_screen) self.assertIn('double - -', help_screen) + def testHelpTextMultipleKeywoardArgumentsWithShortArgs(self): + component = tc.fn_with_multiple_defaults + t = trace.FireTrace(component, name='shortargs') + help_screen = helptext.HelpText(component, t) + self.assertIn(formatting.Bold('NAME') + '\n shortargs', help_screen) + self.assertIn( + formatting.Bold('SYNOPSIS') + '\n shortargs ', + help_screen) + self.assertIn( + formatting.Bold('FLAGS') + '\n -f, --first', + help_screen) + self.assertIn('\n --last', help_screen) + self.assertIn('\n --late', help_screen) + class UsageTest(testutils.BaseTestCase): @@ -497,6 +501,20 @@ def testUsageOutputFunctionWithDocstring(self): textwrap.dedent(expected_output).lstrip('\n'), usage_output) + def testUsageOutputFunctionMixedDefaults(self): + component = tc.py3.HelpTextComponent().identity + t = trace.FireTrace(component, name='FunctionMixedDefaults') + usage_output = helptext.UsageText(component, trace=t, verbose=False) + expected_output = """ + Usage: FunctionMixedDefaults + optional flags: --beta + required flags: --alpha + + For detailed information on this command, run: + FunctionMixedDefaults --help""" + expected_output = textwrap.dedent(expected_output).lstrip('\n') + self.assertEqual(expected_output, usage_output) + def testUsageOutputCallable(self): # This is both a group and a command. component = tc.CallableWithKeywordArgument() diff --git a/fire/inspectutils.py b/fire/inspectutils.py index 0fa8e7d3..d9c62ca7 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -14,23 +14,15 @@ """Inspection utility functions for Python Fire.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - +import asyncio import inspect import sys import types from fire import docstrings -import six - -if six.PY34: - import asyncio # pylint: disable=import-error,g-import-not-at-top # pytype: disable=import-error - -class FullArgSpec(object): +class FullArgSpec: """The arguments of a function, as in Python 3's inspect.FullArgSpec.""" def __init__(self, args=None, varargs=None, varkw=None, defaults=None, @@ -78,8 +70,6 @@ class with an __init__ method. if inspect.isclass(fn): # If the function is a class, we try to use its init method. skip_arg = True - if six.PY2 and hasattr(fn, '__init__'): - fn = fn.__init__ elif inspect.ismethod(fn): # If the function is a bound method, we skip the `self` argument. skip_arg = fn.__self__ is not None @@ -95,16 +85,6 @@ class with an __init__ method. return fn, skip_arg -def Py2GetArgSpec(fn): - """A wrapper around getargspec that tries both fn and fn.__call__.""" - try: - return inspect.getargspec(fn) # pylint: disable=deprecated-method - except TypeError: - if hasattr(fn, '__call__'): - return inspect.getargspec(fn.__call__) # pylint: disable=deprecated-method - raise - - def Py3GetFullArgSpec(fn): """A alternative to the builtin getfullargspec. @@ -189,13 +169,9 @@ def GetFullArgSpec(fn): if sys.version_info[0:2] >= (3, 5): (args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations) = Py3GetFullArgSpec(fn) - elif six.PY3: # Specifically Python 3.4. + else: # Specifically Python 3.4. (args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations) = inspect.getfullargspec(fn) # pylint: disable=deprecated-method,no-member - else: # six.PY2 - args, varargs, varkw, defaults = Py2GetArgSpec(fn) - kwonlyargs = kwonlydefaults = None - annotations = getattr(fn, '__annotations__', None) except TypeError: # If we can't get the argspec, how do we know if the fn should take args? @@ -225,7 +201,7 @@ def GetFullArgSpec(fn): return FullArgSpec() # In Python 3.5+ Py3GetFullArgSpec uses skip_bound_arg=True already. - skip_arg_required = six.PY2 or sys.version_info[0:2] == (3, 4) + skip_arg_required = sys.version_info[0:2] == (3, 4) if skip_arg_required and skip_arg and args: args.pop(0) # Remove 'self' or 'cls' from the list of arguments. return FullArgSpec(args, varargs, varkw, defaults, @@ -253,7 +229,7 @@ def GetFileAndLine(component): try: unused_code, lineindex = inspect.findsource(component) lineno = lineindex + 1 - except (IOError, IndexError): + except (OSError, IndexError): lineno = None return filename, lineno @@ -280,7 +256,10 @@ def Info(component): """ try: from IPython.core import oinspect # pylint: disable=import-outside-toplevel,g-import-not-at-top - inspector = oinspect.Inspector() + try: + inspector = oinspect.Inspector(theme_name="neutral") + except TypeError: # Only recent versions of IPython support theme_name. + inspector = oinspect.Inspector() info = inspector.info(component) # IPython's oinspect.Inspector.info may return '' @@ -292,7 +271,7 @@ def Info(component): try: unused_code, lineindex = inspect.findsource(component) info['line'] = lineindex + 1 - except (TypeError, IOError): + except (TypeError, OSError): info['line'] = None if 'docstring' in info: @@ -367,6 +346,6 @@ def GetClassAttrsDict(component): def IsCoroutineFunction(fn): try: - return six.PY34 and asyncio.iscoroutinefunction(fn) + return asyncio.iscoroutinefunction(fn) except: # pylint: disable=bare-except return False diff --git a/fire/inspectutils_test.py b/fire/inspectutils_test.py index ea8eb0e2..47de7e72 100644 --- a/fire/inspectutils_test.py +++ b/fire/inspectutils_test.py @@ -14,19 +14,12 @@ """Tests for the inspectutils module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import os -import unittest from fire import inspectutils from fire import test_components as tc from fire import testutils -import six - class InspectUtilsTest(testutils.BaseTestCase): @@ -40,7 +33,6 @@ def testGetFullArgSpec(self): self.assertEqual(spec.kwonlydefaults, {}) self.assertEqual(spec.annotations, {'arg2': int, 'arg4': int}) - @unittest.skipIf(six.PY2, 'No keyword arguments in python 2') def testGetFullArgSpecPy3(self): spec = inspectutils.GetFullArgSpec(tc.py3.identity) self.assertEqual(spec.args, ['arg1', 'arg2', 'arg3', 'arg4']) @@ -125,10 +117,7 @@ def testInfoClass(self): def testInfoClassNoInit(self): info = inspectutils.Info(tc.OldStyleEmpty) - if six.PY2: - self.assertEqual(info.get('type_name'), 'classobj') - else: - self.assertEqual(info.get('type_name'), 'type') + self.assertEqual(info.get('type_name'), 'type') self.assertIn(os.path.join('fire', 'test_components.py'), info.get('file')) self.assertGreater(info.get('line'), 0) diff --git a/fire/interact.py b/fire/interact.py index 7df32841..eccd3990 100644 --- a/fire/interact.py +++ b/fire/interact.py @@ -20,10 +20,6 @@ InteractiveConsole class. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import inspect @@ -69,16 +65,17 @@ def _AvailableString(variables, verbose=False): lists = [ ('Modules', modules), ('Objects', other)] - liststrs = [] + list_strs = [] for name, varlist in lists: if varlist: - liststrs.append( - '{name}: {items}'.format(name=name, items=', '.join(sorted(varlist)))) + items_str = ', '.join(sorted(varlist)) + list_strs.append(f'{name}: {items_str}') + lists_str = '\n'.join(list_strs) return ( 'Fire is starting a Python REPL with the following objects:\n' - '{liststrs}\n' - ).format(liststrs='\n'.join(liststrs)) + f'{lists_str}\n' + ) def _EmbedIPython(variables, argv=None): diff --git a/fire/interact_test.py b/fire/interact_test.py index 29fa7597..2f286824 100644 --- a/fire/interact_test.py +++ b/fire/interact_test.py @@ -14,15 +14,11 @@ """Tests for the interact module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function +from unittest import mock from fire import interact from fire import testutils -import mock - try: import IPython # pylint: disable=unused-import, g-import-not-at-top diff --git a/fire/main_test.py b/fire/main_test.py index 41699ac6..a2723347 100644 --- a/fire/main_test.py +++ b/fire/main_test.py @@ -43,12 +43,12 @@ class MainModuleFileTest(testutils.BaseTestCase): """Tests to verify correct import behavior for file executables.""" def setUp(self): - super(MainModuleFileTest, self).setUp() - self.file = tempfile.NamedTemporaryFile(suffix='.py') + super().setUp() + self.file = tempfile.NamedTemporaryFile(suffix='.py') # pylint: disable=consider-using-with self.file.write(b'class Foo:\n def double(self, n):\n return 2 * n\n') self.file.flush() - self.file2 = tempfile.NamedTemporaryFile() + self.file2 = tempfile.NamedTemporaryFile() # pylint: disable=consider-using-with def testFileNameFire(self): # Confirm that the file is correctly imported and doubles the number. diff --git a/fire/parser.py b/fire/parser.py index 404e18e7..d945b8ce 100644 --- a/fire/parser.py +++ b/fire/parser.py @@ -14,12 +14,14 @@ """Provides parsing functionality used by Python Fire.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import argparse import ast +import sys + +if sys.version_info[0:2] < (3, 8): + _StrNode = ast.Str +else: + _StrNode = ast.Constant def CreateParser(): @@ -106,7 +108,7 @@ def _LiteralEval(value): elif isinstance(child, ast.Name): replacement = _Replacement(child) - node.__setattr__(field, replacement) + setattr(node, field, replacement) # ast.literal_eval supports the following types: # strings, bytes, numbers, tuples, lists, dicts, sets, booleans, and None @@ -127,4 +129,4 @@ def _Replacement(node): # These are the only builtin constants supported by literal_eval. if value in ('True', 'False', 'None'): return node - return ast.Str(value) + return _StrNode(value) diff --git a/fire/parser_fuzz_test.py b/fire/parser_fuzz_test.py index af0be038..10f497cf 100644 --- a/fire/parser_fuzz_test.py +++ b/fire/parser_fuzz_test.py @@ -14,10 +14,6 @@ """Fuzz tests for the parser module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import parser from fire import testutils from hypothesis import example @@ -25,7 +21,6 @@ from hypothesis import settings from hypothesis import strategies as st import Levenshtein -import six class ParserFuzzTest(testutils.BaseTestCase): @@ -58,7 +53,7 @@ def testDefaultParseValueFuzz(self, value): result = parser.DefaultParseValue(value) except TypeError: # It's OK to get a TypeError if the string has the null character. - if u'\x00' in value: + if '\x00' in value: return raise except MemoryError: @@ -68,8 +63,8 @@ def testDefaultParseValueFuzz(self, value): raise try: - uvalue = six.text_type(value) - uresult = six.text_type(result) + uvalue = str(value) + uresult = str(result) except UnicodeDecodeError: # This is not what we're testing. return @@ -86,7 +81,7 @@ def testDefaultParseValueFuzz(self, value): if '#' in value: max_distance += len(value) - value.index('#') - if not isinstance(result, six.string_types): + if not isinstance(result, str): max_distance += value.count('0') # Leading 0s are stripped. # Note: We don't check distance for dicts since item order can be changed. diff --git a/fire/parser_test.py b/fire/parser_test.py index 0257be28..a404eea2 100644 --- a/fire/parser_test.py +++ b/fire/parser_test.py @@ -14,10 +14,6 @@ """Tests for the parser module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import parser from fire import testutils @@ -117,8 +113,9 @@ def testDefaultParseValueBareWordsTuple(self): def testDefaultParseValueNestedContainers(self): self.assertEqual( - parser.DefaultParseValue('[(A, 2, "3"), 5, {alph: 10.2, beta: "cat"}]'), - [('A', 2, '3'), 5, {'alph': 10.2, 'beta': 'cat'}]) + parser.DefaultParseValue( + '[(A, 2, "3"), 5, {alpha: 10.2, beta: "cat"}]'), + [('A', 2, '3'), 5, {'alpha': 10.2, 'beta': 'cat'}]) def testDefaultParseValueComments(self): self.assertEqual(parser.DefaultParseValue('"0#comments"'), '0#comments') diff --git a/fire/test_components.py b/fire/test_components.py index eee9a07c..887a0dc6 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -14,18 +14,11 @@ """This module has components that are used for testing Python Fire.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import collections import enum import functools -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 +from fire import test_components_py3 as py3 # pylint: disable=unused-import,no-name-in-module,g-import-not-at-top def identity(arg1, arg2, arg3=10, arg4=20, *arg5, **arg6): # pylint: disable=keyword-arg-before-vararg @@ -50,7 +43,7 @@ def function_with_help(help=True): # pylint: disable=redefined-builtin return help -class Empty(object): +class Empty: pass @@ -58,20 +51,20 @@ class OldStyleEmpty: # pylint: disable=old-style-class,no-init pass -class WithInit(object): +class WithInit: def __init__(self): pass -class ErrorInConstructor(object): +class ErrorInConstructor: def __init__(self, value='value'): self.value = value raise ValueError('Error in constructor') -class WithHelpArg(object): +class WithHelpArg: """Test class for testing when class has a help= arg.""" def __init__(self, help=True): # pylint: disable=redefined-builtin @@ -79,7 +72,7 @@ def __init__(self, help=True): # pylint: disable=redefined-builtin self.dictionary = {'__help': 'help in a dict'} -class NoDefaults(object): +class NoDefaults: def double(self, count): return 2 * count @@ -88,7 +81,7 @@ def triple(self, count): return 3 * count -class WithDefaults(object): +class WithDefaults: """Class with functions that have default arguments.""" def double(self, count=0): @@ -98,7 +91,7 @@ def double(self, count=0): count: Input number that you want to double. Returns: - A number that is the double of count.s + A number that is the double of count. """ return 2 * count @@ -122,7 +115,7 @@ def triple(self, count=0): return 3 * count -class MixedDefaults(object): +class MixedDefaults: def ten(self): return 10 @@ -134,7 +127,7 @@ def identity(self, alpha, beta='0'): return alpha, beta -class SimilarArgNames(object): +class SimilarArgNames: def identity(self, bool_one=False, bool_two=False): return bool_one, bool_two @@ -143,13 +136,13 @@ def identity2(self, a=None, alpha=None): return a, alpha -class CapitalizedArgNames(object): +class CapitalizedArgNames: def sum(self, Delta=1.0, Gamma=2.0): # pylint: disable=invalid-name return Delta + Gamma -class Annotations(object): +class Annotations: def double(self, count=0): return 2 * count @@ -161,7 +154,7 @@ def triple(self, count=0): triple.__annotations__ = {'count': float} -class TypedProperties(object): +class TypedProperties: """Test class for testing Python Fire with properties of various types.""" def __init__(self): @@ -180,7 +173,7 @@ def __init__(self): self.gamma = 'myexcitingstring' -class VarArgs(object): +class VarArgs: """Test class for testing Python Fire with a property with varargs.""" def cumsums(self, *items): @@ -198,7 +191,7 @@ def varchars(self, alpha=0, beta=0, *chars): # pylint: disable=keyword-arg-befo return alpha, beta, ''.join(chars) -class Underscores(object): +class Underscores: def __init__(self): self.underscore_example = 'fish fingers' @@ -207,20 +200,20 @@ def underscore_function(self, underscore_arg): return underscore_arg -class BoolConverter(object): +class BoolConverter: def as_bool(self, arg=False): return bool(arg) -class ReturnsObj(object): +class ReturnsObj: def get_obj(self, *items): del items # Unused return BoolConverter() -class NumberDefaults(object): +class NumberDefaults: def reciprocal(self, divisor=10.0): return 1.0 / divisor @@ -229,7 +222,7 @@ def integer_reciprocal(self, divisor=10): return 1.0 / divisor -class InstanceVars(object): +class InstanceVars: def __init__(self, arg1, arg2): self.arg1 = arg1 @@ -239,7 +232,7 @@ def run(self, arg1, arg2): return (self.arg1, self.arg2, arg1, arg2) -class Kwargs(object): +class Kwargs: def props(self, **kwargs): return kwargs @@ -251,13 +244,13 @@ def run(self, positional, named=None, **kwargs): return (positional, named, kwargs) -class ErrorRaiser(object): +class ErrorRaiser: def fail(self): raise ValueError('This error is part of a test.') -class NonComparable(object): +class NonComparable: def __eq__(self, other): raise ValueError('Instances of this class cannot be compared.') @@ -266,7 +259,7 @@ def __ne__(self, other): raise ValueError('Instances of this class cannot be compared.') -class EmptyDictOutput(object): +class EmptyDictOutput: def totally_empty(self): return {} @@ -275,7 +268,7 @@ def nothing_printable(self): return {'__do_not_print_me': 1} -class CircularReference(object): +class CircularReference: def create(self): x = {} @@ -283,7 +276,7 @@ def create(self): return x -class OrderedDictionary(object): +class OrderedDictionary: def empty(self): return collections.OrderedDict() @@ -295,7 +288,7 @@ def non_empty(self): return ordered_dict -class NamedTuple(object): +class NamedTuple: """Functions returning named tuples used for testing.""" def point(self): @@ -311,7 +304,7 @@ def matching_names(self): return Point(x='x', y='y') -class CallableWithPositionalArgs(object): +class CallableWithPositionalArgs: """Test class for supporting callable.""" TEST = 1 @@ -333,12 +326,12 @@ def coordinate_sum(self): return self.x + self.y -class CallableWithKeywordArgument(object): +class CallableWithKeywordArgument: """Test class for supporting callable.""" def __call__(self, **kwargs): for key, value in kwargs.items(): - print('%s: %s' % (key, value)) + print('{}: {}'.format(key, value)) def print_msg(self, msg): print(msg) @@ -347,7 +340,7 @@ def print_msg(self, msg): CALLABLE_WITH_KEYWORD_ARGUMENT = CallableWithKeywordArgument() -class ClassWithDocstring(object): +class ClassWithDocstring: """Test class for testing help text output. This is some detail description of this test class. @@ -370,7 +363,7 @@ def print_msg(self, msg=None): print(msg) -class ClassWithMultilineDocstring(object): +class ClassWithMultilineDocstring: """Test class for testing help text output with multiline docstring. This is a test class that has a long docstring description that spans across @@ -395,8 +388,7 @@ def example_generator(n): [0, 1, 2, 3] """ - for i in range(n): - yield i + yield from range(n) def simple_set(): @@ -421,7 +413,7 @@ class Color(enum.Enum): BLUE = 3 -class HasStaticAndClassMethods(object): +class HasStaticAndClassMethods: """A class with a static method and a class method.""" CLASS_STATE = 1 @@ -475,7 +467,7 @@ def fn_with_code_in_docstring(): return True -class BinaryCanvas(object): +class BinaryCanvas: """A canvas with which to make binary art, one bit at a time.""" def __init__(self, size=10): @@ -508,7 +500,7 @@ def set(self, value): return self -class DefaultMethod(object): +class DefaultMethod: def double(self, number): return 2 * number @@ -519,7 +511,7 @@ def _missing(): return _missing -class InvalidProperty(object): +class InvalidProperty: def double(self, number): return 2 * number @@ -562,4 +554,15 @@ def fn_with_kwarg_and_defaults(arg1, arg2, opt=True, **kwargs): """ del arg1, arg2, opt return kwargs.get('arg3') + + +def fn_with_multiple_defaults(first='first', last='last', late='late'): + """Function with kwarg and defaults. + + :key first: Description of first. + :key last: Description of last. + :key late: Description of late. + """ + del last, late + return first # pylint: enable=g-doc-args,g-doc-return-or-yield diff --git a/fire/test_components_bin.py b/fire/test_components_bin.py index fbb41952..62afdf11 100644 --- a/fire/test_components_bin.py +++ b/fire/test_components_bin.py @@ -17,10 +17,6 @@ This file is useful for replicating test results manually. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import fire from fire import test_components diff --git a/fire/test_components_py3.py b/fire/test_components_py3.py index 3c21f4ba..192302d3 100644 --- a/fire/test_components_py3.py +++ b/fire/test_components_py3.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Lint as: python3 """This module has components that use Python 3 specific syntax.""" import asyncio @@ -26,7 +25,13 @@ def identity(arg1, arg2: int, arg3=10, arg4: int = 20, *arg5, return arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 -class KeywordOnly(object): +class HelpTextComponent: + + def identity(self, *, alpha, beta='0'): + return alpha, beta + + +class KeywordOnly: def double(self, *, count): return count * 2 @@ -38,7 +43,7 @@ def with_default(self, *, x="x"): print("x: " + x) -class LruCacheDecoratedMethod(object): +class LruCacheDecoratedMethod: @functools.lru_cache() def lru_cache_in_class(self, arg1): @@ -50,14 +55,13 @@ def lru_cache_decorated(arg1): return arg1 -class WithAsyncio(object): +class WithAsyncio: - @asyncio.coroutine - def double(self, count=0): + async def double(self, count=0): return 2 * count -class WithTypes(object): +class WithTypes: """Class with functions that have default arguments and types.""" def double(self, count: float) -> float: @@ -67,7 +71,7 @@ def double(self, count: float) -> float: count: Input number that you want to double. Returns: - A number that is the double of count.s + A number that is the double of count. """ return 2 * count @@ -79,7 +83,7 @@ def long_type( return long_obj -class WithDefaultsAndTypes(object): +class WithDefaultsAndTypes: """Class with functions that have default arguments and types.""" def double(self, count: float = 0) -> float: @@ -89,7 +93,7 @@ def double(self, count: float = 0) -> float: count: Input number that you want to double. Returns: - A number that is the double of count.s + A number that is the double of count. """ return 2 * count diff --git a/fire/test_components_test.py b/fire/test_components_test.py index f35d7ab5..531f882c 100644 --- a/fire/test_components_test.py +++ b/fire/test_components_test.py @@ -14,10 +14,6 @@ """Tests for the test_components module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import test_components as tc from fire import testutils diff --git a/fire/testutils.py b/fire/testutils.py index 31a2badb..eca37f43 100644 --- a/fire/testutils.py +++ b/fire/testutils.py @@ -12,25 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Lint as: python2, python3 """Utilities for Python Fire's tests.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import contextlib +import io import os import re import sys import unittest +from unittest import mock from fire import core from fire import trace -import mock -import six - class BaseTestCase(unittest.TestCase): """Shared test case for Python Fire tests.""" @@ -50,8 +44,8 @@ def assertOutputMatches(self, stdout='.*', stderr='.*', capture=True): Yields: Yields to the wrapped context. """ - stdout_fp = six.StringIO() - stderr_fp = six.StringIO() + stdout_fp = io.StringIO() + stderr_fp = io.StringIO() try: with mock.patch.object(sys, 'stdout', stdout_fp): with mock.patch.object(sys, 'stderr', stderr_fp): @@ -73,12 +67,6 @@ def assertOutputMatches(self, stdout='.*', stderr='.*', capture=True): raise AssertionError('%s: Expected %r to match %r' % (name, value, regexp)) - def assertRaisesRegex(self, *args, **kwargs): # pylint: disable=arguments-differ - if sys.version_info.major == 2: - return super(BaseTestCase, self).assertRaisesRegexp(*args, **kwargs) # pylint: disable=deprecated-method - else: - return super(BaseTestCase, self).assertRaisesRegex(*args, **kwargs) # pylint: disable=no-member - @contextlib.contextmanager def assertRaisesFireExit(self, code, regexp='.*'): """Asserts that a FireExit error is raised in the context. diff --git a/fire/testutils_test.py b/fire/testutils_test.py index ad604193..4cfc0937 100644 --- a/fire/testutils_test.py +++ b/fire/testutils_test.py @@ -14,16 +14,10 @@ """Test the test utilities for Fire's tests.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import sys from fire import testutils -import six - class TestTestUtils(testutils.BaseTestCase): """Let's get meta.""" @@ -34,15 +28,15 @@ def testNoCheckOnException(self): raise ValueError() def testCheckStdoutOrStderrNone(self): - with six.assertRaisesRegex(self, AssertionError, 'stdout:'): + with self.assertRaisesRegex(AssertionError, 'stdout:'): with self.assertOutputMatches(stdout=None): print('blah') - with six.assertRaisesRegex(self, AssertionError, 'stderr:'): + with self.assertRaisesRegex(AssertionError, 'stderr:'): with self.assertOutputMatches(stderr=None): print('blah', file=sys.stderr) - with six.assertRaisesRegex(self, AssertionError, 'stderr:'): + with self.assertRaisesRegex(AssertionError, 'stderr:'): with self.assertOutputMatches(stdout='apple', stderr=None): print('apple') print('blah', file=sys.stderr) diff --git a/fire/trace.py b/fire/trace.py index 7174f994..4a6d4776 100644 --- a/fire/trace.py +++ b/fire/trace.py @@ -25,11 +25,7 @@ component will be None. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import pipes +import shlex from fire import inspectutils @@ -42,7 +38,7 @@ INTERACTIVE_MODE = 'Entered interactive mode' -class FireTrace(object): +class FireTrace: """A FireTrace represents the steps taken during a single Fire execution. A FireTrace consists of a sequence of FireTraceElement objects. Each element @@ -81,7 +77,7 @@ def GetLastHealthyElement(self): for element in reversed(self.elements): if not element.HasError(): return element - return None + return self.elements[0] # The initial element is always healthy. def HasError(self): """Returns whether the Fire execution encountered a Fire usage error.""" @@ -166,8 +162,8 @@ def display(arg1, arg2='!'): def _Quote(self, arg): if arg.startswith('--') and '=' in arg: prefix, value = arg.split('=', 1) - return pipes.quote(prefix) + '=' + pipes.quote(value) - return pipes.quote(arg) + return shlex.quote(prefix) + '=' + shlex.quote(value) + return shlex.quote(arg) def GetCommand(self, include_separators=True): """Returns the command representing the trace up to this point. @@ -216,10 +212,7 @@ def NeedsSeparator(self): def __str__(self): lines = [] for index, element in enumerate(self.elements): - line = '{index}. {trace_string}'.format( - index=index + 1, - trace_string=element, - ) + line = f'{index + 1}. {element}' lines.append(line) return '\n'.join(lines) @@ -245,7 +238,7 @@ def NeedsSeparatingHyphenHyphen(self, flag='help'): or flag in spec.kwonlyargs) -class FireTraceElement(object): +class FireTraceElement: """A FireTraceElement represents a single step taken by a Fire execution. Examples of a FireTraceElement are the instantiation of a class or the @@ -265,7 +258,7 @@ def __init__(self, Args: component: The result of this element of the trace. - action: The type of action (eg instantiating a class) taking place. + action: The type of action (e.g. instantiating a class) taking place. target: (string) The name of the component being acted upon. args: The args consumed by the represented action. filename: The file in which the action is defined, or None if N/A. @@ -305,11 +298,11 @@ def __str__(self): # Format is: {action} "{target}" ({filename}:{lineno}) string = self._action if self._target is not None: - string += ' "{target}"'.format(target=self._target) + string += f' "{self._target}"' if self._filename is not None: path = self._filename if self._lineno is not None: - path += ':{lineno}'.format(lineno=self._lineno) + path += f':{self._lineno}' - string += ' ({path})'.format(path=path) + string += f' ({path})' return string diff --git a/fire/trace_test.py b/fire/trace_test.py index 1621a593..1f858f5e 100644 --- a/fire/trace_test.py +++ b/fire/trace_test.py @@ -14,10 +14,6 @@ """Tests for the trace module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import testutils from fire import trace diff --git a/fire/value_types.py b/fire/value_types.py index c0a137fd..81308973 100644 --- a/fire/value_types.py +++ b/fire/value_types.py @@ -14,17 +14,12 @@ """Types of values.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import inspect from fire import inspectutils -import six -VALUE_TYPES = (bool, six.string_types, six.integer_types, float, complex, +VALUE_TYPES = (bool, str, bytes, int, float, complex, type(Ellipsis), type(None), type(NotImplemented)) diff --git a/mkdocs.yml b/mkdocs.yml index bb815e37..bbe1e848 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,7 +1,7 @@ site_name: Python Fire theme: readthedocs markdown_extensions: [fenced_code] -pages: +nav: - Overview: index.md - Installation: installation.md - Benefits: benefits.md diff --git a/pylintrc b/pylintrc index 1b1c5cc2..8896bb5b 100644 --- a/pylintrc +++ b/pylintrc @@ -7,9 +7,6 @@ # pygtk.require(). #init-hook= -# Profiled execution. -profile=no - # Add to the black list. It should be a base name, not a # path. You may set this option multiple times. ignore= @@ -32,7 +29,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,wrong-import-order,useless-object-inheritance,no-else-return,super-with-arguments,raise-missing-from +disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return,super-with-arguments,raise-missing-from,consider-using-f-string,unspecified-encoding,unnecessary-lambda-assignment,wrong-import-position,ungrouped-imports,deprecated-module [REPORTS] @@ -41,14 +38,6 @@ disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-d # (visual studio) and html output-format=text -# Include message's id in output -include-ids=no - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". -files-output=no - # Tells whether to display a full report or only the messages reports=yes @@ -59,10 +48,6 @@ reports=yes # (R0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) -# Add a comment according to your evaluation note. This is used by the global -# evaluation report (R0004). -comment=no - [VARIABLES] @@ -79,9 +64,6 @@ additional-builtins= [BASIC] -# List of builtins function names that should not be used, separated by a comma -bad-functions=map,filter,apply,input,reduce - # Regular expression which should only match correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ @@ -114,7 +96,7 @@ inlinevar-rgx=^[a-z][a-z0-9_]*$ good-names=i,j,k,ex,main,Run,_ # Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata +bad-names=map,filter,apply,input,reduce,foo,bar,baz,toto,tutu,tata # Regular expression which should only match functions or classes name which do # not require a docstring @@ -186,7 +168,7 @@ max-locals=15 max-returns=6 # Maximum number of branch for function / method body -max-branchs=12 +max-branches=12 # Maximum number of statements in function / method body max-statements=50 diff --git a/setup.cfg b/setup.cfg index 7a980136..ed53d83b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,3 @@ -[metadata] -license-file = LICENSE - -[wheel] -universal = 1 - [aliases] test = pytest diff --git a/setup.py b/setup.py index 1cc64a07..8d4a381b 100644 --- a/setup.py +++ b/setup.py @@ -29,18 +29,15 @@ A library for automatically generating command line interfaces.""".strip() DEPENDENCIES = [ - 'six', 'termcolor', - 'enum34; python_version < "3.4"' ] TEST_DEPENDENCIES = [ 'hypothesis', - 'mock', - 'python-Levenshtein', + 'levenshtein', ] -VERSION = '0.4.0' +VERSION = '0.7.0' URL = 'https://github.com/google/python-fire' setup( @@ -63,15 +60,14 @@ 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Operating System :: OS Independent', 'Operating System :: POSIX', @@ -81,6 +77,7 @@ keywords='command line interface cli python fire interactive bash tool', + requires_python='>=3.7', packages=['fire', 'fire.console'], install_requires=DEPENDENCIES,