mirror of
https://github.com/enpaul/tox-poetry-installer.git
synced 2025-01-15 08:43:29 +00:00
Update dependency identification to account for multiple platforms
Fix an issue where packages that had two or more exclusive ranges for different python versions would only be represented by the sequentially last version to appear in the lockfile, causing compatibility issues with the older versions of that package. Fixes #68
This commit is contained in:
parent
f08a18728e
commit
fb65fa812e
@ -1,8 +0,0 @@
|
|||||||
"""Definitions for typehints/containers used by the plugin"""
|
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from poetry.core.packages import Package as PoetryPackage
|
|
||||||
|
|
||||||
|
|
||||||
# Map of package names to the package object
|
|
||||||
PackageMap = Dict[str, PoetryPackage]
|
|
@ -17,7 +17,6 @@ from tox_poetry_installer import exceptions
|
|||||||
from tox_poetry_installer import installer
|
from tox_poetry_installer import installer
|
||||||
from tox_poetry_installer import logger
|
from tox_poetry_installer import logger
|
||||||
from tox_poetry_installer import utilities
|
from tox_poetry_installer import utilities
|
||||||
from tox_poetry_installer.datatypes import PackageMap
|
|
||||||
|
|
||||||
|
|
||||||
def _postprocess_install_project_deps(
|
def _postprocess_install_project_deps(
|
||||||
@ -186,10 +185,7 @@ def tox_testenv_install_deps(venv: ToxVirtualEnv, action: ToxAction) -> Optional
|
|||||||
f"Unlocked dependencies '{venv.envconfig.deps}' specified for environment '{venv.name}' which requires locked dependencies"
|
f"Unlocked dependencies '{venv.envconfig.deps}' specified for environment '{venv.name}' which requires locked dependencies"
|
||||||
)
|
)
|
||||||
|
|
||||||
packages: PackageMap = {
|
packages = utilities.build_package_map(poetry)
|
||||||
package.name: package
|
|
||||||
for package in poetry.locker.locked_repository(True).packages
|
|
||||||
}
|
|
||||||
|
|
||||||
if venv.envconfig.install_dev_deps:
|
if venv.envconfig.install_dev_deps:
|
||||||
dev_deps = utilities.find_dev_deps(packages, virtualenv, poetry)
|
dev_deps = utilities.find_dev_deps(packages, virtualenv, poetry)
|
||||||
|
@ -2,12 +2,13 @@
|
|||||||
# Silence this one globally to support the internal function imports for the proxied poetry module.
|
# Silence this one globally to support the internal function imports for the proxied poetry module.
|
||||||
# See the docstring in 'tox_poetry_installer._poetry' for more context.
|
# See the docstring in 'tox_poetry_installer._poetry' for more context.
|
||||||
# pylint: disable=import-outside-toplevel
|
# pylint: disable=import-outside-toplevel
|
||||||
|
import collections
|
||||||
import typing
|
import typing
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Dict
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import Set
|
from typing import Set
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
from poetry.core.packages import Dependency as PoetryDependency
|
from poetry.core.packages import Dependency as PoetryDependency
|
||||||
from poetry.core.packages import Package as PoetryPackage
|
from poetry.core.packages import Package as PoetryPackage
|
||||||
@ -17,12 +18,14 @@ 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 import logger
|
from tox_poetry_installer import logger
|
||||||
from tox_poetry_installer.datatypes import PackageMap
|
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from tox_poetry_installer import _poetry
|
from tox_poetry_installer import _poetry
|
||||||
|
|
||||||
|
|
||||||
|
PackageMap = Dict[str, List[PoetryPackage]]
|
||||||
|
|
||||||
|
|
||||||
def check_preconditions(venv: ToxVirtualEnv, action: ToxAction) -> "_poetry.Poetry":
|
def check_preconditions(venv: ToxVirtualEnv, action: ToxAction) -> "_poetry.Poetry":
|
||||||
"""Check that the local project environment meets expectations"""
|
"""Check that the local project environment meets expectations"""
|
||||||
# Skip running the plugin for the provisioning environment. The provisioned environment,
|
# Skip running the plugin for the provisioning environment. The provisioned environment,
|
||||||
@ -82,15 +85,28 @@ def convert_virtualenv(venv: ToxVirtualEnv) -> "_poetry.VirtualEnv":
|
|||||||
return _poetry.VirtualEnv(path=Path(venv.envconfig.envdir))
|
return _poetry.VirtualEnv(path=Path(venv.envconfig.envdir))
|
||||||
|
|
||||||
|
|
||||||
|
def build_package_map(poetry: "_poetry.Poetry") -> PackageMap:
|
||||||
|
"""Build the mapping of package names to objects
|
||||||
|
|
||||||
|
:param poetry: Populated poetry object to load locked packages from
|
||||||
|
:returns: Mapping of package names to Poetry package objects
|
||||||
|
"""
|
||||||
|
packages = collections.defaultdict(list)
|
||||||
|
for package in poetry.locker.locked_repository(True).packages:
|
||||||
|
packages[package.name].append(package)
|
||||||
|
|
||||||
|
return packages
|
||||||
|
|
||||||
|
|
||||||
def identify_transients(
|
def identify_transients(
|
||||||
dep: Union[PoetryDependency, str],
|
dep_name: str,
|
||||||
packages: PackageMap,
|
packages: PackageMap,
|
||||||
venv: "_poetry.VirtualEnv",
|
venv: "_poetry.VirtualEnv",
|
||||||
allow_missing: Sequence[str] = (),
|
allow_missing: Sequence[str] = (),
|
||||||
) -> List[PoetryPackage]:
|
) -> List[PoetryPackage]:
|
||||||
"""Using a pool of packages, identify all transient dependencies of a given package name
|
"""Using a pool of packages, identify all transient dependencies of a given package name
|
||||||
|
|
||||||
:param dep: Either the Poetry dependency or the dependency's bare package name to recursively
|
:param dep_name: Either the Poetry dependency or the dependency's bare package name to recursively
|
||||||
identify the transient dependencies of
|
identify the transient dependencies of
|
||||||
:param packages: All packages from the lockfile to use for identifying dependency relationships.
|
:param packages: All packages from the lockfile to use for identifying dependency relationships.
|
||||||
:param venv: Poetry virtual environment to use for package compatibility checks
|
:param venv: Poetry virtual environment to use for package compatibility checks
|
||||||
@ -102,53 +118,64 @@ def identify_transients(
|
|||||||
.. note:: The package corresponding to the dependency specified by the ``dep`` parameter will
|
.. note:: The package corresponding to the dependency specified by the ``dep`` parameter will
|
||||||
be included in the returned list of packages.
|
be included in the returned list of packages.
|
||||||
"""
|
"""
|
||||||
transients: List[PoetryPackage] = []
|
|
||||||
searched: Set[str] = set()
|
searched: Set[str] = set()
|
||||||
|
|
||||||
def _deps_of_dep(transient: PoetryDependency):
|
def _transients(transient: PoetryDependency) -> List[PoetryPackage]:
|
||||||
searched.add(transient.name)
|
searched.add(transient.name)
|
||||||
|
|
||||||
if venv.is_valid_for_marker(transient.marker):
|
results: List[PoetryPackage] = []
|
||||||
for requirement in packages[transient.name].requires:
|
for option in packages[transient.name]:
|
||||||
|
if venv.is_valid_for_marker(option.to_dependency().marker):
|
||||||
|
for requirement in option.requires:
|
||||||
if requirement.name not in searched:
|
if requirement.name not in searched:
|
||||||
_deps_of_dep(requirement)
|
results += _transients(requirement)
|
||||||
logger.debug(f"Including {transient} for installation")
|
logger.debug(f"Including {option} for installation")
|
||||||
transients.append(packages[transient.name])
|
results.append(option)
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Skipping {transient}: package requires {transient.marker}")
|
logger.debug(
|
||||||
|
f"Skipping {transient.name}: target python version is {'.'.join([str(item) for item in venv.get_version_info()])} but package requires {transient.marker}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if isinstance(dep, str):
|
for option in packages[dep_name]:
|
||||||
dep = packages[dep].to_dependency()
|
if venv.is_valid_for_marker(option.to_dependency().marker):
|
||||||
|
dep = option.to_dependency()
|
||||||
_deps_of_dep(dep)
|
break
|
||||||
except KeyError as err:
|
else:
|
||||||
dep_name = err.args[0]
|
|
||||||
|
|
||||||
if dep_name in constants.UNSAFE_PACKAGES:
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Installing package '{dep_name}' using Poetry is not supported and will be skipped"
|
f"Skipping {dep_name}: no locked version found compatible with target python version {'.'.join([str(item) for item in venv.get_version_info()])}"
|
||||||
)
|
)
|
||||||
logger.debug(f"Skipping {dep_name}: designated unsafe by Poetry")
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if dep_name in allow_missing:
|
return _transients(dep)
|
||||||
logger.debug(f"Skipping {dep_name}: package is allowed to be unlocked")
|
except KeyError as err:
|
||||||
|
missing = err.args[0]
|
||||||
|
|
||||||
|
if missing in constants.UNSAFE_PACKAGES:
|
||||||
|
logger.warning(
|
||||||
|
f"Installing package '{missing}' using Poetry is not supported and will be skipped"
|
||||||
|
)
|
||||||
|
logger.debug(f"Skipping {missing}: designated unsafe by Poetry")
|
||||||
|
return []
|
||||||
|
|
||||||
|
if missing in allow_missing:
|
||||||
|
logger.debug(f"Skipping {missing}: package is allowed to be unlocked")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if any(
|
if any(
|
||||||
delimiter in dep_name for delimiter in constants.PEP508_VERSION_DELIMITERS
|
delimiter in missing for delimiter in constants.PEP508_VERSION_DELIMITERS
|
||||||
):
|
):
|
||||||
raise exceptions.LockedDepVersionConflictError(
|
raise exceptions.LockedDepVersionConflictError(
|
||||||
f"Locked dependency '{dep_name}' cannot include version specifier"
|
f"Locked dependency '{missing}' cannot include version specifier"
|
||||||
) from None
|
) from None
|
||||||
|
|
||||||
raise exceptions.LockedDepNotFoundError(
|
raise exceptions.LockedDepNotFoundError(
|
||||||
f"No version of locked dependency '{dep_name}' found in the project lockfile"
|
f"No version of locked dependency '{missing}' found in the project lockfile"
|
||||||
) from None
|
) from None
|
||||||
|
|
||||||
return transients
|
|
||||||
|
|
||||||
|
|
||||||
def find_project_deps(
|
def find_project_deps(
|
||||||
packages: PackageMap,
|
packages: PackageMap,
|
||||||
@ -171,26 +198,24 @@ def find_project_deps(
|
|||||||
f"Project package requires one or more unsafe dependencies ({', '.join(constants.UNSAFE_PACKAGES)}) which cannot be installed with Poetry"
|
f"Project package requires one or more unsafe dependencies ({', '.join(constants.UNSAFE_PACKAGES)}) which cannot be installed with Poetry"
|
||||||
)
|
)
|
||||||
|
|
||||||
base_deps: List[PoetryPackage] = [
|
required_dep_names = [
|
||||||
packages[item.name]
|
item.name for item in poetry.package.requires if not item.is_optional()
|
||||||
for item in poetry.package.requires
|
|
||||||
if not item.is_optional()
|
|
||||||
]
|
]
|
||||||
|
|
||||||
extra_deps: List[PoetryPackage] = []
|
extra_dep_names: List[str] = []
|
||||||
for extra in extras:
|
for extra in extras:
|
||||||
logger.info(f"Processing project extra '{extra}'")
|
logger.info(f"Processing project extra '{extra}'")
|
||||||
try:
|
try:
|
||||||
extra_deps += [packages[item.name] for item in poetry.package.extras[extra]]
|
extra_dep_names += [item.name for item in poetry.package.extras[extra]]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise exceptions.ExtraNotFoundError(
|
raise exceptions.ExtraNotFoundError(
|
||||||
f"Environment specifies project extra '{extra}' which was not found in the lockfile"
|
f"Environment specifies project extra '{extra}' which was not found in the lockfile"
|
||||||
) from None
|
) from None
|
||||||
|
|
||||||
dependencies: List[PoetryPackage] = []
|
dependencies: List[PoetryPackage] = []
|
||||||
for dep in base_deps + extra_deps:
|
for dep_name in required_dep_names + extra_dep_names:
|
||||||
dependencies += identify_transients(
|
dependencies += identify_transients(
|
||||||
dep.name.lower(), packages, venv, allow_missing=[poetry.package.name]
|
dep_name.lower(), packages, venv, allow_missing=[poetry.package.name]
|
||||||
)
|
)
|
||||||
|
|
||||||
return dependencies
|
return dependencies
|
||||||
@ -212,13 +237,13 @@ def find_additional_deps(
|
|||||||
:param dep_names: Sequence of additional dependency names to recursively find the transient
|
:param dep_names: Sequence of additional dependency names to recursively find the transient
|
||||||
dependencies for
|
dependencies for
|
||||||
"""
|
"""
|
||||||
deps: List[PoetryPackage] = []
|
dependencies: List[PoetryPackage] = []
|
||||||
for dep_name in dep_names:
|
for dep_name in dep_names:
|
||||||
deps += identify_transients(
|
dependencies += identify_transients(
|
||||||
dep_name.lower(), packages, venv, allow_missing=[poetry.package.name]
|
dep_name.lower(), packages, venv, allow_missing=[poetry.package.name]
|
||||||
)
|
)
|
||||||
|
|
||||||
return deps
|
return dependencies
|
||||||
|
|
||||||
|
|
||||||
def find_dev_deps(
|
def find_dev_deps(
|
||||||
|
Loading…
Reference in New Issue
Block a user