Skip to content

Commit

Permalink
Add pipx operations & facts
Browse files Browse the repository at this point in the history
  • Loading branch information
maisim authored Jan 3, 2025
1 parent f52ac49 commit b81423f
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 0 deletions.
74 changes: 74 additions & 0 deletions pyinfra/facts/pipx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import re

from pyinfra.api import FactBase

from .util.packaging import parse_packages


# TODO: move to an utils file
def parse_environment(output):
environment_REGEX = r"^(?P<key>[A-Z_]+)=(?P<value>.*)$"
environment_variables = {}

for line in output:
matches = re.match(environment_REGEX, line)

if matches:
environment_variables[matches.group("key")] = matches.group("value")

return environment_variables


PIPX_REGEX = r"^([a-zA-Z0-9_\-\+\.]+)\s+([0-9\.]+[a-z0-9\-]*)$"


class PipxPackages(FactBase):
"""
Returns a dict of installed pipx packages:
.. code:: python
{
"package_name": ["version"],
}
"""

default = dict

def requires_command(self) -> str:
return "pipx"

def command(self) -> str:
return "pipx list --short"

def process(self, output):
return parse_packages(PIPX_REGEX, output)


class PipxEnvironment(FactBase):
"""
Returns a dict of pipx environment variables:
.. code:: python
{
"PIPX_HOME": "/home/doodba/.local/pipx",
"PIPX_BIN_DIR": "/home/doodba/.local/bin",
"PIPX_SHARED_LIBS": "/home/doodba/.local/pipx/shared",
"PIPX_LOCAL_VENVS": "/home/doodba/.local/pipx/venvs",
"PIPX_LOG_DIR": "/home/doodba/.local/pipx/logs",
"PIPX_TRASH_DIR": "/home/doodba/.local/pipx/.trash",
"PIPX_VENV_CACHEDIR": "/home/doodba/.local/pipx/.cache",
}
"""

default = dict

def requires_command(self) -> str:
return "pipx"

def command(self) -> str:
return "pipx environment"

def process(self, output):
return parse_environment(output)
90 changes: 90 additions & 0 deletions pyinfra/operations/pipx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""
Manage pipx (python) applications.
"""

from pyinfra import host
from pyinfra.api import operation
from pyinfra.facts.pipx import PipxEnvironment, PipxPackages
from pyinfra.facts.server import Path

from .util.packaging import ensure_packages


@operation()
def packages(
packages=None,
present=True,
latest=False,
extra_args=None,
):
"""
Install/remove/update pipx packages.
+ packages: list of packages to ensure
+ present: whether the packages should be installed
+ latest: whether to upgrade packages without a specified version
+ extra_args: additional arguments to the pipx command
Versions:
Package versions can be pinned like pip: ``<pkg>==<version>``.
**Example:**
.. code:: python
pipx.packages(
name="Install ",
packages=["pyinfra"],
)
"""

prep_install_command = ["pipx", "install"]

if extra_args:
prep_install_command.append(extra_args)
install_command = " ".join(prep_install_command)

uninstall_command = "pipx uninstall"
upgrade_command = "pipx upgrade"

current_packages = host.get_fact(PipxPackages)

# pipx support only one package name at a time
for package in packages:
yield from ensure_packages(
host,
[package],
current_packages,
present,
install_command=install_command,
uninstall_command=uninstall_command,
upgrade_command=upgrade_command,
version_join="==",
latest=latest,
)


@operation()
def upgrade_all():
"""
Upgrade all pipx packages.
"""
yield "pipx upgrade-all"


@operation()
def ensure_path():
"""
Ensure pipx bin dir is in the PATH.
"""

# Fetch the current user's PATH
path = host.get_fact(Path)
# Fetch the pipx environment variables
pipx_env = host.get_fact(PipxEnvironment)

# If the pipx bin dir is already in the user's PATH, we're done
if "PIPX_BIN_DIR" in pipx_env and pipx_env["PIPX_BIN_DIR"] in path.split(":"):
host.noop("pipx bin dir is already in the PATH")
else:
yield "pipx ensurepath"
22 changes: 22 additions & 0 deletions tests/facts/pipx.PipxEnvironment/environment.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"command": "pipx environment",
"requires_command": "pipx",
"output": [
"PIPX_HOME=/home/doodba/.local/pipx",
"PIPX_BIN_DIR=/home/doodba/.local/bin",
"PIPX_SHARED_LIBS=/home/doodba/.local/pipx/shared",
"PIPX_LOCAL_VENVS=/home/doodba/.local/pipx/venvs",
"PIPX_LOG_DIR=/home/doodba/.local/pipx/logs",
"PIPX_TRASH_DIR=/home/doodba/.local/pipx/.trash",
"PIPX_VENV_CACHEDIR=/home/doodba/.local/pipx/.cache"
],
"fact": {
"PIPX_HOME": "/home/doodba/.local/pipx",
"PIPX_BIN_DIR": "/home/doodba/.local/bin",
"PIPX_SHARED_LIBS": "/home/doodba/.local/pipx/shared",
"PIPX_LOCAL_VENVS": "/home/doodba/.local/pipx/venvs",
"PIPX_LOG_DIR": "/home/doodba/.local/pipx/logs",
"PIPX_TRASH_DIR": "/home/doodba/.local/pipx/.trash",
"PIPX_VENV_CACHEDIR": "/home/doodba/.local/pipx/.cache"
}
}
12 changes: 12 additions & 0 deletions tests/facts/pipx.PipxPackages/packages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"command": "pipx list --short",
"requires_command": "pipx",
"output": [
"copier 9.0.1",
"invoke 2.2.0"
],
"fact": {
"copier": ["9.0.1"],
"invoke": ["2.2.0"]
}
}
19 changes: 19 additions & 0 deletions tests/operations/pipx.ensure_path/ensure_path.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"args": [],
"facts": {
"pipx.PipxEnvironment": {
"PIPX_HOME": "/home/bob/.local/pipx",
"PIPX_BIN_DIR": "/home/bob/.local/bin",
"PIPX_SHARED_LIBS": "/home/bob/.local/pipx/shared",
"PIPX_LOCAL_VENVS": "/home/bob/.local/pipx/venvs",
"PIPX_LOG_DIR": "/home/bob/.local/pipx/logs",
"PIPX_TRASH_DIR": "/home/bob/.local/pipx/.trash",
"PIPX_VENV_CACHEDIR": "/home/bob/.local/pipx/.cache"
},
"server.Path": "/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games"

},
"commands": [
"pipx ensurepath"
]
}
10 changes: 10 additions & 0 deletions tests/operations/pipx.packages/add_packages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"args": [["copier==0.9.1", "invoke", "ensurepath"]],
"facts": {
"pipx.PipxPackages": {"ensurepath": ["0.1.1"]}
},
"commands": [
"pipx install copier==0.9.1",
"pipx install invoke"
]
}
16 changes: 16 additions & 0 deletions tests/operations/pipx.packages/install_extra_args.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"args": [["pyinfra==1.2", "pytask", "test==1.1"]],
"kwargs": {
"extra_args": "--index-url https://pypi.org/"
},
"facts": {
"pipx.PipxPackages": {
"pyinfra": ["1.0"],
"test": ["1.1"]
}
},
"commands": [
"pipx install --index-url https://pypi.org/ pyinfra==1.2",
"pipx install --index-url https://pypi.org/ pytask"
]
}
16 changes: 16 additions & 0 deletions tests/operations/pipx.packages/remove_packages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"args": [["invoke", "copier"]],
"kwargs": {
"present": false
},
"facts": {
"pipx.PipxPackages": {
"copier": ["9.0.1"],
"invoke": ["2.2.0"]
}
},
"commands": [
"pipx uninstall invoke",
"pipx uninstall copier"
]
}
9 changes: 9 additions & 0 deletions tests/operations/pipx.upgrade_all/upgrade_all.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"args": [],
"facts": {},
"commands": [
"pipx upgrade-all"
],
"idempotent": false,
"disable_idempotent_warning_reason": "package upgrades are always executed"
}

0 comments on commit b81423f

Please sign in to comment.