Fix non-deterministic dependency order resolution

Unordered sets strike again. By casting a list of packages to a set
to ensure uniqueness the installation of the packages becomes non-deterministic.
This is not great, but it trivially breaks installing packages that require
their dependencies for installation.

Fixes #41
This commit is contained in:
Ethan Paul 2021-02-09 23:25:46 -05:00
parent 52c08e9dc5
commit ea8bc3887e
No known key found for this signature in database
GPG Key ID: C5F5542B54A4D9C6
2 changed files with 72 additions and 38 deletions

View File

@ -97,23 +97,18 @@ def tox_testenv_install_deps(venv: ToxVirtualEnv, action: ToxAction) -> Optional
} }
if venv.envconfig.install_dev_deps: if venv.envconfig.install_dev_deps:
dev_deps: List[PoetryPackage] = [ dev_deps = utilities.find_dev_dependencies(poetry, package_map)
dep
for dep in package_map.values()
if dep not in poetry.locker.locked_repository(False).packages
]
else:
dev_deps = []
tox.reporter.verbosity1( tox.reporter.verbosity1(
f"{constants.REPORTER_PREFIX} Identified {len(dev_deps)} development dependencies to install to env" f"{constants.REPORTER_PREFIX} Identified {len(dev_deps)} development dependencies to install to env"
) )
else:
env_deps: List[PoetryPackage] = [] dev_deps = []
for dep in venv.envconfig.locked_deps: tox.reporter.verbosity1(
env_deps += utilities.find_transients( f"{constants.REPORTER_PREFIX} Env does not install development dependencies, skipping"
package_map, dep.lower(), allow_missing=[poetry.package.name]
) )
env_deps = utilities.find_env_dependencies(venv, poetry, package_map)
tox.reporter.verbosity1( tox.reporter.verbosity1(
f"{constants.REPORTER_PREFIX} Identified {len(env_deps)} environment dependencies to install to env" f"{constants.REPORTER_PREFIX} Identified {len(env_deps)} environment dependencies to install to env"
) )
@ -139,7 +134,7 @@ def tox_testenv_install_deps(venv: ToxVirtualEnv, action: ToxAction) -> Optional
tox.reporter.error(f"{constants.REPORTER_PREFIX} Internal plugin error: {err}") tox.reporter.error(f"{constants.REPORTER_PREFIX} Internal plugin error: {err}")
raise err raise err
dependencies = list(set(dev_deps + env_deps + project_deps)) dependencies = dev_deps + env_deps + project_deps
action.setactivity( action.setactivity(
__about__.__title__, __about__.__title__,
f"Installing {len(dependencies)} dependencies from Poetry lock file", f"Installing {len(dependencies)} dependencies from Poetry lock file",

View File

@ -48,9 +48,9 @@ def install_to_venv(
installer.install(dependency) installer.install(dependency)
def find_transients( def identify_transients(
packages: PackageMap, dependency_name: str, allow_missing: Sequence[str] = () packages: PackageMap, dependency_name: str, allow_missing: Sequence[str] = ()
) -> Set[PoetryPackage]: ) -> List[PoetryPackage]:
"""Using a poetry object identify all dependencies of a specific dependency """Using a poetry object identify all dependencies of a specific dependency
: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.
@ -66,7 +66,11 @@ def find_transients(
""" """
from tox_poetry_installer import _poetry from tox_poetry_installer import _poetry
def find_deps_of_deps(name: str, searched: Set[str]) -> PackageMap: transients: List[PoetryPackage] = []
searched: Set[PoetryPackage] = set()
def find_deps_of_deps(name: str):
searched.add(name) searched.add(name)
if name in _poetry.Provider.UNSAFE_PACKAGES: if name in _poetry.Provider.UNSAFE_PACKAGES:
@ -76,9 +80,8 @@ def find_transients(
tox.reporter.verbosity2( tox.reporter.verbosity2(
f"{constants.REPORTER_PREFIX} Skip {name}: designated unsafe by Poetry" f"{constants.REPORTER_PREFIX} Skip {name}: designated unsafe by Poetry"
) )
return dict() return
transients: PackageMap = {}
try: try:
package = packages[name] package = packages[name]
except KeyError as err: except KeyError as err:
@ -86,7 +89,7 @@ def find_transients(
tox.reporter.verbosity2( tox.reporter.verbosity2(
f"{constants.REPORTER_PREFIX} Skip {name}: package is not in lockfile but designated as allowed to be missing" f"{constants.REPORTER_PREFIX} Skip {name}: package is not in lockfile but designated as allowed to be missing"
) )
return dict() return
raise err raise err
if not package.python_constraint.allows(constants.PLATFORM_VERSION): if not package.python_constraint.allows(constants.PLATFORM_VERSION):
@ -98,35 +101,29 @@ def find_transients(
f"{constants.REPORTER_PREFIX} Skip {package}: incompatible platform requirement '{package.platform}' for current platform '{sys.platform}'" f"{constants.REPORTER_PREFIX} Skip {package}: incompatible platform requirement '{package.platform}' for current platform '{sys.platform}'"
) )
else: else:
tox.reporter.verbosity2(
f"{constants.REPORTER_PREFIX} Including {package} for installation"
)
transients[name] = package
for index, dep in enumerate(package.requires): for index, dep in enumerate(package.requires):
tox.reporter.verbosity2( tox.reporter.verbosity2(
f"{constants.REPORTER_PREFIX} Processing dependency {index + 1}/{len(package.requires)} for {package}: {dep.name}" f"{constants.REPORTER_PREFIX} Processing dependency {index + 1}/{len(package.requires)} for {package}: {dep.name}"
) )
if dep.name not in searched: if dep.name not in searched:
transients.update(find_deps_of_deps(dep.name, searched)) find_deps_of_deps(dep.name)
else: else:
tox.reporter.verbosity2( tox.reporter.verbosity2(
f"{constants.REPORTER_PREFIX} Package with name '{dep.name}' has already been processed, skipping" f"{constants.REPORTER_PREFIX} Package with name '{dep.name}' has already been processed, skipping"
) )
tox.reporter.verbosity2(
return transients f"{constants.REPORTER_PREFIX} Including {package} for installation"
)
searched: Set[str] = set() transients.append(package)
try: try:
transients: PackageMap = find_deps_of_deps( find_deps_of_deps(packages[dependency_name].name)
packages[dependency_name].name, searched
)
except KeyError: except KeyError:
if dependency_name in _poetry.Provider.UNSAFE_PACKAGES: if dependency_name in _poetry.Provider.UNSAFE_PACKAGES:
tox.reporter.warning( tox.reporter.warning(
f"{constants.REPORTER_PREFIX} Installing package '{dependency_name}' using Poetry is not supported and will be skipped" f"{constants.REPORTER_PREFIX} Installing package '{dependency_name}' using Poetry is not supported and will be skipped"
) )
return set() return []
if any( if any(
delimiter in dependency_name delimiter in dependency_name
@ -140,7 +137,7 @@ def find_transients(
f"No version of locked dependency '{dependency_name}' found in the project lockfile" f"No version of locked dependency '{dependency_name}' found in the project lockfile"
) from None ) from None
return set(transients.values()) return transients
def check_preconditions(venv: ToxVirtualEnv, action: ToxAction) -> "_poetry.Poetry": def check_preconditions(venv: ToxVirtualEnv, action: ToxAction) -> "_poetry.Poetry":
@ -181,9 +178,9 @@ def check_preconditions(venv: ToxVirtualEnv, action: ToxAction) -> "_poetry.Poet
def find_project_dependencies( def find_project_dependencies(
venv: ToxVirtualEnv, poetry: "_poetry.Poetry", packages: PackageMap venv: ToxVirtualEnv, poetry: "_poetry.Poetry", packages: PackageMap
) -> List[PoetryPackage]: ) -> List[PoetryPackage]:
"""Install the dependencies of the project package """Find the root package dependencies
Install all primary dependencies of the project package. Recursively identify the root package dependencies
:param venv: Tox virtual environment to install the packages to :param venv: Tox virtual environment to install the packages to
:param poetry: Poetry object the packages were sourced from :param poetry: Poetry object the packages were sourced from
@ -212,8 +209,50 @@ def find_project_dependencies(
dependencies: List[PoetryPackage] = [] dependencies: List[PoetryPackage] = []
for dep in base_dependencies + extra_dependencies: for dep in base_dependencies + extra_dependencies:
dependencies += find_transients( dependencies += identify_transients(
packages, dep.name.lower(), allow_missing=[poetry.package.name] packages, dep.name.lower(), allow_missing=[poetry.package.name]
) )
return dependencies return dependencies
def find_dev_dependencies(
poetry: "_poetry.Poetry", packages: PackageMap
) -> List[PoetryPackage]:
"""Find the dev dependencies
Recursively identify the Poetry dev dependencies
:param venv: Tox virtual environment to install the packages to
:param poetry: Poetry object the packages were sourced from
:param packages: Mapping of package names to the corresponding package object
"""
dependencies: List[PoetryPackage] = []
for dep_name in (
poetry.pyproject.data["tool"]["poetry"].get("dev-dependencies", {}).keys()
):
dependencies += identify_transients(
packages, dep_name, allow_missing=[poetry.package.name]
)
return dependencies
def find_env_dependencies(
venv: ToxVirtualEnv, poetry: "_poetry.Poetry", packages: PackageMap
) -> List[PoetryPackage]:
"""Find the environment dependencies
Recursively identify the dependencies to install for the current environment
:param venv: Tox virtual environment to install the packages to
:param poetry: Poetry object the packages were sourced from
:param packages: Mapping of package names to the corresponding package object
"""
dependencies: List[PoetryPackage] = []
for dep in venv.envconfig.locked_deps:
dependencies += identify_transients(
packages, dep.lower(), allow_missing=[poetry.package.name]
)
return dependencies