diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8a7474a..c35d285 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,6 +76,11 @@ jobs: coverage run --branch --source=mim -m pytest tests/ coverage xml coverage report -m + - name: Run unittests with click < 8.0.0 + run: | + python -m pip install click==7.1.2 + pytest tests/ + if: ${{matrix.python-version == '3.8'}} - name: Upload coverage to Codecov uses: codecov/codecov-action@v1.0.10 with: diff --git a/.gitignore b/.gitignore index 6083fe2..59d8a7f 100644 --- a/.gitignore +++ b/.gitignore @@ -144,3 +144,6 @@ data/ *ipynb src/ + +# vscode +.vscode diff --git a/mim/click/__init__.py b/mim/click/__init__.py index fd744d2..34ed8e6 100644 --- a/mim/click/__init__.py +++ b/mim/click/__init__.py @@ -4,11 +4,12 @@ from .autocompletion import ( get_installed_package, get_official_package, ) +from .compat import argument from .customcommand import CustomCommand from .option import OptionEatAll from .utils import param2lowercase __all__ = [ 'get_downstream_package', 'get_installed_package', 'get_official_package', - 'OptionEatAll', 'CustomCommand', 'param2lowercase' + 'OptionEatAll', 'CustomCommand', 'param2lowercase', 'argument' ] diff --git a/mim/click/compat.py b/mim/click/compat.py new file mode 100644 index 0000000..7d00bff --- /dev/null +++ b/mim/click/compat.py @@ -0,0 +1,53 @@ +# Copyright (c) OpenMMLab. All rights reserved. +from distutils.version import LooseVersion +from typing import Callable + +import click + + +def autocompletion_to_shell_complete(autocompletion: Callable) -> Callable: + """Convert autocompletion to shell_complete. + + Reference: + https://github.com/pallets/click/blob/8.0.0/src/click/core.py#L2059 + + Args: + autocompletion (callable): A function that returns custom shell + completions. Takes ``ctx, param, incomplete`` and must return a + list of string. + + Returns: + A shell_complete function converted from autocompletion. + """ + + def shell_complete(ctx, param, incomplete): + from click.shell_completion import CompletionItem + + out = [] + for c in autocompletion(ctx, [], incomplete): + if isinstance(c, tuple): + c = CompletionItem(c[0], help=c[1]) + elif isinstance(c, str): + c = CompletionItem(c) + + if c.value.startswith(incomplete): + out.append(c) + return out + + return shell_complete + + +def argument(*param_decls, **attrs): + """A decorator compatible with click 7.x and 8.x. + + Same as ``click.argument``. + """ + # 'autocompletion' will be removed in Click 8.1 and its new name is + # 'shell_complete'. + if LooseVersion(click.__version__) >= LooseVersion('8.0.0'): + autocompletion = attrs.pop('autocompletion', None) + if autocompletion is not None: + attrs['shell_complete'] = autocompletion_to_shell_complete( + autocompletion) + + return click.argument(*param_decls, **attrs) diff --git a/mim/click/option.py b/mim/click/option.py index 5e58322..ec2b068 100644 --- a/mim/click/option.py +++ b/mim/click/option.py @@ -45,3 +45,15 @@ class OptionEatAll(click.Option): our_parser.process = parser_process break return retval + + def type_cast_value(self, ctx, value): + """Convert and validate a value against the option's + :attr:`type`, :attr:`multiple`, and :attr:`nargs`. + + Since the :attr:`type` of OptionEatAll is `STRING`, override the + `type_cast_value` method to prevent the return value of OptionEatAll + converted from a tuple to a string. + """ + if value is None: + return () if self.multiple or self.nargs == -1 else None + return tuple(self.type(x, self, ctx) for x in value) diff --git a/mim/commands/download.py b/mim/commands/download.py index 1e42ac0..0a9ea1f 100644 --- a/mim/commands/download.py +++ b/mim/commands/download.py @@ -4,7 +4,12 @@ from typing import List, Optional import click -from mim.click import OptionEatAll, get_downstream_package, param2lowercase +from mim.click import ( + OptionEatAll, + argument, + get_downstream_package, + param2lowercase, +) from mim.commands.search import get_model_info from mim.utils import ( DEFAULT_CACHE_DIR, @@ -18,7 +23,7 @@ from mim.utils import ( @click.command('download') -@click.argument( +@argument( 'package', type=str, autocompletion=get_downstream_package, diff --git a/mim/commands/install.py b/mim/commands/install.py index 6ffe40b..92bb980 100644 --- a/mim/commands/install.py +++ b/mim/commands/install.py @@ -10,7 +10,7 @@ from typing import List import click import pip -from mim.click import get_official_package, param2lowercase +from mim.click import argument, get_official_package, param2lowercase from mim.commands.uninstall import uninstall from mim.utils import ( DEFAULT_URL, @@ -35,7 +35,7 @@ from mim.utils import ( @click.command('install') -@click.argument( +@argument( 'package', type=str, autocompletion=get_official_package, diff --git a/mim/commands/search.py b/mim/commands/search.py index 155b0e5..b20eae4 100644 --- a/mim/commands/search.py +++ b/mim/commands/search.py @@ -12,7 +12,12 @@ from modelindex.load_model_index import load from modelindex.models.ModelIndex import ModelIndex from pandas import DataFrame, Series -from mim.click import OptionEatAll, get_downstream_package, param2lowercase +from mim.click import ( + OptionEatAll, + argument, + get_downstream_package, + param2lowercase, +) from mim.utils import ( DEFAULT_CACHE_DIR, PKG2PROJECT, @@ -27,7 +32,7 @@ from mim.utils import ( @click.command('search') -@click.argument( +@argument( 'packages', nargs=-1, type=click.STRING, diff --git a/mim/commands/uninstall.py b/mim/commands/uninstall.py index 5649df4..cb99355 100644 --- a/mim/commands/uninstall.py +++ b/mim/commands/uninstall.py @@ -1,12 +1,12 @@ # Copyright (c) OpenMMLab. All rights reserved. import click -from mim.click import get_installed_package, param2lowercase +from mim.click import argument, get_installed_package, param2lowercase from mim.utils import call_command @click.command('uninstall') -@click.argument( +@argument( 'package', autocompletion=get_installed_package, callback=param2lowercase) @click.option( '-y', diff --git a/requirements/install.txt b/requirements/install.txt index f50d783..96a0867 100644 --- a/requirements/install.txt +++ b/requirements/install.txt @@ -1,4 +1,4 @@ -Click==7.1.2 +Click colorama model-index pandas diff --git a/setup.py b/setup.py index 8d082fe..070dfae 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ setup( include_package_data=True, python_requires='>=3.6', install_requires=[ - 'Click==7.1.2', + 'Click', 'colorama', 'requests', 'model-index', diff --git a/tests/test_search.py b/tests/test_search.py index 5354243..c41e2ea 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -103,6 +103,10 @@ def test_search(): result = runner.invoke(search, ['mmcls', '--sort', 'epochs']) assert result.exit_code == 0 + # mim search mmcls --sort batch_size epochs + result = runner.invoke(search, ['mmcls', '--sort', 'batch_size', 'epochs']) + assert result.exit_code == 0 + # mim search mmcls --field epoch result = runner.invoke(search, ['mmcls', '--field', 'epoch']) assert result.exit_code == 0