diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ba1b7f19..8be46672 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,7 @@ version: 2 updates: # Enable version updates for python - package-ecosystem: "pip" - directory: ".github/scripts/" + directory: "/" schedule: interval: "monthly" labels: ["dependabot"] diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh index 111257ae..d9207dfe 100755 --- a/.github/scripts/build.sh +++ b/.github/scripts/build.sh @@ -19,14 +19,13 @@ set -e 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 a5648989..00000000 --- a/.github/scripts/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -setuptools <=75.1.0 -pip -pylint <3.2.8 -pytest <=8.3.3 -pytest-pylint <=1.1.2 -pytest-runner <7.0.0 -termcolor <2.5.0 -hypothesis <6.113.0 -levenshtein <=0.26.0 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 59b0a4ba..75a687f3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: 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-20.04", python-version: "3.7"} + - {os: "ubuntu-22.04", python-version: "3.7"} steps: # Checkout the repo. 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/fire/__init__.py b/fire/__init__.py index 9ff696d3..b1470692 100644 --- a/fire/__init__.py +++ b/fire/__init__.py @@ -17,4 +17,4 @@ from fire.core import Fire __all__ = ['Fire'] -__version__ = '0.7.0' +__version__ = '0.7.1' diff --git a/fire/__main__.py b/fire/__main__.py index 140b4a76..eb98b1a4 100644 --- a/fire/__main__.py +++ b/fire/__main__.py @@ -60,11 +60,11 @@ def import_from_file_path(path): spec = util.spec_from_file_location(module_name, path) - if spec is None: + if spec is None or spec.loader is None: raise OSError('Unable to load module from specified path.') module = util.module_from_spec(spec) # pylint: disable=no-member - spec.loader.exec_module(module) # pytype: disable=attribute-error + spec.loader.exec_module(module) return module, module_name diff --git a/fire/completion.py b/fire/completion.py index 625e9d86..1597d464 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -321,7 +321,7 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=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 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/encoding.py b/fire/console/encoding.py index 3ce30cb5..662342c6 100644 --- a/fire/console/encoding.py +++ b/fire/console/encoding.py @@ -67,7 +67,7 @@ def Decode(data, encoding=None): try: # Just return the string if its pure ASCII. - return string.decode('ascii') # pytype: disable=attribute-error + return string.decode('ascii') except UnicodeError: # The string is not ASCII encoded. pass @@ -75,7 +75,7 @@ def Decode(data, encoding=None): # Try the suggested encoding if specified. if encoding: try: - return string.decode(encoding) # pytype: disable=attribute-error + return string.decode(encoding) except UnicodeError: # Bad suggestion. pass @@ -84,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') # pytype: disable=attribute-error + return string.decode('utf8') except UnicodeError: # Not a UTF-8 encoding. pass # Try the filesystem encoding. try: - return string.decode(sys.getfilesystemencoding()) # pytype: disable=attribute-error + return string.decode(sys.getfilesystemencoding()) except UnicodeError: # string is not encoded for filesystem paths. pass # Try the system default encoding. try: - return string.decode(sys.getdefaultencoding()) # pytype: disable=attribute-error + return string.decode(sys.getdefaultencoding()) except UnicodeError: # string is not encoded using the default encoding. pass @@ -118,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') # pytype: disable=attribute-error + return string.decode('iso-8859-1') def GetEncodedValue(env, name, default=None): diff --git a/fire/core.py b/fire/core.py index 26a25753..32e0e9cc 100644 --- a/fire/core.py +++ b/fire/core.py @@ -504,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 @@ -519,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 diff --git a/fire/core_test.py b/fire/core_test.py index 90b7f466..f48d6e2d 100644 --- a/fire/core_test.py +++ b/fire/core_test.py @@ -215,12 +215,12 @@ def serialize(x): 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') 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 768f0e23..ef1130a3 100644 --- a/fire/custom_descriptions.py +++ b/fire/custom_descriptions.py @@ -131,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/decorators.py b/fire/decorators.py index 914b1de6..547153c6 100644 --- a/fire/decorators.py +++ b/fire/decorators.py @@ -68,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 diff --git a/fire/docstrings.py b/fire/docstrings.py index 2d7c7e63..2adfe5ec 100644 --- a/fire/docstrings.py +++ b/fire/docstrings.py @@ -436,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/formatting_windows.py b/fire/formatting_windows.py index cee6f393..749ab6d0 100644 --- a/fire/formatting_windows.py +++ b/fire/formatting_windows.py @@ -21,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 @@ -38,9 +38,9 @@ def initialize_or_disable(): # 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 318d6276..347278da 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -29,6 +29,8 @@ information. """ +from __future__ import annotations + import collections import itertools @@ -85,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. @@ -113,7 +116,7 @@ def _NameSection(component, info, trace=None, verbose=False): 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) @@ -136,7 +139,7 @@ def _SynopsisSection(component, actions_grouped_by_kind, spec, metadata, return ('SYNOPSIS', text) -def _DescriptionSection(component, info): +def _DescriptionSection(component, info) -> tuple[str, str] | None: """The "Description" sections of the help string. Args: @@ -408,7 +411,7 @@ def _GetCurrentCommand(trace=None, include_separators=True): return current_command -def _CreateOutputSection(name, content): +def _CreateOutputSection(name: str, content: str) -> str: return f"""{formatting.Bold(name)} {formatting.Indent(content, SECTION_INDENTATION)}""" diff --git a/fire/helptext_test.py b/fire/helptext_test.py index aeff5240..c7098fc4 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -125,7 +125,7 @@ def testHelpTextFunctionWithKwargsAndDefaults(self): 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')) @@ -139,7 +139,7 @@ def testHelpTextFunctionWithDefaultsAndTypes(self): 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')) @@ -153,7 +153,7 @@ def testHelpTextFunctionWithTypesAndDefaultNone(self): self.assertNotIn('NOTES', help_screen) 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')) @@ -168,7 +168,7 @@ def testHelpTextFunctionWithTypes(self): help_screen) 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')) @@ -263,14 +263,14 @@ def testHelpTextNoInit(self): self.assertIn('SYNOPSIS\n OldStyleEmpty', help_screen) 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) 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) diff --git a/fire/inspectutils.py b/fire/inspectutils.py index d1438972..6dd8fd67 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -100,9 +100,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, @@ -129,19 +129,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 @@ -157,7 +157,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): @@ -256,7 +255,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 '' diff --git a/fire/main_test.py b/fire/main_test.py index a2723347..9e1c382b 100644 --- a/fire/main_test.py +++ b/fire/main_test.py @@ -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 d945b8ce..b8e7f19c 100644 --- a/fire/parser.py +++ b/fire/parser.py @@ -96,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): diff --git a/fire/trace.py b/fire/trace.py index 4a6d4776..601026fd 100644 --- a/fire/trace.py +++ b/fire/trace.py @@ -62,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. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..eccee91b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,65 @@ +[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", + "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.136.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 ed53d83b..00000000 --- a/setup.cfg +++ /dev/null @@ -1,10 +0,0 @@ -[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 8d4a381b..00000000 --- a/setup.py +++ /dev/null @@ -1,85 +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 = [ - 'termcolor', -] - -TEST_DEPENDENCIES = [ - 'hypothesis', - 'levenshtein', -] - -VERSION = '0.7.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 :: 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', - - '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', - packages=['fire', 'fire.console'], - - install_requires=DEPENDENCIES, - tests_require=TEST_DEPENDENCIES, -)