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 src/codealmanac/services/updates/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

class UpdateInstallMethod(StrEnum):
UV_TOOL = "uv-tool"
PIPX = "pipx"
PIP = "pip"
EDITABLE = "editable"
UNKNOWN = "unknown"
Expand Down
13 changes: 13 additions & 0 deletions src/codealmanac/services/updates/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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
34 changes: 34 additions & 0 deletions tests/test_update_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down