CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-erdantic

Entity relationship diagrams for Python data model classes like Pydantic.

Pending
Overview
Eval results
Files

plugin-system.mddocs/

Plugin System

Erdantic's plugin system provides extensible support for different data modeling libraries. The system allows registration of plugins that can identify and extract field information from various model types like Pydantic, attrs, dataclasses, and msgspec.

Core Plugin Functions

def list_plugins() -> list[str]:
    """List the keys of all registered plugins."""

def register_plugin(key: str, predicate_fn: ModelPredicate[_ModelType], 
                   get_fields_fn: ModelFieldExtractor[_ModelType]):
    """Register a plugin for a specific model class type.

    Args:
        key (str): An identifier for this plugin.
        predicate_fn (ModelPredicate): A predicate function to determine if an object is a class
            of the model that is supported by this plugin.
        get_fields_fn (ModelFieldExtractor): A function to extract fields from a model class that
            is supported by this plugin.
    """

def get_predicate_fn(key: str) -> ModelPredicate:
    """Get the predicate function for a plugin by its key."""

def get_field_extractor_fn(key: str) -> ModelFieldExtractor:
    """Get the field extractor function for a plugin by its key."""

def identify_field_extractor_fn(tp: type) -> Optional[ModelFieldExtractor]:
    """Identify the field extractor function for a model type.

    Args:
        tp (type): A type annotation.

    Returns:
        ModelFieldExtractor | None: The field extractor function for a known model type, or None if
            the model type is not recognized by any registered plugins.
    """

def load_plugins():
    """Load all core plugins.
    
    This function is called automatically when erdantic is imported, but can be called
    manually if needed. It attempts to load all core plugins (pydantic, attrs, dataclasses,
    msgspec) and silently skips any with missing dependencies.
    """

Plugin Protocol Types

class ModelPredicate(Protocol[_ModelType_co]):
    """Protocol class for a predicate function for a plugin."""
    
    def __call__(self, obj: Any) -> TypeGuard[_ModelType_co]: ...

class ModelFieldExtractor(Protocol[_ModelType_contra]):
    """Protocol class for a field extractor function for a plugin."""
    
    def __call__(self, model: _ModelType_contra) -> Sequence[FieldInfo]: ...

Required Imports

from erdantic.plugins import (
    list_plugins, register_plugin, get_predicate_fn, 
    get_field_extractor_fn, identify_field_extractor_fn,
    load_plugins, ModelPredicate, ModelFieldExtractor
)
from erdantic.core import FieldInfo
from typing import Any, Optional, Sequence, TypeGuard, TypeVar, Protocol

Built-in Plugins

Erdantic comes with built-in support for several popular data modeling libraries:

Available Plugins

# Check what plugins are currently available
from erdantic import list_plugins
print(list_plugins())
# Output: ['pydantic', 'pydantic_v1', 'dataclasses', 'attrs', 'msgspec']

Pydantic Support

def is_pydantic_model(obj: Any) -> TypeGuard[Type[pydantic.BaseModel]]:
    """Predicate function to determine if an object is a Pydantic model (not an instance)."""

def get_fields_from_pydantic_model(model: Type[pydantic.BaseModel]) -> list[FieldInfo]:
    """Given a Pydantic model, return a list of FieldInfo instances for each field in the model."""

def is_pydantic_v1_model(obj: Any) -> TypeGuard[Type[pydantic.v1.BaseModel]]:
    """Predicate function to determine if an object is a Pydantic V1 model."""

def get_fields_from_pydantic_v1_model(model: Type[pydantic.v1.BaseModel]) -> list[FieldInfo]:
    """Given a Pydantic V1 model, return a list of FieldInfo instances for each field in the model."""

Usage Examples

Listing Available Plugins

from erdantic import list_plugins

# See all registered plugins
plugins = list_plugins()
print(f"Available plugins: {plugins}")

# Use in convenience functions to limit model search
from erdantic import draw
import my_models

# Only analyze Pydantic models in the module
draw(my_models, out="pydantic_only.png", limit_search_models_to=["pydantic"])

Creating Custom Plugins

from erdantic.plugins import register_plugin
from erdantic.core import FieldInfo, FullyQualifiedName
from typing import Any, TypeGuard
import inspect

# Example: Custom plugin for a hypothetical ORM
class MyORMModel:
    """Base class for custom ORM models."""
    pass

def is_my_orm_model(obj: Any) -> TypeGuard[type]:
    """Check if object is a MyORM model class."""
    return isinstance(obj, type) and issubclass(obj, MyORMModel)

def get_fields_from_my_orm_model(model: type) -> list[FieldInfo]:
    """Extract field information from MyORM model."""
    fields = []
    model_full_name = FullyQualifiedName.from_object(model)
    
    # Example: Inspect class attributes that are field descriptors
    for name, attr in inspect.getmembers(model):
        if hasattr(attr, '__field_type__'):  # Hypothetical field marker
            field_info = FieldInfo.from_raw_type(
                model_full_name=model_full_name,
                name=name,
                raw_type=attr.__field_type__
            )
            fields.append(field_info)
    
    return fields

# Register the custom plugin
register_plugin(
    key="my_orm",
    predicate_fn=is_my_orm_model,
    get_fields_fn=get_fields_from_my_orm_model
)

Using Custom Plugins

from erdantic import draw, list_plugins

# Verify custom plugin is registered
print(list_plugins())  # Should include "my_orm"

# Use the custom plugin
class User(MyORMModel):
    # Custom field definitions
    pass

class Post(MyORMModel):
    # Custom field definitions
    pass

# Generate diagram using custom models
draw(User, Post, out="custom_orm_models.png")

# Limit to only custom ORM models
draw(my_module, out="my_orm_only.png", limit_search_models_to=["my_orm"])

Introspecting Plugin System

from erdantic.plugins import get_predicate_fn, get_field_extractor_fn, identify_field_extractor_fn
from pydantic import BaseModel

# Get specific plugin functions
pydantic_predicate = get_predicate_fn("pydantic")
pydantic_extractor = get_field_extractor_fn("pydantic")

# Test predicate function
class MyModel(BaseModel):
    name: str

print(pydantic_predicate(MyModel))  # True
print(pydantic_predicate(dict))     # False

# Get extractor for a specific model type
extractor = identify_field_extractor_fn(MyModel)
if extractor:
    fields = extractor(MyModel)
    for field in fields:
        print(f"Field: {field.name} ({field.type_name})")

Plugin-Based Model Discovery

import inspect
from erdantic.plugins import identify_field_extractor_fn

def find_supported_models(module):
    """Find all classes in a module that are supported by registered plugins."""
    supported_models = []
    
    for name, obj in inspect.getmembers(module, inspect.isclass):
        if obj.__module__ == module.__name__:  # Only classes defined in this module
            if identify_field_extractor_fn(obj) is not None:
                supported_models.append(obj)
    
    return supported_models

# Usage
import my_models
supported = find_supported_models(my_models)
print(f"Found {len(supported)} supported model classes")

Error Handling

from erdantic.plugins import get_predicate_fn, get_field_extractor_fn
from erdantic.exceptions import PluginNotFoundError

try:
    # Try to get a non-existent plugin
    predicate = get_predicate_fn("nonexistent")
except PluginNotFoundError as e:
    print(f"Plugin not found: {e.key}")

try:
    extractor = get_field_extractor_fn("nonexistent")
except PluginNotFoundError as e:
    print(f"Plugin not found: {e.key}")

Built-in Plugin Details

Core Plugins Available

  1. "pydantic" - Pydantic V2 models (pydantic.BaseModel)
  2. "pydantic_v1" - Pydantic V1 legacy models (pydantic.v1.BaseModel)
  3. "dataclasses" - Python standard library dataclasses
  4. "attrs" - attrs library models
  5. "msgspec" - msgspec library models

Plugin Loading

# Plugins are automatically loaded when erdantic is imported
import erdantic  # This triggers plugin loading

# Manual plugin loading (normally not needed)
from erdantic.plugins import load_plugins
load_plugins()

Plugin Dependencies

Some plugins require optional dependencies:

# These will only be available if dependencies are installed:
# - "attrs" plugin requires: pip install attrs
# - "msgspec" plugin requires: pip install msgspec

# Check which plugins are actually active
from erdantic import list_plugins
active_plugins = list_plugins()
print(f"Active plugins: {active_plugins}")

Install with Tessl CLI

npx tessl i tessl/pypi-erdantic

docs

cli.md

convenience-functions.md

diagram-creation.md

exceptions.md

index.md

model-info.md

plugin-system.md

tile.json