Skip to content

Commit

Permalink
chore: improve readability and add code explanations
Browse files Browse the repository at this point in the history
  • Loading branch information
bruno-fs committed Aug 9, 2024
1 parent 3cbade5 commit e9ecc75
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 43 deletions.
70 changes: 43 additions & 27 deletions src/pybuild_deps/compile_build_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from __future__ import annotations

from functools import partial
from typing import Iterable

from pip._internal.exceptions import DistributionNotFound
Expand Down Expand Up @@ -42,50 +43,65 @@ def resolve(
log.info(str(ireq))
log.info("-" * 80)
req_version = get_version(ireq)
version_tuple = ireq.name, req_version
if version_tuple in self.dependency_cache:
all_build_deps.extend(self.dependency_cache[version_tuple])
ireq_name = f"{ireq.name}=={req_version}"
if ireq_name in self.dependency_cache:
all_build_deps.extend(self.dependency_cache[ireq_name])
log.debug(f"{ireq} exists in our cache, moving on...")
continue
raw_build_dependencies = find_build_dependencies(
ireq.name, req_version, raise_setuppy_parsing_exc=False
)
if not raw_build_dependencies:
self.dependency_cache[version_tuple] = set()
self.dependency_cache[ireq_name] = set()
continue

constraints = []
for raw_build_req in raw_build_dependencies:
build_req = install_req_from_req_string(
raw_build_req, comes_from=ireq.name
)
constraints.append(build_req)
# override resolver - we only want the latest and greatest
self.resolver = BacktrackingResolver(
constraints=constraints,
existing_constraints={},
repository=self.repository,
allow_unsafe=True,
# 'find_build_dependencies' is very naive - by design - and only returns
# a simple list of strings representing build (or transitive) dependencies.
# We will use the excellent resolver from piptools to find dependencies of
# build dependencies, but first we need to convert our list of requirements
# to the format used by piptools
build_ireqs = map(
partial(install_req_from_req_string, comes_from=ireq.name),
raw_build_dependencies,
)
build_dependencies = self._resolve_with_piptools(
package=ireq_name, ireqs=build_ireqs
)
try:
build_dependencies = self.resolver.resolve()
except DistributionNotFound as err:
if isinstance(err.__cause__, ResolutionImpossible): # pragma: no cover
unsolvable_deps = err.__cause__.args
raise UnsolvableDependenciesError( # noqa: B904
unsolvable_deps, constraints
)
raise err
# dependencies of build dependencies might have their own build
# dependencies, so let's recursively search for those.
build_deps_qty = 0
while len(build_dependencies) != build_deps_qty:
build_deps_qty = len(build_dependencies)
build_dependencies |= set(self.resolve(build_dependencies))
self.dependency_cache[version_tuple] = build_dependencies
self.dependency_cache[ireq_name] = build_dependencies

all_build_deps.extend(build_dependencies)

return deduplicate_install_requirements(all_build_deps)

def _resolve_with_piptools(
self, package: str, ireqs: Iterable[InstallRequirement]
) -> set[InstallRequirement]:
# backup unsafe data before overriding resolver, we will need it later
# on piptools writer to export the file
unsafe_packages = getattr(self.resolver, "unsafe_packages", set())
unsafe_constraints = getattr(self.resolver, "unsafe_constraints", set())
# override resolver - we only want the latest and greatest
self.resolver = BacktrackingResolver(
constraints=ireqs,
existing_constraints={},
repository=self.repository,
allow_unsafe=True,
)
try:
requirements = self.resolver.resolve()
except DistributionNotFound as err:
if isinstance(err.__cause__, ResolutionImpossible):
raise UnsolvableDependenciesError(package, err.__cause__.args) # noqa: B904
raise err
self.resolver.unsafe_packages = unsafe_packages
self.resolver.unsafe_constraints = unsafe_constraints
return requirements


def deduplicate_install_requirements(ireqs: Iterable[InstallRequirement]):
"""Deduplicate InstallRequirements."""
Expand Down
30 changes: 18 additions & 12 deletions src/pybuild_deps/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
"""custom exceptions for pybuild-deps."""

from __future__ import annotations

from typing import Iterable

from pip._internal.req import InstallRequirement
from pip._vendor.resolvelib.resolvers import RequirementInformation


class PyBuildDepsError(Exception):
"""Custom exception for pybuild-deps."""
Expand All @@ -8,20 +15,19 @@ class PyBuildDepsError(Exception):
class UnsolvableDependenciesError(PyBuildDepsError):
"""Unsolvable dependencies."""

def __init__(self, unsolvable_deps, constraints) -> None:
def __init__(
self,
package: InstallRequirement,
unsolvable_deps: Iterable[Iterable[RequirementInformation]],
):
self.unsolvable_deps = unsolvable_deps
self.constraints = constraints
self.package = package

def __str__(self):
packages = {req_list[0].requirement.name for req_list in self.unsolvable_deps}
packages |= {p.replace("-", "_") for p in packages}
packages |= {p.replace("_", "-") for p in packages}
unsolvable_deps = []
for constraint in self.constraints:
if constraint.name in packages:
unsolvable_deps.append(constraint)
unsolvable_deps = "\n".join(str(p) for p in unsolvable_deps)
unsolvable_deps = "\n".join(
str(d.requirement) for deps in self.unsolvable_deps for d in deps
)
return (
"Impossible resolve dependencies. See the conflicting dependencies "
f"below:\n{unsolvable_deps}"
f"Impossible to resolve the following dependencies for package "
f"'{self.package}':\n{unsolvable_deps}"
)
14 changes: 13 additions & 1 deletion tests/test_compile_build_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from pybuild_deps.compile_build_dependencies import BuildDependencyCompiler
from pybuild_deps.constants import PIP_CACHE_DIR
from pybuild_deps.exceptions import PyBuildDepsError
from pybuild_deps.exceptions import PyBuildDepsError, UnsolvableDependenciesError


@pytest.fixture
Expand Down Expand Up @@ -41,3 +41,15 @@ def test_dependency_with_complex_setup_py(compiler, caplog):
ireq = install_req_from_req_string("grpcio==1.59.0")
compiler.resolve([ireq])
assert caplog.messages[-1] == "Unable to parse setup.py for package grpcio==1.59.0."


def test_unsolvable_dependencies(compiler):
"""Test trying to solve impossible dependency combinations."""
ireqs = map(install_req_from_req_string, ("setuptools<42", "setuptools>=42"))

expected_error_msg = (
"Impossible to resolve the following dependencies for package 'foo=1.2.3':"
"\nsetuptools<42\nsetuptools>=42"
)
with pytest.raises(UnsolvableDependenciesError, match=expected_error_msg):
compiler._resolve_with_piptools("foo=1.2.3", ireqs)
7 changes: 4 additions & 3 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,12 @@ def test_compile_unsolvable_dependencies(runner: CliRunner, tmp_path: Path, mock
outfile = tmp_path / "outfile"
mocker.patch(
"pybuild_deps.compile_build_dependencies.find_build_dependencies",
return_value=["bar>=42", "bar<42"],
return_value=["setuptools>=42", "setuptools<42"],
)
result = runner.invoke(main.cli, args=["compile", "-o", str(outfile)])
assert result.exit_code == 2
assert (
"Impossible resolve dependencies. See the conflicting dependencies "
"below:\nbar>=42 (from foo)\nbar<42 (from foo)" in result.stderr
"Impossible to resolve the following dependencies for package 'foo==0.1.2':\n"
"setuptools>=42\n"
"setuptools<42" in result.stderr
)

0 comments on commit e9ecc75

Please sign in to comment.