From ec5b0f2ad68937173557bfc4d5fcacbfc836d231 Mon Sep 17 00:00:00 2001 From: Jiangzhou He Date: Fri, 8 May 2026 09:12:52 -0700 Subject: [PATCH] perf(cli): lazy-load server, pathspec, and protocol imports Cuts `ccc status` cold-cache startup from ~0.5-0.9s to ~0.15s. - `cocoindex_code/__init__.py`: replace eager `from .server import main` with a PEP 562 `__getattr__` so importing the package no longer pulls `mcp.server.fastmcp` (~300ms). The `cocoindex-code = "cocoindex_code:main"` console script still resolves. - `settings.py`: defer `from pathspec import GitIgnoreSpec` into `load_gitignore_spec()` (only called by indexer/daemon). - `cli.py`: move protocol type-only imports under `TYPE_CHECKING` (safe with `from __future__ import annotations`); lazy-import `DaemonStartError` inside the wrapper and `DoctorCheckResult` next to the existing lazy `client` import. Also drops a stale paragraph from CLAUDE.md. --- CLAUDE.md | 2 -- src/cocoindex_code/__init__.py | 15 ++++++++++++++- src/cocoindex_code/cli.py | 17 +++++++++++++---- src/cocoindex_code/settings.py | 8 ++++++-- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b39a20c..8b74612 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,8 +12,6 @@ uv run pytest tests/ # Run Python tests ## Code Conventions -The full review-time detection checklist (with worked examples and post-hoc smell patterns) lives in the [`/review-changes`](~/.claude/skills/review-changes/SKILL.md) skill — run it on uncommitted changes before landing a non-trivial PR. The rules below are write-time mnemonics; project-specific ones (Internal/External modules) stay here in full. - ### Internal vs External Modules We distinguish between **internal modules** (under packages with `_` prefix, e.g. `_internal.*` or `connectors.*._source`) and **external modules** (which users can directly import). diff --git a/src/cocoindex_code/__init__.py b/src/cocoindex_code/__init__.py index 18c0f14..3a91808 100644 --- a/src/cocoindex_code/__init__.py +++ b/src/cocoindex_code/__init__.py @@ -1,10 +1,23 @@ """CocoIndex Code - MCP server for indexing and querying codebases.""" +from __future__ import annotations + import logging +from typing import TYPE_CHECKING, Any logging.basicConfig(level=logging.WARNING) from ._version import __version__ # noqa: E402 -from .server import main # noqa: E402 + +if TYPE_CHECKING: + from .server import main as main __all__ = ["main", "__version__"] + + +def __getattr__(name: str) -> Any: + if name == "main": + from .server import main + + return main + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/src/cocoindex_code/cli.py b/src/cocoindex_code/cli.py index 4cc0a48..71ebab9 100644 --- a/src/cocoindex_code/cli.py +++ b/src/cocoindex_code/cli.py @@ -7,12 +7,18 @@ import sys from collections.abc import Callable from pathlib import Path -from typing import TypeVar +from typing import TYPE_CHECKING, TypeVar import typer as _typer -from .client import DaemonStartError -from .protocol import DoctorCheckResult, IndexingProgress, ProjectStatusResponse, SearchResponse +if TYPE_CHECKING: + from .protocol import ( + DoctorCheckResult, + IndexingProgress, + ProjectStatusResponse, + SearchResponse, + ) + from .settings import ( DEFAULT_ST_MODEL, EmbeddingSettings, @@ -104,6 +110,8 @@ def _catch_daemon_start_error(func: _F) -> _F: @functools.wraps(func) def wrapper(*args: object, **kwargs: object) -> object: + from .client import DaemonStartError + try: return func(*args, **kwargs) except DaemonStartError as e: @@ -204,7 +212,7 @@ def _on_progress(progress: IndexingProgress) -> None: except RuntimeError as e: live.stop() # Let DaemonStartError propagate to the decorator for consistent handling. - if isinstance(e, DaemonStartError): + if isinstance(e, _client.DaemonStartError): raise _typer.echo(f"Indexing failed: {e}", err=True) raise _typer.Exit(code=1) @@ -397,6 +405,7 @@ def _run_init_model_check(settings_path: Path) -> None: from rich.spinner import Spinner as _Spinner from . import client as _client + from .protocol import DoctorCheckResult err_console = _Console(stderr=True) results: list[DoctorCheckResult] = [] diff --git a/src/cocoindex_code/settings.py b/src/cocoindex_code/settings.py index b47a0bb..73b026b 100644 --- a/src/cocoindex_code/settings.py +++ b/src/cocoindex_code/settings.py @@ -5,10 +5,12 @@ import os from dataclasses import dataclass, field from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any import yaml as _yaml -from pathspec import GitIgnoreSpec + +if TYPE_CHECKING: + from pathspec import GitIgnoreSpec # --------------------------------------------------------------------------- # Default file patterns (moved from indexer.py) @@ -391,6 +393,8 @@ def global_settings_mtime_us() -> int | None: def load_gitignore_spec(project_root: Path) -> GitIgnoreSpec | None: """Load a GitIgnoreSpec for the project's ``.gitignore`` if present.""" + from pathspec import GitIgnoreSpec + gitignore = project_root / ".gitignore" if not gitignore.is_file(): return None