import io import sys from types import ModuleType from typing import Sequence, Type import pytest from rich import inspect from rich._inspect import ( get_object_types_mro, get_object_types_mro_as_strings, is_object_one_of_types, ) from rich.console import Console skip_py38 = pytest.mark.skipif( sys.version_info.minor == 8 and sys.version_info.major == 3, reason="rendered differently on py3.8", ) skip_py39 = pytest.mark.skipif( sys.version_info.minor == 9 and sys.version_info.major == 3, reason="rendered differently on py3.9", ) skip_py310 = pytest.mark.skipif( sys.version_info.minor == 10 and sys.version_info.major == 3, reason="rendered differently on py3.10", ) skip_py311 = pytest.mark.skipif( sys.version_info.minor == 11 and sys.version_info.major == 3, reason="rendered differently on py3.11", ) skip_py312 = pytest.mark.skipif( sys.version_info.minor == 12 and sys.version_info.major == 3, reason="rendered differently on py3.12", ) skip_py313 = pytest.mark.skipif( sys.version_info.minor == 13 and sys.version_info.major == 3, reason="rendered differently on py3.13", ) skip_pypy3 = pytest.mark.skipif( hasattr(sys, "pypy_version_info"), reason="rendered differently on pypy3", ) def render(obj, methods=False, value=False, width=50) -> str: console = Console(file=io.StringIO(), width=width, legacy_windows=False) inspect(obj, console=console, methods=methods, value=value) return console.file.getvalue() class InspectError(Exception): def __str__(self) -> str: return "INSPECT ERROR" class Foo: """Foo test Second line """ def __init__(self, foo: int) -> None: """constructor docs.""" self.foo = foo @property def broken(self): raise InspectError() def method(self, a, b) -> str: """Multi line docs. """ return "test" def __dir__(self): return ["__init__", "broken", "method"] class FooSubclass(Foo): pass def test_render(): console = Console(width=100, file=io.StringIO(), legacy_windows=False) foo = Foo("hello") inspect(foo, console=console, all=True, value=False) result = console.file.getvalue() print(repr(result)) expected = "╭────────────── ──────────────╮\n│ Foo test │\n│ │\n│ broken = InspectError() │\n│ __init__ = def __init__(foo: int) -> None: constructor docs. │\n│ method = def method(a, b) -> str: Multi line │\n╰──────────────────────────────────────────────────────────────╯\n" assert result == expected @skip_pypy3 def test_inspect_text(): num_attributes = 34 if sys.version_info >= (3, 11) else 33 expected = ( "╭──────────────── ─────────────────╮\n" "│ str(object='') -> str │\n" "│ str(bytes_or_buffer[, encoding[, errors]]) -> │\n" "│ str │\n" "│ │\n" f"│ {num_attributes} attribute(s) not shown. Run │\n" "│ inspect(inspect) for options. │\n" "╰────────────────────────────────────────────────╯\n" ) print(repr(expected)) assert render("Hello") == expected @skip_pypy3 def test_inspect_empty_dict(): expected = ( "╭──────────────── ────────────────╮\n" "│ dict() -> new empty dictionary │\n" "│ dict(mapping) -> new dictionary initialized │\n" "│ from a mapping object's │\n" "│ (key, value) pairs │\n" "│ dict(iterable) -> new dictionary initialized │\n" "│ as if via: │\n" "│ d = {} │\n" "│ for k, v in iterable: │\n" "│ d[k] = v │\n" "│ dict(**kwargs) -> new dictionary initialized │\n" "│ with the name=value pairs │\n" "│ in the keyword argument list. For │\n" "│ example: dict(one=1, two=2) │\n" "│ │\n" ) assert render({}).startswith(expected) @skip_py313 @skip_py312 @skip_py311 @skip_pypy3 def test_inspect_builtin_function_except_python311(): # Pre-3.11 Python versions - print builtin has no signature available expected = ( "╭────────── ───────────╮\n" "│ def print(...) │\n" "│ │\n" "│ print(value, ..., sep=' ', end='\\n', │\n" "│ file=sys.stdout, flush=False) │\n" "│ │\n" "│ 29 attribute(s) not shown. Run │\n" "│ inspect(inspect) for options. │\n" "╰────────────────────────────────────────────────╯\n" ) assert render(print) == expected @pytest.mark.skipif( sys.version_info < (3, 11), reason="print builtin signature only available on 3.11+" ) @skip_pypy3 def test_inspect_builtin_function_only_python311(): # On 3.11, the print builtin *does* have a signature, unlike in prior versions expected = ( "╭────────── ───────────╮\n" "│ def print(*args, sep=' ', end='\\n', file=None, │\n" "│ flush=False): │\n" "│ │\n" "│ Prints the values to a stream, or to │\n" "│ sys.stdout by default. │\n" "│ │\n" "│ 30 attribute(s) not shown. Run │\n" "│ inspect(inspect) for options. │\n" "╰────────────────────────────────────────────────╯\n" ) assert render(print) == expected @skip_pypy3 def test_inspect_coroutine(): async def coroutine(): pass expected = ( "╭─ .cor─╮\n" "│ async def │\n" "│ test_inspect_coroutine..coroutine(): │\n" ) assert render(coroutine).startswith(expected) def test_inspect_integer(): expected = ( "╭────── ───────╮\n" "│ int([x]) -> integer │\n" "│ int(x, base=10) -> integer │\n" "│ │\n" "│ denominator = 1 │\n" "│ imag = 0 │\n" "│ numerator = 1 │\n" "│ real = 1 │\n" "╰────────────────────────────╯\n" ) assert expected == render(1) def test_inspect_integer_with_value(): expected = "╭────── ───────╮\n│ int([x]) -> integer │\n│ int(x, base=10) -> integer │\n│ │\n│ ╭────────────────────────╮ │\n│ │ 1 │ │\n│ ╰────────────────────────╯ │\n│ │\n│ denominator = 1 │\n│ imag = 0 │\n│ numerator = 1 │\n│ real = 1 │\n╰────────────────────────────╯\n" value = render(1, value=True) print(repr(value)) assert value == expected @skip_py310 @skip_py311 @skip_py312 @skip_py313 def test_inspect_integer_with_methods_python38_and_python39(): expected = ( "╭──────────────── ─────────────────╮\n" "│ int([x]) -> integer │\n" "│ int(x, base=10) -> integer │\n" "│ │\n" "│ denominator = 1 │\n" "│ imag = 0 │\n" "│ numerator = 1 │\n" "│ real = 1 │\n" "│ as_integer_ratio = def as_integer_ratio(): │\n" "│ Return integer ratio. │\n" "│ bit_length = def bit_length(): Number of │\n" "│ bits necessary to represent │\n" "│ self in binary. │\n" "│ conjugate = def conjugate(...) Returns │\n" "│ self, the complex conjugate │\n" "│ of any int. │\n" "│ from_bytes = def from_bytes(bytes, │\n" "│ byteorder, *, │\n" "│ signed=False): Return the │\n" "│ integer represented by the │\n" "│ given array of bytes. │\n" "│ to_bytes = def to_bytes(length, │\n" "│ byteorder, *, │\n" "│ signed=False): Return an │\n" "│ array of bytes representing │\n" "│ an integer. │\n" "╰────────────────────────────────────────────────╯\n" ) assert render(1, methods=True) == expected @skip_py38 @skip_py39 @skip_py311 @skip_py312 @skip_py313 def test_inspect_integer_with_methods_python310only(): expected = ( "╭──────────────── ─────────────────╮\n" "│ int([x]) -> integer │\n" "│ int(x, base=10) -> integer │\n" "│ │\n" "│ denominator = 1 │\n" "│ imag = 0 │\n" "│ numerator = 1 │\n" "│ real = 1 │\n" "│ as_integer_ratio = def as_integer_ratio(): │\n" "│ Return integer ratio. │\n" "│ bit_count = def bit_count(): Number of │\n" "│ ones in the binary │\n" "│ representation of the │\n" "│ absolute value of self. │\n" "│ bit_length = def bit_length(): Number of │\n" "│ bits necessary to represent │\n" "│ self in binary. │\n" "│ conjugate = def conjugate(...) Returns │\n" "│ self, the complex conjugate │\n" "│ of any int. │\n" "│ from_bytes = def from_bytes(bytes, │\n" "│ byteorder, *, │\n" "│ signed=False): Return the │\n" "│ integer represented by the │\n" "│ given array of bytes. │\n" "│ to_bytes = def to_bytes(length, │\n" "│ byteorder, *, │\n" "│ signed=False): Return an │\n" "│ array of bytes representing │\n" "│ an integer. │\n" "╰────────────────────────────────────────────────╯\n" ) assert render(1, methods=True) == expected @skip_py38 @skip_py39 @skip_py310 @skip_py312 @skip_py313 def test_inspect_integer_with_methods_python311(): # to_bytes and from_bytes methods on int had minor signature change - # they now, as of 3.11, have default values for all of their parameters expected = ( "╭──────────────── ─────────────────╮\n" "│ int([x]) -> integer │\n" "│ int(x, base=10) -> integer │\n" "│ │\n" "│ denominator = 1 │\n" "│ imag = 0 │\n" "│ numerator = 1 │\n" "│ real = 1 │\n" "│ as_integer_ratio = def as_integer_ratio(): │\n" "│ Return integer ratio. │\n" "│ bit_count = def bit_count(): Number of │\n" "│ ones in the binary │\n" "│ representation of the │\n" "│ absolute value of self. │\n" "│ bit_length = def bit_length(): Number of │\n" "│ bits necessary to represent │\n" "│ self in binary. │\n" "│ conjugate = def conjugate(...) Returns │\n" "│ self, the complex conjugate │\n" "│ of any int. │\n" "│ from_bytes = def from_bytes(bytes, │\n" "│ byteorder='big', *, │\n" "│ signed=False): Return the │\n" "│ integer represented by the │\n" "│ given array of bytes. │\n" "│ to_bytes = def to_bytes(length=1, │\n" "│ byteorder='big', *, │\n" "│ signed=False): Return an │\n" "│ array of bytes representing │\n" "│ an integer. │\n" "╰────────────────────────────────────────────────╯\n" ) assert render(1, methods=True) == expected @skip_pypy3 def test_broken_call_attr(): class NotCallable: __call__ = 5 # Passes callable() but isn't really callable def __repr__(self): return "NotCallable()" class Foo: foo = NotCallable() foo = Foo() assert callable(foo.foo) expected = "╭─ .Foo'> ─╮\n│ foo = NotCallable() │\n╰───────────────────────────────────────────────────────────────────╯\n" result = render(foo, methods=True, width=100) print(repr(result)) assert expected == result def test_inspect_swig_edge_case(): """Issue #1838 - Edge case with Faiss library - object with empty dir()""" class Thing: @property def __class__(self): raise AttributeError thing = Thing() try: inspect(thing) except Exception as e: assert False, f"Object with no __class__ shouldn't raise {e}" def test_inspect_module_with_class(): def function(): pass class Thing: """Docstring""" pass module = ModuleType("my_module") module.SomeClass = Thing module.function = function expected = ( "╭────────── ──────────╮\n" "│ function = def function(): │\n" "│ SomeClass = class SomeClass(): Docstring │\n" "╰──────────────────────────────────────────╯\n" ) assert render(module, methods=True) == expected @pytest.mark.parametrize( "special_character,expected_replacement", ( ("\a", "\\a"), ("\b", "\\b"), ("\f", "\\f"), ("\r", "\\r"), ("\v", "\\v"), ), ) def test_can_handle_special_characters_in_docstrings( special_character: str, expected_replacement: str ) -> None: class Something: class Thing: pass Something.Thing.__doc__ = f""" Multiline docstring with {special_character} should be handled """ expected = """\ ╭─ .Something(): │ │ │ │ Thing = class Thing(): │ │ Multiline docstring │ │ with %s should be handled │ ╰────────────────────────────────────────────────╯ """ % ( expected_replacement ) assert render(Something, methods=True) == expected @pytest.mark.parametrize( "obj,expected_result", ( [object, (object,)], [object(), (object,)], ["hi", (str, object)], [str, (str, object)], [Foo(1), (Foo, object)], [Foo, (Foo, object)], [FooSubclass(1), (FooSubclass, Foo, object)], [FooSubclass, (FooSubclass, Foo, object)], ), ) def test_object_types_mro(obj: object, expected_result: Sequence[Type]): assert get_object_types_mro(obj) == expected_result @pytest.mark.parametrize( "obj,expected_result", ( # fmt: off ["hi", ["builtins.str", "builtins.object"]], [str, ["builtins.str", "builtins.object"]], [Foo(1), [f"{__name__}.Foo", "builtins.object"]], [Foo, [f"{__name__}.Foo", "builtins.object"]], [FooSubclass(1), [f"{__name__}.FooSubclass", f"{__name__}.Foo", "builtins.object"]], [FooSubclass, [f"{__name__}.FooSubclass", f"{__name__}.Foo", "builtins.object"]], # fmt: on ), ) def test_object_types_mro_as_strings(obj: object, expected_result: Sequence[str]): assert get_object_types_mro_as_strings(obj) == expected_result @pytest.mark.parametrize( "obj,types,expected_result", ( # fmt: off ["hi", ["builtins.str"], True], [str, ["builtins.str"], True], ["hi", ["builtins.str", "foo"], True], [str, ["builtins.str", "foo"], True], [Foo(1), [f"{__name__}.Foo"], True], [Foo, [f"{__name__}.Foo"], True], [Foo(1), ["builtins.str", f"{__name__}.Foo"], True], [Foo, ["builtins.int", f"{__name__}.Foo"], True], [Foo(1), [f"{__name__}.FooSubclass"], False], [Foo, [f"{__name__}.FooSubclass"], False], [Foo(1), [f"{__name__}.FooSubclass", f"{__name__}.Foo"], True], [Foo, [f"{__name__}.Foo", f"{__name__}.FooSubclass"], True], # fmt: on ), ) def test_object_is_one_of_types( obj: object, types: Sequence[str], expected_result: bool ): assert is_object_one_of_types(obj, types) is expected_result