Restructure config options to support tox's native change detection

Remove custom handling of deps option
Add locked_deps option
Stop modifying the envconfig.deps option at runtime
This commit is contained in:
Ethan Paul 2020-11-12 00:24:15 -05:00
parent b6415888d9
commit b32a212e82
No known key found for this signature in database
GPG Key ID: D0E2CBF1245E92BF
5 changed files with 18 additions and 78 deletions

View File

@ -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 # Prefix all reporter messages should include to indicate that they came from this module in the
# console output. # console output.
REPORTER_PREFIX = f"[{__about__.__title__}]:" 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"

View File

@ -1,18 +1,8 @@
"""Definitions for typehints/containers used by the plugin""" """Definitions for typehints/containers used by the plugin"""
from typing import Dict from typing import Dict
from typing import List
from typing import NamedTuple
from poetry.core.packages import Package as PoetryPackage from poetry.core.packages import Package as PoetryPackage
from tox.config import DepConfig as ToxDepConfig
# Map of package names to the package object # Map of package names to the package object
PackageMap = Dict[str, PoetryPackage] PackageMap = Dict[str, PoetryPackage]
class SortedEnvDeps(NamedTuple):
"""Container for the two types of environment dependencies"""
unlocked_deps: List[ToxDepConfig]
locked_deps: List[ToxDepConfig]

View File

@ -26,3 +26,7 @@ class LockedDepNotFoundError(ToxPoetryInstallerException):
class ExtraNotFoundError(ToxPoetryInstallerException): class ExtraNotFoundError(ToxPoetryInstallerException):
"""Project package extra not defined in project's pyproject.toml""" """Project package extra not defined in project's pyproject.toml"""
class LockedDepsRequiredError(ToxPoetryInstallerException):
"""Environment cannot specify unlocked dependencies when locked dependencies are required"""

View File

@ -43,6 +43,12 @@ def tox_addoption(parser: ToxParser):
help="Require all dependencies in the environment be installed using the Poetry lockfile", 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 @hookimpl
def tox_testenv_install_deps(venv: ToxVirtualEnv, action: ToxAction): 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 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 # Handle the installation of any locked env dependencies from the lockfile
_install_env_dependencies(venv, poetry, package_map) _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 poetry: Poetry object the packages were sourced from
:param packages: Mapping of package names to the corresponding package object :param packages: Mapping of package names to the corresponding package object
""" """
env_deps = utilities.sort_env_deps(venv)
dependencies: List[PoetryPackage] = [] dependencies: List[PoetryPackage] = []
for dep in env_deps.locked_deps: for dep in venv.envconfig.locked_deps:
try: try:
dependencies += utilities.find_transients(packages, dep.name.lower()) dependencies += utilities.find_transients(packages, dep.lower())
except exceptions.ToxPoetryInstallerException as err: except exceptions.ToxPoetryInstallerException as err:
venv.status = "lockfile installation failed" venv.status = "lockfile installation failed"
reporter.error(f"{constants.REPORTER_PREFIX} {err}") reporter.error(f"{constants.REPORTER_PREFIX} {err}")
@ -148,14 +158,9 @@ def _install_env_dependencies(
dependencies = list(set(dev_dependencies + dependencies)) dependencies = list(set(dev_dependencies + dependencies))
reporter.verbosity1( 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( reporter.verbosity0(
f"{constants.REPORTER_PREFIX} ({venv.name}) installing {len(dependencies)} env dependencies from lockfile" f"{constants.REPORTER_PREFIX} ({venv.name}) installing {len(dependencies)} env dependencies from lockfile"
) )

View File

@ -15,58 +15,6 @@ from tox.venv import VirtualEnv as ToxVirtualEnv
from tox_poetry_installer import constants from tox_poetry_installer import constants
from tox_poetry_installer import exceptions from tox_poetry_installer import exceptions
from tox_poetry_installer.datatypes import PackageMap 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( def install_to_venv(