diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md index 5a77d58f0..830d4f357 100644 --- a/STYLEGUIDE.md +++ b/STYLEGUIDE.md @@ -138,17 +138,24 @@ This project uses lazy loading for heavy third-party dependencies to optimize im **Heavy third-party libraries** (>100ms import cost) should be lazy-loaded via `lazy_heavy_imports.py`: ```python -# Don't import directly -import pandas as pd -import numpy as np +from __future__ import annotations -# Use lazy loading with IDE support +# Don't import directly: +# import pandas as pd +# import numpy as np + +# Use lazy loading with IDE support: from typing import TYPE_CHECKING -from data_designer.lazy_heavy_imports import pd, np + +import data_designer.lazy_heavy_imports as lazy if TYPE_CHECKING: - import pandas as pd import numpy as np + import pandas as pd + + +def to_frame(values: np.ndarray) -> pd.DataFrame: + return lazy.pd.DataFrame({"x": values}) ``` This pattern provides: @@ -172,11 +179,18 @@ If you add a new dependency with significant import cost (>100ms): 2. **Update imports across codebase:** ```python + from __future__ import annotations + from typing import TYPE_CHECKING - from data_designer.lazy_heavy_imports import your_lib + + import data_designer.lazy_heavy_imports as lazy if TYPE_CHECKING: import your_library_name as your_lib + + + def summarize(model: your_lib.Model) -> str: + return lazy.your_lib.summarize(model) ``` 3. **Verify with performance test:** @@ -191,7 +205,7 @@ If you add a new dependency with significant import cost (>100ms): **DO put in TYPE_CHECKING:** - Internal `data_designer` imports used **only** in type hints - Imports that would cause circular dependencies -- Full imports of lazy-loaded libraries for IDE support (e.g., `import pandas as pd` in addition to runtime `from data_designer.lazy_heavy_imports import pd`) +- Full imports of lazy-loaded libraries for IDE support (e.g., `import pandas as pd` in addition to runtime `import data_designer.lazy_heavy_imports as lazy`) **DON'T put in TYPE_CHECKING:** - Standard library imports (`Path`, `Any`, `Callable`, `Literal`, `TypeAlias`, etc.) @@ -202,15 +216,18 @@ If you add a new dependency with significant import cost (>100ms): **Examples:** ```python +from __future__ import annotations + # CORRECT - Lazy-loaded library with IDE support from typing import TYPE_CHECKING -from data_designer.lazy_heavy_imports import pd + +import data_designer.lazy_heavy_imports as lazy if TYPE_CHECKING: import pandas as pd def load_data(path: str) -> pd.DataFrame: - return pd.read_csv(path) + return lazy.pd.read_csv(path) # CORRECT - Standard library NOT in TYPE_CHECKING from pathlib import Path diff --git a/packages/data-designer/tests/test_lazy_imports.py b/packages/data-designer/tests/test_lazy_imports.py index f8a181147..9231b2804 100644 --- a/packages/data-designer/tests/test_lazy_imports.py +++ b/packages/data-designer/tests/test_lazy_imports.py @@ -78,3 +78,15 @@ def test_runtime_src_avoids_from_lazy_heavy_imports_pattern() -> None: offenders.append(str(path.relative_to(repo_root))) assert not offenders, f"Runtime source files should avoid from-import lazy pattern: {offenders}" + + +def test_scripts_avoid_from_lazy_heavy_imports_pattern() -> None: + repo_root = Path(__file__).resolve().parents[3] + pattern = re.compile(r"^\s*from\s+data_designer\.lazy_heavy_imports\s+import\b", re.MULTILINE) + offenders: list[str] = [] + + for path in sorted((repo_root / "scripts").glob("**/*.py")): + if pattern.search(path.read_text()): + offenders.append(str(path.relative_to(repo_root))) + + assert not offenders, f"Scripts should avoid from-import lazy pattern: {offenders}" diff --git a/scripts/benchmarks/benchmark_engine_v2.py b/scripts/benchmarks/benchmark_engine_v2.py index 5b35662b7..2ee06a654 100644 --- a/scripts/benchmarks/benchmark_engine_v2.py +++ b/scripts/benchmarks/benchmark_engine_v2.py @@ -22,6 +22,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Any +import data_designer.lazy_heavy_imports as lazy from data_designer.config.column_configs import LLMTextColumnConfig, SamplerColumnConfig, ValidationColumnConfig from data_designer.config.config_builder import DataDesignerConfigBuilder from data_designer.config.mcp import MCPProvider, ToolConfig @@ -31,10 +32,8 @@ from data_designer.config.validator_params import LocalCallableValidatorParams, ValidatorType from data_designer.engine.mcp.registry import MCPToolDefinition, MCPToolResult from data_designer.engine.models.clients.types import AssistantMessage, ChatCompletionResponse, ToolCall -from data_designer.lazy_heavy_imports import np, pd if TYPE_CHECKING: - import numpy as np import pandas as pd @@ -277,11 +276,11 @@ def _format_stats(stats: MetricStats, *, unit: str, precision: int = 3) -> str: def _json_default(value: Any) -> Any: - if isinstance(value, np.generic): + if isinstance(value, lazy.np.generic): return value.item() - if isinstance(value, np.ndarray): + if isinstance(value, lazy.np.ndarray): return value.tolist() - if isinstance(value, (pd.Timestamp, pd.Timedelta)): + if isinstance(value, (lazy.pd.Timestamp, lazy.pd.Timedelta)): return value.isoformat() if isinstance(value, set): return sorted(value) @@ -472,10 +471,10 @@ def _validate_recommendation(df: pd.DataFrame) -> pd.DataFrame: series = df["llm_stage3"].astype(str) scores = series.map(lambda text: _extract_metric(text, "score")) latencies = series.map(lambda text: _extract_metric(text, "latency_ms")) - scores_numeric = pd.to_numeric(scores, errors="coerce") - latency_numeric = pd.to_numeric(latencies, errors="coerce") + scores_numeric = lazy.pd.to_numeric(scores, errors="coerce") + latency_numeric = lazy.pd.to_numeric(latencies, errors="coerce") is_valid = scores_numeric.between(0.0, 10.0) & latency_numeric.between(0.0, 900.0) - return pd.DataFrame( + return lazy.pd.DataFrame( { "is_valid": is_valid.fillna(False).astype(bool), "score": scores_numeric, @@ -584,7 +583,7 @@ def _run_single_benchmark(settings: BenchmarkSettings, engine_mode: str) -> Benc from data_designer.engine.storage.artifact_storage import ArtifactStorage random.seed(settings.seed) - np.random.seed(settings.seed) + lazy.np.random.seed(settings.seed) run_config = RunConfig( buffer_size=settings.buffer_size,