Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1672.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Prevent uninject from removing dependencies still required by other packages.
37 changes: 30 additions & 7 deletions src/pipx/commands/uninject.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand All @@ -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 = ""

Expand Down
10 changes: 10 additions & 0 deletions tests/test_uninject.py
Original file line number Diff line number Diff line change
Expand Up @@ -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