diff --git a/tox_poetry_installer/constants.py b/tox_poetry_installer/constants.py index a4e7933..a99f1b5 100644 --- a/tox_poetry_installer/constants.py +++ b/tox_poetry_installer/constants.py @@ -17,10 +17,3 @@ PEP508_VERSION_DELIMITERS: Tuple[str, ...] = ("~=", "==", "!=", ">", "<") # Prefix all reporter messages should include to indicate that they came from this module in the # console output. REPORTER_PREFIX = f"[{__about__.__title__}]:" - -# Suffix that indicates an env dependency should be treated as a locked dependency and thus be -# installed from the lockfile. Will be automatically stripped off of a dependency name during -# sorting so that the resulting string is just the valid package name. This becomes optional when -# the "require_locked_deps" option is true for an environment; in that case a bare dependency like -# 'foo' is treated the same as an explicitly locked dependency like 'foo@poetry' -MAGIC_SUFFIX_MARKER = "@poetry" diff --git a/tox_poetry_installer/datatypes.py b/tox_poetry_installer/datatypes.py index b042024..4233a94 100644 --- a/tox_poetry_installer/datatypes.py +++ b/tox_poetry_installer/datatypes.py @@ -1,18 +1,8 @@ """Definitions for typehints/containers used by the plugin""" from typing import Dict -from typing import List -from typing import NamedTuple from poetry.core.packages import Package as PoetryPackage -from tox.config import DepConfig as ToxDepConfig # Map of package names to the package object PackageMap = Dict[str, PoetryPackage] - - -class SortedEnvDeps(NamedTuple): - """Container for the two types of environment dependencies""" - - unlocked_deps: List[ToxDepConfig] - locked_deps: List[ToxDepConfig] diff --git a/tox_poetry_installer/exceptions.py b/tox_poetry_installer/exceptions.py index 969bf13..4ecd9b8 100644 --- a/tox_poetry_installer/exceptions.py +++ b/tox_poetry_installer/exceptions.py @@ -26,3 +26,7 @@ class LockedDepNotFoundError(ToxPoetryInstallerException): class ExtraNotFoundError(ToxPoetryInstallerException): """Project package extra not defined in project's pyproject.toml""" + + +class LockedDepsRequiredError(ToxPoetryInstallerException): + """Environment cannot specify unlocked dependencies when locked dependencies are required""" diff --git a/tox_poetry_installer/hooks.py b/tox_poetry_installer/hooks.py index d9b80c9..1cf495f 100644 --- a/tox_poetry_installer/hooks.py +++ b/tox_poetry_installer/hooks.py @@ -43,6 +43,12 @@ def tox_addoption(parser: ToxParser): help="Require all dependencies in the environment be installed using the Poetry lockfile", ) + parser.add_testenv_attribute( + name="locked_deps", + type="line-list", + help="List of locked dependencies to install to the environment using the Poetry lockfile", + ) + @hookimpl def tox_testenv_install_deps(venv: ToxVirtualEnv, action: ToxAction): @@ -87,6 +93,11 @@ def tox_testenv_install_deps(venv: ToxVirtualEnv, action: ToxAction): for package in poetry.locker.locked_repository(True).packages } + if venv.envconfig.require_locked_deps and venv.envconfig.deps: + raise exceptions.LockedDepsRequiredError( + f"Unlocked dependencies '{venv.envconfig.deps}' specified for environment '{venv.name}' which requires locked dependencies" + ) + # Handle the installation of any locked env dependencies from the lockfile _install_env_dependencies(venv, poetry, package_map) @@ -119,12 +130,11 @@ def _install_env_dependencies( :param poetry: Poetry object the packages were sourced from :param packages: Mapping of package names to the corresponding package object """ - env_deps = utilities.sort_env_deps(venv) dependencies: List[PoetryPackage] = [] - for dep in env_deps.locked_deps: + for dep in venv.envconfig.locked_deps: try: - dependencies += utilities.find_transients(packages, dep.name.lower()) + dependencies += utilities.find_transients(packages, dep.lower()) except exceptions.ToxPoetryInstallerException as err: venv.status = "lockfile installation failed" reporter.error(f"{constants.REPORTER_PREFIX} {err}") @@ -148,14 +158,9 @@ def _install_env_dependencies( dependencies = list(set(dev_dependencies + dependencies)) reporter.verbosity1( - f"{constants.REPORTER_PREFIX} identified {len(dependencies)} total dependencies from {len(env_deps.locked_deps)} locked env dependencies" + f"{constants.REPORTER_PREFIX} identified {len(dependencies)} total dependencies from {len(venv.envconfig.locked_deps)} locked env dependencies" ) - reporter.verbosity1( - f"{constants.REPORTER_PREFIX} updating env config with {len(env_deps.unlocked_deps)} unlocked env dependencies for installation using the default backend" - ) - venv.envconfig.deps = env_deps.unlocked_deps - reporter.verbosity0( f"{constants.REPORTER_PREFIX} ({venv.name}) installing {len(dependencies)} env dependencies from lockfile" ) diff --git a/tox_poetry_installer/utilities.py b/tox_poetry_installer/utilities.py index 178a1fb..f4bb650 100644 --- a/tox_poetry_installer/utilities.py +++ b/tox_poetry_installer/utilities.py @@ -15,58 +15,6 @@ from tox.venv import VirtualEnv as ToxVirtualEnv from tox_poetry_installer import constants from tox_poetry_installer import exceptions from tox_poetry_installer.datatypes import PackageMap -from tox_poetry_installer.datatypes import SortedEnvDeps - - -def sort_env_deps(venv: ToxVirtualEnv) -> SortedEnvDeps: - """Sorts the environment dependencies by lock status - - Lock status determines whether a given environment dependency will be installed from the - lockfile using the Poetry backend, or whether this plugin will skip it and allow it to be - installed using the default pip-based backend (an unlocked dependency). - - .. note:: A locked dependency must follow a required format. To avoid reinventing the wheel - (no pun intended) this module does not have any infrastructure for parsing PEP-508 - version specifiers, and so requires locked dependencies to be specified with no - version (the installed version being taken from the lockfile). If a dependency is - specified as locked and its name is also a PEP-508 string then an error will be - raised. - """ - - reporter.verbosity1( - f"{constants.REPORTER_PREFIX} sorting {len(venv.envconfig.deps)} env dependencies by lock requirement" - ) - unlocked_deps = [] - locked_deps = [] - - for dep in venv.envconfig.deps: - if venv.envconfig.require_locked_deps: - reporter.verbosity1( - f"{constants.REPORTER_PREFIX} lock required for env, treating '{dep.name}' as locked env dependency" - ) - dep.name = dep.name.replace(constants.MAGIC_SUFFIX_MARKER, "") - locked_deps.append(dep) - else: - if dep.name.endswith(constants.MAGIC_SUFFIX_MARKER): - reporter.verbosity1( - f"{constants.REPORTER_PREFIX} specification includes marker '{constants.MAGIC_SUFFIX_MARKER}', treating '{dep.name}' as locked env dependency" - ) - dep.name = dep.name.replace(constants.MAGIC_SUFFIX_MARKER, "") - locked_deps.append(dep) - else: - reporter.verbosity1( - f"{constants.REPORTER_PREFIX} specification does not include marker '{constants.MAGIC_SUFFIX_MARKER}', treating '{dep.name}' as unlocked env dependency" - ) - unlocked_deps.append(dep) - - reporter.verbosity1( - f"{constants.REPORTER_PREFIX} identified {len(locked_deps)} locked env dependencies: {[item.name for item in locked_deps]}" - ) - reporter.verbosity1( - f"{constants.REPORTER_PREFIX} identified {len(unlocked_deps)} unlocked env dependencies: {[item.name for item in unlocked_deps]}" - ) - - return SortedEnvDeps(locked_deps=locked_deps, unlocked_deps=unlocked_deps) def install_to_venv(