Files
nvidia-nemo--speech/tests/core/test_safe_instantiate_usage.py
wehub-resource-sync ba4be087d5
Create PR to main with cherry-pick from release / cherry-pick (push) Failing after 0s
CICD NeMo / pre-flight (push) Failing after 0s
CICD NeMo / configure (push) Has been skipped
Build, validate, and release Neural Modules / pre-flight (push) Failing after 1s
CICD NeMo / code-linting (push) Has been skipped
Build, validate, and release Neural Modules / release (push) Has been skipped
Build, validate, and release Neural Modules / release-summary (push) Has been cancelled
CICD NeMo / cicd-test-container-build (push) Has been cancelled
CICD NeMo / cicd-import-tests (push) Has been cancelled
CICD NeMo / L0_Setup_Test_Data_And_Models (push) Has been cancelled
CICD NeMo / cicd-main-unit-tests (push) Has been cancelled
CICD NeMo / cicd-main-speech (push) Has been cancelled
CICD NeMo / Nemo_CICD_Test (push) Has been cancelled
CICD NeMo / Coverage (e2e) (push) Has been cancelled
CICD NeMo / Coverage (unit-test) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CICD NeMo / cicd-wait-in-queue (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:28:58 +08:00

118 lines
4.3 KiB
Python

# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import ast
import warnings
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
SAFE_INSTANTIATE_WRAPPER = REPO_ROOT / "nemo/core/classes/common.py"
SOURCE_DIRS = ("nemo", "scripts", "examples")
def _iter_python_files():
for source_dir in SOURCE_DIRS:
yield from (REPO_ROOT / source_dir).rglob("*.py")
def _name_for_call(node: ast.AST) -> str | None:
if isinstance(node, ast.Name):
return node.id
if isinstance(node, ast.Attribute):
parent = _name_for_call(node.value)
if parent is not None:
return f"{parent}.{node.attr}"
return None
class DirectHydraInstantiateVisitor(ast.NodeVisitor):
def __init__(self, path: Path):
self.path = path
self.function_stack = []
self.hydra_aliases = set()
self.hydra_utils_aliases = set()
self.hydra_instantiate_aliases = set()
self.violations = []
def visit_Import(self, node: ast.Import):
for alias in node.names:
if alias.name == "hydra":
self.hydra_aliases.add(alias.asname or "hydra")
elif alias.name == "hydra.utils" and alias.asname:
self.hydra_utils_aliases.add(alias.asname)
self.generic_visit(node)
def visit_ImportFrom(self, node: ast.ImportFrom):
if node.module == "hydra":
for alias in node.names:
if alias.name == "utils":
self.hydra_utils_aliases.add(alias.asname or alias.name)
elif node.module == "hydra.utils":
for alias in node.names:
if alias.name == "instantiate":
self.hydra_instantiate_aliases.add(alias.asname or alias.name)
self.generic_visit(node)
def visit_FunctionDef(self, node: ast.FunctionDef):
self.function_stack.append(node.name)
self.generic_visit(node)
self.function_stack.pop()
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
self.function_stack.append(node.name)
self.generic_visit(node)
self.function_stack.pop()
def visit_Call(self, node: ast.Call):
call_name = _name_for_call(node.func)
if self._is_direct_hydra_instantiate(call_name) and not self._is_allowed_wrapper_call():
self.violations.append((node.lineno, call_name))
self.generic_visit(node)
def _is_direct_hydra_instantiate(self, call_name: str | None) -> bool:
if call_name is None:
return False
if call_name in self.hydra_instantiate_aliases:
return True
if call_name.endswith(".instantiate"):
prefix = call_name[: -len(".instantiate")]
if prefix in self.hydra_utils_aliases:
return True
return any(prefix == f"{hydra_alias}.utils" for hydra_alias in self.hydra_aliases)
return False
def _is_allowed_wrapper_call(self) -> bool:
return self.path == SAFE_INSTANTIATE_WRAPPER and self.function_stack[-1:] == ["safe_instantiate"]
@pytest.mark.unit
def test_hydra_instantiate_is_only_called_by_safe_instantiate():
violations = []
for path in _iter_python_files():
with warnings.catch_warnings():
warnings.simplefilter("ignore", SyntaxWarning)
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
visitor = DirectHydraInstantiateVisitor(path)
visitor.visit(tree)
violations.extend((path.relative_to(REPO_ROOT), lineno, call_name) for lineno, call_name in visitor.violations)
assert (
not violations
), "Use nemo.core.classes.common.safe_instantiate instead of hydra.utils.instantiate:\n" + "\n".join(
f"{path}:{lineno} calls {call_name}" for path, lineno, call_name in violations
)