diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..8be46672 --- /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: "/" + 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 1f9ed766..d9207dfe 100755 --- a/.github/scripts/build.sh +++ b/.github/scripts/build.sh @@ -17,16 +17,15 @@ # Exit when any command fails. set -e -PYTHON_VERSION=${PYTHON_VERSION:-2.7} +PYTHON_VERSION=${PYTHON_VERSION:-3.7} -pip install -U -r .github/scripts/requirements.txt -python setup.py develop +pip install -e .[test] 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} == 3.7 ]]; then - # Run type-checking. - pip install pytype; - pytype -x fire/test_components_py3.py; +if [[ ${PYTHON_VERSION} == 3.12 ]]; then + # Run type-checking + pip install ty + python -m ty check --python $(which python) --exclude fire/test_components_py3.py --exclude fire/console/ --exclude fire/formatting_windows.py fi diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt deleted file mode 100644 index 9e48e20d..00000000 --- a/.github/scripts/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -setuptools <65.7.0 -pip <23.0 -pylint <2.15.10 -pytest <=7.2.1 -pytest-pylint <=1.1.2 -pytest-runner <6.0.0 -termcolor <2.2.0 -hypothesis <6.62.0 -python-Levenshtein <0.20.9 -mock <5.0.0 \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9bf78e8b..6b9d1eae 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-20.04 + runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["2.7", "3.5", "3.7", "3.8", "3.9", "3.10"] + os: ["macos-latest", "ubuntu-latest"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14.0-rc.2"] + include: + - {os: "ubuntu-22.04", python-version: "3.7"} steps: # Checkout the repo. - name: Checkout Python Fire repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Set up Python environment. - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + 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 baae1a6e..b5d67c96 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,12 +40,9 @@ 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 diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 1aba38f6..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include LICENSE diff --git a/docs/guide.md b/docs/guide.md index 44d8a46d..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: @@ -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 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 4cc76210..b1470692 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.5.0' +__version__ = '0.7.1' diff --git a/fire/__main__.py b/fire/__main__.py index 2ad217d6..eb98b1a4 100644 --- a/fire/__main__.py +++ b/fire/__main__.py @@ -18,11 +18,8 @@ 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 @@ -57,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 or spec.loader is None: + raise OSError('Unable to load module from specified path.') - else: - import imp # pylint: disable=g-import-not-at-top,import-outside-toplevel,deprecated-module - module = imp.load_source(module_name, path) + module = util.module_from_spec(spec) # pylint: disable=no-member + spec.loader.exec_module(module) return module, module_name @@ -121,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 9659ec6a..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 @@ -278,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) ) @@ -308,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 @@ -316,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 @@ -336,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 @@ -392,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 @@ -438,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 815e16b8..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): @@ -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. @@ -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 869c5949..a7f38d4f 100644 --- a/fire/console/console_attr_os.py +++ b/fire/console/console_attr_os.py @@ -14,9 +14,6 @@ # limitations under the License. """OS specific console_attr helper functions.""" -# This file contains platform specific code which is not currently handled -# by pytype. -# pytype: skip-file from __future__ import absolute_import from __future__ import division @@ -73,7 +70,7 @@ def _GetXY(fd): try: # This magic incantation converts a struct from ioctl(2) containing two # binary shorts to a (rows, columns) int tuple. - rc = struct.unpack(b'hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, 'junk')) + rc = struct.unpack(b'hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, b'junk')) return (rc[1], rc[0]) if rc else None except: # pylint: disable=bare-except return None 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/encoding.py b/fire/console/encoding.py index 780e5a28..662342c6 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,20 +55,13 @@ 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 @@ -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 c1e97367..8e23e76b 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,10 +68,6 @@ 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, serialize=None): @@ -94,6 +86,8 @@ def Fire(component=None, command=None, name=None, serialize=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, serialize=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, serialize=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, serialize=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: @@ -205,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 @@ -236,9 +230,9 @@ 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 @@ -292,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) @@ -332,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) @@ -347,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. @@ -511,7 +504,7 @@ def _Fire(component, args, parsed_flag_args, context, name=None): # Treat namedtuples as dicts when handling them as a map. if inspectutils.IsNamedTuple(component): - component_dict = component._asdict() # pytype: disable=attribute-error + component_dict = component._asdict() else: component_dict = component @@ -526,7 +519,7 @@ def _Fire(component, args, parsed_flag_args, context, name=None): # a key as another type. # TODO(dbieber): Consider alternatives for accessing non-string keys. for key, value in ( - component_dict.items()): # pytype: disable=attribute-error + component_dict.items()): if target == str(key): component = value handled = True @@ -685,8 +678,14 @@ def _CallAndUpdateTrace(component, args, component_trace, treatment='class', # Call the function. if inspectutils.IsCoroutineFunction(fn): - loop = asyncio.get_event_loop() - component = loop.run_until_complete(fn(*varargs, **kwargs)) + try: + loop = asyncio.get_running_loop() + except RuntimeError: + # No event loop running, create a new one + component = asyncio.run(fn(*varargs, **kwargs)) + else: + # Event loop is already running + component = loop.run_until_complete(fn(*varargs, **kwargs)) else: component = fn(*varargs, **kwargs) @@ -877,6 +876,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 @@ -894,9 +894,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 75b76998..f48d6e2d 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): @@ -218,16 +213,14 @@ def serialize(x): with self.assertRaises(core.FireError): core.Fire(ident, command=['asdf'], serialize=55) - @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.') def testLruCacheDecoratorBoundArg(self): self.assertEqual( - core.Fire(tc.py3.LruCacheDecoratedMethod, # pytype: disable=module-attr + core.Fire(tc.py3.LruCacheDecoratedMethod, 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 + core.Fire(tc.py3.lru_cache_decorated, command=['foo']), 'foo') diff --git a/fire/custom_descriptions.py b/fire/custom_descriptions.py index 266671f1..ef1130a3 100644 --- a/fire/custom_descriptions.py +++ b/fire/custom_descriptions.py @@ -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 @@ -138,14 +131,14 @@ 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: - return CUSTOM_DESC_SUM_FN_DICT.get(obj_type_name)[0](obj, available_space, - line_length) + return CUSTOM_DESC_SUM_FN_DICT[obj_type_name][0](obj, available_space, + line_length) return None def GetDescription(obj, available_space, line_length): obj_type_name = type(obj).__name__ 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 CUSTOM_DESC_SUM_FN_DICT[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 b2e9b322..547153c6 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' @@ -71,7 +68,7 @@ def SetParseFns(*positional, **named): def _Decorator(fn): parse_fns = GetParseFns(fn) parse_fns['positional'] = positional - parse_fns['named'].update(named) # pytype: disable=attribute-error + parse_fns['named'].update(named) _SetMetadata(fn, FIRE_PARSE_FNS, parse_fns) return fn @@ -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 = {"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..2adfe5ec 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 @@ -440,7 +436,7 @@ def _consume_line(line_info, state): if state.section.new and state.section.format == Formats.RST: # The current line starts with an RST directive, e.g. ":param arg:". directive = _get_directive(line_info) - directive_tokens = directive.split() # pytype: disable=attribute-error + directive_tokens = directive.split() if state.section.title == Sections.ARGS: name = directive_tokens[-1] arg = _get_or_create_arg_by_name( 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 0d6e5d18..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 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 05a88c49..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 diff --git a/fire/formatting_windows.py b/fire/formatting_windows.py index 2b85820d..749ab6d0 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 @@ -25,7 +21,7 @@ import sys try: - import colorama # pylint: disable=g-import-not-at-top, # pytype: disable=import-error + import colorama # pylint: disable=g-import-not-at-top HAS_COLORAMA = True except ImportError: HAS_COLORAMA = False @@ -35,14 +31,16 @@ 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. wrap = False - kernel32 = ctypes.windll.kernel32 # pytype: disable=module-attr + kernel32 = ctypes.windll.kernel32 enable_virtual_terminal_processing = 0x04 - out_handle = kernel32.GetStdHandle(subprocess.STD_OUTPUT_HANDLE) # pylint: disable=line-too-long, # pytype: disable=module-attr + out_handle = kernel32.GetStdHandle(subprocess.STD_OUTPUT_HANDLE) # pylint: disable=line-too-long, # GetConsoleMode fails if the terminal isn't native. mode = ctypes.wintypes.DWORD() if kernel32.GetConsoleMode(out_handle, ctypes.byref(mode)) == 0: diff --git a/fire/helptext.py b/fire/helptext.py index 6e7fbb07..347278da 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -29,13 +29,10 @@ information. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function +from __future__ import annotations import collections import itertools -import sys from fire import completion from fire import custom_descriptions @@ -90,13 +87,14 @@ def HelpText(component, trace=None, verbose=False): + usage_details_sections + notes_sections ) + valid_sections = [section for section in sections if section is not None] return '\n\n'.join( - _CreateOutputSection(*section) - for section in sections if section is not None + _CreateOutputSection(name, content) + for name, content in valid_sections ) -def _NameSection(component, info, trace=None, verbose=False): +def _NameSection(component, info, trace=None, verbose=False) -> tuple[str, str]: """The "Name" section of the help string.""" # Only include separators in the name in verbose mode. @@ -111,14 +109,14 @@ 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) def _SynopsisSection(component, actions_grouped_by_kind, spec, metadata, - trace=None): + trace=None) -> tuple[str, str]: """The "Synopsis" section of the help string.""" current_command = _GetCurrentCommand(trace=trace, include_separators=True) @@ -137,15 +135,11 @@ 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) -def _DescriptionSection(component, info): +def _DescriptionSection(component, info) -> tuple[str, str] | None: """The "Description" sections of the help string. Args: @@ -248,8 +242,6 @@ def _ArgsAndFlagsSections(info, spec, metadata): if spec.varkw: # Include kwargs documented via :key param: documented_kwargs = [] - flag_string = '--{name}' - short_flag_string = '-{short_name}, --{name}' # add short flags if possible flags = docstring_info.args or [] @@ -258,11 +250,10 @@ def _ArgsAndFlagsSections(info, spec, metadata): for flag in flags: if isinstance(flag, docstrings.KwargInfo): if flag.name[0] in unique_short_flags: - flag_string = short_flag_string.format( - name=flag.name, short_name=flag.name[0] - ) + short_name = flag.name[0] + flag_string = f'-{short_name}, --{flag.name}' else: - flag_string = flag_string.format(name=flag.name) + flag_string = f'--{flag.name}' flag_item = _CreateFlagItem( flag.name, docstring_info, spec, @@ -352,9 +343,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: @@ -362,8 +353,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) @@ -406,7 +397,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] @@ -420,11 +411,9 @@ def _GetCurrentCommand(trace=None, include_separators=True): return current_command -def _CreateOutputSection(name, content): - return """{name} -{content}""".format( - name=formatting.Bold(name), - content=formatting.Indent(content, SECTION_INDENTATION)) +def _CreateOutputSection(name: str, content: str) -> str: + return f"""{formatting.Bold(name)} +{formatting.Indent(content, SECTION_INDENTATION)}""" def _CreateArgItem(arg, docstring_info, spec): @@ -435,7 +424,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. @@ -450,7 +439,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)) @@ -489,14 +478,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: - flag_string = '-{short_flag}, '.format(short_flag=flag[0]) + flag_string + short_flag = flag[0] + flag_string = f'-{short_flag}, {flag_string}' arg_type = _GetArgType(flag, spec) arg_default = _GetArgDefault(flag, spec) @@ -504,14 +492,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)) @@ -537,9 +525,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. @@ -574,15 +560,15 @@ def _GetArgDefault(flag, spec): 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 @@ -628,9 +614,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) @@ -646,11 +632,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() @@ -694,15 +675,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 @@ -711,7 +693,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) @@ -727,7 +709,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] @@ -737,7 +719,7 @@ 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 @@ -752,10 +734,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: @@ -788,7 +770,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 404d9812..c7098fc4 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): @@ -129,12 +123,9 @@ def testHelpTextFunctionWithKwargsAndDefaults(self): '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 + tc.py3.WithDefaultsAndTypes().double) help_screen = helptext.HelpText( component=component, trace=trace.FireTrace(component, name='double')) @@ -146,12 +137,9 @@ def testHelpTextFunctionWithDefaultsAndTypes(self): 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 + tc.py3.WithDefaultsAndTypes().get_int) help_screen = helptext.HelpText( component=component, trace=trace.FireTrace(component, name='get_int')) @@ -164,11 +152,8 @@ def testHelpTextFunctionWithTypesAndDefaultNone(self): 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 + component = tc.py3.WithTypes().double help_screen = helptext.HelpText( component=component, trace=trace.FireTrace(component, name='double')) @@ -182,11 +167,8 @@ 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 + component = tc.py3.WithTypes().long_type help_screen = helptext.HelpText( component=component, trace=trace.FireTrace(component, name='long_type')) @@ -280,27 +262,20 @@ 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 + component = tc.py3.KeywordOnly.with_default output = helptext.HelpText( component=component, trace=trace.FireTrace(component, 'with_default')) self.assertIn('NAME\n with_default', 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 + component = tc.py3.KeywordOnly.double output = helptext.HelpText( component=component, trace=trace.FireTrace(component, 'double')) self.assertIn('NAME\n double', output) self.assertIn('FLAGS\n -c, --count=COUNT (required)', output) - @testutils.skipIf( - six.PY2, - 'Python 2 does not support required name-only arguments.') def testHelpTextFunctionMixedDefaults(self): component = tc.py3.HelpTextComponent().identity t = trace.FireTrace(component, name='FunctionMixedDefaults') @@ -453,7 +428,6 @@ def testHelpTextMultipleKeywoardArgumentsWithShortArgs(self): self.assertIn('\n --late', help_screen) - class UsageTest(testutils.BaseTestCase): def testUsageOutput(self): @@ -527,9 +501,6 @@ def testUsageOutputFunctionWithDocstring(self): textwrap.dedent(expected_output).lstrip('\n'), usage_output) - @testutils.skipIf( - six.PY2, - 'Python 2 does not support required name-only arguments.') def testUsageOutputFunctionMixedDefaults(self): component = tc.py3.HelpTextComponent().identity t = trace.FireTrace(component, name='FunctionMixedDefaults') diff --git a/fire/inspectutils.py b/fire/inspectutils.py index 0fa8e7d3..17508e30 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -14,23 +14,14 @@ """Inspection utility functions for Python Fire.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - 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 +69,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 +84,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. @@ -120,9 +99,9 @@ def Py3GetFullArgSpec(fn): An inspect.FullArgSpec namedtuple with the full arg spec of the function. """ # pylint: disable=no-member - # pytype: disable=module-attr + try: - sig = inspect._signature_from_callable( # pylint: disable=protected-access + sig = inspect._signature_from_callable( # pylint: disable=protected-access # type: ignore fn, skip_bound_arg=True, follow_wrapper_chains=True, @@ -149,19 +128,19 @@ def Py3GetFullArgSpec(fn): name = param.name # pylint: disable=protected-access - if kind is inspect._POSITIONAL_ONLY: + if kind is inspect._POSITIONAL_ONLY: # type: ignore args.append(name) - elif kind is inspect._POSITIONAL_OR_KEYWORD: + elif kind is inspect._POSITIONAL_OR_KEYWORD: # type: ignore args.append(name) if param.default is not param.empty: defaults += (param.default,) - elif kind is inspect._VAR_POSITIONAL: + elif kind is inspect._VAR_POSITIONAL: # type: ignore varargs = name - elif kind is inspect._KEYWORD_ONLY: + elif kind is inspect._KEYWORD_ONLY: # type: ignore kwonlyargs.append(name) if param.default is not param.empty: kwdefaults[name] = param.default - elif kind is inspect._VAR_KEYWORD: + elif kind is inspect._VAR_KEYWORD: # type: ignore varkw = name if param.annotation is not param.empty: annotations[name] = param.annotation @@ -177,7 +156,6 @@ def Py3GetFullArgSpec(fn): return inspect.FullArgSpec(args, varargs, varkw, defaults, kwonlyargs, kwdefaults, annotations) # pylint: enable=no-member - # pytype: enable=module-attr def GetFullArgSpec(fn): @@ -189,13 +167,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 +199,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 +227,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 +254,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() # type: ignore info = inspector.info(component) # IPython's oinspect.Inspector.info may return '' @@ -291,12 +268,12 @@ def Info(component): try: unused_code, lineindex = inspect.findsource(component) - info['line'] = lineindex + 1 - except (TypeError, IOError): - info['line'] = None + info['line'] = lineindex + 1 # type: ignore + except (TypeError, OSError): + info['line'] = None # type: ignore if 'docstring' in info: - info['docstring_info'] = docstrings.parse(info['docstring']) + info['docstring_info'] = docstrings.parse(info['docstring']) # type: ignore return info @@ -367,6 +344,6 @@ def GetClassAttrsDict(component): def IsCoroutineFunction(fn): try: - return six.PY34 and asyncio.iscoroutinefunction(fn) + return inspect.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 a0184620..9e1c382b 100644 --- a/fire/main_test.py +++ b/fire/main_test.py @@ -43,7 +43,7 @@ class MainModuleFileTest(testutils.BaseTestCase): """Tests to verify correct import behavior for file executables.""" def setUp(self): - super(MainModuleFileTest, self).setUp() + 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() @@ -78,7 +78,7 @@ def testFileNameModuleDuplication(self): def testFileNameModuleFileFailure(self): # Confirm that an invalid file that masks a non-existent module fails. with self.assertRaisesRegex(ValueError, - r'Fire can only be called on \.py files\.'): # pylint: disable=line-too-long, # pytype: disable=attribute-error + r'Fire can only be called on \.py files\.'): # pylint: disable=line-too-long, dirname = os.path.dirname(self.file.name) with testutils.ChangeDirectory(dirname): with open('foobar', 'w'): diff --git a/fire/parser.py b/fire/parser.py index 2aff8bd7..a335cc2c 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 # type: ignore # pylint: disable=no-member # deprecated but needed for Python < 3.8 +else: + _StrNode = ast.Constant def CreateParser(): @@ -94,7 +96,7 @@ def _LiteralEval(value): SyntaxError: If the value string has a syntax error. """ root = ast.parse(value, mode='eval') - if isinstance(root.body, ast.BinOp): # pytype: disable=attribute-error + if isinstance(root.body, ast.BinOp): raise ValueError(value) for node in ast.walk(root): @@ -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 8aeabc61..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 diff --git a/fire/test_components.py b/fire/test_components.py index 5fcb056e..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): @@ -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,7 +554,7 @@ def fn_with_kwarg_and_defaults(arg1, arg2, opt=True, **kwargs): """ del arg1, arg2, opt return kwargs.get('arg3') -# pylint: enable=g-doc-args,g-doc-return-or-yield + def fn_with_multiple_defaults(first='first', last='last', late='late'): """Function with kwarg and defaults. @@ -573,3 +565,4 @@ def fn_with_multiple_defaults(first='first', last='last', late='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 5140921d..192302d3 100644 --- a/fire/test_components_py3.py +++ b/fire/test_components_py3.py @@ -31,7 +31,7 @@ def identity(self, *, alpha, beta='0'): return alpha, beta -class KeywordOnly(object): +class KeywordOnly: def double(self, *, count): return count * 2 @@ -43,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): @@ -55,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: @@ -84,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: 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 ea410e82..eca37f43 100644 --- a/fire/testutils.py +++ b/fire/testutils.py @@ -14,22 +14,17 @@ """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.""" @@ -49,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): @@ -72,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..601026fd 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 @@ -66,9 +62,7 @@ def __init__(self, initial_component, name=None, separator='-', verbose=False, def GetResult(self): """Returns the component from the last element of the trace.""" - # pytype: disable=attribute-error return self.GetLastHealthyElement().component - # pytype: enable=attribute-error def GetLastHealthyElement(self): """Returns the last element of the trace that is not an error. @@ -81,7 +75,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 +160,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 +210,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 +236,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 +256,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 +296,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/pylintrc b/pylintrc index b89b16d1..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,consider-using-f-string,unspecified-encoding,unnecessary-lambda-assignment +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/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..912c08aa --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,66 @@ +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "fire" +version = "0.7.1" +description = "A library for automatically generating command line interfaces." +readme = "README.md" +license = {text = "Apache-2.0"} +authors = [ + {name = "David Bieber", email = "david810+fire@gmail.com"} +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "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", + "Programming Language :: Python :: 3.14", + "Operating System :: OS Independent", + "Operating System :: POSIX", + "Operating System :: MacOS", + "Operating System :: Unix", +] +keywords = ["command", "line", "interface", "cli", "python", "fire", "interactive", "bash", "tool"] +requires-python = ">=3.7" +dependencies = [ + "termcolor", +] + +[project.urls] +Homepage = "https://github.com/google/python-fire" +Repository = "https://github.com/google/python-fire" + +[project.optional-dependencies] +test = [ + "setuptools<=80.9.0", + "pip", + "pylint<3.3.8", + "pytest<=8.4.1", + "pytest-pylint<=1.1.2", + "pytest-runner<7.0.0", + "termcolor<3.2.0", + "hypothesis<6.137.0", + "levenshtein<=0.27.1", +] + +[tool.setuptools.packages.find] +include = ["fire*"] + +[tool.setuptools.package-data] +fire = ["console/*"] + +[tool.pytest.ini_options] +addopts = [ + "--ignore=fire/test_components_py3.py", + "--ignore=fire/parser_fuzz_test.py" +] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 9c558e35..00000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -. diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 977056b0..00000000 --- a/setup.cfg +++ /dev/null @@ -1,13 +0,0 @@ -[wheel] -universal = 1 - -[aliases] -test = pytest - -[tool:pytest] -addopts = --ignore=fire/test_components_py3.py - --ignore=fire/parser_fuzz_test.py - -[pytype] -inputs = . -output = .pytype diff --git a/setup.py b/setup.py deleted file mode 100644 index 8e95f414..00000000 --- a/setup.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (C) 2018 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""The setup.py file for Python Fire.""" - -from setuptools import setup - -LONG_DESCRIPTION = """ -Python Fire is a library for automatically generating command line interfaces -(CLIs) with a single line of code. - -It will turn any Python module, class, object, function, etc. (any Python -component will work!) into a CLI. It's called Fire because when you call Fire(), -it fires off your command. -""".strip() - -SHORT_DESCRIPTION = """ -A library for automatically generating command line interfaces.""".strip() - -DEPENDENCIES = [ - 'six', - 'termcolor', - 'enum34; python_version < "3.4"' -] - -TEST_DEPENDENCIES = [ - 'hypothesis', - 'mock', - 'python-Levenshtein', -] - -VERSION = '0.5.0' -URL = 'https://github.com/google/python-fire' - -setup( - name='fire', - version=VERSION, - description=SHORT_DESCRIPTION, - long_description=LONG_DESCRIPTION, - url=URL, - - author='David Bieber', - author_email='dbieber@google.com', - license='Apache Software License', - - classifiers=[ - 'Development Status :: 4 - Beta', - - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Libraries :: Python Modules', - - '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.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', - - 'Operating System :: OS Independent', - 'Operating System :: POSIX', - 'Operating System :: MacOS', - 'Operating System :: Unix', - ], - - keywords='command line interface cli python fire interactive bash tool', - - packages=['fire', 'fire.console'], - - install_requires=DEPENDENCIES, - tests_require=TEST_DEPENDENCIES, -)