diff --git a/src/rapids_pre_commit_hooks/alpha_spec.py b/src/rapids_pre_commit_hooks/alpha_spec.py index 7dfb800..01cc317 100644 --- a/src/rapids_pre_commit_hooks/alpha_spec.py +++ b/src/rapids_pre_commit_hooks/alpha_spec.py @@ -1,10 +1,11 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import argparse import os import re from functools import cache, total_ordering +from typing import Any, Optional import yaml from packaging.requirements import InvalidRequirement, Requirement @@ -12,6 +13,7 @@ from rapids_metadata.remote import fetch_latest from .lint import Linter, LintMain +from .utils.dependencies_yaml import Handler, traverse_dependencies_yaml ALPHA_SPECIFIER: str = ">=0.0.0a0" @@ -28,10 +30,6 @@ def all_metadata() -> "RAPIDSMetadata": return fetch_latest() -def node_has_type(node: "yaml.Node", tag_type: str) -> bool: - return node.tag == f"tag:yaml.org,2002:{tag_type}" - - def get_rapids_version(args: argparse.Namespace) -> "RAPIDSVersion": md = all_metadata() return ( @@ -49,277 +47,87 @@ def strip_cuda_suffix(args: argparse.Namespace, name: str) -> str: return name -def check_and_mark_anchor( - anchors: dict[str, "yaml.Node"], used_anchors: set[str], node: "yaml.Node" -) -> tuple[bool, str | None]: - for key, value in anchors.items(): - if value == node: - anchor = key - break - else: - anchor = None - if anchor in used_anchors: - return False, anchor - if anchor is not None: - used_anchors.add(anchor) - return True, anchor - - -def check_package_spec( - linter: Linter, - args: argparse.Namespace, - anchors: dict[str, "yaml.Node"], - used_anchors: set[str], - node: "yaml.Node", -) -> None: - @total_ordering - class SpecPriority: - def __init__(self, spec: str): - self.spec: str = spec - - def __eq__(self, other: object) -> bool: - assert isinstance(other, SpecPriority) - return self.spec == other.spec +class AlphaSpecHandler(Handler): + def __init__(self, linter: Linter, args: argparse.Namespace): + self.linter = linter + self.args = args + + def handle_package( + self, + packages_context: "Any", # noqa: ARG002 + anchor: "Optional[str]", + node: "yaml.Node", + ) -> None: + @total_ordering + class SpecPriority: + def __init__(self, spec: str): + self.spec: str = spec + + def __eq__(self, other: object) -> bool: + assert isinstance(other, SpecPriority) + return self.spec == other.spec + + def __lt__(self, other: object) -> bool: + assert isinstance(other, SpecPriority) + if self.spec == other.spec: + return False + if self.spec == ALPHA_SPECIFIER: + return False + if other.spec == ALPHA_SPECIFIER: + return True + return self.sort_str() < other.sort_str() + + def sort_str(self) -> str: + return "".join(c for c in self.spec if c not in "<>=") + + def create_specifier_string(specifiers: set[str]) -> str: + return ",".join(sorted(specifiers, key=SpecPriority)) - def __lt__(self, other: object) -> bool: - assert isinstance(other, SpecPriority) - if self.spec == other.spec: - return False - if self.spec == ALPHA_SPECIFIER: - return False - if other.spec == ALPHA_SPECIFIER: - return True - return self.sort_str() < other.sort_str() - - def sort_str(self) -> str: - return "".join(c for c in self.spec if c not in "<>=") - - def create_specifier_string(specifiers: set[str]) -> str: - return ",".join(sorted(specifiers, key=SpecPriority)) - - if node_has_type(node, "str"): try: req = Requirement(node.value) except InvalidRequirement: return + if ( - strip_cuda_suffix(args, req.name) - in get_rapids_version(args).prerelease_packages + strip_cuda_suffix(self.args, req.name) + not in get_rapids_version(self.args).prerelease_packages ): - descend, anchor = check_and_mark_anchor( - anchors, used_anchors, node - ) - if descend: - has_alpha_spec = any( - str(s) == ALPHA_SPECIFIER for s in req.specifier - ) - if args.mode == "development" and not has_alpha_spec: - linter.add_warning( - (node.start_mark.index, node.end_mark.index), - f"add alpha spec for RAPIDS package {req.name}", - ).add_replacement( - (node.start_mark.index, node.end_mark.index), - str( - (f"&{anchor} " if anchor else "") - + req.name - + create_specifier_string( - {str(s) for s in req.specifier} - | {ALPHA_SPECIFIER}, - ) - ), + return + + has_alpha_spec = any(str(s) == ALPHA_SPECIFIER for s in req.specifier) + if self.args.mode == "development" and not has_alpha_spec: + self.linter.add_warning( + (node.start_mark.index, node.end_mark.index), + f"add alpha spec for RAPIDS package {req.name}", + ).add_replacement( + (node.start_mark.index, node.end_mark.index), + str( + (f"&{anchor} " if anchor else "") + + req.name + + create_specifier_string( + {str(s) for s in req.specifier} | {ALPHA_SPECIFIER}, ) - elif args.mode == "release" and has_alpha_spec: - linter.add_warning( - (node.start_mark.index, node.end_mark.index), - f"remove alpha spec for RAPIDS package {req.name}", - ).add_replacement( - (node.start_mark.index, node.end_mark.index), - str( - (f"&{anchor} " if anchor else "") - + req.name - + create_specifier_string( - {str(s) for s in req.specifier} - - {ALPHA_SPECIFIER}, - ) - ), + ), + ) + elif self.args.mode == "release" and has_alpha_spec: + self.linter.add_warning( + (node.start_mark.index, node.end_mark.index), + f"remove alpha spec for RAPIDS package {req.name}", + ).add_replacement( + (node.start_mark.index, node.end_mark.index), + str( + (f"&{anchor} " if anchor else "") + + req.name + + create_specifier_string( + {str(s) for s in req.specifier} - {ALPHA_SPECIFIER}, ) - - -def check_packages( - linter: Linter, - args: argparse.Namespace, - anchors: dict[str, "yaml.Node"], - used_anchors: set[str], - node: "yaml.Node", -) -> None: - if node_has_type(node, "seq"): - descend, _ = check_and_mark_anchor(anchors, used_anchors, node) - if descend: - for package_spec in node.value: - check_package_spec( - linter, args, anchors, used_anchors, package_spec - ) - - -def check_common( - linter: Linter, - args: argparse.Namespace, - anchors: dict[str, "yaml.Node"], - used_anchors: set[str], - node: "yaml.Node", -) -> None: - if node_has_type(node, "seq"): - for dependency_set in node.value: - if node_has_type(dependency_set, "map"): - for ( - dependency_set_key, - dependency_set_value, - ) in dependency_set.value: - if ( - node_has_type(dependency_set_key, "str") - and dependency_set_key.value == "packages" - ): - check_packages( - linter, - args, - anchors, - used_anchors, - dependency_set_value, - ) - - -def check_matrices( - linter: Linter, - args: argparse.Namespace, - anchors: dict[str, "yaml.Node"], - used_anchors: set[str], - node: "yaml.Node", -) -> None: - if node_has_type(node, "seq"): - for item in node.value: - if node_has_type(item, "map"): - for matrix_key, matrix_value in item.value: - if ( - node_has_type(matrix_key, "str") - and matrix_key.value == "packages" - ): - check_packages( - linter, args, anchors, used_anchors, matrix_value - ) - - -def check_specific( - linter: Linter, - args: argparse.Namespace, - anchors: dict[str, "yaml.Node"], - used_anchors: set[str], - node: "yaml.Node", -) -> None: - if node_has_type(node, "seq"): - for matrix_matcher in node.value: - if node_has_type(matrix_matcher, "map"): - for ( - matrix_matcher_key, - matrix_matcher_value, - ) in matrix_matcher.value: - if ( - node_has_type(matrix_matcher_key, "str") - and matrix_matcher_key.value == "matrices" - ): - check_matrices( - linter, - args, - anchors, - used_anchors, - matrix_matcher_value, - ) - - -def check_dependencies( - linter: Linter, - args: argparse.Namespace, - anchors: dict[str, "yaml.Node"], - used_anchors: set[str], - node: "yaml.Node", -) -> None: - if node_has_type(node, "map"): - for _, dependencies_value in node.value: - if node_has_type(dependencies_value, "map"): - for ( - dependency_key, - dependency_value, - ) in dependencies_value.value: - if node_has_type(dependency_key, "str"): - if dependency_key.value == "common": - check_common( - linter, - args, - anchors, - used_anchors, - dependency_value, - ) - elif dependency_key.value == "specific": - check_specific( - linter, - args, - anchors, - used_anchors, - dependency_value, - ) - - -def check_root( - linter: Linter, - args: argparse.Namespace, - anchors: dict[str, "yaml.Node"], - used_anchors: set[str], - node: "yaml.Node", -) -> None: - if node_has_type(node, "map"): - for root_key, root_value in node.value: - if ( - node_has_type(root_key, "str") - and root_key.value == "dependencies" - ): - check_dependencies( - linter, args, anchors, used_anchors, root_value - ) - - -class AnchorPreservingLoader(yaml.SafeLoader): - """A SafeLoader that preserves the anchors for later reference. The anchors - can be found in the document_anchors member, which is a list of - dictionaries, one dictionary for each parsed document. - """ - - def __init__(self, stream) -> None: - super().__init__(stream) - self.document_anchors: list[dict[str, yaml.Node]] = [] - - def compose_document(self) -> "yaml.Node": - # Drop the DOCUMENT-START event. - self.get_event() - - # Compose the root node. - node = self.compose_node(None, None) # type: ignore[arg-type] - - # Drop the DOCUMENT-END event. - self.get_event() - - self.document_anchors.append(self.anchors) - self.anchors = {} - assert node is not None - return node + ), + ) def check_alpha_spec(linter: Linter, args: argparse.Namespace) -> None: - loader = AnchorPreservingLoader(linter.content) - try: - root = loader.get_single_node() - assert root is not None - finally: - loader.dispose() - check_root(linter, args, loader.document_anchors[0], set(), root) + handler = AlphaSpecHandler(linter, args) + traverse_dependencies_yaml(handler, linter.content) def main() -> None: diff --git a/src/rapids_pre_commit_hooks/utils/dependencies_yaml.py b/src/rapids_pre_commit_hooks/utils/dependencies_yaml.py new file mode 100644 index 0000000..aa592e2 --- /dev/null +++ b/src/rapids_pre_commit_hooks/utils/dependencies_yaml.py @@ -0,0 +1,470 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import contextlib +from typing import Any, Optional + +import yaml + + +class Handler: + def handle_root( + self, + value: "yaml.Node", # noqa: ARG002 + ) -> "contextlib.AbstractContextManager[Any]": + return contextlib.nullcontext() + + def handle_dependencies( + self, + root_context: "Any", + key: "yaml.Node", # noqa: ARG002 + value: "yaml.Node", # noqa: ARG002 + ) -> "contextlib.AbstractContextManager[Any]": + return contextlib.nullcontext(root_context) + + def handle_dependency_set( + self, + dependencies_context: "Any", + key: "yaml.Node", # noqa: ARG002 + value: "yaml.Node", # noqa: ARG002 + ) -> "contextlib.AbstractContextManager[Any]": + return contextlib.nullcontext(dependencies_context) + + def handle_common( + self, + dependency_set_context: "Any", + key: "yaml.Node", # noqa: ARG002 + value: "yaml.Node", # noqa: ARG002 + ) -> "contextlib.AbstractContextManager[Any]": + return contextlib.nullcontext(dependency_set_context) + + def handle_common_item( + self, + common_context: "Any", + item: "yaml.Node", # noqa: ARG002 + ) -> "contextlib.AbstractContextManager[Any]": + return contextlib.nullcontext(common_context) + + def handle_specific( + self, + dependency_set_context: "Any", + key: "yaml.Node", # noqa: ARG002 + value: "yaml.Node", # noqa: ARG002 + ) -> "contextlib.AbstractContextManager[Any]": + return contextlib.nullcontext(dependency_set_context) + + def handle_specific_item( + self, + specific_context: "Any", + item: "yaml.Node", # noqa: ARG002 + ) -> "contextlib.AbstractContextManager[Any]": + return contextlib.nullcontext(specific_context) + + def handle_matrices( + self, + specific_item_context: "Any", + key: "yaml.Node", # noqa: ARG002 + value: "yaml.Node", # noqa: ARG002 + ) -> "contextlib.AbstractContextManager[Any]": + return contextlib.nullcontext(specific_item_context) + + def handle_matrices_item( + self, + matrices_context: "Any", + item: "yaml.Node", # noqa: ARG002 + ) -> "contextlib.AbstractContextManager[Any]": + return contextlib.nullcontext(matrices_context) + + def handle_matrix( + self, + matrices_item_context: "Any", + key: "yaml.Node", # noqa: ARG002 + value: "yaml.Node", # noqa: ARG002 + ) -> "contextlib.AbstractContextManager[Any]": + return contextlib.nullcontext(matrices_item_context) + + def handle_matrix_item( + self, + matrix_context: "Any", # noqa: ARG002 + key: "yaml.Node", # noqa: ARG002 + value: "yaml.Node", # noqa: ARG002 + ) -> None: + pass + + def handle_packages( + self, + common_or_matrices_item_context: "Any", + key: "yaml.Node", # noqa: ARG002 + value: "yaml.Node", # noqa: ARG002 + ) -> "contextlib.AbstractContextManager[Any]": + return contextlib.nullcontext(common_or_matrices_item_context) + + def handle_package( + self, + packages_context: "Any", # noqa: ARG002 + anchor: "Optional[str]", # noqa: ARG002 + item: "yaml.Node", # noqa: ARG002 + ) -> None: + pass + + +class AnchorPreservingLoader(yaml.SafeLoader): + """A SafeLoader that preserves the anchors for later reference. The anchors + can be found in the document_anchors member, which is a list of + dictionaries, one dictionary for each parsed document. + """ + + def __init__(self, stream) -> None: + super().__init__(stream) + self.document_anchors: list[dict[str, yaml.Node]] = [] + + def compose_document(self) -> "yaml.Node": + # Drop the DOCUMENT-START event. + self.get_event() + + # Compose the root node. + node = self.compose_node(None, None) # type: ignore[arg-type] + + # Drop the DOCUMENT-END event. + self.get_event() + + self.document_anchors.append(self.anchors) + self.anchors = {} + assert node is not None + return node + + +def node_has_type(node: "yaml.Node", tag_type: str) -> bool: + return node.tag == f"tag:yaml.org,2002:{tag_type}" + + +def check_and_mark_anchor( + anchors: dict[str, "yaml.Node"], used_anchors: set[str], node: "yaml.Node" +) -> tuple[bool, str | None]: + for key, value in anchors.items(): + if value == node: + anchor = key + break + else: + anchor = None + if anchor in used_anchors: + return False, anchor + if anchor is not None: + used_anchors.add(anchor) + return True, anchor + + +def traverse_package( + handler: Handler, + packages_context: "Any", + anchors: dict[str, "yaml.Node"], + used_anchors: set[str], + node: "yaml.Node", +) -> None: + if node_has_type(node, "str"): + descend, anchor = check_and_mark_anchor(anchors, used_anchors, node) + if descend: + handler.handle_package(packages_context, anchor, node) + + +def traverse_packages( + handler: Handler, + common_or_matrices_item_context: "Any", + anchors: dict[str, "yaml.Node"], + used_anchors: set[str], + key_node: "yaml.Node", + node: "yaml.Node", +) -> None: + if node_has_type(node, "seq"): + descend, _ = check_and_mark_anchor(anchors, used_anchors, node) + if descend: + with handler.handle_packages( + common_or_matrices_item_context, key_node, node + ) as packages_context: + for package in node.value: + traverse_package( + handler, + packages_context, + anchors, + used_anchors, + package, + ) + + +def traverse_common_item( + handler: Handler, + common_context: "Any", + anchors: dict[str, "yaml.Node"], + used_anchors: set[str], + node: "yaml.Node", +) -> None: + if node_has_type(node, "map"): + with handler.handle_common_item( + common_context, node + ) as common_item_context: + for ( + common_item_key, + common_item_value, + ) in node.value: + if ( + node_has_type(common_item_key, "str") + and common_item_key.value == "packages" + ): + traverse_packages( + handler, + common_item_context, + anchors, + used_anchors, + common_item_key, + common_item_value, + ) + + +def traverse_common( + handler: Handler, + dependency_set_context: "Any", + anchors: dict[str, "yaml.Node"], + used_anchors: set[str], + key_node: "yaml.Node", + node: "yaml.Node", +) -> None: + if node_has_type(node, "seq"): + with handler.handle_common( + dependency_set_context, key_node, node + ) as common_context: + for common_item in node.value: + traverse_common_item( + handler, common_context, anchors, used_anchors, common_item + ) + + +def traverse_matrix_item( + handler: Handler, + matrix_context: "Any", + key_node: "yaml.Node", + node: "yaml.Node", +) -> None: + if node_has_type(node, "str"): + handler.handle_matrix_item(matrix_context, key_node, node) + + +def traverse_matrix( + handler: Handler, + matrices_item_context: "Any", + key_node: "yaml.Node", + node: "yaml.Node", +) -> None: + if node_has_type(node, "map"): + with handler.handle_matrix( + matrices_item_context, key_node, node + ) as matrices_context: + for matrix_item_key, matrix_item in node.value: + traverse_matrix_item( + handler, + matrices_context, + matrix_item_key, + matrix_item, + ) + elif node_has_type(node, "null"): + with handler.handle_matrix( + matrices_item_context, key_node, node + ) as matrices_context: + pass + + +def traverse_matrices_item( + handler: Handler, + matrices_context: "Any", + anchors: dict[str, "yaml.Node"], + used_anchors: set[str], + node: "yaml.Node", +) -> None: + if node_has_type(node, "map"): + with handler.handle_matrices_item( + matrices_context, node + ) as matrices_item_context: + for matrix_key, matrix_value in node.value: + if ( + node_has_type(matrix_key, "str") + and matrix_key.value == "packages" + ): + traverse_packages( + handler, + matrices_item_context, + anchors, + used_anchors, + matrix_key, + matrix_value, + ) + elif ( + node_has_type(matrix_key, "str") + and matrix_key.value == "matrix" + ): + traverse_matrix( + handler, + matrices_item_context, + matrix_key, + matrix_value, + ) + + +def traverse_matrices( + handler: Handler, + specific_item_context: "Any", + anchors: dict[str, "yaml.Node"], + used_anchors: set[str], + key_node: "yaml.Node", + node: "yaml.Node", +) -> None: + if node_has_type(node, "seq"): + with handler.handle_matrices( + specific_item_context, key_node, node + ) as matrices_context: + for item in node.value: + traverse_matrices_item( + handler, matrices_context, anchors, used_anchors, item + ) + + +def traverse_specific_item( + handler: Handler, + specific_context: "Any", + anchors: dict[str, "yaml.Node"], + used_anchors: set[str], + node: "yaml.Node", +) -> None: + if node_has_type(node, "map"): + with handler.handle_specific_item( + specific_context, node + ) as specific_item_context: + for ( + specific_item_key, + specific_item_value, + ) in node.value: + if ( + node_has_type(specific_item_key, "str") + and specific_item_key.value == "matrices" + ): + traverse_matrices( + handler, + specific_item_context, + anchors, + used_anchors, + specific_item_key, + specific_item_value, + ) + + +def traverse_specific( + handler: Handler, + dependency_set_context: "Any", + anchors: dict[str, "yaml.Node"], + used_anchors: set[str], + key_node: "yaml.Node", + node: "yaml.Node", +) -> None: + if node_has_type(node, "seq"): + with handler.handle_specific( + dependency_set_context, key_node, node + ) as specific_context: + for specific_item in node.value: + traverse_specific_item( + handler, + specific_context, + anchors, + used_anchors, + specific_item, + ) + + +def traverse_dependency_set( + handler: Handler, + dependencies_context: "Any", + anchors: dict[str, "yaml.Node"], + used_anchors: set[str], + key_node: "yaml.Node", + node: "yaml.Node", +) -> None: + if node_has_type(node, "map"): + with handler.handle_dependency_set( + dependencies_context, key_node, node + ) as dependency_set_context: + for ( + dependency_key, + dependency_value, + ) in node.value: + if node_has_type(dependency_key, "str"): + if dependency_key.value == "common": + traverse_common( + handler, + dependency_set_context, + anchors, + used_anchors, + dependency_key, + dependency_value, + ) + elif dependency_key.value == "specific": + traverse_specific( + handler, + dependency_set_context, + anchors, + used_anchors, + dependency_key, + dependency_value, + ) + + +def traverse_dependencies( + handler: Handler, + root_context: "Any", + anchors: dict[str, "yaml.Node"], + used_anchors: set[str], + key_node: "yaml.Node", + node: "yaml.Node", +) -> None: + if node_has_type(node, "map"): + with handler.handle_dependencies( + root_context, key_node, node + ) as dependencies_context: + for dependencies_key, dependencies_value in node.value: + traverse_dependency_set( + handler, + dependencies_context, + anchors, + used_anchors, + dependencies_key, + dependencies_value, + ) + + +def traverse_root( + handler: Handler, + anchors: dict[str, "yaml.Node"], + used_anchors: set[str], + node: "yaml.Node", +) -> None: + if node_has_type(node, "map"): + with handler.handle_root(node) as root_context: + for root_key, root_value in node.value: + if ( + node_has_type(root_key, "str") + and root_key.value == "dependencies" + ): + traverse_dependencies( + handler, + root_context, + anchors, + used_anchors, + root_key, + root_value, + ) + + +def traverse_dependencies_yaml(handler: Handler, content: str) -> None: + loader = AnchorPreservingLoader(content) + try: + root = loader.get_single_node() + assert root is not None + finally: + loader.dispose() + traverse_root(handler, loader.document_anchors[0], set(), root) diff --git a/tests/rapids_pre_commit_hooks/test_alpha_spec.py b/tests/rapids_pre_commit_hooks/test_alpha_spec.py index 1680c41..e52b634 100644 --- a/tests/rapids_pre_commit_hooks/test_alpha_spec.py +++ b/tests/rapids_pre_commit_hooks/test_alpha_spec.py @@ -1,14 +1,12 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import contextlib import os.path from itertools import chain -from textwrap import dedent -from unittest.mock import MagicMock, Mock, call, patch +from unittest.mock import Mock, patch import pytest -import yaml from packaging.version import Version from rapids_metadata.metadata import ( RAPIDSMetadata, @@ -17,6 +15,8 @@ ) from rapids_pre_commit_hooks import alpha_spec, lint +from rapids_pre_commit_hooks.utils import dependencies_yaml +from rapids_pre_commit_hooks_test_utils import parse_named_spans latest_version, latest_metadata = max( alpha_spec.all_metadata().versions.items(), @@ -93,15 +93,6 @@ def test_get_rapids_version( assert version == MOCK_METADATA.versions[expected_version] -def test_anchor_preserving_loader(): - loader = alpha_spec.AnchorPreservingLoader("- &a A\n- *a") - try: - root = loader.get_single_node() - finally: - loader.dispose() - assert loader.document_anchors == [{"a": root.value[0]}] - - @pytest.mark.parametrize( ["name", "stripped_name"], [ @@ -139,82 +130,15 @@ def test_strip_cuda_suffix(name, stripped_name): @pytest.mark.parametrize( - [ - "used_anchors_before", - "node_index", - "descend", - "anchor", - "used_anchors_after", - ], - [ - ( - set(), - 0, - True, - "anchor1", - {"anchor1"}, - ), - ( - {"anchor1"}, - 1, - True, - "anchor2", - {"anchor1", "anchor2"}, - ), - ( - set(), - 2, - True, - None, - set(), - ), - ( - {"anchor1", "anchor2"}, - 0, - False, - "anchor1", - {"anchor1", "anchor2"}, - ), - ( - {"anchor1", "anchor2"}, - 1, - False, - "anchor2", - {"anchor1", "anchor2"}, - ), - ], -) -def test_check_and_mark_anchor( - used_anchors_before, - node_index, - descend, - anchor, - used_anchors_after, -): - NODES = [Mock() for _ in range(3)] - ANCHORS = { - "anchor1": NODES[0], - "anchor2": NODES[1], - } - used_anchors = set(used_anchors_before) - actual_descend, actual_anchor = alpha_spec.check_and_mark_anchor( - ANCHORS, used_anchors, NODES[node_index] - ) - assert actual_descend == descend - assert actual_anchor == anchor - assert used_anchors == used_anchors_after - - -@pytest.mark.parametrize( - ["package", "content", "mode", "replacement"], + ["package", "anchor", "content", "mode", "replacement"], [ *chain( *( [ - (p, p, "development", f"{p}>=0.0.0a0"), - (p, p, "release", None), - (p, f"{p}>=0.0.0a0", "development", None), - (p, f"{p}>=0.0.0a0", "release", p), + (p, None, p, "development", f"{p}>=0.0.0a0"), + (p, None, p, "release", None), + (p, None, f"{p}>=0.0.0a0", "development", None), + (p, None, f"{p}>=0.0.0a0", "release", p), ] for p in latest_metadata.prerelease_packages ) @@ -224,14 +148,22 @@ def test_check_and_mark_anchor( [ ( f"{p}-cu12", + None, f"{p}-cu12", "development", f"{p}-cu12>=0.0.0a0", ), - (f"{p}-cu11", f"{p}-cu11", "release", None), - (f"{p}-cu12", f"{p}-cu12>=0.0.0a0", "development", None), + (f"{p}-cu11", None, f"{p}-cu11", "release", None), + ( + f"{p}-cu12", + None, + f"{p}-cu12>=0.0.0a0", + "development", + None, + ), ( f"{p}-cu11", + None, f"{p}-cu11>=0.0.0a0", "release", f"{p}-cu11", @@ -244,8 +176,8 @@ def test_check_and_mark_anchor( *chain( *( [ - (f"{p}-cu12", f"{p}-cu12", "development", None), - (f"{p}-cu12", f"{p}-cu12>=0.0.0a0", "release", None), + (f"{p}-cu12", None, f"{p}-cu12", "development", None), + (f"{p}-cu12", None, f"{p}-cu12>=0.0.0a0", "release", None), ] for p in latest_metadata.prerelease_packages & ( @@ -256,49 +188,58 @@ def test_check_and_mark_anchor( ), ( "cuml", + None, "cuml>=24.04,<24.06", "development", "cuml>=24.04,<24.06,>=0.0.0a0", ), ( "cuml", + None, "cuml>=24.04,<24.06,>=0.0.0a0", "release", "cuml>=24.04,<24.06", ), ( + "cuml", "cuml", "&cuml cuml>=24.04,<24.06,>=0.0.0a0", "release", "&cuml cuml>=24.04,<24.06", ), - ("packaging", "packaging", "development", None), + ("packaging", None, "packaging", "development", None), ( + None, None, "--extra-index-url=https://pypi.nvidia.com", "development", None, ), - (None, "--extra-index-url=https://pypi.nvidia.com", "release", None), - (None, "gcc_linux-64=11.*", "development", None), - (None, "gcc_linux-64=11.*", "release", None), + ( + None, + None, + "--extra-index-url=https://pypi.nvidia.com", + "release", + None, + ), + (None, None, "gcc_linux-64=11.*", "development", None), + (None, None, "gcc_linux-64=11.*", "release", None), ], ) @patch( "rapids_pre_commit_hooks.alpha_spec.get_rapids_version", Mock(return_value=latest_metadata), ) -def test_check_package_spec(package, content, mode, replacement): +def test_check_package_spec(package, anchor, content, mode, replacement): args = Mock(mode=mode) linter = lint.Linter("dependencies.yaml", content, "verify-alpha-spec") - loader = alpha_spec.AnchorPreservingLoader(content) + loader = dependencies_yaml.AnchorPreservingLoader(content) try: composed = loader.get_single_node() finally: loader.dispose() - alpha_spec.check_package_spec( - linter, args, loader.document_anchors[0], set(), composed - ) + handler = alpha_spec.AlphaSpecHandler(linter, args) + handler.handle_package(Mock(), anchor, composed) if replacement is None: assert linter.warnings == [] else: @@ -315,417 +256,52 @@ def test_check_package_spec(package, content, mode, replacement): assert linter.warnings == expected_linter.warnings -@patch( - "rapids_pre_commit_hooks.alpha_spec.get_rapids_version", - Mock(return_value=latest_metadata), -) -def test_check_package_spec_anchor(): - CONTENT = dedent( - """\ - - &cudf cudf>=24.04,<24.06 - - *cudf - - cuml>=24.04,<24.06 - - rmm>=24.04,<24.06 - """ - ) - args = Mock(mode="development") - linter = lint.Linter("dependencies.yaml", CONTENT, "verify-alpha-spec") - loader = alpha_spec.AnchorPreservingLoader(CONTENT) - try: - composed = loader.get_single_node() - finally: - loader.dispose() - used_anchors = set() - - expected_linter = lint.Linter( - "dependencies.yaml", CONTENT, "verify-alpha-spec" - ) - expected_linter.add_warning( - (2, 26), "add alpha spec for RAPIDS package cudf" - ).add_replacement((2, 26), "&cudf cudf>=24.04,<24.06,>=0.0.0a0") - - alpha_spec.check_package_spec( - linter, - args, - loader.document_anchors[0], - used_anchors, - composed.value[0], - ) - assert linter.warnings == expected_linter.warnings - assert used_anchors == {"cudf"} - - alpha_spec.check_package_spec( - linter, - args, - loader.document_anchors[0], - used_anchors, - composed.value[1], - ) - assert linter.warnings == expected_linter.warnings - assert used_anchors == {"cudf"} - - expected_linter.add_warning( - (37, 55), "add alpha spec for RAPIDS package cuml" - ).add_replacement((37, 55), "cuml>=24.04,<24.06,>=0.0.0a0") - alpha_spec.check_package_spec( - linter, - args, - loader.document_anchors[0], - used_anchors, - composed.value[2], - ) - assert linter.warnings == expected_linter.warnings - assert used_anchors == {"cudf"} - - expected_linter.add_warning( - (58, 75), "add alpha spec for RAPIDS package rmm" - ).add_replacement((58, 75), "rmm>=24.04,<24.06,>=0.0.0a0") - alpha_spec.check_package_spec( - linter, - args, - loader.document_anchors[0], - used_anchors, - composed.value[3], - ) - assert linter.warnings == expected_linter.warnings - assert used_anchors == {"cudf"} - - -@pytest.mark.parametrize( - ["content", "indices", "use_anchor"], - [ - ( - dedent( - """\ - - package_a - - &package_b package_b - """ - ), - [0, 1], - True, - ), - ( - "null", - [], - False, - ), - ], -) -def test_check_packages(content, indices, use_anchor): - with patch( - "rapids_pre_commit_hooks.alpha_spec.check_package_spec", Mock() - ) as mock_check_package_spec: - args = Mock() - linter = lint.Linter("dependencies.yaml", content, "verify-alpha-spec") - composed = yaml.compose(content) - anchors = {"anchor": composed} - used_anchors = set() - alpha_spec.check_packages( - linter, args, anchors, used_anchors, composed - ) - assert used_anchors == ({"anchor"} if use_anchor else set()) - alpha_spec.check_packages( - linter, args, anchors, used_anchors, composed - ) - assert mock_check_package_spec.mock_calls == [ - call(linter, args, anchors, used_anchors, composed.value[i]) - for i in indices - ] - - -@pytest.mark.parametrize( - ["content", "indices"], - [ - ( - dedent( - """\ - - output_types: [pyproject, conda] - packages: - - package_a - - output_types: [conda] - packages: - - package_b - - packages: - - package_c - output_types: pyproject - """ - ), - [(0, 1), (1, 1), (2, 0)], - ), - ], -) -def test_check_common(content, indices): - with patch( - "rapids_pre_commit_hooks.alpha_spec.check_packages", Mock() - ) as mock_check_packages: - args = Mock() - linter = lint.Linter("dependencies.yaml", content, "verify-alpha-spec") - anchors = Mock() - used_anchors = Mock() - composed = yaml.compose(content) - alpha_spec.check_common(linter, args, anchors, used_anchors, composed) - assert mock_check_packages.mock_calls == [ - call( - linter, args, anchors, used_anchors, composed.value[i].value[j][1] - ) - for i, j in indices - ] - - -@pytest.mark.parametrize( - ["content", "indices"], - [ - ( - dedent( - """\ - - matrix: - arch: x86_64 - packages: - - package_a - - packages: - - package_b - matrix: - """ - ), - [(0, 1), (1, 0)], - ), - ], -) -def test_check_matrices(content, indices): - with patch( - "rapids_pre_commit_hooks.alpha_spec.check_packages", Mock() - ) as mock_check_packages: - args = Mock() - linter = lint.Linter("dependencies.yaml", content, "verify-alpha-spec") - anchors = Mock() - used_anchors = Mock() - composed = yaml.compose(content) - alpha_spec.check_matrices( - linter, args, anchors, used_anchors, composed - ) - assert mock_check_packages.mock_calls == [ - call( - linter, args, anchors, used_anchors, composed.value[i].value[j][1] - ) - for i, j in indices - ] - - -@pytest.mark.parametrize( - ["content", "indices"], - [ - ( - dedent( - """\ - - output_types: [pyproject, conda] - matrices: - - matrix: - arch: x86_64 - packages: - - package_a - - output_types: [conda] - matrices: - - matrix: - arch: x86_64 - packages: - - package_b - - matrices: - - matrix: - arch: x86_64 - packages: - - package_c - output_types: pyproject - """ - ), - [(0, 1), (1, 1), (2, 0)], - ), - ], -) -def test_check_specific(content, indices): - with patch( - "rapids_pre_commit_hooks.alpha_spec.check_matrices", Mock() - ) as mock_check_matrices: - args = Mock() - linter = lint.Linter("dependencies.yaml", content, "verify-alpha-spec") - anchors = Mock() - used_anchors = Mock() - composed = yaml.compose(content) - alpha_spec.check_specific( - linter, args, anchors, used_anchors, composed - ) - assert mock_check_matrices.mock_calls == [ - call( - linter, args, anchors, used_anchors, composed.value[i].value[j][1] - ) - for i, j in indices - ] - - -@pytest.mark.parametrize( - ["content", "common_indices", "specific_indices"], - [ - ( - dedent( - """\ - set_a: - common: - - output_types: [pyproject] - packages: - - package_a - specific: - - output_types: [pyproject] - matrices: - - matrix: - arch: x86_64 - packages: - - package_b - set_b: - specific: - - output_types: [pyproject] - matrices: - - matrix: - arch: x86_64 - packages: - - package_c - common: - - output_types: [pyproject] - packages: - - package_d - """ - ), - [(0, 0), (1, 1)], - [(0, 1), (1, 0)], - ), - ], -) -def test_check_dependencies( - content, - common_indices, - specific_indices, -): - with ( - patch( - "rapids_pre_commit_hooks.alpha_spec.check_common", Mock() - ) as mock_check_common, - patch( - "rapids_pre_commit_hooks.alpha_spec.check_specific", Mock() - ) as mock_check_specific, - ): - args = Mock() - linter = lint.Linter("dependencies.yaml", content, "verify-alpha-spec") - anchors = Mock() - used_anchors = Mock() - composed = yaml.compose(content) - alpha_spec.check_dependencies( - linter, args, anchors, used_anchors, composed - ) - assert mock_check_common.mock_calls == [ - call( - linter, - args, - anchors, - used_anchors, - composed.value[i][1].value[j][1], - ) - for i, j in common_indices - ] - assert mock_check_specific.mock_calls == [ - call( - linter, - args, - anchors, - used_anchors, - composed.value[i][1].value[j][1], - ) - for i, j in specific_indices - ] - - -@pytest.mark.parametrize( - ["content", "indices"], - [ - ( - dedent( - """\ - files: {} - channels: [] - dependencies: {} - """ - ), - [2], - ), - ], -) -def test_check_root(content, indices): - with patch( - "rapids_pre_commit_hooks.alpha_spec.check_dependencies", Mock() - ) as mock_check_dependencies: - args = Mock() - linter = lint.Linter("dependencies.yaml", content, "verify-alpha-spec") - anchors = Mock() - used_anchors = Mock() - composed = yaml.compose(content) - alpha_spec.check_root(linter, args, anchors, used_anchors, composed) - assert mock_check_dependencies.mock_calls == [ - call(linter, args, anchors, used_anchors, composed.value[i][1]) - for i in indices - ] - - def test_check_alpha_spec(): CONTENT = "dependencies: []" with ( patch( - "rapids_pre_commit_hooks.alpha_spec.check_root", Mock() - ) as mock_check_root, + "rapids_pre_commit_hooks.alpha_spec.AlphaSpecHandler", Mock() + ) as mock_alpha_spec_handler, patch( - "rapids_pre_commit_hooks.alpha_spec.AnchorPreservingLoader", - MagicMock(), - ) as mock_anchor_preserving_loader, + "rapids_pre_commit_hooks.alpha_spec.traverse_dependencies_yaml", + Mock(), + ) as mock_traverse_dependencies_yaml, ): args = Mock() linter = lint.Linter("dependencies.yaml", CONTENT, "verify-alpha-spec") alpha_spec.check_alpha_spec(linter, args) - mock_anchor_preserving_loader.assert_called_once_with(CONTENT) - mock_check_root.assert_called_once_with( - linter, - args, - mock_anchor_preserving_loader().document_anchors[0], - set(), - mock_anchor_preserving_loader().get_single_node(), + mock_alpha_spec_handler.assert_called_once() + mock_traverse_dependencies_yaml.assert_called_once_with( + mock_alpha_spec_handler(), CONTENT ) def test_check_alpha_spec_integration(tmp_path): - CONTENT = dedent( + content, spans = parse_named_spans( """\ - dependencies: - test: - common: - - output_types: pyproject - packages: - - cudf>=24.04,<24.06 + + dependencies: + + test: + + common: + + - output_types: pyproject + + packages: + + - cudf>=24.04,<24.06 + : ~~~~~~~~~~~~~~~~~~package """ ) - REPLACED = "cudf>=24.04,<24.06" args = Mock( mode="development", rapids_version=None, rapids_version_file="VERSION" ) - linter = lint.Linter("dependencies.yaml", CONTENT, "verify-alpha-spec") + linter = lint.Linter("dependencies.yaml", content, "verify-alpha-spec") with open(os.path.join(tmp_path, "VERSION"), "w") as f: f.write(f"{latest_version}\n") with set_cwd(tmp_path): alpha_spec.check_alpha_spec(linter, args) - start = CONTENT.find(REPLACED) - end = start + len(REPLACED) - span = (start, end) - expected_linter = lint.Linter( - "dependencies.yaml", CONTENT, "verify-alpha-spec" + "dependencies.yaml", content, "verify-alpha-spec" ) expected_linter.add_warning( - span, "add alpha spec for RAPIDS package cudf" - ).add_replacement(span, "cudf>=24.04,<24.06,>=0.0.0a0") + spans["package"], "add alpha spec for RAPIDS package cudf" + ).add_replacement(spans["package"], "cudf>=24.04,<24.06,>=0.0.0a0") assert linter.warnings == expected_linter.warnings diff --git a/tests/rapids_pre_commit_hooks/utils/test_dependencies_yaml.py b/tests/rapids_pre_commit_hooks/utils/test_dependencies_yaml.py new file mode 100644 index 0000000..0af8749 --- /dev/null +++ b/tests/rapids_pre_commit_hooks/utils/test_dependencies_yaml.py @@ -0,0 +1,768 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from unittest.mock import MagicMock, Mock, call, patch + +import pytest +import yaml +from rapids_pre_commit_hooks.utils import dependencies_yaml + + +def test_anchor_preserving_loader(): + loader = dependencies_yaml.AnchorPreservingLoader("- &a A\n- *a") + try: + root = loader.get_single_node() + finally: + loader.dispose() + assert loader.document_anchors == [{"a": root.value[0]}] + + +@pytest.mark.parametrize( + [ + "used_anchors_before", + "node_index", + "descend", + "anchor", + "used_anchors_after", + ], + [ + ( + set(), + 0, + True, + "anchor1", + {"anchor1"}, + ), + ( + {"anchor1"}, + 1, + True, + "anchor2", + {"anchor1", "anchor2"}, + ), + ( + set(), + 2, + True, + None, + set(), + ), + ( + {"anchor1", "anchor2"}, + 0, + False, + "anchor1", + {"anchor1", "anchor2"}, + ), + ( + {"anchor1", "anchor2"}, + 1, + False, + "anchor2", + {"anchor1", "anchor2"}, + ), + ], +) +def test_check_and_mark_anchor( + used_anchors_before, + node_index, + descend, + anchor, + used_anchors_after, +): + NODES = [Mock() for _ in range(3)] + ANCHORS = { + "anchor1": NODES[0], + "anchor2": NODES[1], + } + used_anchors = set(used_anchors_before) + actual_descend, actual_anchor = dependencies_yaml.check_and_mark_anchor( + ANCHORS, used_anchors, NODES[node_index] + ) + assert actual_descend == descend + assert actual_anchor == anchor + assert used_anchors == used_anchors_after + + +def test_traverse_package(): + packages = yaml.SafeLoader("""\ + - lib1 + """).get_single_node() + package = packages.value[0] + packages_context = Mock() + manager = MagicMock() + + expected_calls = [ + call.handler.handle_package(packages_context, None, package), + ] + manager.reset_mock() + + dependencies_yaml.traverse_package( + manager.handler, packages_context, {}, set(), package + ) + + assert manager.mock_calls == expected_calls + + +def test_traverse_package_anchor(): + packages = yaml.SafeLoader("""\ + - &lib1 lib1 + - *lib1 + """).get_single_node() + package = packages.value[0] + packages_context = Mock() + manager = MagicMock() + + expected_calls = [ + call.handler.handle_package(packages_context, "lib1", package), + ] + manager.reset_mock() + + dependencies_yaml.traverse_package( + manager.handler, packages_context, {"lib1": package}, set(), package + ) + + assert manager.mock_calls == expected_calls + + +def test_traverse_package_used_anchor(): + packages = yaml.SafeLoader("""\ + - &lib1 lib1 + - *lib1 + """).get_single_node() + package = packages.value[1] + packages_context = Mock() + manager = MagicMock() + + expected_calls = [] + manager.reset_mock() + + dependencies_yaml.traverse_package( + manager.handler, packages_context, {"lib1": package}, {"lib1"}, package + ) + + assert manager.mock_calls == expected_calls + + +def test_traverse_packages(): + item = yaml.SafeLoader("""\ + packages: + - lib1 + - lib2 + """).get_single_node() + packages_key, packages = item.value[0] + item_context = Mock() + manager = MagicMock() + + expected_calls = [ + call.handler.handle_packages(item_context, packages_key, packages), + call.handler.handle_packages().__enter__(), + call.traverse_package( + manager.handler, + manager.handler.handle_packages().__enter__(), + {}, + set(), + packages.value[0], + ), + call.traverse_package( + manager.handler, + manager.handler.handle_packages().__enter__(), + {}, + set(), + packages.value[1], + ), + call.handler.handle_packages().__exit__(None, None, None), + ] + manager.reset_mock() + + with ( + patch( + "rapids_pre_commit_hooks.utils.dependencies_yaml.traverse_package", + manager.traverse_package, + ), + ): + dependencies_yaml.traverse_packages( + manager.handler, item_context, {}, set(), packages_key, packages + ) + + assert manager.mock_calls == expected_calls + + +def test_traverse_packages_anchor(): + items = yaml.SafeLoader("""\ + - packages: &packages + - lib1 + - lib2 + - packages: *packages + """).get_single_node() + packages_key, packages = items.value[0].value[0] + item_context = Mock() + manager = MagicMock() + + expected_calls = [ + call.handler.handle_packages(item_context, packages_key, packages), + call.handler.handle_packages().__enter__(), + call.traverse_package( + manager.handler, + manager.handler.handle_packages().__enter__(), + {"packages": items.value[0].value[0][1]}, + {"packages"}, + packages.value[0], + ), + call.traverse_package( + manager.handler, + manager.handler.handle_packages().__enter__(), + {"packages": items.value[0].value[0][1]}, + {"packages"}, + packages.value[1], + ), + call.handler.handle_packages().__exit__(None, None, None), + ] + manager.reset_mock() + + with ( + patch( + "rapids_pre_commit_hooks.utils.dependencies_yaml.traverse_package", + manager.traverse_package, + ), + ): + dependencies_yaml.traverse_packages( + manager.handler, + item_context, + {"packages": items.value[0].value[0][1]}, + set(), + packages_key, + packages, + ) + + assert manager.mock_calls == expected_calls + + +def test_traverse_packages_used_anchor(): + items = yaml.SafeLoader("""\ + - packages: &packages + - lib1 + - lib2 + - packages: *packages + """).get_single_node() + packages_key, packages = items.value[1].value[0] + item_context = Mock() + manager = MagicMock() + + expected_calls = [] + manager.reset_mock() + + with ( + patch( + "rapids_pre_commit_hooks.utils.dependencies_yaml.traverse_package", + manager.traverse_package, + ), + ): + dependencies_yaml.traverse_packages( + manager.handler, + item_context, + {"packages": items.value[0].value[0][1]}, + {"packages"}, + packages_key, + packages, + ) + + assert manager.mock_calls == expected_calls + + +def test_traverse_common_item(): + common = yaml.SafeLoader("""\ + - output_types: pyproject + packages: [] + """).get_single_node() + common_item = common.value[0] + common_context = Mock() + manager = MagicMock() + + expected_calls = [ + call.handler.handle_common_item(common_context, common_item), + call.handler.handle_common_item().__enter__(), + call.traverse_packages( + manager.handler, + manager.handler.handle_common_item().__enter__(), + {}, + set(), + common_item.value[1][0], + common_item.value[1][1], + ), + call.handler.handle_common_item().__exit__(None, None, None), + ] + manager.reset_mock() + + with ( + patch( + "rapids_pre_commit_hooks.utils.dependencies_yaml.traverse_packages", + manager.traverse_packages, + ), + ): + dependencies_yaml.traverse_common_item( + manager.handler, common_context, {}, set(), common_item + ) + + assert manager.mock_calls == expected_calls + + +def test_traverse_common(): + dependency_set = yaml.SafeLoader("""\ + common: + - {} + - {} + """).get_single_node() + common_key, common = dependency_set.value[0] + dependency_set_context = Mock() + manager = MagicMock() + + expected_calls = [ + call.handler.handle_common(dependency_set_context, common_key, common), + call.handler.handle_common().__enter__(), + call.traverse_common_item( + manager.handler, + manager.handler.handle_common().__enter__(), + {}, + set(), + common.value[0], + ), + call.traverse_common_item( + manager.handler, + manager.handler.handle_common().__enter__(), + {}, + set(), + common.value[1], + ), + call.handler.handle_common().__exit__(None, None, None), + ] + manager.reset_mock() + + with ( + patch( + "rapids_pre_commit_hooks.utils.dependencies_yaml.traverse_common_item", + manager.traverse_common_item, + ), + ): + dependencies_yaml.traverse_common( + manager.handler, + dependency_set_context, + {}, + set(), + common_key, + common, + ) + + assert manager.mock_calls == expected_calls + + +def test_traverse_matrix_item(): + matrix = yaml.SafeLoader("""\ + value_1: "true" + """).get_single_node() + matrix_item_key, matrix_item = matrix.value[0] + matrix_context = Mock() + manager = MagicMock() + + expected_calls = [ + call.handler.handle_matrix_item( + matrix_context, matrix_item_key, matrix_item + ), + ] + manager.reset_mock() + + dependencies_yaml.traverse_matrix_item( + manager.handler, + matrix_context, + matrix_item_key, + matrix_item, + ) + + assert manager.mock_calls == expected_calls + + +def test_traverse_matrix(): + matrices_item = yaml.SafeLoader("""\ + matrix: + value_1: "true" + value_2: "true" + """).get_single_node() + matrix_key, matrix = matrices_item.value[0] + matrices_item_context = Mock() + manager = MagicMock() + + expected_calls = [ + call.handler.handle_matrix(matrices_item_context, matrix_key, matrix), + call.handler.handle_matrix().__enter__(), + call.traverse_matrix_item( + manager.handler, + manager.handler.handle_matrix().__enter__(), + matrix.value[0][0], + matrix.value[0][1], + ), + call.traverse_matrix_item( + manager.handler, + manager.handler.handle_matrix().__enter__(), + matrix.value[1][0], + matrix.value[1][1], + ), + call.handler.handle_matrix().__exit__(None, None, None), + ] + manager.reset_mock() + + with ( + patch( + "rapids_pre_commit_hooks.utils.dependencies_yaml.traverse_matrix_item", + manager.traverse_matrix_item, + ), + ): + dependencies_yaml.traverse_matrix( + manager.handler, + matrices_item_context, + matrix_key, + matrix, + ) + + assert manager.mock_calls == expected_calls + + +def test_traverse_matrices_item(): + matrices = yaml.SafeLoader("""\ + - matrix: {} + packages: [] + """).get_single_node() + matrices_item = matrices.value[0] + matrices_context = Mock() + manager = MagicMock() + + expected_calls = [ + call.handler.handle_matrices_item(matrices_context, matrices_item), + call.handler.handle_matrices_item().__enter__(), + call.traverse_matrix( + manager.handler, + manager.handler.handle_matrices_item().__enter__(), + matrices_item.value[0][0], + matrices_item.value[0][1], + ), + call.traverse_packages( + manager.handler, + manager.handler.handle_matrices_item().__enter__(), + {}, + set(), + matrices_item.value[1][0], + matrices_item.value[1][1], + ), + call.handler.handle_matrices_item().__exit__(None, None, None), + ] + manager.reset_mock() + + with ( + patch( + "rapids_pre_commit_hooks.utils.dependencies_yaml.traverse_matrix", + manager.traverse_matrix, + ), + patch( + "rapids_pre_commit_hooks.utils.dependencies_yaml.traverse_packages", + manager.traverse_packages, + ), + ): + dependencies_yaml.traverse_matrices_item( + manager.handler, matrices_context, {}, set(), matrices_item + ) + + assert manager.mock_calls == expected_calls + + +def test_traverse_matrices(): + specific_item = yaml.SafeLoader("""\ + matrices: + - {} + - {} + - {} + """).get_single_node() + matrices_key, matrices = specific_item.value[0] + specific_item_context = Mock() + manager = MagicMock() + + expected_calls = [ + call.handler.handle_matrices( + specific_item_context, matrices_key, matrices + ), + call.handler.handle_matrices().__enter__(), + call.traverse_matrices_item( + manager.handler, + manager.handler.handle_matrices().__enter__(), + {}, + set(), + matrices.value[0], + ), + call.traverse_matrices_item( + manager.handler, + manager.handler.handle_matrices().__enter__(), + {}, + set(), + matrices.value[1], + ), + call.traverse_matrices_item( + manager.handler, + manager.handler.handle_matrices().__enter__(), + {}, + set(), + matrices.value[2], + ), + call.handler.handle_matrices().__exit__(None, None, None), + ] + manager.reset_mock() + + with ( + patch( + "rapids_pre_commit_hooks.utils.dependencies_yaml.traverse_matrices_item", + manager.traverse_matrices_item, + ), + ): + dependencies_yaml.traverse_matrices( + manager.handler, + specific_item_context, + {}, + set(), + matrices_key, + matrices, + ) + + assert manager.mock_calls == expected_calls + + +def test_traverse_specific_item(): + specific = yaml.SafeLoader("""\ + - output_types: pyproject + matrices: [] + """).get_single_node() + specific_item = specific.value[0] + specific_context = Mock() + manager = MagicMock() + + expected_calls = [ + call.handler.handle_specific_item(specific_context, specific_item), + call.handler.handle_specific_item().__enter__(), + call.traverse_matrices( + manager.handler, + manager.handler.handle_specific_item().__enter__(), + {}, + set(), + specific_item.value[1][0], + specific_item.value[1][1], + ), + call.handler.handle_specific_item().__exit__(None, None, None), + ] + manager.reset_mock() + + with ( + patch( + "rapids_pre_commit_hooks.utils.dependencies_yaml.traverse_matrices", + manager.traverse_matrices, + ), + ): + dependencies_yaml.traverse_specific_item( + manager.handler, specific_context, {}, set(), specific_item + ) + + assert manager.mock_calls == expected_calls + + +def test_traverse_specific(): + dependency_set = yaml.SafeLoader("""\ + specific: + - {} + - {} + - {} + """).get_single_node() + specific_key, specific = dependency_set.value[0] + dependency_set_context = Mock() + manager = MagicMock() + + expected_calls = [ + call.handler.handle_specific( + dependency_set_context, specific_key, specific + ), + call.handler.handle_specific().__enter__(), + call.traverse_specific_item( + manager.handler, + manager.handler.handle_specific().__enter__(), + {}, + set(), + specific.value[0], + ), + call.traverse_specific_item( + manager.handler, + manager.handler.handle_specific().__enter__(), + {}, + set(), + specific.value[1], + ), + call.traverse_specific_item( + manager.handler, + manager.handler.handle_specific().__enter__(), + {}, + set(), + specific.value[2], + ), + call.handler.handle_specific().__exit__(None, None, None), + ] + manager.reset_mock() + + with ( + patch( + "rapids_pre_commit_hooks.utils.dependencies_yaml.traverse_specific_item", + manager.traverse_specific_item, + ), + ): + dependencies_yaml.traverse_specific( + manager.handler, + dependency_set_context, + {}, + set(), + specific_key, + specific, + ) + + assert manager.mock_calls == expected_calls + + +def test_traverse_dependency_set(): + dependencies = yaml.SafeLoader("""\ + dependency_set_1: + common: {} + specific: {} + """).get_single_node() + dependency_set_key, dependency_set = dependencies.value[0] + dependencies_context = Mock() + manager = MagicMock() + + expected_calls = [ + call.handler.handle_dependency_set( + dependencies_context, dependency_set_key, dependency_set + ), + call.handler.handle_dependency_set().__enter__(), + call.traverse_common( + manager.handler, + manager.handler.handle_dependency_set().__enter__(), + {}, + set(), + dependency_set.value[0][0], + dependency_set.value[0][1], + ), + call.traverse_specific( + manager.handler, + manager.handler.handle_dependency_set().__enter__(), + {}, + set(), + dependency_set.value[1][0], + dependency_set.value[1][1], + ), + call.handler.handle_dependency_set().__exit__(None, None, None), + ] + manager.reset_mock() + + with ( + patch( + "rapids_pre_commit_hooks.utils.dependencies_yaml.traverse_common", + manager.traverse_common, + ), + patch( + "rapids_pre_commit_hooks.utils.dependencies_yaml.traverse_specific", + manager.traverse_specific, + ), + ): + dependencies_yaml.traverse_dependency_set( + manager.handler, + dependencies_context, + {}, + set(), + dependency_set_key, + dependency_set, + ) + + assert manager.mock_calls == expected_calls + + +def test_traverse_dependencies(): + root = yaml.SafeLoader("""\ + dependencies: + dependency_set_1: {} + dependency_set_2: {} + """).get_single_node() + dependencies_key, dependencies = root.value[0] + root_context = Mock() + manager = MagicMock() + + expected_calls = [ + call.handler.handle_dependencies( + root_context, dependencies_key, dependencies + ), + call.handler.handle_dependencies().__enter__(), + call.traverse_dependency_set( + manager.handler, + manager.handler.handle_dependencies().__enter__(), + {}, + set(), + dependencies.value[0][0], + dependencies.value[0][1], + ), + call.traverse_dependency_set( + manager.handler, + manager.handler.handle_dependencies().__enter__(), + {}, + set(), + dependencies.value[1][0], + dependencies.value[1][1], + ), + call.handler.handle_dependencies().__exit__(None, None, None), + ] + manager.reset_mock() + + with patch( + "rapids_pre_commit_hooks.utils.dependencies_yaml.traverse_dependency_set", + manager.traverse_dependency_set, + ): + dependencies_yaml.traverse_dependencies( + manager.handler, + root_context, + {}, + set(), + dependencies_key, + dependencies, + ) + + assert manager.mock_calls == expected_calls + + +def test_traverse_root(): + root = yaml.SafeLoader("""\ + files: {} + channels: [] + dependencies: {} + """).get_single_node() + manager = MagicMock() + + expected_calls = [ + call.handler.handle_root(root), + call.handler.handle_root().__enter__(), + call.traverse_dependencies( + manager.handler, + manager.handler.handle_root().__enter__(), + {}, + set(), + root.value[2][0], + root.value[2][1], + ), + call.handler.handle_root().__exit__(None, None, None), + ] + manager.reset_mock() + + with patch( + "rapids_pre_commit_hooks.utils.dependencies_yaml.traverse_dependencies", + manager.traverse_dependencies, + ): + dependencies_yaml.traverse_root(manager.handler, {}, set(), root) + + assert manager.mock_calls == expected_calls