diff --git a/src/codealmanac/services/updates/models.py b/src/codealmanac/services/updates/models.py index ab6f68ec..aef3c336 100644 --- a/src/codealmanac/services/updates/models.py +++ b/src/codealmanac/services/updates/models.py @@ -12,6 +12,7 @@ class UpdateInstallMethod(StrEnum): UV_TOOL = "uv-tool" + PIPX = "pipx" PIP = "pip" EDITABLE = "editable" UNKNOWN = "unknown" diff --git a/src/codealmanac/services/updates/service.py b/src/codealmanac/services/updates/service.py index 4cb714b4..b41c7c94 100644 --- a/src/codealmanac/services/updates/service.py +++ b/src/codealmanac/services/updates/service.py @@ -134,6 +134,9 @@ def plan_update(metadata: PackageInstallMetadata) -> UpdatePlan: if method == UpdateInstallMethod.UV_TOOL: command = ("uv", "tool", "upgrade", PACKAGE_NAME) return runnable_plan(metadata, method, command) + if method == UpdateInstallMethod.PIPX: + command = ("pipx", "upgrade", PACKAGE_NAME) + return runnable_plan(metadata, method, command) if method == UpdateInstallMethod.PIP: command = ( str(metadata.python_executable), @@ -179,9 +182,19 @@ def runnable_plan( def update_method(metadata: PackageInstallMetadata) -> UpdateInstallMethod: if metadata.editable: return UpdateInstallMethod.EDITABLE + if is_pipx_install(metadata): + return UpdateInstallMethod.PIPX installer = (metadata.installer or "").strip().casefold() if installer == "uv": return UpdateInstallMethod.UV_TOOL if installer == "pip": return UpdateInstallMethod.PIP return UpdateInstallMethod.UNKNOWN + + +def is_pipx_install(metadata: PackageInstallMetadata) -> bool: + installer = (metadata.installer or "").strip().casefold() + if installer == "pipx": + return True + parts = tuple(part.casefold() for part in metadata.python_executable.parts) + return "pipx" in parts and "venvs" in parts diff --git a/tests/test_update_service.py b/tests/test_update_service.py index 10e4b134..81df5748 100644 --- a/tests/test_update_service.py +++ b/tests/test_update_service.py @@ -84,6 +84,40 @@ def test_update_service_plans_pip_upgrade_with_current_python(): ) +def test_update_service_plans_pipx_upgrade_for_pipx_metadata(): + service = update_service( + PackageInstallMetadata( + version="0.1.0", + installer="pipx", + python_executable=Path("/Users/me/.local/pipx/venvs/codealmanac/bin/python"), + ), + ) + + plan = service.check(CheckUpdateRequest()) + + assert plan.status == UpdateStatus.READY + assert plan.method == UpdateInstallMethod.PIPX + assert plan.command == ("pipx", "upgrade", "codealmanac") + + +def test_update_service_detects_pipx_venv_when_installer_reports_pip(): + service = update_service( + PackageInstallMetadata( + version="0.1.0", + installer="pip", + python_executable=Path( + "/Users/me/.local/pipx/venvs/codealmanac/bin/python" + ), + ), + ) + + plan = service.check(CheckUpdateRequest()) + + assert plan.status == UpdateStatus.READY + assert plan.method == UpdateInstallMethod.PIPX + assert plan.command == ("pipx", "upgrade", "codealmanac") + + def test_update_service_refuses_editable_install_without_running_command(): runner = FakeCommandRunner(PackageCommandResult(exit_code=0)) service = update_service_with_runner(