Spec.dependencies: allow to filter on virtuals (#47284)

Signed-off-by: Massimiliano Culpo <massimiliano.culpo@gmail.com>
This commit is contained in:
Massimiliano Culpo 2024-10-30 12:15:01 +01:00 committed by GitHub
parent 9ac261af58
commit 354615d491
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 96 additions and 49 deletions

View File

@ -871,15 +871,6 @@ class UnhashableArguments(TypeError):
"""Raise when an @memoized function receives unhashable arg or kwarg values."""
def enum(**kwargs):
"""Return an enum-like class.
Args:
**kwargs: explicit dictionary of enums
"""
return type("Enum", (object,), kwargs)
T = TypeVar("T")

View File

@ -963,10 +963,6 @@ def _sort_by_dep_types(dspec: DependencySpec):
return dspec.depflag
#: Enum for edge directions
EdgeDirection = lang.enum(parent=0, child=1)
@lang.lazy_lexicographic_ordering
class _EdgeMap(collections.abc.Mapping):
"""Represent a collection of edges (DependencySpec objects) in the DAG.
@ -980,26 +976,20 @@ class _EdgeMap(collections.abc.Mapping):
__slots__ = "edges", "store_by_child"
def __init__(self, store_by=EdgeDirection.child):
# Sanitize input arguments
msg = 'unexpected value for "store_by" argument'
assert store_by in (EdgeDirection.child, EdgeDirection.parent), msg
def __init__(self, store_by_child: bool = True) -> None:
self.edges: Dict[str, List[DependencySpec]] = {}
self.store_by_child = store_by_child
#: This dictionary maps a package name to a list of edges
#: i.e. to a list of DependencySpec objects
self.edges = {}
self.store_by_child = store_by == EdgeDirection.child
def __getitem__(self, key):
def __getitem__(self, key: str) -> List[DependencySpec]:
return self.edges[key]
def __iter__(self):
return iter(self.edges)
def __len__(self):
def __len__(self) -> int:
return len(self.edges)
def add(self, edge: DependencySpec):
def add(self, edge: DependencySpec) -> None:
key = edge.spec.name if self.store_by_child else edge.parent.name
if key in self.edges:
lst = self.edges[key]
@ -1008,8 +998,8 @@ def add(self, edge: DependencySpec):
else:
self.edges[key] = [edge]
def __str__(self):
return "{deps: %s}" % ", ".join(str(d) for d in sorted(self.values()))
def __str__(self) -> str:
return f"{{deps: {', '.join(str(d) for d in sorted(self.values()))}}}"
def _cmp_iter(self):
for item in sorted(itertools.chain.from_iterable(self.edges.values())):
@ -1026,24 +1016,32 @@ def copy(self):
return clone
def select(self, parent=None, child=None, depflag: dt.DepFlag = dt.ALL):
"""Select a list of edges and return them.
def select(
self,
*,
parent: Optional[str] = None,
child: Optional[str] = None,
depflag: dt.DepFlag = dt.ALL,
virtuals: Optional[List[str]] = None,
) -> List[DependencySpec]:
"""Selects a list of edges and returns them.
If an edge:
- Has *any* of the dependency types passed as argument,
- Matches the parent and/or child name, if passed
- Matches the parent and/or child name
- Provides *any* of the virtuals passed as argument
then it is selected.
The deptypes argument needs to be a flag, since the method won't
convert it for performance reason.
Args:
parent (str): name of the parent package
child (str): name of the child package
parent: name of the parent package
child: name of the child package
depflag: allowed dependency types in flag form
Returns:
List of DependencySpec objects
virtuals: list of virtuals on the edge
"""
if not depflag:
return []
@ -1062,6 +1060,10 @@ def select(self, parent=None, child=None, depflag: dt.DepFlag = dt.ALL):
# Filter by allowed dependency types
selected = (dep for dep in selected if not dep.depflag or (depflag & dep.depflag))
# Filter by virtuals
if virtuals is not None:
selected = (dep for dep in selected if any(v in dep.virtuals for v in virtuals))
return list(selected)
def clear(self):
@ -1470,8 +1472,8 @@ def __init__(
self.architecture = None
self.compiler = None
self.compiler_flags = FlagMap(self)
self._dependents = _EdgeMap(store_by=EdgeDirection.parent)
self._dependencies = _EdgeMap(store_by=EdgeDirection.child)
self._dependents = _EdgeMap(store_by_child=False)
self._dependencies = _EdgeMap(store_by_child=True)
self.namespace = None
# initial values for all spec hash types
@ -1591,7 +1593,7 @@ def _get_dependency(self, name):
return deps[0]
def edges_from_dependents(
self, name=None, depflag: dt.DepFlag = dt.ALL
self, name=None, depflag: dt.DepFlag = dt.ALL, *, virtuals: Optional[List[str]] = None
) -> List[DependencySpec]:
"""Return a list of edges connecting this node in the DAG
to parents.
@ -1599,20 +1601,25 @@ def edges_from_dependents(
Args:
name (str): filter dependents by package name
depflag: allowed dependency types
virtuals: allowed virtuals
"""
return [d for d in self._dependents.select(parent=name, depflag=depflag)]
return [
d for d in self._dependents.select(parent=name, depflag=depflag, virtuals=virtuals)
]
def edges_to_dependencies(
self, name=None, depflag: dt.DepFlag = dt.ALL
self, name=None, depflag: dt.DepFlag = dt.ALL, *, virtuals: Optional[List[str]] = None
) -> List[DependencySpec]:
"""Return a list of edges connecting this node in the DAG
to children.
"""Returns a list of edges connecting this node in the DAG to children.
Args:
name (str): filter dependencies by package name
depflag: allowed dependency types
virtuals: allowed virtuals
"""
return [d for d in self._dependencies.select(child=name, depflag=depflag)]
return [
d for d in self._dependencies.select(child=name, depflag=depflag, virtuals=virtuals)
]
@property
def edge_attributes(self) -> str:
@ -1635,17 +1642,24 @@ def edge_attributes(self) -> str:
return f"[{result}]"
def dependencies(
self, name=None, deptype: Union[dt.DepTypes, dt.DepFlag] = dt.ALL
self,
name=None,
deptype: Union[dt.DepTypes, dt.DepFlag] = dt.ALL,
*,
virtuals: Optional[List[str]] = None,
) -> List["Spec"]:
"""Return a list of direct dependencies (nodes in the DAG).
"""Returns a list of direct dependencies (nodes in the DAG)
Args:
name (str): filter dependencies by package name
name: filter dependencies by package name
deptype: allowed dependency types
virtuals: allowed virtuals
"""
if not isinstance(deptype, dt.DepFlag):
deptype = dt.canonicalize(deptype)
return [d.spec for d in self.edges_to_dependencies(name, depflag=deptype)]
return [
d.spec for d in self.edges_to_dependencies(name, depflag=deptype, virtuals=virtuals)
]
def dependents(
self, name=None, deptype: Union[dt.DepTypes, dt.DepFlag] = dt.ALL
@ -3519,8 +3533,8 @@ def _dup(self, other, deps: Union[bool, dt.DepTypes, dt.DepFlag] = True, clearde
self.architecture = other.architecture.copy() if other.architecture else None
self.compiler = other.compiler.copy() if other.compiler else None
if cleardeps:
self._dependents = _EdgeMap(store_by=EdgeDirection.parent)
self._dependencies = _EdgeMap(store_by=EdgeDirection.child)
self._dependents = _EdgeMap(store_by_child=False)
self._dependencies = _EdgeMap(store_by_child=True)
self.compiler_flags = other.compiler_flags.copy()
self.compiler_flags.spec = self
self.variants = other.variants.copy()

View File

@ -756,6 +756,48 @@ def test_spec_tree_respect_deptypes(self):
out = s.tree(deptypes=("link", "run"))
assert "version-test-pkg" not in out
@pytest.mark.parametrize(
"query,expected_length,expected_satisfies",
[
({"virtuals": ["mpi"]}, 1, ["mpich", "mpi"]),
({"depflag": dt.BUILD}, 2, ["mpich", "mpi", "callpath"]),
({"depflag": dt.BUILD, "virtuals": ["mpi"]}, 1, ["mpich", "mpi"]),
({"depflag": dt.LINK}, 2, ["mpich", "mpi", "callpath"]),
({"depflag": dt.BUILD | dt.LINK}, 2, ["mpich", "mpi", "callpath"]),
({"virtuals": ["lapack"]}, 0, []),
],
)
def test_query_dependency_edges(
self, default_mock_concretization, query, expected_length, expected_satisfies
):
"""Tests querying edges to dependencies on the following DAG:
[ ] mpileaks@=2.3
[bl ] ^callpath@=1.0
[bl ] ^dyninst@=8.2
[bl ] ^libdwarf@=20130729
[bl ] ^libelf@=0.8.13
[bl ] ^mpich@=3.0.4
"""
mpileaks = default_mock_concretization("mpileaks")
edges = mpileaks.edges_to_dependencies(**query)
assert len(edges) == expected_length
for constraint in expected_satisfies:
assert any(x.spec.satisfies(constraint) for x in edges)
def test_query_dependents_edges(self, default_mock_concretization):
"""Tests querying edges from dependents"""
mpileaks = default_mock_concretization("mpileaks")
mpich = mpileaks["mpich"]
# Recover the root with 2 different queries
edges_of_link_type = mpich.edges_from_dependents(depflag=dt.LINK)
edges_with_mpi = mpich.edges_from_dependents(virtuals=["mpi"])
assert edges_with_mpi == edges_of_link_type
# Check a node dependend upon by 2 parents
assert len(mpileaks["libelf"].edges_from_dependents(depflag=dt.LINK)) == 2
def test_tree_cover_nodes_reduce_deptype():
"""Test that tree output with deptypes sticks to the sub-dag of interest, instead of looking