Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update the format of the solution file to allow the source of extras … #80

Merged
merged 2 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

module(
name = "rules_req_compile",
version = "1.0.0rc21",
version = "1.0.0rc22",
)
20 changes: 15 additions & 5 deletions req_compile/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,12 +346,12 @@ def __init__(self, node: DependencyNode, multiline):

def __str__(self):
write_to = StringIO()
constraints = list(req_compile.dists.build_constraints(self.node))
constraints = req_compile.dists.build_explanation(self.node)

if len(constraints) == 1:
if self.multiline:
write_to.write("via ")
write_to.write(f"{constraints[0]}")
write_to.write(f"{next(iter(constraints))}")
else:
if self.multiline:
write_to.write("via\n")
Expand Down Expand Up @@ -763,22 +763,25 @@ def compile_main(raw_args: Optional[Sequence[str]] = None) -> None:
"from stdin.",
)
group.add_argument(
"-c,--constraints",
"-c",
"--constraints",
action="append",
dest="constraints",
metavar="constraints_file",
help="Constraints file or project directory to use as constraints.",
)
group.add_argument(
"-e,--extra",
"-e",
"--extra",
action="append",
dest="extras",
default=[],
metavar="extra",
help="Extras to apply automatically to source packages.",
)
group.add_argument(
"-P,--upgrade-package",
"-P",
"--upgrade-package",
action="append",
dest="upgrade_packages",
metavar="package_name",
Expand All @@ -796,6 +799,12 @@ def compile_main(raw_args: Optional[Sequence[str]] = None) -> None:
action="store_true",
help="Remove distributions not satisfied via --source from the output.",
)
group.add_argument(
"--remove-constraints",
default=False,
action="store_true",
help="Remove constraints pins from the output.",
)
group.add_argument(
"-p",
"--pre",
Expand Down Expand Up @@ -979,6 +988,7 @@ def compile_main(raw_args: Optional[Sequence[str]] = None) -> None:
repo,
extras=args.extras,
constraint_reqs=constraint_reqs,
remove_constraints=args.remove_constraints,
only_binary=args.only_binary,
)
except RepositoryInitializationError as ex:
Expand Down
14 changes: 10 additions & 4 deletions req_compile/compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ def perform_compile(
input_reqs: Iterable[RequirementContainer],
repo: Repository,
constraint_reqs: Optional[Iterable[RequirementContainer]] = None,
remove_constraints: bool = False,
extras: Optional[Iterable[str]] = None,
allow_circular_dependencies: bool = True,
only_binary: Optional[Set[NormName]] = None,
Expand All @@ -282,6 +283,9 @@ def perform_compile(
repo: Repository to use as a source of Python packages.
extras: Extras to apply automatically to source projects
constraint_reqs: Constraints to use when compiling
remove_constraints: Whether to remove the constraints from the solution. By default,
constraints are added, so you can see why a requirement was pinned to a particular
version.
allow_circular_dependencies: Whether to allow circular dependencies
only_binary: Set of projects that should only consider binary distributions.
max_downgrade: The maximum number of version downgrades that will be allowed for conflicts.
Expand Down Expand Up @@ -333,13 +337,15 @@ def perform_compile(
node, None, repo, results, options, max_downgrade=max_downgrade
)
except (NoCandidateException, MetadataError) as ex:
_add_constraints(all_pinned, constraint_reqs, results)
if not remove_constraints:
_add_constraints(all_pinned, constraint_reqs, results)
ex.results = results
raise

# Add the constraints in, so it will show up as a contributor in the results.
# The same is done in the exception block above
_add_constraints(all_pinned, constraint_reqs, results)
if not remove_constraints:
# Add the constraints in, so it will show up as a contributor in the results.
# The same is done in the exception block above
_add_constraints(all_pinned, constraint_reqs, results)

return results, roots

Expand Down
97 changes: 31 additions & 66 deletions req_compile/dists.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,8 @@
import itertools
import logging
import sys
from typing import (
Any,
Callable,
Dict,
Iterable,
Iterator,
List,
Optional,
Set,
Tuple,
Union,
)
from typing import Any, Dict, Iterable, Iterator, List, Optional, Set, Union

import packaging.requirements
import packaging.version
import pkg_resources

from req_compile.containers import RequirementContainer
Expand Down Expand Up @@ -68,6 +55,7 @@ def __lt__(self, other: Any) -> bool:

@property
def extras(self) -> Set[str]:
"""Extras for this node that its reverse dependencies have requested."""
extras = set()
for rdep in self.reverse_deps:
assert (
Expand Down Expand Up @@ -112,7 +100,12 @@ def build_constraints(self) -> pkg_resources.Requirement:
return result


def build_constraints(root_node: DependencyNode) -> Iterable[str]:
def build_explanation(root_node: DependencyNode) -> collections.abc.Collection[str]:
"""Build an explanation for why a node was included in the solution.

The explanation provides the version constraints supplied by the reverse
dependencies for this node.
"""
constraints: List[str] = []
for node in root_node.reverse_deps:
assert (
Expand All @@ -123,26 +116,41 @@ def build_constraints(root_node: DependencyNode) -> Iterable[str]:
all_reqs |= set(node.metadata.requires(extra=extra))
for req in all_reqs:
if normalize_project_name(req.project_name) == root_node.key:
_process_constraint_req(req, node, constraints)
constraints.append(_process_constraint_req(req, node))
return constraints


def _process_constraint_req(
req: pkg_resources.Requirement, node: DependencyNode, constraints: List[str]
) -> None:
req: pkg_resources.Requirement, node: DependencyNode
) -> str:
assert node.metadata is not None, "Node {} must be solved".format(node)
extra = None
extras: Set[str] = set()
# Determine which extras, if any, were the reason this req was included.
if req.marker:
for marker in req.marker._markers: # pylint: disable=protected-access
if (
isinstance(marker, tuple)
and marker[0].value == "extra"
and marker[1].value == "=="
):
extra = marker[2].value
source = node.metadata.name + (("[" + extra + "]") if extra else "")
specifics = " (" + str(req.specifier) + ")" if req.specifier else "" # type: ignore[attr-defined]
constraints.extend([source + specifics])
extras.add(marker[2].value.strip().lower())
source = node.metadata.name + (
("[" + ",".join(sorted(extras)) + "]") if extras else ""
)

specifics = ""
if req.specifier: # type: ignore[attr-defined]
specifics = str(req.specifier) # type: ignore[attr-defined]

# Determine which extras this req was itself requesting.
if req.extras:
specifics += (
f" [{','.join(sorted(extra.strip().lower() for extra in req.extras))}]"
)

if specifics:
specifics = f" ({specifics.strip()})"
return source + specifics


class DistributionCollection:
Expand Down Expand Up @@ -273,15 +281,6 @@ def remove_dists(
node.metadata = None
node.complete = False

def build(
self, roots: Iterable[DependencyNode]
) -> Iterable[pkg_resources.Requirement]:
results = self.generate_lines(roots)
return [
parse_requirement("==".join([result[0][0], str(result[0][1])]))
for result in results
]

def visit_nodes(
self,
roots: Iterable[DependencyNode],
Expand Down Expand Up @@ -320,40 +319,6 @@ def visit_nodes(
)
return _visited

def generate_lines(
self,
roots: Iterable[DependencyNode],
req_filter: Optional[Callable[[DependencyNode], bool]] = None,
strip_extras: bool = False,
) -> Iterable[
Tuple[Tuple[str, Optional[packaging.version.Version], Optional[str]], str]
]:
"""
Generate the lines of a results file from this collection
Args:
roots (iterable[DependencyNode]): List of roots to generate lines from
req_filter (Callable): Filter to apply to each element of the collection.
Return True to keep a node, False to exclude it
Returns:
(list[str]) List of rendered node entries in the form of
reqname==version # reasons
"""
req_filter = req_filter or (lambda _: True)
results: List[
Tuple[Tuple[str, Optional[packaging.version.Version], Optional[str]], str]
] = []
for node in self.visit_nodes(roots):
if node.metadata is None:
continue
if not node.metadata.meta and req_filter(node):
constraints = build_constraints(node)
name, version = node.metadata.to_definition(node.extras)
if strip_extras:
name = name.split("[", 1)[0]
constraint_text = ", ".join(sorted(constraints))
results.append(((name, version, node.metadata.hash), constraint_text))
return results

def __contains__(self, project_name: str) -> bool:
req_name = project_name.split("[")[0]
return normalize_project_name(req_name) in self.nodes
Expand Down
22 changes: 19 additions & 3 deletions req_compile/repos/solution.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,10 +249,12 @@ def _add_sources(
url: Optional[str] = None,
dist_hash: Optional[str] = None,
) -> None:
pkg_names = map(lambda x: x.split(" ")[0], sources)
pkg_names = map(lambda x: x.split(" ", 1)[0], sources)
constraints = map(
lambda x: (
x.split(" ")[1].replace("(", "").replace(")", "") if "(" in x else None
x.split(" ", 1)[1].replace("(", "").replace(")", "")
if "(" in x
else None
),
sources,
)
Expand Down Expand Up @@ -330,10 +332,24 @@ def _create_metadata_req(
extra = next(iter(req_compile.utils.parse_requirement(name).extras))
marker = ' ; extra == "{}"'.format(extra)

# req will only have extras if the solution file had them in the left-hand
# side of == expression, e.g. req[extra]==1.0. Since pip doesn't support having
# extras on the left-hand side for constraints files, we don't emit this
# any longer.
extras = req.extras
if constraints and ("[" in constraints and "]" in constraints):
# Parse out the extras that brought in this requirement. It will look like
# (>1.0 [extra1,extra2]). Usually it would just be one unless the distribution
# includes a requirement under multiple extras.
constraints, extra_string = constraints.split("[", 1)
constraints = constraints.strip()
extra_string = extra_string.replace("]", "")
extras = {extra.strip() for extra in extra_string.split(",")}

return req_compile.utils.parse_requirement(
"{}{}{}{}".format(
metadata.name,
("[" + ",".join(req.extras) + "]") if req.extras else "",
("[" + ",".join(sorted(extras)) + "]") if extras else "",
constraints if constraints else "",
marker,
)
Expand Down
13 changes: 13 additions & 0 deletions tests/repos/requests_kerberos_solution.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
certifi==2024.6.2 # requests (>=2017.4.17)
cffi==1.15.1 # cryptography (>=1.12)
charset-normalizer==3.3.2 # requests (<4,>=2)
cryptography==42.0.8 # pyspnego, requests-kerberos (>=1.3)
decorator==5.1.1 # gssapi
gssapi==1.8.2 # pyspnego[kerberos] (>=1.6.0)
idna==3.7 # requests (<4,>=2.5)
krb5==0.5.1 # pyspnego[kerberos] (>=0.3.0)
pycparser==2.21 # cffi
pyspnego==0.9.2 # requests-kerberos (>=0.9.2 [kerberos])
requests==2.31.0 # requests-kerberos (>=1.1.0)
requests-kerberos==0.14.0 # -
urllib3==2.0.7 # requests (<3,>=1.21.1)
12 changes: 11 additions & 1 deletion tests/repos/test_solution.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,6 @@ def test_round_trip(


def test_writing_repo_sources(mock_metadata, mock_pypi, tmp_path):

mock_pypi.load_scenario("normal")

results, nodes = req_compile.compile.perform_compile(
Expand Down Expand Up @@ -230,6 +229,17 @@ def test_load_additive_constraints():
assert constraints == pkg_resources.Requirement.parse("idna<2.9,>=2.5")


def test_load_extras() -> None:
"""Test that if the correct extras are associated with the correct requirements."""
solution_repo = SolutionRepository(
os.path.join(os.path.dirname(__file__), "requests_kerberos_solution.txt")
)
assert solution_repo.solution["pyspnego"].extras == {"kerberos"}
assert [req for req in solution_repo.solution["requests-kerberos"].metadata.requires(
None
) if req.project_name=="pyspnego"][0] == parse_requirement("pyspnego[kerberos]>=0.9.2")


def test_load_extra_first():
"""Test that solutions that refer to a requirement with an extra before it is defined correctly
add the requirement with the extra"""
Expand Down
12 changes: 7 additions & 5 deletions tests/test_compile.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# pylint: disable=redefined-outer-name
import os
from typing import Iterable, Optional, Tuple
from typing import Iterable, Optional, Set, Tuple
from unittest import mock

import pkg_resources
Expand All @@ -14,6 +14,7 @@
import req_compile.utils
from req_compile.compile import AllOnlyBinarySet
from req_compile.containers import DistInfo, RequirementContainer
from req_compile.dists import DependencyNode, DistributionCollection
from req_compile.repos.multi import MultiRepository
from req_compile.repos.repository import Candidate, Repository, filename_to_candidate
from req_compile.repos.source import SourceRepository
Expand All @@ -29,10 +30,11 @@ def test_mock_pypi(mock_pypi):
assert metadata.version == req_compile.utils.parse_version("1.0.0")


def _real_outputs(results):
outputs = results[0].build(results[1])
outputs = sorted(outputs, key=lambda x: x.name)
return set(str(req) for req in outputs)
def _real_outputs(results: Tuple[DistributionCollection, Set[DependencyNode]]):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why change this format?

return set(
"{}=={}".format(*node.metadata.to_definition(node.extras))
for node in results[0].visit_nodes(results[1])
)


@fixture
Expand Down
Loading
Loading