Skip to content

Commit

Permalink
✨ command line interface
Browse files Browse the repository at this point in the history
  • Loading branch information
juftin committed Jan 18, 2024
1 parent 969c7ca commit 13c7604
Show file tree
Hide file tree
Showing 19 changed files with 414 additions and 30 deletions.
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ repos:
args: [--print-width=88, --tab-width=4]
exclude: |
(?x)(
.github/semantic_release/release_notes.hbs
.github/semantic_release/release_notes.hbs |
docs/cli.md
)
additional_dependencies:
- prettier
Expand Down
60 changes: 59 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,11 +355,18 @@ hatch env run --env default -- python --version
```

If you're a user of the `--upgrade` / `--upgrade-package` options on `pip-compile`,
these features can be enabled on this plugin using the environment variables
these features can be enabled on this plugin by using the environment variables
`PIP_COMPILE_UPGRADE` and `PIP_COMPILE_UPGRADE_PACKAGE`. When either of these
environment variables are set `hatch` will force the lockfile to be regenerated
whenever the environment is activated.

> NOTE: **command line interface**
>
> `hatch-pip-compile` also makes a CLI available to handle the
> the `PIP_COMPILE_UPGRADE` / `PIP_COMPILE_UPGRADE_PACKAGE` workflow
> automatically. See the [hatch-pip-compile CLI](#using-the-hatch-pip-compile-cli)
> section for more information.
To run with `upgrade` functionality on the `default` environment:

```shell
Expand All @@ -375,6 +382,57 @@ PIP_COMPILE_UPGRADE_PACKAGE="mkdocs,mkdocs-material" hatch env run --env docs --
The above commands call `python --version` on a particular environment,
but the same behavior applies to any script that activates the environment.

## Using the `hatch-pip-compile` CLI

For convenience this package also makes a CLI available to handle the setting /
unsetting of the `PIP_COMPILE_UPGRADE` / `PIP_COMPILE_UPGRADE_PACKAGE` environment variables
and invoking the `hatch env run` command for you automatically. To use the CLI you'll need to
install it outside your `pyproject.toml` / `hatch.toml` file.

I recommend using [pipx] to
install the CLI, but you can also install it directly with [pip]:

```shell
pipx install hatch-pip-compile
```

Once installed, you can run the CLI with the `hatch-pip-compile` command.

### Examples

#### Upgrade the `default` environment

The below command will upgrade all packages in the `default` environment.

```shell
hatch-pip-compile --upgrade
```

#### Upgrade a non-default environment

The below command will upgrade all packages in the `docs` environment.

```shell
hatch-pip-compile docs --upgrade
```

#### Upgrade a specific package

The below command will upgrade the `requests` package in the `default`
environment.

```shell
hatch-pip-compile --upgrade-package requests
```

#### Upgrade all `pip-compile` environments

The below command will upgrade all packages in all `pip-compile` environments.

```shell
hatch-pip-compile --upgrade --all
```

## Notes

### Dev Dependencies
Expand Down
63 changes: 63 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Command Line Interface

It's recommend to use [pipx] to install the CLI, but
you can also install it with [pip]:

```shell
pipx install hatch-pip-compile
```

::: mkdocs-click
:module: hatch_pip_compile.cli
:command: cli
:prog_name: hatch-pip-compile
:style: table
:list_subcommands: True

## How it works

The `hatch-pip-compile` CLI is a wrapper around `hatch` that simply
sets the `PIP_COMPILE_UPGRADE` / `PIP_COMPILE_UPGRADE_PACKAGE` environment
variables before running a `hatch` command in a given environment.

These environment variables are used by the `hatch-pip-compile` plugin
to run the `pip-compile` command with the `--upgrade` / `--upgrade-package`
flags.

## Examples

### Upgrade the `default` environment

The below command will upgrade all packages in the `default` environment.

```shell
hatch-pip-compile --upgrade
```

### Upgrade a non-default environment

The below command will upgrade all packages in the `docs` environment.

```shell
hatch-pip-compile docs --upgrade
```

### Upgrade a specific package

The below command will upgrade the `requests` package in the `default`
environment.

```shell
hatch-pip-compile --upgrade-package requests
```

### Upgrade all `pip-compile` environments

The below command will upgrade all packages in all `pip-compile` environments.

```shell
hatch-pip-compile --upgrade --all
```

[pipx]: https://github.com/pypa/pipx
[pip]: https://pip.pypa.io/en/stable/
8 changes: 8 additions & 0 deletions hatch_pip_compile/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""
hatch_pip_compile module hook
"""

from hatch_pip_compile.cli import cli

if __name__ == "__main__":
cli()
192 changes: 192 additions & 0 deletions hatch_pip_compile/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
"""
hatch-pip-compile CLI
"""

from __future__ import annotations

import dataclasses
import json
import os
import subprocess
from typing import Any, Self, Sequence

import click
import rich.traceback


@dataclasses.dataclass
class _HatchCommandRunner:
"""
Hatch Command Runner
"""

environments: Sequence[str] = dataclasses.field(default_factory=list)
upgrade: bool = False
upgrade_all: bool = False
upgrade_packages: Sequence[str] = dataclasses.field(default_factory=list)

former_env_vars: dict[str, Any] = dataclasses.field(init=False, default_factory=dict)
console: rich.console.Console = dataclasses.field(init=False)
supported_environments: set[str] = dataclasses.field(init=False)

def __post_init__(self):
"""
Initialize the internal state
"""
self.console = rich.console.Console()
rich.traceback.install(show_locals=True, console=self.console)
self.supported_environments = self._get_supported_environments()
if all(
[not self.environments, "default" in self.supported_environments, not self.upgrade_all]
):
self.environments = ["default"]
elif not self.environments and not self.upgrade_all:
msg = "Either `--all` or an environment name must be specified"
raise click.BadParameter(msg)
elif self.upgrade_all:
self.environments = list(self.supported_environments)
elif isinstance(self.environments, str):
self.environments = [self.environments]
unsupported_environments = set(self.environments).difference(self.supported_environments)
if unsupported_environments:
msg = (
f"The following environments are not supported or unknown: "
f"{', '.join(unsupported_environments)}. "
f"Supported environments are: {', '.join(sorted(self.supported_environments))}"
)
raise click.BadParameter(msg)

def __enter__(self) -> Self:
"""
Set the environment variables
"""
env_vars = {"__PIP_COMPILE_FORCE__": "1"}
if self.upgrade:
env_vars["PIP_COMPILE_UPGRADE"] = "1"
self.console.print(
"[bold green]hatch-pip-compile[/bold green]: Upgrading all dependencies"
)
elif self.upgrade_packages:
env_vars["PIP_COMPILE_UPGRADE_PACKAGES"] = ",".join(self.upgrade_packages)
message = (
"[bold green]hatch-pip-compile[/bold green]: "
f"Upgrading packages: {', '.join(self.upgrade_packages)}"
)
self.console.print(message)
self.former_env_vars = {
key: os.environ.get(key) for key in env_vars.keys() if os.environ.get(key) is not None
}
os.environ.update(env_vars)
return self

def __exit__(self, *args, **kwargs):
"""
Restore the environment variables
"""
os.environ.update(self.former_env_vars)

def hatch_cli(self):
"""
Run the `hatch` CLI
"""
self.console.print(
"[bold green]hatch-pip-compile[/bold green]: Targeting environments: "
f"{', '.join(sorted(self.environments))}"
)
for environment in sorted(self.environments):
environment_command = [
"hatch",
"env",
"run",
"--env",
environment,
"--",
"python",
"--version",
]
self.console.print(
f"[bold green]hatch-pip-compile[/bold green]: Running "
f"`[bold blue]{' '.join(environment_command)}`[/bold blue]"
)
result = subprocess.run(
args=environment_command,
capture_output=True,
check=False,
)
if result.returncode != 0:
self.console.print(
"[bold yellow]hatch command[/bold yellow]: "
f"[bold blue]`{' '.join(environment_command)}`[/bold blue]"
)
self.console.print(result.stdout.decode("utf-8"))
self.console.print(
"[bold red]hatch-pip-compile[/bold red]: Error running hatch command"
)
raise click.exceptions.Exit(1)

@classmethod
def _get_supported_environments(cls) -> set[str]:
"""
Get the names of the environments from `hatch env show --json`
Returns
-------
List[str]
The name of the environments
"""
result = subprocess.run(
args=["hatch", "env", "show", "--json"],
capture_output=True,
check=True,
)
environment_dict: dict[str, Any] = json.loads(result.stdout)
return {
key for key, value in environment_dict.items() if value.get("type") == "pip-compile"
}


@click.command("hatch-pip-compile")
@click.argument("environment", default=None, type=click.STRING, required=False, nargs=-1)
@click.option(
"-U",
"--upgrade/--no-upgrade",
is_flag=True,
default=False,
help="Try to upgrade all dependencies to their latest versions",
)
@click.option(
"-P",
"--upgrade-package",
"upgrade_packages",
nargs=1,
multiple=True,
help="Specify a particular package to upgrade; may be used more than once",
)
@click.option(
"--all",
"upgrade_all",
is_flag=True,
default=False,
help="Upgrade all environments",
)
def cli(
environment: Sequence[str],
upgrade: bool,
upgrade_packages: Sequence[str],
upgrade_all: bool,
):
"""
Upgrade your `hatch-pip-compile` managed dependencies
from the command line.
"""
with _HatchCommandRunner(
environments=environment,
upgrade=upgrade,
upgrade_packages=upgrade_packages,
upgrade_all=upgrade_all,
) as hatch_runner:
hatch_runner.hatch_cli()


if __name__ == "__main__":
cli()
9 changes: 8 additions & 1 deletion hatch_pip_compile/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,14 @@ def lockfile_up_to_date(self) -> bool:
"""
upgrade = os.getenv("PIP_COMPILE_UPGRADE") or False
upgrade_packages = os.getenv("PIP_COMPILE_UPGRADE_PACKAGE") or False
force_upgrade = upgrade is not False or upgrade_packages is not False
pip_compile_force = bool(os.getenv("__PIP_COMPILE_FORCE__"))
force_upgrade = any(
[
upgrade is not False,
upgrade_packages is not False,
pip_compile_force is not False,
]
)
if not self.dependencies and not self.piptools_lock_file.exists():
return True
if self.piptools_constraints_file:
Expand Down
2 changes: 2 additions & 0 deletions mkdocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
site_name: hatch-pip-compile
nav:
- Home 🏠: index.md
- Command Line Interface ⌨️: cli.md
- API Documentation 🤖: reference/
- Contributing 🤝: contributing.md
theme:
Expand Down Expand Up @@ -58,6 +59,7 @@ markdown_extensions:
- pymdownx.emoji
- pymdownx.tabbed:
alternate_style: true
- mkdocs-click
plugins:
- search
- markdown-exec
Expand Down
Loading

0 comments on commit 13c7604

Please sign in to comment.