diff --git a/README.md b/README.md index 275155c..5e0d9c2 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,8 @@ your environment type set to `pip-compile` (see [Configuration](#configuration)) The `hatch-pip-compile` plugin will automatically run `pip-compile` whenever your environment needs to be updated. Behind the scenes, this plugin creates a lockfile 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. +`requirements/requirements-{env_name}.txt`). Once the dependencies are resolved +the plugin will install the lockfile into your virtual environment. - [lock-filename](#lock-filename) - changing the default lockfile path - [pip-compile-constraint](#pip-compile-constraint) - syncing dependency versions across environments @@ -84,6 +84,11 @@ type to `pip-compile` to use this plugin for the respective environment. ### Configuration Options +The plugin gives you options to configure how lockfiles are generated and how they are installed +into your environment. + +#### Generating Lockfiles + | 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` | @@ -92,6 +97,13 @@ type to `pip-compile` to use this plugin for the respective environment. | 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` | +#### Installing Lockfiles + +| name | type | description | +| ------------------------ | ----------- | ---------------------------------------------------------------------------------------------- | +| pip-compile-installer | `str` | Whether to use `pip` or `pip-sync` to install dependencies into the project. Defaults to `pip` | +| pip-compile-install-args | `list[str]` | Additional command-line arguments to pass to `pip-compile-installer` | + #### Examples ##### lock-filename @@ -272,6 +284,61 @@ Optionally, if you would like to silence any warnings set the `pip-compile-verbo pip-compile-verbose = true ``` +##### pip-compile-installer + +Whether to use [pip] or [pip-sync] to install dependencies into the project. Defaults to `pip`. +When you choose the `pip` option the plugin will run `pip install -r {lockfile}` under the hood +to install the dependencies. When you choose the `pip-sync` option `pip-sync {lockfile}` is invoked +by the plugin. + +The key difference between these options is that `pip-sync` will uninstall any packages that are +not in the lockfile and remove them from your environment. `pip-sync` is useful if you want to ensure +that your environment is exactly the same as the lockfile. If the environment should be used +across different Python versions and platforms `pip` is the safer option to use. + +- **_pyproject.toml_** + + ```toml + [tool.hatch.envs.] + type = "pip-compile" + pip-compile-installer = "pip-sync" + ``` + +- **_hatch.toml_** + + ```toml + [envs.] + type = "pip-compile" + pip-compile-installer = "pip-sync" + ``` + +##### pip-compile-install-args + +Extra arguments to pass to `pip-compile-installer`. For example, if you'd like to use `pip` as the +installer but want to pass the `--no-deps` flag to `pip install` you can do so with this option: + +- **_pyproject.toml_** + + ```toml + [tool.hatch.envs.] + type = "pip-compile" + pip-compile-installer = "pip" + pip-compile-install-args = [ + "--no-deps" + ] + ``` + +- **_hatch.toml_** + + ```toml + [envs.] + type = "pip-compile" + pip-compile-installer = "pip" + pip-compile-install-args = [ + "--no-deps" + ] + ``` + ## Upgrading Dependencies Upgrading all dependencies can be as simple as deleting your lockfile and diff --git a/docs/index.md b/docs/index.md index 5b3b227..54fb6bc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -54,8 +54,8 @@ your environment type set to `pip-compile` (see [Configuration](#configuration)) The `hatch-pip-compile` plugin will automatically run `pip-compile` whenever your environment needs to be updated. Behind the scenes, this plugin creates a lockfile 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. +`requirements/requirements-{env_name}.txt`). Once the dependencies are resolved +the plugin will install the lockfile into your virtual environment. - [lock-filename](#lock-filename) - changing the default lockfile path - [pip-compile-constraint](#pip-compile-constraint) - syncing dependency versions across environments @@ -82,6 +82,11 @@ type to `pip-compile` to use this plugin for the respective environment. ### Configuration Options +The plugin gives you options to configure how lockfiles are generated and how they are installed +into your environment. + +#### Generating Lockfiles + | 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` | @@ -90,6 +95,13 @@ type to `pip-compile` to use this plugin for the respective environment. | 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` | +#### Installing Lockfiles + +| name | type | description | +| ------------------------ | ----------- | ---------------------------------------------------------------------------------------------- | +| pip-compile-installer | `str` | Whether to use `pip` or `pip-sync` to install dependencies into the project. Defaults to `pip` | +| pip-compile-install-args | `list[str]` | Additional command-line arguments to pass to `pip-compile-installer` | + #### Examples ##### lock-filename @@ -270,6 +282,61 @@ Optionally, if you would like to silence any warnings set the `pip-compile-verbo pip-compile-verbose = true ``` +##### pip-compile-installer + +Whether to use [pip] or [pip-sync] to install dependencies into the project. Defaults to `pip`. +When you choose the `pip` option the plugin will run `pip install -r {lockfile}` under the hood +to install the dependencies. When you choose the `pip-sync` option `pip-sync {lockfile}` is invoked +by the plugin. + +The key difference between these options is that `pip-sync` will uninstall any packages that are +not in the lockfile and remove them from your environment. `pip-sync` is useful if you want to ensure +that your environment is exactly the same as the lockfile. If the environment should be used +across different Python versions and platforms `pip` is the safer option to use. + +- **_pyproject.toml_** + + ```toml + [tool.hatch.envs.] + type = "pip-compile" + pip-compile-installer = "pip-sync" + ``` + +- **_hatch.toml_** + + ```toml + [envs.] + type = "pip-compile" + pip-compile-installer = "pip-sync" + ``` + +##### pip-compile-install-args + +Extra arguments to pass to `pip-compile-installer`. For example, if you'd like to use `pip` as the +installer but want to pass the `--no-deps` flag to `pip install` you can do so with this option: + +- **_pyproject.toml_** + + ```toml + [tool.hatch.envs.] + type = "pip-compile" + pip-compile-installer = "pip" + pip-compile-install-args = [ + "--no-deps" + ] + ``` + +- **_hatch.toml_** + + ```toml + [envs.] + type = "pip-compile" + pip-compile-installer = "pip" + pip-compile-install-args = [ + "--no-deps" + ] + ``` + ## Upgrading Dependencies Upgrading all dependencies can be as simple as deleting your lockfile and diff --git a/hatch_pip_compile/plugin.py b/hatch_pip_compile/plugin.py index 13fd741..7e2f2d7 100644 --- a/hatch_pip_compile/plugin.py +++ b/hatch_pip_compile/plugin.py @@ -59,6 +59,17 @@ def __init__(self, *args, **kwargs): env_name=self.name, project_name=self.metadata.name, ) + install_method = self.config.get("pip-compile-installer", "pip") + if install_method == "pip": + self.__class__ = PipCompileEnvironmentWithPipInstall + elif install_method == "pip-sync": + self.__class__ = PipCompileEnvironmentWithPipSync + else: + msg = ( + f"Invalid pip-tools install method: {self.install_method} - " + "must be 'pip' or 'pip-sync'" + ) + raise HatchPipCompileError(msg) @staticmethod def get_option_types() -> Dict[str, Any]: @@ -70,39 +81,17 @@ def get_option_types() -> Dict[str, Any]: "pip-compile-hashes": bool, "pip-compile-args": List[str], "pip-compile-constraint": str, + "pip-compile-installer": str, + "pip-compile-install-args": List[str], } - def _hatch_pip_compile_install(self): - """ - Run the full hatch-pip-compile install process - - 1) Create virtual environment if not exists - 2) Install pip-tools - 3) Run pip-compile (if lock file does not exist / is out of date) - 4) Run pip-sync - 5) Install project in dev mode - """ - try: - _ = self.virtual_env.executables_directory - except OSError: - self.create() - with self.safe_activation(): - self.virtual_env.platform.check_command( - self.construct_pip_install_command(["pip-tools"]) - ) - if not self.lockfile_up_to_date: - self._pip_compile_cli() - self._pip_sync_cli() - if not self.skip_install: - if self.dev_mode: - super().install_project_dev_mode() - else: - super().install_project() - def _pip_compile_cli(self) -> None: """ Run pip-compile """ + if not self.dependencies: + self._piptools_lock_file.unlink(missing_ok=True) + return no_compile = bool(os.getenv("PIP_COMPILE_DISABLE")) if no_compile: msg = "hatch-pip-compile is disabled but attempted to run a lockfile update." @@ -147,51 +136,6 @@ def _pip_compile_cli(self) -> None: self.piptools_lock.process_lock(lockfile=output_file) shutil.move(output_file, self._piptools_lock_file) - def _pip_sync_cli(self) -> None: - """ - run pip-sync - - In the event that a lockfile exists, but there are no dependencies, - pip-sync will uninstall everything in the environment before - deleting the lockfile. - """ - _ = self.piptools_lock.compare_python_versions( - verbose=self.config.get("pip-compile-verbose", None) - ) - cmd = [ - self.virtual_env.python_info.executable, - "-m", - "piptools", - "sync", - "--verbose" if self.config.get("pip-compile-verbose", None) is True else "--quiet", - "--python-executable", - str(self.virtual_env.python_info.executable), - str(self._piptools_lock_file), - ] - if not self.dependencies: - self._piptools_lock_file.write_text("") - self.virtual_env.platform.check_command(cmd) - if not self.dependencies: - self._piptools_lock_file.unlink() - - def install_project(self): - """ - Install the project the first time - - The same implementation as `sync_dependencies` - due to the way `pip-sync` uninstalls our root package - """ - self._hatch_pip_compile_install() - - def install_project_dev_mode(self): - """ - Install the project the first time in dev mode - - The same implementation as `sync_dependencies` - due to the way `pip-sync` uninstalls our root package - """ - self._hatch_pip_compile_install() - @functools.cached_property def lockfile_up_to_date(self) -> bool: """ @@ -253,12 +197,6 @@ def dependencies_in_sync(self): else: return super().dependencies_in_sync() - def sync_dependencies(self): - """ - Sync dependencies - """ - self._hatch_pip_compile_install() - @property def piptools_constraints_file(self) -> Optional[pathlib.Path]: """ @@ -347,3 +285,122 @@ def pipools_environment_dict(self) -> Dict[str, Any]: Get the environment dictionary """ return self.metadata.hatch.config.get("envs", {}) + + def install_project(self) -> None: + """ + Install the project (`--no-deps`) + """ + with self.safe_activation(): + self.platform.check_command( + self.construct_pip_install_command(args=["--no-deps", str(self.root)]) + ) + + def install_project_dev_mode(self) -> None: + """ + Install the project in editable mode (`--no-deps`) + """ + with self.safe_activation(): + self.platform.check_command( + self.construct_pip_install_command(args=["--no-deps", "--editable", str(self.root)]) + ) + + +class PipCompileEnvironmentWithPipInstall(PipCompileEnvironment): + def sync_dependencies(self) -> None: + """ + Install the project with `pip` + """ + with self.safe_activation(): + if not self.lockfile_up_to_date: + self.virtual_env.platform.check_command( + self.construct_pip_install_command(["pip-tools"]) + ) + self._pip_compile_cli() + if not self._piptools_lock_file.exists(): + return + extra_args = self.config.get("pip-compile-install-args", []) + args = [*extra_args, "--requirement", str(self._piptools_lock_file)] + install_command = self.construct_pip_install_command(args=args) + self.virtual_env.platform.check_command(install_command) + + +class PipCompileEnvironmentWithPipSync(PipCompileEnvironment): + def _hatch_pip_compile_install(self): + """ + Run the full hatch-pip-compile install process + + 1) Create virtual environment if not exists + 2) Install pip-tools + 3) Run pip-compile (if lock file does not exist / is out of date) + 4) Run pip-sync + 5) Install project in dev mode + """ + try: + _ = self.virtual_env.executables_directory + except OSError: + self.create() + with self.safe_activation(): + self.virtual_env.platform.check_command( + self.construct_pip_install_command(["pip-tools"]) + ) + if not self.lockfile_up_to_date: + self._pip_compile_cli() + self._pip_sync_cli() + if not self.skip_install: + if self.dev_mode: + super().install_project_dev_mode() + else: + super().install_project() + + def _pip_sync_cli(self) -> None: + """ + Install the dependencies with `pip-sync` + + In the event that a lockfile exists, but there are no dependencies, + pip-sync will uninstall everything in the environment before + deleting the lockfile. + """ + _ = self.piptools_lock.compare_python_versions( + verbose=self.config.get("pip-compile-verbose", None) + ) + cmd = [ + self.virtual_env.python_info.executable, + "-m", + "piptools", + "sync", + "--verbose" if self.config.get("pip-compile-verbose", None) is True else "--quiet", + "--python-executable", + str(self.virtual_env.python_info.executable), + ] + if not self.dependencies: + self._piptools_lock_file.write_text("") + extra_args = self.config.get("pip-compile-install-args", []) + cmd.extend(extra_args) + cmd.append(str(self._piptools_lock_file)) + self.virtual_env.platform.check_command(cmd) + if not self.dependencies: + self._piptools_lock_file.unlink() + + def install_project(self): + """ + Install the project the first time + + The same implementation as `sync_dependencies` + due to the way `pip-sync` uninstalls our root package + """ + self._hatch_pip_compile_install() + + def install_project_dev_mode(self): + """ + Install the project the first time in dev mode + + The same implementation as `sync_dependencies` + due to the way `pip-sync` uninstalls our root package + """ + self._hatch_pip_compile_install() + + def sync_dependencies(self): + """ + Sync dependencies + """ + self._hatch_pip_compile_install()