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 4 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
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,12 @@ 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-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
13 changes: 6 additions & 7 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,12 @@ 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-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
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