160 lines
5.3 KiB
Python
160 lines
5.3 KiB
Python
from typing import Any, get_args, get_origin
|
||
|
||
from pydantic import BaseModel
|
||
from pydantic.fields import FieldInfo
|
||
from rich.console import Console
|
||
from rich.table import Table
|
||
|
||
|
||
def format_field_type(annotation: Any) -> str:
|
||
"""Форматирует тип поля для отображения"""
|
||
if annotation is None:
|
||
return "None"
|
||
|
||
origin = get_origin(annotation)
|
||
|
||
if origin is not None:
|
||
args = get_args(annotation)
|
||
origin_name = getattr(origin, "__name__", str(origin))
|
||
|
||
if origin_name == "UnionType" or str(origin) == "typing.Union":
|
||
args_str = " | ".join(format_field_type(a) for a in args)
|
||
return args_str
|
||
|
||
if args:
|
||
args_str = ", ".join(format_field_type(a) for a in args)
|
||
return f"{origin_name}[{args_str}]"
|
||
return origin_name
|
||
|
||
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
||
return f"[magenta]{annotation.__name__}[/magenta]"
|
||
|
||
return getattr(annotation, "__name__", str(annotation))
|
||
|
||
|
||
def print_model_fields(
|
||
console: Console,
|
||
model_class: type[BaseModel],
|
||
indent: int = 0,
|
||
visited: set | None = None,
|
||
) -> None:
|
||
"""Рекурсивно выводит поля модели с поддержкой вложенных моделей"""
|
||
if visited is None:
|
||
visited = set()
|
||
|
||
if model_class in visited:
|
||
console.print(
|
||
f"{' ' * indent}[dim](циклическая ссылка на {model_class.__name__})[/dim]"
|
||
)
|
||
return
|
||
visited.add(model_class)
|
||
|
||
prefix = " " * indent
|
||
|
||
for field_name, field_info in model_class.model_fields.items():
|
||
annotation = field_info.annotation
|
||
field_type = format_field_type(annotation)
|
||
default = field_info.default
|
||
description = field_info.description or ""
|
||
|
||
if default is None:
|
||
default_str = "[dim]None[/dim]"
|
||
elif default is ...:
|
||
default_str = "[red]required[/red]"
|
||
elif isinstance(default, bool):
|
||
default_str = "[green]true[/green]" if default else "[red]false[/red]"
|
||
else:
|
||
default_str = str(default)
|
||
|
||
console.print(
|
||
f"{prefix}[yellow]{field_name}[/yellow]: {field_type} = {default_str}"
|
||
+ (f" [dim]# {description}[/dim]" if description else "")
|
||
)
|
||
|
||
nested_model = None
|
||
|
||
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
||
nested_model = annotation
|
||
else:
|
||
origin = get_origin(annotation)
|
||
if origin is not None:
|
||
for arg in get_args(annotation):
|
||
if isinstance(arg, type) and issubclass(arg, BaseModel):
|
||
nested_model = arg
|
||
break
|
||
|
||
if nested_model is not None:
|
||
console.print(f"{prefix} [dim]└─ {nested_model.__name__}:[/dim]")
|
||
print_model_fields(console, nested_model, indent + 2, visited.copy())
|
||
|
||
|
||
def print_model_table(
|
||
console: Console,
|
||
model_class: type[BaseModel],
|
||
prefix: str = "",
|
||
visited: set | None = None,
|
||
) -> Table:
|
||
"""Выводит поля модели в виде таблицы с вложенными моделями"""
|
||
if visited is None:
|
||
visited = set()
|
||
|
||
table = Table(show_header=True, box=None if prefix else None)
|
||
table.add_column("Поле", style="yellow")
|
||
table.add_column("Тип", style="cyan")
|
||
table.add_column("По умолчанию")
|
||
table.add_column("Описание", style="dim")
|
||
|
||
_add_model_rows(table, model_class, prefix, visited)
|
||
|
||
return table
|
||
|
||
|
||
def _add_model_rows(
|
||
table: Table,
|
||
model_class: type[BaseModel],
|
||
prefix: str = "",
|
||
visited: set | None = None,
|
||
) -> None:
|
||
"""Добавляет строки модели в таблицу рекурсивно"""
|
||
if visited is None:
|
||
visited = set()
|
||
|
||
if model_class in visited:
|
||
return
|
||
visited.add(model_class)
|
||
|
||
for field_name, field_info in model_class.model_fields.items():
|
||
annotation = field_info.annotation
|
||
field_type = format_field_type(annotation)
|
||
default = field_info.default
|
||
description = field_info.description or ""
|
||
|
||
if default is None:
|
||
default_str = "-"
|
||
elif default is ...:
|
||
default_str = "[red]required[/red]"
|
||
elif isinstance(default, bool):
|
||
default_str = "true" if default else "false"
|
||
elif isinstance(default, BaseModel):
|
||
default_str = "{...}"
|
||
else:
|
||
default_str = str(default)[:20]
|
||
|
||
full_name = f"{prefix}{field_name}" if prefix else field_name
|
||
table.add_row(full_name, field_type, default_str, description[:40])
|
||
|
||
nested_model = None
|
||
|
||
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
||
nested_model = annotation
|
||
else:
|
||
origin = get_origin(annotation)
|
||
if origin is not None:
|
||
for arg in get_args(annotation):
|
||
if isinstance(arg, type) and issubclass(arg, BaseModel):
|
||
nested_model = arg
|
||
break
|
||
|
||
if nested_model is not None and nested_model not in visited:
|
||
_add_model_rows(table, nested_model, f" {full_name}.", visited.copy())
|