diff options
author | Massimiliano Culpo <massimiliano.culpo@gmail.com> | 2024-10-30 12:15:01 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-10-30 12:15:01 +0100 |
commit | 354615d4918d5ceb42b9477b068ce525c54f9ad9 (patch) | |
tree | 173294954cced5a6041436f1724628217bfc4f5e /lib | |
parent | 9ac261af584c1ad6bdbcbad8b5ab1aec9283fa11 (diff) | |
download | spack-354615d4918d5ceb42b9477b068ce525c54f9ad9.tar.gz spack-354615d4918d5ceb42b9477b068ce525c54f9ad9.tar.bz2 spack-354615d4918d5ceb42b9477b068ce525c54f9ad9.tar.xz spack-354615d4918d5ceb42b9477b068ce525c54f9ad9.zip |
Spec.dependencies: allow to filter on virtuals (#47284)
Signed-off-by: Massimiliano Culpo <massimiliano.culpo@gmail.com>
Diffstat (limited to 'lib')
-rw-r--r-- | lib/spack/llnl/util/lang.py | 9 | ||||
-rw-r--r-- | lib/spack/spack/spec.py | 94 | ||||
-rw-r--r-- | lib/spack/spack/test/spec_dag.py | 42 |
3 files changed, 96 insertions, 49 deletions
diff --git a/lib/spack/llnl/util/lang.py b/lib/spack/llnl/util/lang.py index 326d25e79b..f43773346a 100644 --- a/lib/spack/llnl/util/lang.py +++ b/lib/spack/llnl/util/lang.py @@ -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") diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py index d64507a9a1..fc65dcb64b 100644 --- a/lib/spack/spack/spec.py +++ b/lib/spack/spack/spec.py @@ -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 - - #: 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 __init__(self, store_by_child: bool = True) -> None: + self.edges: Dict[str, List[DependencySpec]] = {} + self.store_by_child = store_by_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 @@ class _EdgeMap(collections.abc.Mapping): 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 @@ class _EdgeMap(collections.abc.Mapping): 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 @@ class _EdgeMap(collections.abc.Mapping): # 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 @@ class Spec: 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 @@ class Spec: 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 @@ class Spec: 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 @@ class Spec: 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 @@ class Spec: 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() diff --git a/lib/spack/spack/test/spec_dag.py b/lib/spack/spack/test/spec_dag.py index 0d1fe4abcf..05cc15f3da 100644 --- a/lib/spack/spack/test/spec_dag.py +++ b/lib/spack/spack/test/spec_dag.py @@ -756,6 +756,48 @@ class TestSpecDag: 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 |