diff --git a/changelog.d/1672.bugfix.md b/changelog.d/1672.bugfix.md new file mode 100644 index 000000000..2d36d3ad0 --- /dev/null +++ b/changelog.d/1672.bugfix.md @@ -0,0 +1 @@ +Prevent uninject from removing dependencies still required by other packages. diff --git a/src/pipx/commands/uninject.py b/src/pipx/commands/uninject.py index 9711275a2..0bad17c18 100644 --- a/src/pipx/commands/uninject.py +++ b/src/pipx/commands/uninject.py @@ -78,6 +78,8 @@ def uninject_dep( new_resource_paths = get_include_resource_paths(package_name, venv, local_bin_dir, local_man_dir) + deps_of_uninstalled: Set[str] = set() + if not leave_deps: orig_not_required_packages = venv.list_installed_packages(not_required=True) logger.info(f"Original not required packages: {orig_not_required_packages}") @@ -89,15 +91,36 @@ def uninject_dep( logger.info(f"New not required packages: {new_not_required_packages}") deps_of_uninstalled = new_not_required_packages - orig_not_required_packages - if len(deps_of_uninstalled) == 0: - pass - else: - logger.info(f"Dependencies of uninstalled package: {deps_of_uninstalled}") + if deps_of_uninstalled: + logger.info(f"Dependencies of uninstalled package (candidates): {deps_of_uninstalled}") + + protected_deps: Set[str] = set() + + main_pkg_name = canonicalize_name(venv.pipx_metadata.main_package.package) + main_meta = venv.package_metadata.get(main_pkg_name) + if main_meta is not None: + protected_deps.update(getattr(main_meta, "depends", set())) - for dep_package_name in deps_of_uninstalled: - venv.uninstall_package(package=dep_package_name, was_injected=False) + for injected_name in venv.pipx_metadata.injected_packages: + injected_name_canon = canonicalize_name(injected_name) + if injected_name_canon == package_name: + continue + injected_meta = venv.package_metadata.get(injected_name_canon) + if injected_meta is None: + continue + protected_deps.update(getattr(injected_meta, "depends", set())) - deps_string = " and its dependencies" + logger.info(f"Protected dependencies (still required in venv): {protected_deps}") + + deps_to_uninstall = sorted(deps_of_uninstalled - protected_deps) + logger.info(f"Dependencies to uninstall after filtering: {deps_to_uninstall}") + + for dep_package_name in deps_to_uninstall: + venv.uninstall_package(package=dep_package_name, was_injected=False) + + deps_string = " and its dependencies" if deps_to_uninstall else "" + else: + deps_string = "" else: deps_string = "" diff --git a/tests/test_uninject.py b/tests/test_uninject.py index a785a4c2e..09a0b4ad6 100644 --- a/tests/test_uninject.py +++ b/tests/test_uninject.py @@ -39,3 +39,13 @@ def test_uninject_leave_deps(pipx_temp_env, capsys, caplog): captured = capsys.readouterr() assert "Uninjected package black from venv pycowsay" in captured.out assert "Dependencies of uninstalled package:" not in caplog.text + + +def test_uninject_keeps_shared_dependencies(pipx_temp_env, capsys): + assert not run_pipx_cli(["install", "pycowsay"]) + assert not run_pipx_cli(["inject", "pycowsay", PKG["black"]["spec"]]) + assert not run_pipx_cli(["inject", "pycowsay", PKG["pylint"]["spec"]]) + assert not run_pipx_cli(["uninject", "pycowsay", "black"]) + assert not run_pipx_cli(["list", "--include-injected"]) + captured = capsys.readouterr() + assert "pylint" in captured.out