diff --git a/instructor/__init__.py b/instructor/__init__.py index 0b4f6245b..16e1b6065 100644 --- a/instructor/__init__.py +++ b/instructor/__init__.py @@ -1,40 +1,45 @@ +import importlib import importlib.util +from typing import Any __version__ = "1.15.1" -from .mode import Mode -from .processing.multimodal import Image, Audio - -from .dsl import ( - CitationMixin, - Maybe, - Partial, - IterableModel, -) - -from .validation import llm_validator, openai_moderation -from .processing.function_calls import OpenAISchema, openai_schema -from .processing.schema import ( - generate_openai_schema, - generate_anthropic_schema, - generate_gemini_schema, -) -from .core.patch import apatch, patch -from .core.client import ( - Instructor, - AsyncInstructor, - from_openai, - from_litellm, -) -from .core import hooks -from .utils.providers import Provider -from .auto_client import from_provider -from .batch import BatchProcessor, BatchRequest, BatchJob -from .distil import FinetuneFormat, Instructions - -# Backward compatibility: Re-export removed functions -from .processing.response import handle_response_model -from .dsl.parallel import handle_parallel_model +_LAZY_ATTRS: dict[str, tuple[str, str | None]] = { + "Mode": (".mode", "Mode"), + "Image": (".processing.multimodal", "Image"), + "Audio": (".processing.multimodal", "Audio"), + "CitationMixin": (".dsl", "CitationMixin"), + "Maybe": (".dsl", "Maybe"), + "Partial": (".dsl", "Partial"), + "IterableModel": (".dsl", "IterableModel"), + "llm_validator": (".validation", "llm_validator"), + "openai_moderation": (".validation", "openai_moderation"), + "OpenAISchema": (".processing.function_calls", "OpenAISchema"), + "openai_schema": (".processing.function_calls", "openai_schema"), + "generate_openai_schema": (".processing.schema", "generate_openai_schema"), + "generate_anthropic_schema": ( + ".processing.schema", + "generate_anthropic_schema", + ), + "generate_gemini_schema": (".processing.schema", "generate_gemini_schema"), + "apatch": (".core.patch", "apatch"), + "patch": (".core.patch", "patch"), + "Instructor": (".core.client", "Instructor"), + "AsyncInstructor": (".core.client", "AsyncInstructor"), + "from_openai": (".core.client", "from_openai"), + "from_litellm": (".core.client", "from_litellm"), + "hooks": (".core.hooks", None), + "Provider": (".utils.providers", "Provider"), + "from_provider": (".auto_client", "from_provider"), + "BatchProcessor": (".batch", "BatchProcessor"), + "BatchRequest": (".batch", "BatchRequest"), + "BatchJob": (".batch", "BatchJob"), + "FinetuneFormat": (".distil", "FinetuneFormat"), + "Instructions": (".distil", "Instructions"), + "handle_response_model": (".processing.response", "handle_response_model"), + "handle_parallel_model": (".dsl.parallel", "handle_parallel_model"), + "client": (".client", None), +} __all__ = [ "Instructor", @@ -65,90 +70,93 @@ "llm_validator", "openai_moderation", "hooks", - "client", # Backward compatibility - # Backward compatibility exports + "client", "handle_response_model", "handle_parallel_model", ] -# Backward compatibility: Make instructor.client available as an attribute -# This allows code like `instructor.client.Instructor` to work -from . import client +def _register_optional_attr( + export_name: str, + module_name: str, + attr_name: str, +) -> None: + _LAZY_ATTRS[export_name] = (module_name, attr_name) + __all__.append(export_name) -if importlib.util.find_spec("anthropic") is not None: - from .providers.anthropic.client import from_anthropic - __all__ += ["from_anthropic"] +if importlib.util.find_spec("anthropic") is not None: + _register_optional_attr( + "from_anthropic", ".providers.anthropic.client", "from_anthropic" + ) -# Keep from_gemini for backward compatibility but it's deprecated if ( importlib.util.find_spec("google") and importlib.util.find_spec("google.generativeai") is not None ): - from .providers.gemini.client import from_gemini - - __all__ += ["from_gemini"] + _register_optional_attr("from_gemini", ".providers.gemini.client", "from_gemini") if importlib.util.find_spec("fireworks") is not None: - from .providers.fireworks.client import from_fireworks - - __all__ += ["from_fireworks"] + _register_optional_attr( + "from_fireworks", ".providers.fireworks.client", "from_fireworks" + ) if importlib.util.find_spec("cerebras") is not None: - from .providers.cerebras.client import from_cerebras - - __all__ += ["from_cerebras"] + _register_optional_attr( + "from_cerebras", ".providers.cerebras.client", "from_cerebras" + ) if importlib.util.find_spec("groq") is not None: - from .providers.groq.client import from_groq - - __all__ += ["from_groq"] + _register_optional_attr("from_groq", ".providers.groq.client", "from_groq") if importlib.util.find_spec("mistralai") is not None: - from .providers.mistral.client import from_mistral - - __all__ += ["from_mistral"] + _register_optional_attr("from_mistral", ".providers.mistral.client", "from_mistral") if importlib.util.find_spec("cohere") is not None: - from .providers.cohere.client import from_cohere - - __all__ += ["from_cohere"] + _register_optional_attr("from_cohere", ".providers.cohere.client", "from_cohere") if all(importlib.util.find_spec(pkg) for pkg in ("vertexai", "jsonref")): - try: - from .providers.vertexai.client import from_vertexai - except Exception: - # Optional dependency may be present but broken/misconfigured at import time. - # Avoid failing `import instructor` in that case. - pass - else: - __all__ += ["from_vertexai"] + _register_optional_attr( + "from_vertexai", ".providers.vertexai.client", "from_vertexai" + ) if importlib.util.find_spec("boto3") is not None: - from .providers.bedrock.client import from_bedrock - - __all__ += ["from_bedrock"] + _register_optional_attr("from_bedrock", ".providers.bedrock.client", "from_bedrock") if importlib.util.find_spec("writerai") is not None: - from .providers.writer.client import from_writer - - __all__ += ["from_writer"] + _register_optional_attr("from_writer", ".providers.writer.client", "from_writer") if importlib.util.find_spec("xai_sdk") is not None: - from .providers.xai.client import from_xai - - __all__ += ["from_xai"] + _register_optional_attr("from_xai", ".providers.xai.client", "from_xai") if importlib.util.find_spec("openai") is not None: - from .providers.perplexity.client import from_perplexity - - __all__ += ["from_perplexity"] + _register_optional_attr( + "from_perplexity", ".providers.perplexity.client", "from_perplexity" + ) if ( importlib.util.find_spec("google") and importlib.util.find_spec("google.genai") is not None ): - from .providers.genai.client import from_genai + _register_optional_attr("from_genai", ".providers.genai.client", "from_genai") + + +def __getattr__(name: str) -> Any: + if name not in _LAZY_ATTRS: + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + + module_name, attr_name = _LAZY_ATTRS[name] + module = importlib.import_module(module_name, __name__) + value: Any + + if attr_name is None: + value = module + else: + value = getattr(module, attr_name) + + globals()[name] = value + return value + - __all__ += ["from_genai"] +def __dir__() -> list[str]: + return sorted(set(globals()) | set(__all__)) diff --git a/instructor/core/__init__.py b/instructor/core/__init__.py index 7e1e46f51..f88a6305a 100644 --- a/instructor/core/__init__.py +++ b/instructor/core/__init__.py @@ -1,46 +1,47 @@ """Core components of the instructor package.""" -from .client import Instructor, AsyncInstructor, Response, from_openai, from_litellm -from .exceptions import ( - InstructorRetryException, - InstructorError, - ConfigurationError, - IncompleteOutputException, - ValidationError, - ProviderError, - ModeError, - ClientError, - AsyncValidationError, - FailedAttempt, - ResponseParsingError, - MultimodalError, -) -from .hooks import Hooks, HookName -from .patch import patch, apatch -from .retry import retry_sync, retry_async - -__all__ = [ - "Instructor", - "AsyncInstructor", - "Response", - "InstructorRetryException", - "InstructorError", - "ConfigurationError", - "IncompleteOutputException", - "ValidationError", - "ProviderError", - "ModeError", - "ClientError", - "AsyncValidationError", - "FailedAttempt", - "ResponseParsingError", - "MultimodalError", - "Hooks", - "HookName", - "patch", - "apatch", - "from_openai", - "from_litellm", - "retry_sync", - "retry_async", -] +import importlib +from typing import Any + +_LAZY_ATTRS: dict[str, tuple[str, str]] = { + "Instructor": (".client", "Instructor"), + "AsyncInstructor": (".client", "AsyncInstructor"), + "Response": (".client", "Response"), + "from_openai": (".client", "from_openai"), + "from_litellm": (".client", "from_litellm"), + "InstructorRetryException": (".exceptions", "InstructorRetryException"), + "InstructorError": (".exceptions", "InstructorError"), + "ConfigurationError": (".exceptions", "ConfigurationError"), + "IncompleteOutputException": (".exceptions", "IncompleteOutputException"), + "ValidationError": (".exceptions", "ValidationError"), + "ProviderError": (".exceptions", "ProviderError"), + "ModeError": (".exceptions", "ModeError"), + "ClientError": (".exceptions", "ClientError"), + "AsyncValidationError": (".exceptions", "AsyncValidationError"), + "FailedAttempt": (".exceptions", "FailedAttempt"), + "ResponseParsingError": (".exceptions", "ResponseParsingError"), + "MultimodalError": (".exceptions", "MultimodalError"), + "Hooks": (".hooks", "Hooks"), + "HookName": (".hooks", "HookName"), + "patch": (".patch", "patch"), + "apatch": (".patch", "apatch"), + "retry_sync": (".retry", "retry_sync"), + "retry_async": (".retry", "retry_async"), +} + +__all__ = list(_LAZY_ATTRS) + + +def __getattr__(name: str) -> Any: + if name not in _LAZY_ATTRS: + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + + module_name, attr_name = _LAZY_ATTRS[name] + module = importlib.import_module(module_name, __name__) + value = getattr(module, attr_name) + globals()[name] = value + return value + + +def __dir__() -> list[str]: + return sorted(set(globals()) | set(__all__)) diff --git a/instructor/processing/__init__.py b/instructor/processing/__init__.py index 477a686dc..b0395e87c 100644 --- a/instructor/processing/__init__.py +++ b/instructor/processing/__init__.py @@ -1,30 +1,35 @@ """Processing components for request/response handling.""" -from .function_calls import OpenAISchema, openai_schema -from .multimodal import convert_messages -from .response import ( - handle_response_model, - process_response, - process_response_async, - handle_reask_kwargs, -) -from .schema import ( - generate_openai_schema, - generate_anthropic_schema, - generate_gemini_schema, -) -from .validators import Validator - -__all__ = [ - "OpenAISchema", - "openai_schema", - "convert_messages", - "handle_response_model", - "process_response", - "process_response_async", - "handle_reask_kwargs", - "generate_openai_schema", - "generate_anthropic_schema", - "generate_gemini_schema", - "Validator", -] +import importlib +from typing import Any + +_LAZY_ATTRS: dict[str, tuple[str, str]] = { + "OpenAISchema": (".function_calls", "OpenAISchema"), + "openai_schema": (".function_calls", "openai_schema"), + "convert_messages": (".multimodal", "convert_messages"), + "handle_response_model": (".response", "handle_response_model"), + "process_response": (".response", "process_response"), + "process_response_async": (".response", "process_response_async"), + "handle_reask_kwargs": (".response", "handle_reask_kwargs"), + "generate_openai_schema": (".schema", "generate_openai_schema"), + "generate_anthropic_schema": (".schema", "generate_anthropic_schema"), + "generate_gemini_schema": (".schema", "generate_gemini_schema"), + "Validator": (".validators", "Validator"), +} + +__all__ = list(_LAZY_ATTRS) + + +def __getattr__(name: str) -> Any: + if name not in _LAZY_ATTRS: + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + + module_name, attr_name = _LAZY_ATTRS[name] + module = importlib.import_module(module_name, __name__) + value = getattr(module, attr_name) + globals()[name] = value + return value + + +def __dir__() -> list[str]: + return sorted(set(globals()) | set(__all__)) diff --git a/instructor/processing/response.py b/instructor/processing/response.py index 93ac72b37..2bce0d04e 100644 --- a/instructor/processing/response.py +++ b/instructor/processing/response.py @@ -44,7 +44,7 @@ class User(BaseModel): from pydantic import BaseModel from typing_extensions import ParamSpec -from instructor.core.exceptions import InstructorError, ConfigurationError +from ..core.exceptions import InstructorError, ConfigurationError from ..dsl.iterable import IterableBase from ..dsl.parallel import ParallelBase diff --git a/tests/test_import_lazy_loading.py b/tests/test_import_lazy_loading.py new file mode 100644 index 000000000..6cd9403bc --- /dev/null +++ b/tests/test_import_lazy_loading.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import subprocess +import sys + + +def run_python(script: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, "-c", script], + capture_output=True, + text=True, + check=False, + ) + + +def test_import_instructor_does_not_eagerly_load_response_or_provider_utils() -> None: + result = run_python( + """ +import sys +import instructor + +unexpected = [ + name + for name in ( + "instructor.processing.response", + "instructor.providers.anthropic.utils", + "instructor.providers.bedrock.utils", + "instructor.providers.cohere.utils", + "instructor.providers.gemini.utils", + "instructor.providers.mistral.utils", + "instructor.providers.openai.utils", + ) + if name in sys.modules +] + +if unexpected: + raise SystemExit(f"unexpected eager imports: {unexpected}") +""" + ) + + assert result.returncode == 0, result.stderr or result.stdout + + +def test_lazy_top_level_exports_import_on_demand() -> None: + result = run_python( + """ +import sys +import instructor + +assert "instructor.processing.response" not in sys.modules +_ = instructor.patch +assert "instructor.processing.response" in sys.modules +""" + ) + + assert result.returncode == 0, result.stderr or result.stdout