diff --git a/mim/commands/uninstall.py b/mim/commands/uninstall.py index cb99355..34fa2fe 100644 --- a/mim/commands/uninstall.py +++ b/mim/commands/uninstall.py @@ -1,39 +1,83 @@ # Copyright (c) OpenMMLab. All rights reserved. +import sys +from typing import Any, List, Tuple, Union + import click from mim.click import argument, get_installed_package, param2lowercase from mim.utils import call_command -@click.command('uninstall') +@click.command( + 'uninstall', + context_settings=dict(ignore_unknown_options=True), +) @argument( - 'package', autocompletion=get_installed_package, callback=param2lowercase) + 'args', + autocompletion=get_installed_package, + callback=param2lowercase, + nargs=-1, + type=click.UNPROCESSED) @click.option( '-y', '--yes', 'confirm_yes', is_flag=True, help='Don’t ask for confirmation of uninstall deletions.') -def cli(package: str, confirm_yes: bool) -> None: +@click.option( + '-r', + '--requirement', + 'requirements', + multiple=True, + help='Uninstall all the packages listed in the given requirements ' + 'file. This option can be used multiple times.') +def cli(args: Tuple, + confirm_yes: bool = False, + requirements: Tuple = ()) -> None: """Uninstall package. + Same as `pip uninstall`. + + \b Example: > mim uninstall mmcv-full + > mim uninstall -y mmcv-full + > mim uninstall mmdet mmcls + + Here we list several commonly used options. + + For more options, please refer to `pip uninstall --help`. """ - uninstall(package, confirm_yes) + exit_code = uninstall(list(args), confirm_yes, requirements) + exit(exit_code) -def uninstall(package: str, confirm_yes=False) -> None: +def uninstall(uninstall_args: Union[str, List], + confirm_yes: bool = True, + requirements: Tuple = ()) -> Any: """Uninstall package. Args: - package (str): The name of uninstalled package, such as mmcv-full. + uninstall_args (str or list): A package name or a list of package names + to uninstalled. You can also put some `pip uninstal` options here. confirm_yes (bool): Don’t ask for confirmation of uninstall deletions. Default: True. - """ - uninstall_cmd = ['python', '-m', 'pip', 'uninstall', package] - if confirm_yes: - uninstall_cmd.append('-y') + requirements (tuple): A tuple of requirements files to uninstalled. - call_command(uninstall_cmd) + Returns: + The status code returned by `pip uninstall`. + """ + if isinstance(uninstall_args, str): + uninstall_args = [uninstall_args] + + if confirm_yes: + uninstall_args.append('-y') + + for requirement_file in requirements: + uninstall_args += ['-r', requirement_file] + + # Use the pip official recommend way to invoke `pip uninstall`: + # https://pip.pypa.io/en/stable/user_guide/#using-pip-from-your-program + pip_uninstall_cmd = [sys.executable, '-m', 'pip', 'uninstall'] + return call_command(pip_uninstall_cmd + uninstall_args) # type: ignore diff --git a/tests/test_uninstall.py b/tests/test_uninstall.py new file mode 100644 index 0000000..9d385eb --- /dev/null +++ b/tests/test_uninstall.py @@ -0,0 +1,69 @@ +# Copyright (c) OpenMMLab. All rights reserved. +from click.testing import CliRunner + +from mim.commands.install import cli as install +from mim.commands.list import list_package +from mim.commands.uninstall import cli as uninstall + + +def setup_module(): + runner = CliRunner() + result = runner.invoke(uninstall, ['mmcv-full', '--yes']) + assert result.exit_code == 0 + result = runner.invoke(uninstall, ['mmcls', '--yes']) + assert result.exit_code == 0 + result = runner.invoke(uninstall, ['mmsegmentation', '--yes']) + assert result.exit_code == 0 + + +def test_uninstall(): + runner = CliRunner() + + # mim install mmsegmentation --yes + result = runner.invoke(install, ['mmsegmentation', '--yes']) + # Use importlib reload module in the same process may cause `isinstance` + # invalidation. + # A known issue: `METADATA not found in /tmp/xxx/xxx.whel` will be warning + # in pip 21.3.1, and mmcv-full could not install success as expected. + # So here we install mmsegmentation twice as an ugly workaround. + # TODO: find a better way to deal with this issues. + result = runner.invoke(install, ['mmsegmentation', '--yes']) + assert result.exit_code == 0 + + # check if install success + result = list_package() + installed_packages = [item[0] for item in result] + assert 'mmsegmentation' in installed_packages + assert 'mmcv-full' in installed_packages + # `mim install mmsegmentation` will install mim extra requirements (via + # mminstall.txt) automatically since PR#132, so we got installed mmcls here. # noqa: E501 + assert 'mmcls' in installed_packages + + # mim uninstall mmsegmentation --yes + result = runner.invoke(uninstall, ['mmsegmentation', '--yes']) + assert result.exit_code == 0 + + # check if uninstall success + result = list_package() + installed_packages = [item[0] for item in result] + assert 'mmsegmentation' not in installed_packages + + # mim uninstall mmcls mmcv-full --yes + result = runner.invoke(uninstall, ['mmcls', 'mmcv-full', '--yes']) + assert result.exit_code == 0 + + # check if uninstall success + result = list_package() + installed_packages = [item[0] for item in result] + assert 'mmcls' not in installed_packages + assert 'mmcv-full' not in installed_packages + + +def teardown_module(): + runner = CliRunner() + result = runner.invoke(uninstall, ['mmcv-full', '--yes']) + assert result.exit_code == 0 + result = runner.invoke(uninstall, ['mmcls', '--yes']) + assert result.exit_code == 0 + result = runner.invoke(uninstall, ['mmsegmentation', '--yes']) + assert result.exit_code == 0