Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ universal locks across environments #17

Merged
merged 6 commits into from
Nov 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 81 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ at `requirements.txt` (non-default lockfiles are located at
`requirements/requirements-{env_name}.txt`). Alongside `pip-compile`, this plugin also
uses [pip-sync] to install the dependencies from the lockfile into your environment.

See [lock-filename](#lock-filename) for more information on changing the default
lockfile paths and [pip-compile-constraint](#pip-compile-constraint) for more
information on syncing dependency versions across environments.

## Configuration

The [environment plugin] name is `pip-compile`. Set your environment
Expand All @@ -70,13 +74,13 @@ type to `pip-compile` to use this plugin for the respective environment.

### Configuration Options

| name | type | description |
| ------------------------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| lock-filename | `str` | The filename of the ultimate lockfile. `default` env is `requirements.txt`, non-default is `requirements/requirements-{env_name}.txt` |
| pip-compile-hashes | `bool` | Whether to generate hashes in the lockfile. Defaults to `true`. |
| pip-compile-strip-extras | `bool` | Whether to strip the extras from the lockfile ensuring it is constraints compatible, defaults to `true` |
| pip-compile-verbose | `bool` | Set to `true` to run `pip-compile` in verbose mode instead of quiet mode, set to `false` to silence warnings |
| pip-compile-args | `list[str]` | Additional command-line arguments to pass to `pip-compile` |
| name | type | description |
| ---------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| lock-filename | `str` | The filename of the ultimate lockfile. `default` env is `requirements.txt`, non-default is `requirements/requirements-{env_name}.txt` |
| pip-compile-constraint | `str` | An environment to use as a constraint file, ensuring that all shared dependencies are pinned to the same versions. |
| pip-compile-hashes | `bool` | Whether to generate hashes in the lockfile. Defaults to `true`. |
| pip-compile-verbose | `bool` | Set to `true` to run `pip-compile` in verbose mode instead of quiet mode, set to `false` to silence warnings |
| pip-compile-args | `list[str]` | Additional command-line arguments to pass to `pip-compile` |

#### Examples

Expand Down Expand Up @@ -121,6 +125,75 @@ Changing the lock filename to a path in the project root:
lock-filename = "linting-requirements.txt"
```

##### pip-compile-constraint

An environment to use as a constraint, ensuring that all shared dependencies are
pinned to the same versions. For example, if you have a `default` environment and
a `test` environment, you can set the `pip-compile-constraint` option to `default`
on the `test` environment to ensure that all shared dependencies are pinned to the
same versions.

- **_pyproject.toml_**

```toml
[tool.hatch.envs.default]
type = "pip-compile"

[tool.hatch.envs.test]
dependencies = [
"pytest"
]
type = "pip-compile"
pip-compile-constraint = "default"
```

- **_hatch.toml_**

```toml
[envs.default]
type = "pip-compile"

[envs.test]
dependencies = [
"pytest"
]
type = "pip-compile"
pip-compile-constraint = "default"
```

By default, all environments inherit from the `default` environment via
[inheritance]. A common use case is to set the `pip-compile-constraint`
and `type` options on the `default` environment and inherit them on
all other environments. It's important to note that when `detached = true`,
inheritance is disabled and the `type` and `pip-compile-constraint` options
must be set explicitly.

- **_pyproject.toml_**

```toml
[tool.hatch.envs.default]
type = "pip-compile"
pip-compile-constraint = "default"

[tool.hatch.envs.test]
dependencies = [
"pytest"
]
```

- **_hatch.toml_**

```toml
[envs.default]
type = "pip-compile"
pip-compile-constraint = "default"

[envs.test]
dependencies = [
"pytest"
]
```

##### pip-compile-hashes

Whether to generate hashes in the lockfile. Defaults to `true`.
Expand Down Expand Up @@ -242,3 +315,4 @@ pip install hatch hatch-pip-compile
[Changelog]: https://github.com/juftin/hatch-pip-compile/releases
[environment plugin]: https://hatch.pypa.io/latest/plugins/environment/
[pip]: https://pip.pypa.io/en/stable/
[inheritance]: https://hatch.pypa.io/1.7/config/environment/overview/#inheritance
88 changes: 81 additions & 7 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ at `requirements.txt` (non-default lockfiles are located at
`requirements/requirements-{env_name}.txt`). Alongside `pip-compile`, this plugin also
uses [pip-sync] to install the dependencies from the lockfile into your environment.

See [lock-filename](#lock-filename) for more information on changing the default
lockfile paths and [pip-compile-constraint](#pip-compile-constraint) for more
information on syncing dependency versions across environments.

## Configuration

The [environment plugin] name is `pip-compile`. Set your environment
Expand All @@ -70,13 +74,13 @@ type to `pip-compile` to use this plugin for the respective environment.

### Configuration Options

| name | type | description |
| ------------------------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| lock-filename | `str` | The filename of the ultimate lockfile. `default` env is `requirements.txt`, non-default is `requirements/requirements-{env_name}.txt` |
| pip-compile-hashes | `bool` | Whether to generate hashes in the lockfile. Defaults to `true`. |
| pip-compile-strip-extras | `bool` | Whether to strip the extras from the lockfile ensuring it is constraints compatible, defaults to `true` |
| pip-compile-verbose | `bool` | Set to `true` to run `pip-compile` in verbose mode instead of quiet mode, set to `false` to silence warnings |
| pip-compile-args | `list[str]` | Additional command-line arguments to pass to `pip-compile` |
| name | type | description |
| ---------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| lock-filename | `str` | The filename of the ultimate lockfile. `default` env is `requirements.txt`, non-default is `requirements/requirements-{env_name}.txt` |
| pip-compile-constraint | `str` | An environment to use as a constraint file, ensuring that all shared dependencies are pinned to the same versions. |
| pip-compile-hashes | `bool` | Whether to generate hashes in the lockfile. Defaults to `true`. |
| pip-compile-verbose | `bool` | Set to `true` to run `pip-compile` in verbose mode instead of quiet mode, set to `false` to silence warnings |
| pip-compile-args | `list[str]` | Additional command-line arguments to pass to `pip-compile` |

#### Examples

Expand Down Expand Up @@ -121,6 +125,75 @@ Changing the lock filename to a path in the project root:
lock-filename = "linting-requirements.txt"
```

##### pip-compile-constraint

An environment to use as a constraint, ensuring that all shared dependencies are
pinned to the same versions. For example, if you have a `default` environment and
a `test` environment, you can set the `pip-compile-constraint` option to `default`
on the `test` environment to ensure that all shared dependencies are pinned to the
same versions.

- **_pyproject.toml_**

```toml
[tool.hatch.envs.default]
type = "pip-compile"

[tool.hatch.envs.test]
dependencies = [
"pytest"
]
type = "pip-compile"
pip-compile-constraint = "default"
```

- **_hatch.toml_**

```toml
[envs.default]
type = "pip-compile"

[envs.test]
dependencies = [
"pytest"
]
type = "pip-compile"
pip-compile-constraint = "default"
```

By default, all environments inherit from the `default` environment via
[inheritance]. A common use case is to set the `pip-compile-constraint`
and `type` options on the `default` environment and inherit them on
all other environments. It's important to note that when `detached = true`,
inheritance is disabled and the `type` and `pip-compile-constraint` options
must be set explicitly.

- **_pyproject.toml_**

```toml
[tool.hatch.envs.default]
type = "pip-compile"
pip-compile-constraint = "default"

[tool.hatch.envs.test]
dependencies = [
"pytest"
]
```

- **_hatch.toml_**

```toml
[envs.default]
type = "pip-compile"
pip-compile-constraint = "default"

[envs.test]
dependencies = [
"pytest"
]
```

##### pip-compile-hashes

Whether to generate hashes in the lockfile. Defaults to `true`.
Expand Down Expand Up @@ -232,3 +305,4 @@ pip install hatch hatch-pip-compile
[Changelog]: https://github.com/juftin/hatch-pip-compile/releases
[environment plugin]: https://hatch.pypa.io/latest/plugins/environment/
[pip]: https://pip.pypa.io/en/stable/
[inheritance]: https://hatch.pypa.io/1.7/config/environment/overview/#inheritance
21 changes: 21 additions & 0 deletions hatch_pip_compile/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""
hatch-pip-compile exceptions
"""


class HatchPipCompileError(Exception):
"""
Base exception for hatch-pip-compile
"""


class LockFileNotFoundError(HatchPipCompileError, FileNotFoundError):
"""
A lock file was not found
"""


class LockFileError(HatchPipCompileError, ValueError):
"""
A lock file content Error
"""
140 changes: 140 additions & 0 deletions hatch_pip_compile/lock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""
hatch-pip-compile header operations
"""

import logging
import pathlib
import re
from dataclasses import dataclass
from textwrap import dedent
from typing import Iterable, List, Optional

from hatch.env.virtual import VirtualEnv
from packaging.requirements import Requirement
from packaging.version import Version

from hatch_pip_compile.exceptions import LockFileError

logger = logging.getLogger(__name__)


@dataclass
class PipCompileLock:
"""
Pip Compile Lock File Operations
"""

lock_file: pathlib.Path
dependencies: List[str]
project_root: pathlib.Path
constraints_file: Optional[pathlib.Path]
env_name: str
project_name: str
virtualenv: Optional[VirtualEnv] = None

def process_lock(self) -> None:
"""
Post process lockfile
"""
version = f"{self.current_python_version.major}.{self.current_python_version.minor}"
raw_prefix = f"""
#
# This file is autogenerated by hatch-pip-compile with Python {version}
#
"""
prefix = dedent(raw_prefix).strip()
joined_dependencies = "\n".join([f"# - {dep}" for dep in self.dependencies])
lockfile_text = self.lock_file.read_text()
cleaned_input_file = re.sub(
rf"-r \S*/{self.env_name}\.in",
f"hatch.envs.{self.env_name}",
lockfile_text,
)
if self.constraints_file is not None:
constraints_path = self.constraints_file.relative_to(self.project_root)
constraints_line = f"# [constraints] {constraints_path}"
joined_dependencies = "\n".join([constraints_line, "#", joined_dependencies])
cleaned_input_file = re.sub(
r"-c \S*",
f"-c {constraints_path}",
cleaned_input_file,
)
prefix += "\n" + joined_dependencies + "\n#"
new_text = prefix + "\n\n" + cleaned_input_file
self.lock_file.write_text(new_text)

def read_requirements(self) -> List[Requirement]:
"""
Read requirements from lock file
"""
lock_file_text = self.lock_file.read_text()
parsed_requirements = []
for line in lock_file_text.splitlines():
if line.startswith("# - "):
requirement = Requirement(line[4:])
parsed_requirements.append(requirement)
elif not line.startswith("#"):
break
return parsed_requirements

@property
def current_python_version(self) -> Version:
"""
Get python version

In the case of running as a hatch plugin, the `virtualenv` will be set,
otherwise it will be None and the Python version will be read differently.
"""
if self.virtualenv is not None:
return Version(self.virtualenv.environment["python_version"])
else:
msg = "VirtualEnv is not set"
raise NotImplementedError(msg)

@property
def lock_file_version(self) -> Version:
"""
Get lock file version
"""
lock_file_text = self.lock_file.read_text()
match = re.search(
r"# This file is autogenerated by hatch-pip-compile with Python (.*)", lock_file_text
)
if match is None:
msg = "Could not find lock file python version"
raise LockFileError(msg)
return Version(match.group(1))

def compare_python_versions(self, verbose: Optional[bool] = None) -> bool:
"""
Compare python versions

Parameters
----------
verbose : Optional[bool]
Print warning if python versions are different, by default None
which will print the warning. Used as a plugin flag.
"""
lock_version = self.lock_file_version
current_version = self.current_python_version
match = (current_version.major == lock_version.major) and (
current_version.minor == lock_version.minor
)
if match is False and verbose is not False:
logger.error(
"[hatch-pip-compile] Your Python version is different "
"from the lock file, your results may vary."
)
return lock_version == current_version

def compare_requirements(self, requirements: Iterable[Requirement]) -> bool:
"""
Compare requirements

Parameters
----------
requirements : Iterable[Requirement]
List of requirements to compare against the lock file
"""
lock_requirements = self.read_requirements()
return set(requirements) == set(lock_requirements)
Loading