Skip to content

Commit

Permalink
feat: implemented reinstall and list methods, save .metadata file in …
Browse files Browse the repository at this point in the history
…app-specific venv
  • Loading branch information
robinvandernoord committed Mar 4, 2024
1 parent 609e990 commit 8a44dab
Show file tree
Hide file tree
Showing 7 changed files with 322 additions and 43 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies = [
"typer",
"plumbum",
"threadful>=0.2",
"quickle",
]

[project.optional-dependencies]
Expand Down
6 changes: 6 additions & 0 deletions src/uvx/_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pathlib import Path

BIN_DIR = Path.home() / ".local/bin"
WORK_DIR = (
Path.home() / ".local/uvx"
) # use 'ensure_local_folder()' instead, whenever possible!
41 changes: 41 additions & 0 deletions src/uvx/_python.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import textwrap
from pathlib import Path

import plumbum
from plumbum.cmd import grep, uv


def _run_python_in_venv(*args: str, venv: Path) -> str:
python = venv / "bin" / "python"

return plumbum.local[python](*args)


def run_python_code_in_venv(code: str, venv: Path) -> str:
"""
Run Python code in a virtual environment.
Args:
code (str): The Python code to run.
venv (Path): The path of the virtual environment.
Returns:
str: The output of the Python code.
"""
code = textwrap.dedent(code)
return _run_python_in_venv("-c", code, venv=venv)


def get_python_version(venv: Path):
return _run_python_in_venv("--version", venv=venv).strip()


def get_python_executable(venv: Path):
executable = venv / "bin" / "python"
return str(executable.resolve()) # /usr/bin/python3.xx


def get_package_version(package: str) -> str:
# assumes `with virtualenv(venv)` block executing this function
# uv pip freeze | grep ^su6==
return (uv["pip", "freeze"] | grep[f"^{package}=="])().strip().split("==")[-1]
14 changes: 14 additions & 0 deletions src/uvx/_symlinks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import typing

from ._constants import BIN_DIR, WORK_DIR


def check_symlink(symlink: str, venv: str) -> bool:
symlink_path = BIN_DIR / symlink
target_path = WORK_DIR / "venvs" / venv

return symlink_path.is_symlink() and target_path in symlink_path.resolve().parents


def check_symlinks(symlinks: typing.Iterable[str], venv: str) -> dict[str, bool]:
return {k: check_symlink(k, venv) for k in symlinks}
84 changes: 80 additions & 4 deletions src/uvx/cli.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
"""This file builds the Typer cli."""

from typing import Optional

import rich
import typer

from .core import create_venv, install_package, uninstall_package
from .core import (
format_bools,
install_package,
list_packages,
reinstall_package,
uninstall_package,
)
from .metadata import Metadata, read_metadata

app = typer.Typer()


@app.command()
def install(package_name: str, force: bool = False):
def install(package_name: str, force: bool = False, python: str = None):
"""Install a package (by pip name)."""
venv = create_venv(package_name)
install_package(package_name, venv, force=force)
install_package(package_name, python=python, force=force)


@app.command(name="remove")
Expand All @@ -21,12 +30,79 @@ def uninstall(package_name: str, force: bool = False):
uninstall_package(package_name, force=force)


@app.command()
def reinstall(package: str, python: Optional[str] = None, force: bool = False):
reinstall_package(package, python=python, force=force)


# list
def list_short(name: str, metadata: Optional[Metadata]):
rich.print("-", name, metadata.installed_version if metadata else "[red]?[/red]")


TAB = " " * 3


def list_normal(name: str, metadata: Optional[Metadata], verbose: bool = False):

if not metadata:
print("-", name)
rich.print(TAB, "[red]Missing metadata [/red]")
return
else:
extras = list(metadata.extras)
name_with_extras = name if not extras else f"{name}{extras}"
print("-", name_with_extras)

metadata.check_script_symlinks(name)

if verbose:
rich.print(TAB, metadata)
else:
rich.print(
TAB,
f"Installed Version: {metadata.installed_version} on {metadata.python}.",
)
rich.print(TAB, "Scripts:", format_bools(metadata.scripts))


def list_venvs_json():
from json import dumps

print(
dumps(
{
name: metadata.check_script_symlinks(name).to_dict() if metadata else {}
for name, metadata in list_packages()
}
)
)


@app.command(name="list")
def list_venvs(short: bool = False, verbose: bool = False, json: bool = False):
"""
List packages and apps installed with uvx.
"""

if json:
return list_venvs_json()

for name, metadata in list_packages():
if short:
list_short(name, metadata)
else:
list_normal(name, metadata, verbose=verbose)


# run

# upgrade

# self-upgrade (uv and uvx)

# inject

# version or --version (incl. 'uv' version and Python version)

# ...
Loading

0 comments on commit 8a44dab

Please sign in to comment.