1 """Helpers to install PyPi packages."""
3 from __future__
import annotations
6 from functools
import cache
7 from importlib.metadata
import PackageNotFoundError, version
10 from pathlib
import Path
12 from subprocess
import PIPE, Popen
14 from urllib.parse
import urlparse
16 from packaging.requirements
import InvalidRequirement, Requirement
18 _LOGGER = logging.getLogger(__name__)
22 """Return if we run in a virtual environment."""
24 return getattr(sys,
"base_prefix", sys.prefix) != sys.prefix
or hasattr(
31 """Return True if we run in a docker env."""
32 return Path(
"/.dockerenv").exists()
36 """Return a set of installed packages and versions."""
37 return {specifier
for specifier
in specifiers
if is_installed(specifier)}
41 """Check if a package is installed and will be loaded when we import it.
43 expected input is a pip compatible package specifier (requirement string)
44 e.g. "package==1.0.0" or "package>=1.0.0,<2.0.0"
46 For backward compatibility, it also accepts a URL with a fragment
47 e.g. "git+https://github.com/pypa/pip#pip>=1"
49 Returns True when the requirement is met.
50 Returns False when the package is not installed or doesn't meet req.
53 req = Requirement(requirement_str)
54 except InvalidRequirement:
55 if "#" not in requirement_str:
56 _LOGGER.error(
"Invalid requirement '%s'", requirement_str)
68 req = Requirement(urlparse(requirement_str).fragment)
69 except InvalidRequirement:
70 _LOGGER.error(
"Invalid requirement '%s'", requirement_str)
74 if (installed_version :=
version(req.name))
is None:
79 "Installed version for %s resolved to None", req.name
82 return req.specifier.contains(installed_version, prereleases=
True)
83 except PackageNotFoundError:
87 _UV_ENV_PYTHON_VARS = (
96 target: str |
None =
None,
97 constraints: str |
None =
None,
98 timeout: int |
None =
None,
100 """Install a package on PyPi. Accepts pip compatible package strings.
102 Return boolean if install successful.
104 _LOGGER.info(
"Attempting install of %s", package)
105 env = os.environ.copy()
118 "unsafe-first-match",
121 env[
"HTTP_TIMEOUT"] =
str(timeout)
123 args.append(
"--upgrade")
124 if constraints
is not None:
125 args += [
"--constraint", constraints]
127 abs_target = os.path.abspath(target)
128 args += [
"--target", abs_target]
131 and not (any(var
in env
for var
in _UV_ENV_PYTHON_VARS))
132 and (abs_target := site.getusersitepackages())
139 args += [
"--python", sys.executable,
"--target", abs_target]
141 _LOGGER.debug(
"Running uv pip command: args=%s", args)
150 _, stderr = process.communicate()
151 if process.returncode != 0:
153 "Unable to install package %s: %s",
155 stderr.decode(
"utf-8").lstrip().strip(),
163 """Return user local library path.
165 This function is a coroutine.
167 env = os.environ.copy()
168 env[
"PYTHONUSERBASE"] = os.path.abspath(deps_dir)
169 args = [sys.executable,
"-m",
"site",
"--user-site"]
170 process = await asyncio.create_subprocess_exec(
172 stdin=asyncio.subprocess.PIPE,
173 stdout=asyncio.subprocess.PIPE,
174 stderr=asyncio.subprocess.DEVNULL,
178 stdout, _ = await process.communicate()
179 return stdout.decode().strip()
str async_get_user_site(str deps_dir)
set[str] get_installed_versions(set[str] specifiers)
bool is_installed(str requirement_str)
bool install_package(str package, bool upgrade=True, str|None target=None, str|None constraints=None, int|None timeout=None)