This commit is contained in:
+159
@@ -0,0 +1,159 @@
|
||||
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())
|
||||
Reference in New Issue
Block a user