Skip to content

Plugin discovery fails with circular import when plugin config inherits from SingleColumnConfig #281

@webmaxru

Description

@webmaxru

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 fields

3. 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:

  1. User imports my_plugin.config
  2. config.py imports from data_designer.config.column_configs import SingleColumnConfig
  3. This triggers data_designer.config initialization
  4. Which triggers plugin discovery via PluginManager
  5. Plugin discovery tries to validate my_plugin.config.MyPluginConfig
  6. _load() calls importlib.import_module("my_plugin.config")
  7. But that module is already being imported (step 1), so sys.modules["my_plugin.config"] exists but is incomplete
  8. 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() returns False — 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:

  1. Using uv run python instead of system Python (ensures correct venv)
  2. Importing DataDesigner before importing plugin config classes
  3. 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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions