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

Reporting overrides in the graph #13680

Merged
merged 4 commits into from
Apr 13, 2023
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
6 changes: 6 additions & 0 deletions conan/cli/printers/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ def _format_resolved(title, reqs_to_print):
output.warning("Consider using version-ranges instead.")
_format_resolved("Resolved version ranges", graph.resolved_ranges)

overrides = graph.overrides()
if overrides:
output.info("Overrides", Color.BRIGHT_YELLOW)
for req, override_info in overrides.serialize().items():
output.info(" {}: {}".format(req, override_info), Color.BRIGHT_CYAN)

if deprecated:
output.info("Deprecated", Color.BRIGHT_YELLOW)
for d, reason in deprecated.items():
Expand Down
57 changes: 57 additions & 0 deletions conans/client/graph/graph.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from collections import OrderedDict

from conans.model.package_ref import PkgReference
from conans.model.recipe_ref import RecipeReference

RECIPE_DOWNLOADED = "Downloaded"
RECIPE_INCACHE = "Cache" # The previously installed recipe in cache is being used
Expand Down Expand Up @@ -233,13 +234,69 @@ def __init__(self, src, dst, require):
self.require = require


class Overrides:
def __init__(self):
self._overrides = {} # {require_ref: {override_ref1, override_ref2}}

def __bool__(self):
return bool(self._overrides)

@staticmethod
def create(nodes):
overrides = {}
for n in nodes:
for r in n.conanfile.requires.values():
if r.override:
continue
if r.overriden_ref and not r.force:
overrides.setdefault(r.overriden_ref, set()).add(r.ref)
else:
overrides.setdefault(r.ref, set()).add(None)

# reduce, eliminate those overrides definitions that only override to None, that is, not
# really an override
result = Overrides()
for require, override_info in overrides.items():
if len(override_info) != 1 or None not in override_info:
result._overrides[require] = override_info
return result

def get(self, require):
return self._overrides.get(require)

def update(self, other):
"""
@type other: Overrides
"""
for require, override_info in other._overrides.items():
self._overrides.setdefault(require, set()).update(override_info)

def items(self):
return self._overrides.items()

def serialize(self):
return {k.repr_notime(): [e.repr_notime() if e else None for e in v]
for k, v in self._overrides.items()}

@staticmethod
def deserialize(data):
result = Overrides()
result._overrides = {RecipeReference.loads(k):
set([RecipeReference.loads(e) if e else None for e in v])
for k, v in data.items()}
return result


class DepsGraph(object):
def __init__(self):
self.nodes = []
self.aliased = {}
self.resolved_ranges = {}
self.error = False

def overrides(self):
return Overrides.create(self.nodes)

def __repr__(self):
return "\n".join((repr(n) for n in self.nodes))

Expand Down
1 change: 1 addition & 0 deletions conans/client/graph/graph_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def _expand_require(self, require, node, graph, profile_host, profile_build, gra

prev_ref = prev_node.ref if prev_node else prev_require.ref
if prev_require.force or prev_require.override: # override
require.overriden_ref = require.ref # Store that the require has been overriden
require.ref = prev_ref
else:
self._conflicting_version(require, node, prev_require, prev_node,
Expand Down
1 change: 1 addition & 0 deletions conans/model/requires.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def __init__(self, ref, *, headers=None, libs=None, build=False, run=None, visib
self._override = override
self._direct = direct
self.options = options
self.overriden_ref = None # to store if the requirement has been overriden (store old ref)

@property
def skip(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ def test_missing_dep(self):
.with_require("dep2/1.0@lasote/testing")
client.save({"conanfile.py": conanfile}, clean_first=True)
client.run("create . --user=lasote --channel=testing", assert_error=True)
client.assert_overrides({"dep1/1.0@lasote/testing":
['dep1/2.0@lasote/testing']})

self.assertIn("Can't find a 'dep2/1.0@lasote/testing' package", client.out)
self.assertIn("dep1/2.Y.Z", client.out)
Expand Down
16 changes: 16 additions & 0 deletions conans/test/integration/graph/conflict_diamond_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ def test_version_diamond_conflict(self):
"""
test that we obtain a version conflict with a diamond, and that we can fix it by
defining an override in the "game" consumer
game -> engine/1.0 -> math/1.0
|---> ai/1.0 -----> math/1.0.1 (conflict)
"""
c = TestClient()
c.save({"math/conanfile.py": GenConanfile("math"),
Expand All @@ -29,22 +31,36 @@ def test_version_diamond_conflict(self):

def _game_conanfile(version, reverse=False):
if reverse:
"""
game ---(override)--_> math/newversion
|---> engine/1.0 -> math/1.0
|---> ai/1.0 -----> math/1.0.1 (conflict solved by override)
"""
return GenConanfile("game", "1.0")\
.with_requirement(f"math/{version}", override=True)\
.with_requirement("engine/1.0")\
.with_requirement("ai/1.0")
else:
"""
game --> engine/1.0 -> math/1.0
|---> ai/1.0 -----> math/1.0.1 (conflict solved by override)
|---(override)--_> math/newversion
"""
return GenConanfile("game", "1.0").with_requirement("engine/1.0") \
.with_requirement("ai/1.0") \
.with_requirement(f"math/{version}", override=True)

for v in ("1.0", "1.0.1", "1.0.2"):
c.save({"game/conanfile.py": _game_conanfile(v)})
c.run("install game")
c.assert_overrides({"math/1.0": [f"math/{v}"],
"math/1.0.1": [f"math/{v}#8e1a7a5ce869d8c54ae3d33468fd657c"]})
c.assert_listed_require({f"math/{v}": "Cache"})

# Check that order of requirements doesn't affect
for v in ("1.0", "1.0.1", "1.0.2"):
c.save({"game/conanfile.py": _game_conanfile(v, reverse=True)})
c.run("install game")
c.assert_overrides({"math/1.0": [f"math/{v}"],
"math/1.0.1": [f"math/{v}#8e1a7a5ce869d8c54ae3d33468fd657c"]})
c.assert_listed_require({f"math/{v}": "Cache"})
16 changes: 16 additions & 0 deletions conans/test/utils/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,22 @@ def assert_listed_require(self, requires, build=False, python=False, test=False,
else:
raise AssertionError(f"Cant find {r}-{kind} in {reqs}")

def assert_overrides(self, overrides):
""" parses the current command output, and extract the first "Requirements" section
"""
lines = self.out.splitlines()
header = "Overrides"
line_req = lines.index(header)
reqs = []
for line in lines[line_req+1:]:
if not line.startswith(" "):
break
reqs.append(line.strip())
for r, o in overrides.items():
msg = f"{r}: {o}"
if msg not in reqs:
raise AssertionError(f"Cant find {msg} in {reqs}")

def assert_listed_binary(self, requires, build=False, test=False, test_package=False):
""" parses the current command output, and extract the second "Requirements" section
belonging to the computed package binaries
Expand Down