Skip to content

Commit

Permalink
✨ HatchPipCompileBase
Browse files Browse the repository at this point in the history
  • Loading branch information
juftin committed Feb 22, 2024
1 parent 8d53957 commit 3807cb9
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 34 deletions.
48 changes: 48 additions & 0 deletions hatch_pip_compile/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""
Base classes for hatch-pip-compile
"""

from __future__ import annotations

from typing import TYPE_CHECKING, ClassVar

from hatchling.dep.core import dependencies_in_sync
from packaging.requirements import Requirement

if TYPE_CHECKING:
from hatch_pip_compile.plugin import PipCompileEnvironment


class HatchPipCompileBase:
"""
Base Class for hatch-pip-compile tools
"""

pypi_dependencies: ClassVar[list[str]] = []

def __init__(self, environment: PipCompileEnvironment) -> None:
"""
Inject the environment into the base class
"""
self.environment = environment
self.pypi_dependencies_installed = False

def install_pypi_dependencies(self) -> None:
"""
Install the resolver from PyPI
"""
if not self.pypi_dependencies:
return
elif self.pypi_dependencies_installed:
return
with self.environment.safe_activation():
in_sync = dependencies_in_sync(
requirements=[Requirement(item) for item in self.pypi_dependencies],
sys_path=self.environment.virtual_env.sys_path,
environment=self.environment.virtual_env.environment,
)
if not in_sync:
self.environment.plugin_check_command(
self.environment.construct_pip_install_command(self.pypi_dependencies)
)
self.pypi_dependencies_installed = True
16 changes: 12 additions & 4 deletions hatch_pip_compile/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,28 @@
Package + Dependency Installers
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, ClassVar

from hatch_pip_compile.base import HatchPipCompileBase

if TYPE_CHECKING:
from hatch_pip_compile.plugin import PipCompileEnvironment


@dataclass
class PluginInstaller(ABC):
class PluginInstaller(HatchPipCompileBase, ABC):
"""
Package Installer for the plugin
This abstract base class is used to define the interface for
how the plugin should install packages and dependencies.
"""

environment: "PipCompileEnvironment"
environment: PipCompileEnvironment

@abstractmethod
def install_dependencies(self) -> None:
Expand Down Expand Up @@ -61,6 +65,8 @@ class PipInstaller(PluginInstaller):
Plugin Installer for `pip`
"""

pypi_dependencies: ClassVar[list[str]] = []

def install_dependencies(self) -> None:
"""
Install the dependencies with `pip`
Expand All @@ -79,6 +85,8 @@ class PipSyncInstaller(PluginInstaller):
Plugin Installer for `pip-sync`
"""

pypi_dependencies: ClassVar[list[str]] = ["pip-tools"]

def install_dependencies(self) -> None:
"""
Install the dependencies with `pip-sync`
Expand All @@ -87,7 +95,7 @@ def install_dependencies(self) -> None:
uninstall everything in the environment before deleting the
lockfile.
"""
self.environment.install_pip_tools()
self.install_pypi_dependencies()
cmd = [
self.environment.virtual_env.python_info.executable,
"-m",
Expand Down
54 changes: 24 additions & 30 deletions hatch_pip_compile/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,30 @@
hatch-pip-compile header operations
"""

from __future__ import annotations

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

from hatch.env.virtual import VirtualEnv
from packaging.requirements import Requirement
from packaging.version import Version
from piptools._compat.pip_compat import PipSession, parse_requirements

from hatch_pip_compile.base import HatchPipCompileBase
from hatch_pip_compile.exceptions import LockFileError

logger = logging.getLogger(__name__)


@dataclass
class PipCompileLock:
class PipCompileLock(HatchPipCompileBase):
"""
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, lockfile: pathlib.Path) -> None:
"""
Post process lockfile
Expand All @@ -45,18 +37,20 @@ def process_lock(self, lockfile: pathlib.Path) -> None:
#
"""
prefix = dedent(raw_prefix).strip()
joined_dependencies = "\n".join([f"# - {dep}" for dep in self.dependencies])
joined_dependencies = "\n".join([f"# - {dep}" for dep in self.environment.dependencies])
lockfile_text = lockfile.read_text()
cleaned_input_file = re.sub(
rf"-r \S*/{self.env_name}\.in",
f"hatch.envs.{self.env_name}",
rf"-r \S*/{self.environment.name}\.in",
f"hatch.envs.{self.environment.name}",
lockfile_text,
)
if self.constraints_file is not None:
lockfile_contents = self.constraints_file.read_bytes()
if self.environment.piptools_constraints_file is not None:
lockfile_contents = self.environment.piptools_constraints_file.read_bytes()
cross_platform_contents = lockfile_contents.replace(b"\r\n", b"\n")
constraint_sha = hashlib.sha256(cross_platform_contents).hexdigest()
constraints_path = self.constraints_file.relative_to(self.project_root).as_posix()
constraints_path = self.environment.piptools_constraints_file.relative_to(
self.environment.root
).as_posix()
constraints_line = f"# [constraints] {constraints_path} (SHA256: {constraint_sha})"
joined_dependencies = "\n".join([constraints_line, "#", joined_dependencies])
cleaned_input_file = re.sub(
Expand All @@ -68,11 +62,11 @@ def process_lock(self, lockfile: pathlib.Path) -> None:
new_text = prefix + "\n\n" + cleaned_input_file
lockfile.write_text(new_text)

def read_header_requirements(self) -> List[Requirement]:
def read_header_requirements(self) -> list[Requirement]:
"""
Read requirements from lock file header
"""
lock_file_text = self.lock_file.read_text()
lock_file_text = self.environment.piptools_lock_file.read_text()
parsed_requirements = []
for line in lock_file_text.splitlines():
if line.startswith("# - "):
Expand All @@ -90,8 +84,8 @@ def current_python_version(self) -> 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"])
if self.environment.virtual_env is not None:
return Version(self.environment.virtual_env.environment["python_version"])
else:
msg = "VirtualEnv is not set"
raise NotImplementedError(msg)
Expand All @@ -101,7 +95,7 @@ def lock_file_version(self) -> Version:
"""
Get lock file version
"""
lock_file_text = self.lock_file.read_text()
lock_file_text = self.environment.piptools_lock_file.read_text()
match = re.search(
r"# This file is autogenerated by hatch-pip-compile with Python (.*)", lock_file_text
)
Expand All @@ -110,7 +104,7 @@ def lock_file_version(self) -> Version:
raise LockFileError(msg)
return Version(match.group(1))

def compare_python_versions(self, verbose: Optional[bool] = None) -> bool:
def compare_python_versions(self, verbose: bool | None = None) -> bool:
"""
Compare python versions
Expand Down Expand Up @@ -148,7 +142,7 @@ def compare_constraint_sha(self, sha: str) -> bool:
"""
Compare SHA to the SHA on the lockfile
"""
lock_file_text = self.lock_file.read_text()
lock_file_text = self.environment.piptools_lock_file.read_text()
match = re.search(r"# \[constraints\] \S* \(SHA256: (.*)\)", lock_file_text)
if match is None:
return False
Expand All @@ -158,18 +152,18 @@ def get_file_content_hash(self) -> str:
"""
Get hash of lock file
"""
lockfile_contents = self.lock_file.read_bytes()
lockfile_contents = self.environment.piptools_lock_file.read_bytes()
cross_platform_contents = lockfile_contents.replace(b"\r\n", b"\n")
return hashlib.sha256(cross_platform_contents).hexdigest()

def read_lock_requirements(self) -> List[Requirement]:
def read_lock_requirements(self) -> list[Requirement]:
"""
Read all requirements from lock file
"""
if not self.dependencies:
if not self.environment.dependencies:
return []
install_requirements = parse_requirements(
str(self.lock_file),
str(self.environment.piptools_lock_file),
session=PipSession(),
)
return [ireq.req for ireq in install_requirements] # type: ignore[misc]

0 comments on commit 3807cb9

Please sign in to comment.