codekingpro/portable-devtools
114k
1from __future__ import absolute_import2 3import inspect4from inspect import cleandoc, getdoc, getfile, isclass, ismodule, signature5from typing import Any, Collection, Iterable, Optional, Tuple, Type, Union6 7from .console import Group, RenderableType8from .control import escape_control_codes9from .highlighter import ReprHighlighter10from .jupyter import JupyterMixin11from .panel import Panel12from .pretty import Pretty13from .table import Table14from .text import Text, TextType15 16 17def _first_paragraph(doc: str) -> str:18 """Get the first paragraph from a docstring."""19 paragraph, _, _ = doc.partition("\n\n")20 return paragraph21 22 23class Inspect(JupyterMixin):24 """A renderable to inspect any Python Object.25 26 Args:27 obj (Any): An object to inspect.28 title (str, optional): Title to display over inspect result, or None use type. Defaults to None.29 help (bool, optional): Show full help text rather than just first paragraph. Defaults to False.30 methods (bool, optional): Enable inspection of callables. Defaults to False.31 docs (bool, optional): Also render doc strings. Defaults to True.32 private (bool, optional): Show private attributes (beginning with underscore). Defaults to False.33 dunder (bool, optional): Show attributes starting with double underscore. Defaults to False.34 sort (bool, optional): Sort attributes alphabetically. Defaults to True.35 all (bool, optional): Show all attributes. Defaults to False.36 value (bool, optional): Pretty print value of object. Defaults to True.37 """38 39 def __init__(40 self,41 obj: Any,42 *,43 title: Optional[TextType] = None,44 help: bool = False,45 methods: bool = False,46 docs: bool = True,47 private: bool = False,48 dunder: bool = False,49 sort: bool = True,50 all: bool = True,51 value: bool = True,52 ) -> None:53 self.highlighter = ReprHighlighter()54 self.obj = obj55 self.title = title or self._make_title(obj)56 if all:57 methods = private = dunder = True58 self.help = help59 self.methods = methods60 self.docs = docs or help61 self.private = private or dunder62 self.dunder = dunder63 self.sort = sort64 self.value = value65 66 def _make_title(self, obj: Any) -> Text:67 """Make a default title."""68 title_str = (69 str(obj)70 if (isclass(obj) or callable(obj) or ismodule(obj))71 else str(type(obj))72 )73 title_text = self.highlighter(title_str)74 return title_text75 76 def __rich__(self) -> Panel:77 return Panel.fit(78 Group(*self._render()),79 title=self.title,80 border_style="scope.border",81 padding=(0, 1),82 )83 84 def _get_signature(self, name: str, obj: Any) -> Optional[Text]:85 """Get a signature for a callable."""86 try:87 _signature = str(signature(obj)) + ":"88 except ValueError:89 _signature = "(...)"90 except TypeError:91 return None92 93 source_filename: Optional[str] = None94 try:95 source_filename = getfile(obj)96 except (OSError, TypeError):97 # OSError is raised if obj has no source file, e.g. when defined in REPL.98 pass99 100 callable_name = Text(name, style="inspect.callable")101 if source_filename:102 callable_name.stylize(f"link file://{source_filename}")103 signature_text = self.highlighter(_signature)104 105 qualname = name or getattr(obj, "__qualname__", name)106 107 # If obj is a module, there may be classes (which are callable) to display108 if inspect.isclass(obj):109 prefix = "class"110 elif inspect.iscoroutinefunction(obj):111 prefix = "async def"112 else:113 prefix = "def"114 115 qual_signature = Text.assemble(116 (f"{prefix} ", f"inspect.{prefix.replace(' ', '_')}"),117 (qualname, "inspect.callable"),118 signature_text,119 )120 121 return qual_signature122 123 def _render(self) -> Iterable[RenderableType]:124 """Render object."""125 126 def sort_items(item: Tuple[str, Any]) -> Tuple[bool, str]:127 key, (_error, value) = item128 return (callable(value), key.strip("_").lower())129 130 def safe_getattr(attr_name: str) -> Tuple[Any, Any]:131 """Get attribute or any exception."""132 try:133 return (None, getattr(obj, attr_name))134 except Exception as error:135 return (error, None)136 137 obj = self.obj138 keys = dir(obj)139 total_items = len(keys)140 if not self.dunder:141 keys = [key for key in keys if not key.startswith("__")]142 if not self.private:143 keys = [key for key in keys if not key.startswith("_")]144 not_shown_count = total_items - len(keys)145 items = [(key, safe_getattr(key)) for key in keys]146 if self.sort:147 items.sort(key=sort_items)148 149 items_table = Table.grid(padding=(0, 1), expand=False)150 items_table.add_column(justify="right")151 add_row = items_table.add_row152 highlighter = self.highlighter153 154 if callable(obj):155 signature = self._get_signature("", obj)156 if signature is not None:157 yield signature158 yield ""159 160 if self.docs:161 _doc = self._get_formatted_doc(obj)162 if _doc is not None:163 doc_text = Text(_doc, style="inspect.help")164 doc_text = highlighter(doc_text)165 yield doc_text166 yield ""167 168 if self.value and not (isclass(obj) or callable(obj) or ismodule(obj)):169 yield Panel(170 Pretty(obj, indent_guides=True, max_length=10, max_string=60),171 border_style="inspect.value.border",172 )173 yield ""174 175 for key, (error, value) in items:176 key_text = Text.assemble(177 (178 key,179 "inspect.attr.dunder" if key.startswith("__") else "inspect.attr",180 ),181 (" =", "inspect.equals"),182 )183 if error is not None:184 warning = key_text.copy()185 warning.stylize("inspect.error")186 add_row(warning, highlighter(repr(error)))187 continue188 189 if callable(value):190 if not self.methods:191 continue192 193 _signature_text = self._get_signature(key, value)194 if _signature_text is None:195 add_row(key_text, Pretty(value, highlighter=highlighter))196 else:197 if self.docs:198 docs = self._get_formatted_doc(value)199 if docs is not None:200 _signature_text.append("\n" if "\n" in docs else " ")201 doc = highlighter(docs)202 doc.stylize("inspect.doc")203 _signature_text.append(doc)204 205 add_row(key_text, _signature_text)206 else:207 add_row(key_text, Pretty(value, highlighter=highlighter))208 if items_table.row_count:209 yield items_table210 elif not_shown_count:211 yield Text.from_markup(212 f"[b cyan]{not_shown_count}[/][i] attribute(s) not shown.[/i] "213 f"Run [b][magenta]inspect[/]([not b]inspect[/])[/b] for options."214 )215 216 def _get_formatted_doc(self, object_: Any) -> Optional[str]:217 """218 Extract the docstring of an object, process it and returns it.219 The processing consists in cleaning up the doctring's indentation,220 taking only its 1st paragraph if `self.help` is not True,221 and escape its control codes.222 223 Args:224 object_ (Any): the object to get the docstring from.225 226 Returns:227 Optional[str]: the processed docstring, or None if no docstring was found.228 """229 docs = getdoc(object_)230 if docs is None:231 return None232 docs = cleandoc(docs).strip()233 if not self.help:234 docs = _first_paragraph(docs)235 return escape_control_codes(docs)236 237 238def get_object_types_mro(obj: Union[object, Type[Any]]) -> Tuple[type, ...]:239 """Returns the MRO of an object's class, or of the object itself if it's a class."""240 if not hasattr(obj, "__mro__"):241 # N.B. we cannot use `if type(obj) is type` here because it doesn't work with242 # some types of classes, such as the ones that use abc.ABCMeta.243 obj = type(obj)244 return getattr(obj, "__mro__", ())245 246 247def get_object_types_mro_as_strings(obj: object) -> Collection[str]:248 """249 Returns the MRO of an object's class as full qualified names, or of the object itself if it's a class.250 251 Examples:252 `object_types_mro_as_strings(JSONDecoder)` will return `['json.decoder.JSONDecoder', 'builtins.object']`253 """254 return [255 f'{getattr(type_, "__module__", "")}.{getattr(type_, "__qualname__", "")}'256 for type_ in get_object_types_mro(obj)257 ]258 259 260def is_object_one_of_types(261 obj: object, fully_qualified_types_names: Collection[str]262) -> bool:263 """264 Returns `True` if the given object's class (or the object itself, if it's a class) has one of the265 fully qualified names in its MRO.266 """267 for type_name in get_object_types_mro_as_strings(obj):268 if type_name in fully_qualified_types_names:269 return True270 return False271 