Skip to content

Plugin lazy loading #475

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: release-candidate/2025-04
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions pinecone/control/pinecone.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
""" @private """


class Pinecone(PineconeDBControlInterface, PluginAware):
class Pinecone(PluginAware, PineconeDBControlInterface):
"""
A client for interacting with Pinecone's vector database.

Expand Down Expand Up @@ -107,9 +107,8 @@ def __init__(
self.index_host_store = IndexHostStore()
""" @private """

self.load_plugins(
config=self.config, openapi_config=self.openapi_config, pool_threads=self.pool_threads
)
# Initialize PluginAware first, which will then call PineconeDBControlInterface.__init__
super().__init__()

@property
def inference(self):
Expand Down
4 changes: 1 addition & 3 deletions pinecone/data/features/inference/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,7 @@ def __init__(self, config, openapi_config, **kwargs) -> None:
api_version=API_VERSION,
)

self.load_plugins(
config=self.config, openapi_config=self.openapi_config, pool_threads=self.pool_threads
)
super().__init__() # Initialize PluginAware

def embed(
self,
Expand Down
6 changes: 1 addition & 5 deletions pinecone/data/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def parse_query_response(response: QueryResponse):
return response


class Index(IndexInterface, ImportFeatureMixin, PluginAware):
class Index(PluginAware, IndexInterface, ImportFeatureMixin):
"""
A client for interacting with a Pinecone index via REST API.
For improved performance, use the Pinecone GRPC index client.
Expand Down Expand Up @@ -101,10 +101,6 @@ def __init__(
# Pass the same api_client to the ImportFeatureMixin
super().__init__(api_client=self._api_client)

self.load_plugins(
config=self.config, openapi_config=self.openapi_config, pool_threads=self.pool_threads
)

def _openapi_kwargs(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
return filter_dict(kwargs, OPENAPI_ENDPOINT_PARAMS)

Expand Down
107 changes: 101 additions & 6 deletions pinecone/utils/plugin_aware.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from typing import Any
from .setup_openapi_client import build_plugin_setup_client
from pinecone.config import Config
from pinecone.openapi_support.configuration import Configuration as OpenApiConfig


from pinecone_plugin_interface import load_and_install as install_plugins
import logging

Expand All @@ -11,17 +11,112 @@


class PluginAware:
"""
Base class for classes that support plugin loading.

This class provides functionality to lazily load plugins when they are first accessed.
Subclasses must set the following attributes before calling super().__init__():
- config: Config
- openapi_config: OpenApiConfig
- pool_threads: int
"""

def __init__(self, *args: Any, **kwargs: Any) -> None:
"""
Initialize the PluginAware class.

Args:
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.

Raises:
AttributeError: If required attributes are not set in the subclass.
"""
logger.debug("PluginAware __init__ called for %s", self.__class__.__name__)

self._plugins_loaded = False
""" @private """

# Check for required attributes after super().__init__ has been called
missing_attrs = []
if not hasattr(self, "config"):
missing_attrs.append("config")
if not hasattr(self, "openapi_config"):
missing_attrs.append("openapi_config")
if not hasattr(self, "pool_threads"):
missing_attrs.append("pool_threads")

if missing_attrs:
raise AttributeError(
f"PluginAware class requires the following attributes: {', '.join(missing_attrs)}. "
f"These must be set in the {self.__class__.__name__} class's __init__ method "
f"before calling super().__init__()."
)

def __getattr__(self, name: str) -> Any:
"""
Called when an attribute is not found through the normal lookup process.
This allows for lazy loading of plugins when they are first accessed.

Args:
name: The name of the attribute being accessed.

Returns:
The requested attribute.

Raises:
AttributeError: If the attribute cannot be found after loading plugins.
"""
# Check if this is one of the required attributes that should be set by subclasses
required_attrs = ["config", "openapi_config", "pool_threads"]
if name in required_attrs:
raise AttributeError(
f"'{self.__class__.__name__}' object has no attribute '{name}'. "
f"This attribute must be set in the subclass's __init__ method "
f"before calling super().__init__()."
)

if not self._plugins_loaded:
logger.debug("Loading plugins for %s", self.__class__.__name__)
# Use object.__getattribute__ to avoid triggering __getattr__ again
try:
config = object.__getattribute__(self, "config")
openapi_config = object.__getattribute__(self, "openapi_config")
pool_threads = object.__getattribute__(self, "pool_threads")
self.load_plugins(
config=config, openapi_config=openapi_config, pool_threads=pool_threads
)
self._plugins_loaded = True
try:
return object.__getattribute__(self, name)
except AttributeError:
pass
except AttributeError:
# If we can't get the required attributes, we can't load plugins
pass

raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")

def load_plugins(
self, config: Config, openapi_config: OpenApiConfig, pool_threads: int
) -> None:
"""@private"""
"""
Load plugins for the parent class.

Args:
config: The Pinecone configuration.
openapi_config: The OpenAPI configuration.
pool_threads: The number of threads in the pool.
"""
try:
# I don't expect this to ever throw, but wrapping this in a
# try block just in case to make sure a bad plugin doesn't
# halt client initialization.
# Build the OpenAPI client for plugin setup
openapi_client_builder = build_plugin_setup_client(
config=config, openapi_config=openapi_config, pool_threads=pool_threads
)
# Install plugins
install_plugins(self, openapi_client_builder)
logger.debug("Plugins loaded successfully for %s", self.__class__.__name__)
except ImportError as e:
logger.warning("Failed to import plugin module: %s", e)
except Exception as e:
logger.error(f"Error loading plugins: {e}")
logger.error("Error loading plugins: %s", e, exc_info=True)
25 changes: 20 additions & 5 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ urllib3_mock = "0.3.3"
responses = ">=0.8.1"
ruff = "^0.9.3"
beautifulsoup4 = "^4.13.3"
pinecone-plugin-assistant = "^1.6.0"


[tool.poetry.extras]
Expand Down
7 changes: 5 additions & 2 deletions tests/unit/test_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,12 @@ def index_list_response():


class TestControl:
def test_plugins_are_installed(self):
def test_plugins_are_lazily_loaded(self):
with patch.object(PluginAware, "load_plugins") as mock_install_plugins:
Pinecone(api_key="asdf")
pc = Pinecone(api_key="asdf")
mock_install_plugins.assert_not_called()
with pytest.raises(AttributeError):
pc.foo() # Accessing a non-existent attribute should raise an AttributeError after PluginAware installs any applicable plugins
mock_install_plugins.assert_called_once()

def test_default_host(self):
Expand Down
49 changes: 49 additions & 0 deletions tests/unit/test_plugin_aware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import pytest
from pinecone.utils.plugin_aware import PluginAware
from pinecone.config import Config
from pinecone.openapi_support.configuration import Configuration as OpenApiConfig


class TestPluginAware:
def test_errors_when_required_attributes_are_missing(self):
class Foo(PluginAware):
def __init__(self):
# does not set config, openapi_config, or pool_threads
super().__init__()

with pytest.raises(AttributeError) as e:
Foo()

assert "config" in str(e.value)
assert "openapi_config" in str(e.value)
assert "pool_threads" in str(e.value)

def test_correctly_raise_attribute_errors(self):
class Foo(PluginAware):
def __init__(self):
self.config = Config()
self.openapi_config = OpenApiConfig()
self.pool_threads = 1

super().__init__()

foo = Foo()

with pytest.raises(AttributeError) as e:
foo.bar()

assert "bar" in str(e.value)

def test_plugins_are_lazily_loaded(self):
class Pinecone(PluginAware):
def __init__(self):
self.config = Config()
self.openapi_config = OpenApiConfig()
self.pool_threads = 10

super().__init__()

pc = Pinecone()
assert "assistant" not in dir(pc)

assert pc.assistant is not None
Loading