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())