From bc16251ac58221131f1192ea78fc95f80650421f Mon Sep 17 00:00:00 2001 From: Robin van der Noord Date: Wed, 10 Apr 2024 16:08:32 +0200 Subject: [PATCH] fix: make linters etc happy --- src/uvx/_maybe.py | 8 +++++++ src/uvx/cli.py | 54 ++++++++++++++++++++++++++++++++++++--------- src/uvx/core.py | 33 ++++++++++++++++----------- src/uvx/metadata.py | 2 ++ 4 files changed, 74 insertions(+), 23 deletions(-) diff --git a/src/uvx/_maybe.py b/src/uvx/_maybe.py index 1654f98..a49a51e 100644 --- a/src/uvx/_maybe.py +++ b/src/uvx/_maybe.py @@ -6,7 +6,15 @@ class Empty(Err[None]): + """ + Alias for Err[None], used by Maybe. + + Usage: + Result[str, None] = Maybe[str] + """ + def __init__(self) -> None: + """Set up the Err result with None as it's value.""" super().__init__(None) diff --git a/src/uvx/cli.py b/src/uvx/cli.py index 2af346d..c04efc4 100644 --- a/src/uvx/cli.py +++ b/src/uvx/cli.py @@ -5,7 +5,7 @@ import sys from datetime import datetime from pathlib import Path -from typing import Optional +from typing import Annotated import rich import typer @@ -34,29 +34,60 @@ def output(result: Result[str, Exception]) -> None: + """Output positive (ok) result to stdout and error result to stderr.""" match result: case Ok(msg): - rich.print(msg) # :trash: + rich.print(msg) case Err(err): rich.print(err, file=sys.stderr) +OPTION_PYTHON_HELP_TEXT = "Python version or executable to use, e.g. `3.12`, `python3.12`, `/usr/bin/python3.12`" + + @app.command() -def install(package_name: str, force: bool = False, python: str = "", no_cache: bool = False): +def install( + package_name: str, + force: Annotated[ + bool, + typer.Option( + "-f", "--force", help="Overwrite currently installed executables with the same name (in ~/.local/bin)" + ), + ] = False, + python: Annotated[str, typer.Option(help=OPTION_PYTHON_HELP_TEXT)] = "", + no_cache: Annotated[bool, typer.Option("--no-cache", help="Run without `uv` cache")] = False, +): """Install a package (by pip name).""" - # todo: support 'install .' output(install_package(package_name, python=python, force=force, no_cache=no_cache)) @app.command(name="upgrade") @app.command(name="update") -def upgrade(package_name: str, force: bool = False, skip_injected: bool = False, no_cache: bool = False): +def upgrade( + package_name: str, + force: Annotated[bool, typer.Option("-f", "--force", help="Ignore previous version constraint")] = False, + skip_injected: Annotated[ + bool, typer.Option("--skip-injected", help="Don't also upgrade injected packages") + ] = False, + no_cache: Annotated[bool, typer.Option("--no-cache", help="Run without `uv` cache")] = False, +): + """Upgrade a package.""" output(upgrade_package(package_name, force=force, skip_injected=skip_injected, no_cache=no_cache)) @app.command(name="remove") @app.command(name="uninstall") -def uninstall(package_name: str, force: bool = False): +def uninstall( + package_name: str, + force: Annotated[ + bool, + typer.Option( + "-f", + "--force", + help="Remove executable with the same name (in ~/.local/bin) even if related venv was not found", + ), + ] = False, +): """Uninstall a package (by pip name).""" output(uninstall_package(package_name, force=force).map(lambda version: f"🗑️ {package_name}{version} removed!")) @@ -64,10 +95,12 @@ def uninstall(package_name: str, force: bool = False): @app.command() def reinstall( package: str, - python: Optional[str] = None, - force: bool = False, - without_injected: bool = False, - no_cache: bool = False, + python: Annotated[str, typer.Option(help=OPTION_PYTHON_HELP_TEXT)] = "", + force: Annotated[bool, typer.Option("-f", "--force", help="See `install --force`")] = False, + without_injected: Annotated[ + bool, typer.Option("--without-injected", help="Don't include previously injected libraries in reinstall") + ] = False, + no_cache: Annotated[bool, typer.Option("--no-cache", help="Run without `uv` cache")] = False, ): """Uninstall a package (by pip name) and re-install from the original spec (unless a new spec is supplied).""" output( @@ -83,6 +116,7 @@ def reinstall( @app.command() def inject(into: str, package_specs: list[str]): + """Install additional packages to a virtual environment managed by uvx.""" output( inject_packages( into, diff --git a/src/uvx/core.py b/src/uvx/core.py index 691bfbb..0bc861b 100644 --- a/src/uvx/core.py +++ b/src/uvx/core.py @@ -111,7 +111,7 @@ def install_package( venv: Optional[Path] = None, python: Optional[str] = None, force: bool = False, - extras: Optional[list[str]] = None, + extras: Optional[typing.Iterable[str]] = None, no_cache: bool = False, ) -> Result[str, Exception]: """ @@ -138,23 +138,23 @@ def install_package( # todo: make --force use --no-cache with virtualenv(venv), exit_on_pb_error(): - try: - args = [] - text = f"installing {meta.name}" - if extras: - args.extend(extras) - text += f" with {extras}" + args: list[str] = [] + text = f"installing {meta.name}" + if extras: + args.extend(extras) + text += f" with {extras}" - if no_cache: - args += ["--no-cache"] + if no_cache: + args += ["--no-cache"] + try: animate(uv("pip", "install", meta.install_spec, *args), text=text) - # must still be in the venv for these: + # must still be in the venv and try for these: meta.installed_version = get_package_version(meta.name, venv) meta.python = get_python_version(venv) meta.python_raw = get_python_executable(venv) - meta.injected = extras + meta.injected = set(extras) except plumbum.ProcessExecutionError as e: remove_dir(venv) @@ -235,6 +235,7 @@ def reinstall_package( def inject_packages(into: str, package_specs: set[str]) -> Result[str, Exception]: + """Install extra libraries into a package-specific venv.""" match collect_metadata(into): case Err(e): return Err(e) @@ -277,6 +278,7 @@ def remove_dir(path: Path): def upgrade_package( package_name: str, force: bool = False, skip_injected: bool = False, no_cache: bool = False ) -> Result[str, Exception]: + """Upgrade a package in its venv.""" # run `uv pip install --upgrade package` with requested install spec (version, extras, injected) # if --force is used, the previous version is ignored. match collect_metadata(package_name): @@ -302,8 +304,13 @@ def upgrade_package( # if --force, drop version spec base_pkg = meta.name extras = meta.extras - injected = [] if skip_injected else (meta.injected or []) - version = spec_metadata.requested_version or ("" if force else meta.requested_version) + + injected: set[str] = (not skip_injected and meta.injected) or set() + # injected = set() if skip_injected else (meta.injected or set()) + + version: str = spec_metadata.requested_version or (not force and meta.requested_version) or "" + # version = spec_metadata.requested_version or ("" if force else meta.requested_version) + options = [] if force: options.append("--no-cache") diff --git a/src/uvx/metadata.py b/src/uvx/metadata.py index 3049770..610945f 100644 --- a/src/uvx/metadata.py +++ b/src/uvx/metadata.py @@ -64,6 +64,7 @@ def check_script_symlinks(self, name: str) -> "Self": def fake_install(spec: str) -> dict: + """Dry run pip to extract metadata of a local package.""" _uv("pip", "install", "pip") # ensure we have pip with tempfile.NamedTemporaryFile() as f: @@ -74,6 +75,7 @@ def fake_install(spec: str) -> dict: @threadful.thread def resolve_local(spec: str) -> tuple[Maybe[str], Maybe[str]]: + """Resolve the package name of a local package by dry run installing it.""" try: full_data = fake_install(spec) install_data = full_data["install"][0]