Skip to content

Commit d429632

Browse files
authored
perf(cli): lazy-load server, pathspec, and protocol imports (#164)
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.
1 parent efd1273 commit d429632

4 files changed

Lines changed: 33 additions & 9 deletions

File tree

CLAUDE.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ uv run pytest tests/ # Run Python tests
1212

1313
## Code Conventions
1414

15-
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.
16-
1715
### Internal vs External Modules
1816

1917
We distinguish between **internal modules** (under packages with `_` prefix, e.g. `_internal.*` or `connectors.*._source`) and **external modules** (which users can directly import).

src/cocoindex_code/__init__.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
"""CocoIndex Code - MCP server for indexing and querying codebases."""
22

3+
from __future__ import annotations
4+
35
import logging
6+
from typing import TYPE_CHECKING, Any
47

58
logging.basicConfig(level=logging.WARNING)
69

710
from ._version import __version__ # noqa: E402
8-
from .server import main # noqa: E402
11+
12+
if TYPE_CHECKING:
13+
from .server import main as main
914

1015
__all__ = ["main", "__version__"]
16+
17+
18+
def __getattr__(name: str) -> Any:
19+
if name == "main":
20+
from .server import main
21+
22+
return main
23+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

src/cocoindex_code/cli.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@
77
import sys
88
from collections.abc import Callable
99
from pathlib import Path
10-
from typing import TypeVar
10+
from typing import TYPE_CHECKING, TypeVar
1111

1212
import typer as _typer
1313

14-
from .client import DaemonStartError
15-
from .protocol import DoctorCheckResult, IndexingProgress, ProjectStatusResponse, SearchResponse
14+
if TYPE_CHECKING:
15+
from .protocol import (
16+
DoctorCheckResult,
17+
IndexingProgress,
18+
ProjectStatusResponse,
19+
SearchResponse,
20+
)
21+
1622
from .settings import (
1723
DEFAULT_ST_MODEL,
1824
EmbeddingSettings,
@@ -104,6 +110,8 @@ def _catch_daemon_start_error(func: _F) -> _F:
104110

105111
@functools.wraps(func)
106112
def wrapper(*args: object, **kwargs: object) -> object:
113+
from .client import DaemonStartError
114+
107115
try:
108116
return func(*args, **kwargs)
109117
except DaemonStartError as e:
@@ -204,7 +212,7 @@ def _on_progress(progress: IndexingProgress) -> None:
204212
except RuntimeError as e:
205213
live.stop()
206214
# Let DaemonStartError propagate to the decorator for consistent handling.
207-
if isinstance(e, DaemonStartError):
215+
if isinstance(e, _client.DaemonStartError):
208216
raise
209217
_typer.echo(f"Indexing failed: {e}", err=True)
210218
raise _typer.Exit(code=1)
@@ -397,6 +405,7 @@ def _run_init_model_check(settings_path: Path) -> None:
397405
from rich.spinner import Spinner as _Spinner
398406

399407
from . import client as _client
408+
from .protocol import DoctorCheckResult
400409

401410
err_console = _Console(stderr=True)
402411
results: list[DoctorCheckResult] = []

src/cocoindex_code/settings.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
import os
66
from dataclasses import dataclass, field
77
from pathlib import Path
8-
from typing import Any
8+
from typing import TYPE_CHECKING, Any
99

1010
import yaml as _yaml
11-
from pathspec import GitIgnoreSpec
11+
12+
if TYPE_CHECKING:
13+
from pathspec import GitIgnoreSpec
1214

1315
# ---------------------------------------------------------------------------
1416
# Default file patterns (moved from indexer.py)
@@ -391,6 +393,8 @@ def global_settings_mtime_us() -> int | None:
391393

392394
def load_gitignore_spec(project_root: Path) -> GitIgnoreSpec | None:
393395
"""Load a GitIgnoreSpec for the project's ``.gitignore`` if present."""
396+
from pathspec import GitIgnoreSpec
397+
394398
gitignore = project_root / ".gitignore"
395399
if not gitignore.is_file():
396400
return None

0 commit comments

Comments
 (0)