diff --git a/hatch_pip_compile/base.py b/hatch_pip_compile/base.py new file mode 100644 index 0000000..b453d6f --- /dev/null +++ b/hatch_pip_compile/base.py @@ -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 diff --git a/hatch_pip_compile/installer.py b/hatch_pip_compile/installer.py index 3961628..3ce0907 100644 --- a/hatch_pip_compile/installer.py +++ b/hatch_pip_compile/installer.py @@ -2,16 +2,20 @@ 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 @@ -19,7 +23,7 @@ class PluginInstaller(ABC): how the plugin should install packages and dependencies. """ - environment: "PipCompileEnvironment" + environment: PipCompileEnvironment @abstractmethod def install_dependencies(self) -> None: @@ -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` @@ -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` @@ -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", diff --git a/hatch_pip_compile/lock.py b/hatch_pip_compile/lock.py index c350155..375d9c4 100644 --- a/hatch_pip_compile/lock.py +++ b/hatch_pip_compile/lock.py @@ -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 @@ -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( @@ -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("# - "): @@ -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) @@ -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 ) @@ -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 @@ -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 @@ -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]