-
Notifications
You must be signed in to change notification settings - Fork 57
Description
Summary
When a third-party plugin's config module imports from data_designer.config, a circular import race condition causes plugin discovery to fail with a misleading error message, even though the plugin is correctly implemented.
Problem
During plugin discovery, the Plugin._load() method uses importlib.import_module() to load the config class. However, if the plugin's config module is currently being imported (i.e., partially initialized), getattr(module, class_name) fails because the class definition hasn't been executed yet.
Error Message
🛑 Failed to load plugin from entry point 'my-plugin': Could not find class 'MyPluginConfig' in module 'my_plugin.config'
This is confusing because the class does exist in the file — it's just not available yet due to import timing.
Reproduction
1. Create a plugin with this structure:
my-plugin/
├── pyproject.toml
└── src/
└── my_plugin/
├── __init__.py
├── config.py
├── generator.py
└── plugin.py
2. Plugin config (config.py):
from data_designer.config.column_configs import SingleColumnConfig
from typing import Literal
class MyPluginConfig(SingleColumnConfig):
column_type: Literal["my-plugin"] = "my-plugin"
# ... other fields3. Plugin entry point (plugin.py):
from data_designer.plugins import Plugin, PluginType
plugin = Plugin(
impl_qualified_name="my_plugin.generator.MyPluginGenerator",
config_qualified_name="my_plugin.config.MyPluginConfig",
plugin_type=PluginType.COLUMN_GENERATOR,
)4. User script:
from my_plugin.config import MyPluginConfig # This triggers the bug!
from data_designer.interface import DataDesigner
# ...Import chain that causes the issue:
- User imports
my_plugin.config config.pyimportsfrom data_designer.config.column_configs import SingleColumnConfig- This triggers
data_designer.configinitialization - Which triggers plugin discovery via
PluginManager - Plugin discovery tries to validate
my_plugin.config.MyPluginConfig _load()callsimportlib.import_module("my_plugin.config")- But that module is already being imported (step 1), so
sys.modules["my_plugin.config"]exists but is incomplete getattr(module, "MyPluginConfig")fails →PluginLoadError
Proposed Solution
Modify Plugin._load() to handle partially-initialized modules by attempting a reload:
@staticmethod
def _load(fully_qualified_object: str) -> type:
module_name, object_name = _get_module_and_object_names(fully_qualified_object)
module = importlib.import_module(module_name)
# Handle case where module is partially initialized (circular import during plugin discovery).
# If the class isn't available yet, try reloading the module.
if not hasattr(module, object_name):
# Module may be partially loaded due to circular imports during plugin discovery.
# Try to reload it to complete initialization.
try:
module = importlib.reload(module)
except Exception:
pass # If reload fails, fall through to the error below
try:
return getattr(module, object_name)
except AttributeError:
raise PluginLoadError(f"Could not find class {object_name!r} in module {module_name!r}")Why This Fix Is Safe
| Scenario | Before | After |
|---|---|---|
| Normal load (class exists) | ✅ Works | ✅ Works (no change) |
| Class doesn't exist | ❌ PluginLoadError |
❌ Same error |
| Circular import (class not yet defined) | ❌ PluginLoadError |
✅ Reload succeeds |
- The reload only triggers when
hasattr()returnsFalse— meaning it would have failed anyway - Same exception type and message for actual missing classes
- No API changes
Workarounds (current)
Users can avoid this by:
- Using
uv run pythoninstead of system Python (ensures correct venv) - Importing
DataDesignerbefore importing plugin config classes - Using lazy imports in the plugin's
__init__.py
However, these are unintuitive and the error message doesn't guide users toward them.
Environment
- Data Designer version: latest (main branch)
- Python: 3.11+
- OS: Windows/Linux/macOS