From b8031743a8c0dde1bf5d50ee38816cb17a44203a Mon Sep 17 00:00:00 2001 From: Deepak Jain Date: Tue, 25 Aug 2026 15:23:33 -0700 Subject: [PATCH] fix(cli): defer graph initialization Signed-off-by: Deepak Jain --- src/skillspector/__init__.py | 11 ++++++++- src/skillspector/cli.py | 2 +- src/skillspector/graph_proxy.py | 44 +++++++++++++++++++++++++++++++++ tests/unit/test_cli.py | 28 +++++++++++++++++++++ 4 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 src/skillspector/graph_proxy.py diff --git a/src/skillspector/__init__.py b/src/skillspector/__init__.py index 30ce93226..125619939 100644 --- a/src/skillspector/__init__.py +++ b/src/skillspector/__init__.py @@ -17,6 +17,9 @@ import warnings from importlib.metadata import version as _pkg_version +from typing import Any + +from skillspector.graph_proxy import graph __version__ = _pkg_version("skillspector") @@ -32,6 +35,12 @@ category=Warning, ) -from skillspector.graph import create_graph, graph # noqa: E402 (after filter setup) + +def create_graph() -> Any: + """Build and return a new SkillSpector workflow graph.""" + from skillspector.graph import create_graph as build_graph + + return build_graph() + __all__ = ["create_graph", "graph", "__version__"] diff --git a/src/skillspector/cli.py b/src/skillspector/cli.py index 4bb3e34df..2a75ca2aa 100644 --- a/src/skillspector/cli.py +++ b/src/skillspector/cli.py @@ -39,7 +39,7 @@ from skillspector import __version__, transitive from skillspector.cleanup import cleanup_result from skillspector.constants import RISK_THRESHOLD -from skillspector.graph import graph +from skillspector.graph_proxy import graph from skillspector.input_handler import validate_local_input_path from skillspector.inspection_ledger import ( MAX_INSPECTION_LEDGER_EVENTS, diff --git a/src/skillspector/graph_proxy.py b/src/skillspector/graph_proxy.py new file mode 100644 index 000000000..23526910a --- /dev/null +++ b/src/skillspector/graph_proxy.py @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# 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. + +"""Lightweight lazy access to the compiled SkillSpector workflow graph.""" + +from __future__ import annotations + +from threading import Lock +from typing import Any + + +class LazyGraph: + """Load the compiled workflow only when a caller first uses it.""" + + def __init__(self) -> None: + self._compiled: Any | None = None + self._lock = Lock() + + def _get_compiled(self) -> Any: + if self._compiled is None: + with self._lock: + if self._compiled is None: + from skillspector.graph import graph as compiled_graph + + self._compiled = compiled_graph + return self._compiled + + def __getattr__(self, name: str) -> Any: + return getattr(self._get_compiled(), name) + + +graph = LazyGraph() diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index bbb62c6e5..e4d1319b2 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -17,7 +17,9 @@ import ast import json +import os import re +import subprocess import sys from collections.abc import Callable, Iterator from contextlib import AbstractContextManager, ExitStack, contextmanager, nullcontext @@ -88,6 +90,32 @@ def test_cli_version() -> None: assert "v" in result.output +def test_cli_help_does_not_initialize_analyzers() -> None: + """Help should not compile the scan graph or warn about missing credentials.""" + env = os.environ.copy() + env["SKILLSPECTOR_PROVIDER"] = "nv_build" + for name in ("ANTHROPIC_API_KEY", "NVIDIA_INFERENCE_KEY", "OPENAI_API_KEY"): + env.pop(name, None) + + completed = subprocess.run( + [ + sys.executable, + "-c", + "from skillspector.cli import app; app()", + "--help", + ], + capture_output=True, + check=False, + env=env, + text=True, + timeout=15, + ) + + assert completed.returncode == 0 + assert "Usage:" in completed.stdout + assert "Skipping analyzer" not in completed.stderr + + def test_cli_scan_local_directory(tmp_path: Path) -> None: """scan with local directory runs graph and prints report.""" (tmp_path / "SKILL.md").write_text("---\nname: scan-test\n---\n# Safe", encoding="utf-8")