Entity relationship diagrams for Python data model classes like Pydantic.
—
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.
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.
"""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]: ...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, ProtocolErdantic comes with built-in support for several popular data modeling libraries:
# Check what plugins are currently available
from erdantic import list_plugins
print(list_plugins())
# Output: ['pydantic', 'pydantic_v1', 'dataclasses', 'attrs', 'msgspec']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."""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"])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
)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"])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})")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")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}")"pydantic" - Pydantic V2 models (pydantic.BaseModel)"pydantic_v1" - Pydantic V1 legacy models (pydantic.v1.BaseModel)"dataclasses" - Python standard library dataclasses"attrs" - attrs library models"msgspec" - msgspec library models# 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()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