Source code for interfacy.appearance.type_help

from types import NoneType
from typing import Any, Protocol

from objinspect.typing import is_union_type, type_args, type_origin
from stdl.st import TextStyle, with_style

from interfacy.util import resolve_type_alias, simplified_type_name


class TypeStyleTheme(Protocol):
    type_keyword: TextStyle
    type_bracket: TextStyle
    type_punctuation: TextStyle
    type_operator: TextStyle
    type_literal: TextStyle


[docs] def format_type_for_help( annotation: Any, style: TextStyle, theme: TypeStyleTheme | None = None, ) -> str: return TypeHelpFormatter(style, theme=theme).format(annotation)
[docs] class TypeHelpFormatter: _TYPE_BRACKETS = frozenset("[](){}") _TYPE_PUNCTUATION = frozenset({",", ":"}) _TYPE_OPERATORS = frozenset({"|", "?"}) _TYPE_KEYWORDS = frozenset( { "Annotated", "Any", "ClassVar", "Final", "Literal", "Never", "NoReturn", "None", "NoneType", "NotRequired", "Optional", "Required", "Self", "TypeAlias", "TypeGuard", "TypeVar", "Union", } ) def __init__( self, style: TextStyle, *, theme: TypeStyleTheme | None = None, ) -> None: self.style = style self.theme = theme self.token_styles = self._resolve_type_token_styles() def format(self, annotation: Any) -> str: type_text = self._stringify(annotation) rendered: list[str] = [] for kind, value in self._tokenize(type_text): if kind == "space": rendered.append(value) else: rendered.append(with_style(value, self.token_styles.get(kind, self.style))) return "".join(rendered) def _resolve_type_token_styles(self) -> dict[str, TextStyle]: def pick(name: str) -> TextStyle: if self.theme is None: return self.style return getattr(self.theme, name) return { "name": self.style, "keyword": pick("type_keyword"), "bracket": pick("type_bracket"), "punctuation": pick("type_punctuation"), "operator": pick("type_operator"), "literal": pick("type_literal"), "other": self.style, } def _stringify(self, annotation: Any) -> str: if isinstance(annotation, str): return simplified_type_name(annotation) resolved = resolve_type_alias(annotation) optional_union_name = self._stringify_optional_union_type(resolved) if optional_union_name is not None: return optional_union_name generic_type_name = self._stringify_generic_type(resolved) if generic_type_name is not None: return generic_type_name return simplified_type_name(self._stringify_fallback_type_name(resolved)) def _stringify_optional_union_type(self, annotation: Any) -> str | None: try: if not is_union_type(annotation): return None args = list(type_args(annotation)) except (AttributeError, KeyError, NameError, TypeError, ValueError): return None if len(args) != 2 or not any(arg is NoneType for arg in args): return None base = next((arg for arg in args if arg is not NoneType), None) if base is None: return "Any?" base_name = self._stringify(base) if base_name.endswith("?"): return base_name return f"{base_name}?" @staticmethod def _safe_stringify(value: Any) -> str: try: return str(value) except (RecursionError, TypeError, ValueError): return object.__repr__(value) def _stringify_generic_type(self, annotation: Any) -> str | None: try: origin = type_origin(annotation) args = type_args(annotation) except (AttributeError, KeyError, NameError, TypeError, ValueError): return None if origin is None or not args: return None return simplified_type_name(self._safe_stringify(annotation)) def _stringify_fallback_type_name(self, annotation: Any) -> str: if isinstance(annotation, type): return annotation.__name__ name = getattr(annotation, "__name__", None) if isinstance(name, str): return name return self._safe_stringify(annotation) @staticmethod def _consume_space(text: str, start: int) -> int: index = start + 1 while index < len(text) and text[index].isspace(): index += 1 return index @staticmethod def _consume_quoted_literal(text: str, start: int) -> int: quote = text[start] index = start + 1 escaped = False while index < len(text): current = text[index] if escaped: escaped = False elif current == "\\": escaped = True elif current == quote: index += 1 break index += 1 return index @staticmethod def _consume_numeric_literal(text: str, start: int) -> int: index = start + 1 while index < len(text) and (text[index].isdigit() or text[index] == "."): index += 1 return index @staticmethod def _consume_identifier(text: str, start: int) -> int: index = start + 1 while index < len(text) and (text[index].isalnum() or text[index] in {"_", "."}): index += 1 return index def _next_type_token(self, type_text: str, start: int) -> tuple[str, str, int]: ch = type_text[start] kind = "other" value = ch next_index = start + 1 if ch.isspace(): next_index = self._consume_space(type_text, start) kind = "space" value = type_text[start:next_index] elif ch in self._TYPE_BRACKETS: kind = "bracket" elif ch in self._TYPE_PUNCTUATION: kind = "punctuation" elif ch in self._TYPE_OPERATORS: kind = "operator" elif ch in {"'", '"'}: next_index = self._consume_quoted_literal(type_text, start) kind = "literal" value = type_text[start:next_index] elif ch.isdigit(): next_index = self._consume_numeric_literal(type_text, start) kind = "literal" value = type_text[start:next_index] elif ch.isalpha() or ch == "_": next_index = self._consume_identifier(type_text, start) value = type_text[start:next_index] kind = "keyword" if value in self._TYPE_KEYWORDS else "name" return kind, value, next_index def _tokenize(self, type_text: str) -> list[tuple[str, str]]: tokens: list[tuple[str, str]] = [] index = 0 while index < len(type_text): kind, value, index = self._next_type_token(type_text, index) tokens.append((kind, value)) return tokens
__all__ = ["TypeHelpFormatter", "TypeStyleTheme", "format_type_for_help"]