Skip to content

inspector

This module provides introspection utilities.

The inspect method can be used to create an Inspector widget, which can then be used to see what is happening inside any python object. This method is usually preferred for instantiating an Inspector, as it sets up overwriteable default arguments passed to the new widget.

These defaults are meant to hide the non-important information when they are not needed, in order to allow the least amount of code for the most usability. For example, by default, when passed a class, inspect will clip the docstrings to their first lines, but show all methods. When an class' method is given it will hide show the full docstring, and also use the method's fully qualified name.

Inspector

Bases: Container

A widget to inspect any Python object.

Source code in pytermgui/inspector.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
class Inspector(Container):
    """A widget to inspect any Python object."""

    def __init__(  # pylint: disable=too-many-arguments
        self,
        target: object = None,
        show_private: bool = False,
        show_dunder: bool = False,
        show_methods: bool = False,
        show_full_doc: bool = False,
        show_qualname: bool = True,
        show_header: bool = True,
        **attrs: Any,
    ):
        """Initializes an inspector.

        Note that most of the time, using `inspect` to do this is going to be more
        useful.

        Some styles of the inspector can be changed using the `code.name`,
        `code.file` and `code.keyword` markup aliases. The rest of the
        highlighting is done using `pprint`, with all of its respective colors.

        Args:
            show_private: Whether `_private` attributes should be shown.
            show_dunder: Whether `__dunder__` attributes should be shown.
            show_methods: Whether methods should be shown when encountering a class.
            show_full_doc: If not set, docstrings are cut to only include their first
                line.
            show_qualname: Show fully-qualified name, e.g. `module.submodule.name`
                instead of `name`.
            show_header: If not set, the header containing the path to the object and
                its qualname will not be added.
        """

        if "box" not in attrs:
            attrs["box"] = "EMPTY"

        super().__init__(**attrs)

        self.width = self.terminal.width

        self.show_private = show_private
        self.show_dunder = show_dunder
        self.show_methods = show_methods
        self.show_full_doc = show_full_doc
        self.show_qualname = show_qualname
        self.show_header = show_header

        # TODO: Fix attr-showing
        self.show_attrs = False

        self.target: object
        if target is not None:
            self.inspect(target)
            self.target = target

    def _get_header(self) -> Container:
        """Creates a header containing the name and location of the object."""

        header = Container(box="SINGLE")

        line = "[code.name]"
        if self.target_type is ObjectType.MODULE:
            line += self.target.__name__  # type: ignore

        else:
            cls = (
                self.target
                if isclass(self.target) or isfunction(self.target)
                else self.target.__class__
            )
            line += cls.__module__ + "." + cls.__qualname__  # type: ignore

        header += line

        try:
            file = getfile(self.target)  # type: ignore
        except TypeError:
            return header

        header += f"Located in [code.file ~file://{file}]{file}[/]"

        return header

    def _get_definition(self) -> Label:
        """Returns the definition str of self.target."""

        target = self.target

        if self.show_qualname:
            name = getattr(target, "__qualname__", type(target).__name__)
        else:
            name = getattr(target, "__name__", type(target).__name__)

        if self.target_type == ObjectType.LIVE:
            target = type(target)

        otype = _determine_type(target)

        keyword = ""
        if otype == ObjectType.CLASS:
            keyword = "class "

        elif otype == ObjectType.FUNCTION:
            keyword = "def "

        try:
            assert callable(target)
            definition = self.highlight(keyword + name + str(signature(target)) + ":")

        except (TypeError, ValueError, AssertionError):
            definition = self.highlight(keyword + name + "(...)")

        return Label(definition, parent_align=0, non_first_padding=4)

    def _get_docs(self, padding: int) -> Label:
        """Returns a list of Labels of the object's documentation."""

        default = Label("...", style="102")
        if self.target.__doc__ is None:
            return default

        doc = getdoc(self.target)

        if doc is None:
            return default

        lines = doc.splitlines()
        if not self.show_full_doc and len(lines) > 0:
            lines = [lines[0]]

        trimmed = "\n".join(lines)

        return Label(
            trimmed.replace("[", r"\["),
            style="102",
            parent_align=0,
            padding=padding,
        )

    def _get_keys(self) -> list[str]:
        """Gets all inspectable keys of an object.

        It first checks for an `__all__` attribute, and substitutes `dir` if not found.
        Then, if there are too many keys and the given target is a module it tries to
        list all of the present submodules.
        """

        keys = getattr(self.target, "__all__", dir(self.target))

        if not self.show_dunder:
            keys = [key for key in keys if not key.startswith("__")]

        if not self.show_private:
            keys = [key for key in keys if not (key.startswith("_") and key[1] != "_")]

        if not self.show_methods:
            keys = [
                key for key in keys if not callable(getattr(self.target, key, None))
            ]

        keys.sort(key=lambda item: callable(getattr(self.target, item, None)))

        return keys

    def _get_preview(self) -> Container:
        """Gets a Container with self.target inside."""

        preview = Container(static_width=self.width // 2, parent_align=0, box="SINGLE")

        if isinstance(self.target, str) and RE_MARKUP.match(self.target) is not None:
            preview += Label(prettify(self.target, parse=False), parent_align=0)
            return preview

        for line in prettify(self.target).splitlines():

            if real_length(line) > preview.width - preview.sidelength:
                preview.width = real_length(line) + preview.sidelength

            preview += Label(tim.get_markup(line), parent_align=0)

        preview.width = min(preview.width, self.terminal.width - preview.sidelength)
        return preview

    @staticmethod
    def highlight(text: str) -> str:
        """Applies highlighting to a given string.

        This highlight includes keywords, builtin types and more.

        Args:
            text: The string to highlight.

        Returns:
            Unparsed markup.
        """

        def _split(text: str, chars: str = " ,:|()[]{}") -> list[tuple[str, str]]:
            """Splits given text by the given chars.

            Args:
                text: The text to split.
                chars: A string of characters we will split by.

            Returns:
                A tuple of (delimiter, word) tuples. Delimiter is one of the characters
                of `chars`.
            """

            last_delim = ""
            output = []
            word = ""
            for char in text:
                if char in chars:
                    output.append((last_delim, word))
                    last_delim = char
                    word = ""
                    continue

                word += char

            output.append((last_delim, word))
            return output

        buff = ""
        for (delim, word) in _split(text):
            stripped = word.strip("'")
            highlighted = highlight_python(stripped)

            if highlighted != stripped:
                buff += delim + stripped
                continue

            buff += delim + stripped

        return highlight_python(buff)

    def inspect(self, target: object) -> Inspector:
        """Inspects a given object, and sets self.target to it.

        Returns:
            Self, with the new content based on the inspection.
        """

        self.target = target
        self.target_type = _determine_type(target)

        # Header
        if self.show_header and self.box is not INDENTED_EMPTY_BOX:
            self.lazy_add(self._get_header())

        # Body
        if self.target_type is not ObjectType.MODULE:
            self.lazy_add(self._get_definition())

        padding = 0 if self.target_type is ObjectType.MODULE else 4

        self.lazy_add(self._get_docs(padding))

        keys = self._get_keys()

        for key in keys:
            attr = getattr(target, key, None)

            # Don't show type aliases
            if _is_type_alias(attr):
                continue

            # Only show functions if they are not lambdas
            if (isfunction(attr) or callable(attr)) and (
                hasattr(attr, "__name__") and not attr.__name__ == "<lambda>"
            ):
                self.lazy_add(
                    Inspector(
                        box=INDENTED_EMPTY_BOX,
                        show_dunder=self.show_dunder,
                        show_private=self.show_private,
                        show_full_doc=False,
                        show_qualname=self.show_qualname,
                    ).inspect(attr)
                )
                continue

            if not self.show_attrs:
                continue

            for i, line in enumerate(prettify(attr, parse=False).splitlines()):
                if i == 0:
                    line = f"- {key}: {line}"

                self.lazy_add(Label(line, parent_align=0))

        # Footer
        if self.target_type in [ObjectType.LIVE, ObjectType.BUILTIN]:
            self.lazy_add(self._get_preview())

        return self

    def debug(self) -> str:
        """Returns identifiable information used in repr."""

        if self.terminal.is_interactive and not self.terminal.displayhook_installed:
            return "\n".join(self.get_lines())

        return Widget.debug(self)

__init__(target=None, show_private=False, show_dunder=False, show_methods=False, show_full_doc=False, show_qualname=True, show_header=True, **attrs)

Initializes an inspector.

Note that most of the time, using inspect to do this is going to be more useful.

Some styles of the inspector can be changed using the code.name, code.file and code.keyword markup aliases. The rest of the highlighting is done using pprint, with all of its respective colors.

Parameters:

Name Type Description Default
show_private bool

Whether _private attributes should be shown.

False
show_dunder bool

Whether __dunder__ attributes should be shown.

False
show_methods bool

Whether methods should be shown when encountering a class.

False
show_full_doc bool

If not set, docstrings are cut to only include their first line.

False
show_qualname bool

Show fully-qualified name, e.g. module.submodule.name instead of name.

True
show_header bool

If not set, the header containing the path to the object and its qualname will not be added.

True
Source code in pytermgui/inspector.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
def __init__(  # pylint: disable=too-many-arguments
    self,
    target: object = None,
    show_private: bool = False,
    show_dunder: bool = False,
    show_methods: bool = False,
    show_full_doc: bool = False,
    show_qualname: bool = True,
    show_header: bool = True,
    **attrs: Any,
):
    """Initializes an inspector.

    Note that most of the time, using `inspect` to do this is going to be more
    useful.

    Some styles of the inspector can be changed using the `code.name`,
    `code.file` and `code.keyword` markup aliases. The rest of the
    highlighting is done using `pprint`, with all of its respective colors.

    Args:
        show_private: Whether `_private` attributes should be shown.
        show_dunder: Whether `__dunder__` attributes should be shown.
        show_methods: Whether methods should be shown when encountering a class.
        show_full_doc: If not set, docstrings are cut to only include their first
            line.
        show_qualname: Show fully-qualified name, e.g. `module.submodule.name`
            instead of `name`.
        show_header: If not set, the header containing the path to the object and
            its qualname will not be added.
    """

    if "box" not in attrs:
        attrs["box"] = "EMPTY"

    super().__init__(**attrs)

    self.width = self.terminal.width

    self.show_private = show_private
    self.show_dunder = show_dunder
    self.show_methods = show_methods
    self.show_full_doc = show_full_doc
    self.show_qualname = show_qualname
    self.show_header = show_header

    # TODO: Fix attr-showing
    self.show_attrs = False

    self.target: object
    if target is not None:
        self.inspect(target)
        self.target = target

debug()

Returns identifiable information used in repr.

Source code in pytermgui/inspector.py
474
475
476
477
478
479
480
def debug(self) -> str:
    """Returns identifiable information used in repr."""

    if self.terminal.is_interactive and not self.terminal.displayhook_installed:
        return "\n".join(self.get_lines())

    return Widget.debug(self)

highlight(text) staticmethod

Applies highlighting to a given string.

This highlight includes keywords, builtin types and more.

Parameters:

Name Type Description Default
text str

The string to highlight.

required

Returns:

Type Description
str

Unparsed markup.

Source code in pytermgui/inspector.py
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
@staticmethod
def highlight(text: str) -> str:
    """Applies highlighting to a given string.

    This highlight includes keywords, builtin types and more.

    Args:
        text: The string to highlight.

    Returns:
        Unparsed markup.
    """

    def _split(text: str, chars: str = " ,:|()[]{}") -> list[tuple[str, str]]:
        """Splits given text by the given chars.

        Args:
            text: The text to split.
            chars: A string of characters we will split by.

        Returns:
            A tuple of (delimiter, word) tuples. Delimiter is one of the characters
            of `chars`.
        """

        last_delim = ""
        output = []
        word = ""
        for char in text:
            if char in chars:
                output.append((last_delim, word))
                last_delim = char
                word = ""
                continue

            word += char

        output.append((last_delim, word))
        return output

    buff = ""
    for (delim, word) in _split(text):
        stripped = word.strip("'")
        highlighted = highlight_python(stripped)

        if highlighted != stripped:
            buff += delim + stripped
            continue

        buff += delim + stripped

    return highlight_python(buff)

inspect(target)

Inspects a given object, and sets self.target to it.

Returns:

Type Description
Inspector

Self, with the new content based on the inspection.

Source code in pytermgui/inspector.py
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
def inspect(self, target: object) -> Inspector:
    """Inspects a given object, and sets self.target to it.

    Returns:
        Self, with the new content based on the inspection.
    """

    self.target = target
    self.target_type = _determine_type(target)

    # Header
    if self.show_header and self.box is not INDENTED_EMPTY_BOX:
        self.lazy_add(self._get_header())

    # Body
    if self.target_type is not ObjectType.MODULE:
        self.lazy_add(self._get_definition())

    padding = 0 if self.target_type is ObjectType.MODULE else 4

    self.lazy_add(self._get_docs(padding))

    keys = self._get_keys()

    for key in keys:
        attr = getattr(target, key, None)

        # Don't show type aliases
        if _is_type_alias(attr):
            continue

        # Only show functions if they are not lambdas
        if (isfunction(attr) or callable(attr)) and (
            hasattr(attr, "__name__") and not attr.__name__ == "<lambda>"
        ):
            self.lazy_add(
                Inspector(
                    box=INDENTED_EMPTY_BOX,
                    show_dunder=self.show_dunder,
                    show_private=self.show_private,
                    show_full_doc=False,
                    show_qualname=self.show_qualname,
                ).inspect(attr)
            )
            continue

        if not self.show_attrs:
            continue

        for i, line in enumerate(prettify(attr, parse=False).splitlines()):
            if i == 0:
                line = f"- {key}: {line}"

            self.lazy_add(Label(line, parent_align=0))

    # Footer
    if self.target_type in [ObjectType.LIVE, ObjectType.BUILTIN]:
        self.lazy_add(self._get_preview())

    return self

ObjectType

Bases: Enum

All types an object can be.

Source code in pytermgui/inspector.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class ObjectType(Enum):
    """All types an object can be."""

    LIVE = _auto()
    """An instance that does not fit the other types."""

    CLASS = _auto()
    """A class object."""

    MODULE = _auto()
    """A module object."""

    BUILTIN = _auto()
    """Some sort of a builtin object.

    As builtins are often implemented in C, a lot of the standard python APIs
    won't work on them, so we need to treat them separately."""

    FUNCTION = _auto()
    """A callable object, that is not a class."""

BUILTIN = _auto() class-attribute instance-attribute

Some sort of a builtin object.

As builtins are often implemented in C, a lot of the standard python APIs won't work on them, so we need to treat them separately.

CLASS = _auto() class-attribute instance-attribute

A class object.

FUNCTION = _auto() class-attribute instance-attribute

A callable object, that is not a class.

LIVE = _auto() class-attribute instance-attribute

An instance that does not fit the other types.

MODULE = _auto() class-attribute instance-attribute

A module object.

get_origin(_)

Spoofs typing.get_origin, which is used to determine type-hints.

Since this function is only available >=3.8, we need to have some implementation on it for 3.7. The code checks for the origin to be non-null, as that is the value returned by this method on non-typing objects.

This will cause annotations to show up on 3.7, but not on 3.8+.

Source code in pytermgui/inspector.py
38
39
40
41
42
43
44
45
46
47
48
49
def get_origin(_: object) -> Any:  # type: ignore
    """Spoofs typing.get_origin, which is used to determine type-hints.

    Since this function is only available >=3.8, we need to have some
    implementation on it for 3.7. The code checks for the origin to be
    non-null, as that is the value returned by this method on non-typing
    objects.

    This will cause annotations to show up on 3.7, but not on 3.8+.
    """

    return None

inspect(target, **inspector_args)

Inspects an object.

Parameters:

Name Type Description Default
target object

The object to inspect.

required
**inspector_args Any

See Inspector.__init__.

{}
Source code in pytermgui/inspector.py
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
def inspect(target: object, **inspector_args: Any) -> Inspector:
    """Inspects an object.

    Args:
        target: The object to inspect.
        **inspector_args: See `Inspector.__init__`.
    """

    def _conditionally_overwrite_kwarg(**kwargs) -> None:
        for key, value in kwargs.items():
            if inspector_args.get(key) is None:
                inspector_args[key] = value

    if ismodule(target):
        _conditionally_overwrite_kwarg(
            show_dunder=False,
            show_private=False,
            show_full_doc=False,
            show_methods=True,
            show_qualname=False,
        )

    elif isclass(target):
        _conditionally_overwrite_kwarg(
            show_dunder=False,
            show_private=False,
            show_full_doc=True,
            show_methods=True,
            show_qualname=False,
        )

    elif callable(target) or isbuiltin(target):
        _conditionally_overwrite_kwarg(
            show_dunder=False,
            show_private=False,
            show_full_doc=True,
            show_methods=False,
            show_qualname=True,
        )

    else:
        _conditionally_overwrite_kwarg(
            show_dunder=False,
            show_private=False,
            show_full_doc=True,
            show_methods=True,
            show_qualname=False,
        )

    inspector = Inspector(**inspector_args).inspect(target)

    return inspector