Coverage for src/lpip/pipxutils.py: 64%

23 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-17 20:20 +0000

1"""Locate and invoke the pipx executable (ADR-0024). No pipx Python library.""" 

2 

3from __future__ import annotations 

4 

5import os 

6import shutil 

7import subprocess 

8from collections.abc import Mapping 

9from pathlib import Path 

10 

11from lpip.errors import PipxNotFoundError 

12 

13 

14def find_pipx_executable(*, environ: Mapping[str, str] | None = None) -> Path | None: 

15 env = environ if environ is not None else os.environ 

16 found = shutil.which("pipx", path=env.get("PATH")) 

17 return Path(found) if found else None 

18 

19 

20def require_pipx(*, environ: Mapping[str, str] | None = None) -> Path: 

21 found = find_pipx_executable(environ=environ) 

22 if found is None: 

23 raise PipxNotFoundError() 

24 return found 

25 

26 

27def pipx_install_argv(pipx: str, requirement: str) -> list[str]: 

28 """Replace an existing pipx app of this name; do not call pipx upgrade.""" 

29 return [pipx, "install", "--force", requirement] 

30 

31 

32def format_pipx_install_command(pipx: str, requirement: str) -> str: 

33 return f'{pipx} install --force "{requirement}"' 

34 

35 

36def run_pipx_install( 

37 pipx: str, 

38 requirement: str, 

39 *, 

40 environ: Mapping[str, str] | None = None, 

41) -> int: 

42 completed = subprocess.run( # nosec B603 

43 pipx_install_argv(pipx, requirement), 

44 env=dict(environ) if environ is not None else None, 

45 ) 

46 return completed.returncode