test_oinspect.py
579 lines
| 15.3 KiB
| text/x-python
|
PythonLexer
|
r3051 | """Tests for the object inspection functionality. | ||
""" | ||||
|
r22171 | # Copyright (c) IPython Development Team. | ||
# Distributed under the terms of the Modified BSD License. | ||||
|
r3051 | |||
|
r27815 | from contextlib import contextmanager | ||
|
r24881 | from inspect import signature, Signature, Parameter | ||
|
r27234 | import inspect | ||
|
r7290 | import os | ||
|
r27234 | import pytest | ||
|
r7434 | import re | ||
|
r27234 | import sys | ||
|
r3051 | |||
from .. import oinspect | ||||
|
r25079 | |||
|
r20813 | from decorator import decorator | ||
|
r25079 | |||
|
r23021 | from IPython.testing.tools import AssertPrints, AssertNotPrints | ||
|
r20578 | from IPython.utils.path import compress_user | ||
|
r3051 | |||
|
r24882 | |||
|
r3051 | #----------------------------------------------------------------------------- | ||
# Globals and constants | ||||
#----------------------------------------------------------------------------- | ||||
|
r25117 | inspector = None | ||
def setup_module(): | ||||
global inspector | ||||
inspector = oinspect.Inspector() | ||||
|
r3051 | |||
|
r27234 | class SourceModuleMainTest: | ||
__module__ = "__main__" | ||||
|
r3051 | #----------------------------------------------------------------------------- | ||
# Local utilities | ||||
#----------------------------------------------------------------------------- | ||||
|
r7290 | # WARNING: since this test checks the line number where a function is | ||
# defined, if any code is inserted above, the following line will need to be | ||||
# updated. Do NOT insert any whitespace between the next line and the function | ||||
# definition below. | ||||
|
r27815 | THIS_LINE_NUMBER = 47 # Put here the actual number of this line | ||
|
r27234 | |||
def test_find_source_lines(): | ||||
assert oinspect.find_source_lines(test_find_source_lines) == THIS_LINE_NUMBER + 3 | ||||
assert oinspect.find_source_lines(type) is None | ||||
assert oinspect.find_source_lines(SourceModuleMainTest) is None | ||||
assert oinspect.find_source_lines(SourceModuleMainTest()) is None | ||||
|
r22911 | |||
|
r27234 | def test_getsource(): | ||
assert oinspect.getsource(type) is None | ||||
assert oinspect.getsource(SourceModuleMainTest) is None | ||||
assert oinspect.getsource(SourceModuleMainTest()) is None | ||||
|
r22911 | |||
|
r27234 | def test_inspect_getfile_raises_exception(): | ||
"""Check oinspect.find_file/getsource/find_source_lines expectations""" | ||||
with pytest.raises(TypeError): | ||||
inspect.getfile(type) | ||||
|
r28529 | with pytest.raises(OSError): | ||
|
r27234 | inspect.getfile(SourceModuleMainTest) | ||
|
r7290 | |||
|
r7434 | # A couple of utilities to ensure these tests work the same from a source or a | ||
# binary install | ||||
def pyfile(fname): | ||||
|
r7452 | return os.path.normcase(re.sub('.py[co]$', '.py', fname)) | ||
|
r7434 | |||
def match_pyfiles(f1, f2): | ||||
|
r26724 | assert pyfile(f1) == pyfile(f2) | ||
|
r7434 | |||
|
r7290 | def test_find_file(): | ||
|
r7434 | match_pyfiles(oinspect.find_file(test_find_file), os.path.abspath(__file__)) | ||
|
r27234 | assert oinspect.find_file(type) is None | ||
assert oinspect.find_file(SourceModuleMainTest) is None | ||||
assert oinspect.find_file(SourceModuleMainTest()) is None | ||||
|
r7290 | |||
|
r7432 | def test_find_file_decorated1(): | ||
@decorator | ||||
def noop1(f): | ||||
|
r23020 | def wrapper(*a, **kw): | ||
|
r7432 | return f(*a, **kw) | ||
return wrapper | ||||
@noop1 | ||||
def f(x): | ||||
"My docstring" | ||||
|
r26904 | |||
|
r7434 | match_pyfiles(oinspect.find_file(f), os.path.abspath(__file__)) | ||
|
r26724 | assert f.__doc__ == "My docstring" | ||
|
r7432 | |||
def test_find_file_decorated2(): | ||||
@decorator | ||||
def noop2(f, *a, **kw): | ||||
return f(*a, **kw) | ||||
@noop2 | ||||
|
r21505 | @noop2 | ||
@noop2 | ||||
|
r7432 | def f(x): | ||
"My docstring 2" | ||||
|
r26904 | |||
|
r7434 | match_pyfiles(oinspect.find_file(f), os.path.abspath(__file__)) | ||
|
r26724 | assert f.__doc__ == "My docstring 2" | ||
|
r26904 | |||
|
r7432 | |||
|
r7290 | def test_find_file_magic(): | ||
run = ip.find_line_magic('run') | ||||
|
r26904 | assert oinspect.find_file(run) is not None | ||
|
r7290 | |||
|
r3051 | # A few generic objects we can then inspect in the tests below | ||
class Call(object): | ||||
"""This is the class docstring.""" | ||||
def __init__(self, x, y=1): | ||||
"""This is the constructor docstring.""" | ||||
def __call__(self, *a, **kw): | ||||
"""This is the call docstring.""" | ||||
def method(self, x, z=2): | ||||
"""Some method's docstring""" | ||||
|
r4872 | |||
|
r22171 | class HasSignature(object): | ||
"""This is the class docstring.""" | ||||
__signature__ = Signature([Parameter('test', Parameter.POSITIONAL_OR_KEYWORD)]) | ||||
def __init__(self, *args): | ||||
"""This is the init docstring""" | ||||
|
r15362 | class SimpleClass(object): | ||
def method(self, x, z=2): | ||||
"""Some method's docstring""" | ||||
|
r6995 | |||
|
r11058 | class Awkward(object): | ||
def __getattr__(self, name): | ||||
raise Exception(name) | ||||
|
r21886 | class NoBoolCall: | ||
""" | ||||
callable with `__bool__` raising should still be inspect-able. | ||||
""" | ||||
def __call__(self): | ||||
"""does nothing""" | ||||
pass | ||||
def __bool__(self): | ||||
"""just raise NotImplemented""" | ||||
raise NotImplementedError('Must be implemented') | ||||
|
r11058 | |||
|
r21906 | class SerialLiar(object): | ||
"""Attribute accesses always get another copy of the same class. | ||||
unittest.mock.call does something similar, but it's not ideal for testing | ||||
as the failure mode is to eat all your RAM. This gives up after 10k levels. | ||||
""" | ||||
def __init__(self, max_fibbing_twig, lies_told=0): | ||||
if lies_told > 10000: | ||||
raise RuntimeError('Nose too long, honesty is the best policy') | ||||
self.max_fibbing_twig = max_fibbing_twig | ||||
self.lies_told = lies_told | ||||
max_fibbing_twig[0] = max(max_fibbing_twig[0], lies_told) | ||||
def __getattr__(self, item): | ||||
return SerialLiar(self.max_fibbing_twig, self.lies_told + 1) | ||||
|
r3051 | #----------------------------------------------------------------------------- | ||
# Tests | ||||
#----------------------------------------------------------------------------- | ||||
|
r3859 | def test_info(): | ||
"Check that Inspector.info fills out various fields as expected." | ||||
|
r26724 | i = inspector.info(Call, oname="Call") | ||
assert i["type_name"] == "type" | ||||
|
r26904 | expected_class = str(type(type)) # <class 'type'> (Python 3) or <type 'type'> | ||
assert i["base_class"] == expected_class | ||||
assert re.search( | ||||
|
r26724 | "<class 'IPython.core.tests.test_oinspect.Call'( at 0x[0-9a-f]{1,9})?>", | ||
|
r26904 | i["string_form"], | ||
|
r26724 | ) | ||
|
r3859 | fname = __file__ | ||
if fname.endswith(".pyc"): | ||||
fname = fname[:-1] | ||||
|
r4105 | # case-insensitive comparison needed on some filesystems | ||
# e.g. Windows: | ||||
|
r26724 | assert i["file"].lower() == compress_user(fname).lower() | ||
assert i["definition"] == None | ||||
assert i["docstring"] == Call.__doc__ | ||||
assert i["source"] == None | ||||
|
r26904 | assert i["isclass"] is True | ||
|
r26724 | assert i["init_definition"] == "Call(x, y=1)" | ||
assert i["init_docstring"] == Call.__init__.__doc__ | ||||
|
r4872 | |||
|
r3859 | i = inspector.info(Call, detail_level=1) | ||
|
r26904 | assert i["source"] is not None | ||
|
r26724 | assert i["docstring"] == None | ||
|
r4872 | |||
|
r3859 | c = Call(1) | ||
c.__doc__ = "Modified instance docstring" | ||||
i = inspector.info(c) | ||||
|
r26724 | assert i["type_name"] == "Call" | ||
assert i["docstring"] == "Modified instance docstring" | ||||
assert i["class_docstring"] == Call.__doc__ | ||||
assert i["init_docstring"] == Call.__init__.__doc__ | ||||
assert i["call_docstring"] == Call.__call__.__doc__ | ||||
|
r4872 | |||
|
r22171 | def test_class_signature(): | ||
|
r26724 | info = inspector.info(HasSignature, "HasSignature") | ||
assert info["init_definition"] == "HasSignature(test)" | ||||
assert info["init_docstring"] == HasSignature.__init__.__doc__ | ||||
|
r22171 | |||
|
r11058 | def test_info_awkward(): | ||
# Just test that this doesn't throw an error. | ||||
|
r22536 | inspector.info(Awkward()) | ||
|
r11058 | |||
|
r21886 | def test_bool_raise(): | ||
inspector.info(NoBoolCall()) | ||||
|
r21906 | def test_info_serialliar(): | ||
fib_tracker = [0] | ||||
|
r22911 | inspector.info(SerialLiar(fib_tracker)) | ||
|
r21906 | |||
# Nested attribute access should be cut off at 100 levels deep to avoid | ||||
# infinite loops: https://github.com/ipython/ipython/issues/9122 | ||||
|
r26904 | assert fib_tracker[0] < 9000 | ||
|
r21906 | |||
|
r25081 | def support_function_one(x, y=2, *a, **kw): | ||
"""A simple function.""" | ||||
|
r15362 | def test_calldef_none(): | ||
# We should ignore __call__ for all of these. | ||||
|
r25081 | for obj in [support_function_one, SimpleClass().method, any, str.upper]: | ||
|
r15362 | i = inspector.info(obj) | ||
|
r26904 | assert i["call_def"] is None | ||
|
r15362 | |||
|
r22911 | def f_kwarg(pos, *, kwonly): | ||
pass | ||||
|
r15335 | |||
def test_definition_kwonlyargs(): | ||||
|
r26724 | i = inspector.info(f_kwarg, oname="f_kwarg") # analysis:ignore | ||
assert i["definition"] == "f_kwarg(pos, *, kwonly)" | ||||
|
r15335 | |||
|
r5538 | def test_getdoc(): | ||
class A(object): | ||||
"""standard docstring""" | ||||
pass | ||||
|
r22460 | |||
|
r5538 | class B(object): | ||
"""standard docstring""" | ||||
def getdoc(self): | ||||
return "custom docstring" | ||||
|
r26904 | |||
|
r5573 | class C(object): | ||
"""standard docstring""" | ||||
def getdoc(self): | ||||
return None | ||||
|
r26904 | |||
|
r5538 | a = A() | ||
b = B() | ||||
|
r5573 | c = C() | ||
|
r26904 | |||
|
r26724 | assert oinspect.getdoc(a) == "standard docstring" | ||
assert oinspect.getdoc(b) == "custom docstring" | ||||
assert oinspect.getdoc(c) == "standard docstring" | ||||
|
r7460 | |||
|
r17023 | |||
def test_empty_property_has_no_source(): | ||||
i = inspector.info(property(), detail_level=1) | ||||
|
r26904 | assert i["source"] is None | ||
|
r17023 | |||
def test_property_sources(): | ||||
|
r24026 | # A simple adder whose source and signature stays | ||
# the same across Python distributions | ||||
def simple_add(a, b): | ||||
"Adds two numbers" | ||||
return a + b | ||||
|
r26904 | |||
|
r17023 | class A(object): | ||
@property | ||||
def foo(self): | ||||
return 'bar' | ||||
foo = foo.setter(lambda self, v: setattr(self, 'bar', v)) | ||||
|
r27105 | dname = property(oinspect.getdoc) | ||
|
r26904 | adder = property(simple_add) | ||
|
r17023 | |||
i = inspector.info(A.foo, detail_level=1) | ||||
|
r26904 | assert "def foo(self):" in i["source"] | ||
assert "lambda self, v:" in i["source"] | ||||
|
r17023 | |||
|
r24026 | i = inspector.info(A.dname, detail_level=1) | ||
|
r27105 | assert "def getdoc(obj)" in i["source"] | ||
|
r26904 | |||
|
r24026 | i = inspector.info(A.adder, detail_level=1) | ||
|
r26904 | assert "def simple_add(a, b)" in i["source"] | ||
|
r17023 | |||
def test_property_docstring_is_in_info_for_detail_level_0(): | ||||
class A(object): | ||||
@property | ||||
|
r22354 | def foobar(self): | ||
|
r17023 | """This is `foobar` property.""" | ||
pass | ||||
|
r26724 | ip.user_ns["a_obj"] = A() | ||
assert ( | ||||
"This is `foobar` property." | ||||
== ip.object_inspect("a_obj.foobar", detail_level=0)["docstring"] | ||||
) | ||||
|
r17023 | |||
|
r26724 | ip.user_ns["a_cls"] = A | ||
assert ( | ||||
"This is `foobar` property." | ||||
== ip.object_inspect("a_cls.foobar", detail_level=0)["docstring"] | ||||
) | ||||
|
r17023 | |||
|
r7460 | def test_pdef(): | ||
# See gh-1914 | ||||
def foo(): pass | ||||
inspector.pdef(foo, 'foo') | ||||
|
r11067 | |||
|
r22539 | |||
|
r27815 | @contextmanager | ||
def cleanup_user_ns(**kwargs): | ||||
""" | ||||
On exit delete all the keys that were not in user_ns before entering. | ||||
It does not restore old values ! | ||||
Parameters | ||||
---------- | ||||
**kwargs | ||||
used to update ip.user_ns | ||||
""" | ||||
try: | ||||
known = set(ip.user_ns.keys()) | ||||
ip.user_ns.update(kwargs) | ||||
yield | ||||
finally: | ||||
added = set(ip.user_ns.keys()) - known | ||||
for k in added: | ||||
del ip.user_ns[k] | ||||
|
r28210 | def test_pinfo_bool_raise(): | ||
""" | ||||
Test that bool method is not called on parent. | ||||
""" | ||||
class RaiseBool: | ||||
attr = None | ||||
def __bool__(self): | ||||
raise ValueError("pinfo should not access this method") | ||||
raise_bool = RaiseBool() | ||||
with cleanup_user_ns(raise_bool=raise_bool): | ||||
ip._inspect("pinfo", "raise_bool.attr", detail_level=0) | ||||
|
r27815 | def test_pinfo_getindex(): | ||
def dummy(): | ||||
""" | ||||
MARKER | ||||
""" | ||||
container = [dummy] | ||||
with cleanup_user_ns(container=container): | ||||
with AssertPrints("MARKER"): | ||||
ip._inspect("pinfo", "container[0]", detail_level=0) | ||||
assert "container" not in ip.user_ns.keys() | ||||
def test_qmark_getindex(): | ||||
def dummy(): | ||||
""" | ||||
MARKER 2 | ||||
""" | ||||
container = [dummy] | ||||
with cleanup_user_ns(container=container): | ||||
with AssertPrints("MARKER 2"): | ||||
ip.run_cell("container[0]?") | ||||
assert "container" not in ip.user_ns.keys() | ||||
|
r27819 | |||
|
r27818 | def test_qmark_getindex_negatif(): | ||
def dummy(): | ||||
""" | ||||
MARKER 3 | ||||
""" | ||||
container = [dummy] | ||||
with cleanup_user_ns(container=container): | ||||
with AssertPrints("MARKER 3"): | ||||
ip.run_cell("container[-1]?") | ||||
assert "container" not in ip.user_ns.keys() | ||||
|
r27815 | |||
|
r11067 | def test_pinfo_nonascii(): | ||
# See gh-1177 | ||||
from . import nonascii2 | ||||
ip.user_ns['nonascii2'] = nonascii2 | ||||
ip._inspect('pinfo', 'nonascii2', detail_level=1) | ||||
|
r20699 | |||
|
r24946 | def test_pinfo_type(): | ||
""" | ||||
type can fail in various edge case, for example `type.__subclass__()` | ||||
""" | ||||
ip._inspect('pinfo', 'type') | ||||
|
r22539 | |||
|
r23021 | def test_pinfo_docstring_no_source(): | ||
"""Docstring should be included with detail_level=1 if there is no source""" | ||||
with AssertPrints('Docstring:'): | ||||
ip._inspect('pinfo', 'str.format', detail_level=0) | ||||
with AssertPrints('Docstring:'): | ||||
ip._inspect('pinfo', 'str.format', detail_level=1) | ||||
def test_pinfo_no_docstring_if_source(): | ||||
"""Docstring should not be included with detail_level=1 if source is found""" | ||||
def foo(): | ||||
"""foo has a docstring""" | ||||
ip.user_ns['foo'] = foo | ||||
with AssertPrints('Docstring:'): | ||||
ip._inspect('pinfo', 'foo', detail_level=0) | ||||
with AssertPrints('Source:'): | ||||
ip._inspect('pinfo', 'foo', detail_level=1) | ||||
with AssertNotPrints('Docstring:'): | ||||
ip._inspect('pinfo', 'foo', detail_level=1) | ||||
|
r23660 | def test_pinfo_docstring_if_detail_and_no_source(): | ||
""" Docstring should be displayed if source info not available """ | ||||
obj_def = '''class Foo(object): | ||||
""" This is a docstring for Foo """ | ||||
def bar(self): | ||||
""" This is a docstring for Foo.bar """ | ||||
pass | ||||
|
r26904 | ''' | ||
|
r23660 | ip.run_cell(obj_def) | ||
ip.run_cell('foo = Foo()') | ||||
|
r26904 | |||
|
r23660 | with AssertNotPrints("Source:"): | ||
with AssertPrints('Docstring:'): | ||||
ip._inspect('pinfo', 'foo', detail_level=0) | ||||
with AssertPrints('Docstring:'): | ||||
ip._inspect('pinfo', 'foo', detail_level=1) | ||||
with AssertPrints('Docstring:'): | ||||
ip._inspect('pinfo', 'foo.bar', detail_level=0) | ||||
with AssertNotPrints('Docstring:'): | ||||
with AssertPrints('Source:'): | ||||
ip._inspect('pinfo', 'foo.bar', detail_level=1) | ||||
|
r28195 | def test_pinfo_docstring_dynamic(): | ||
obj_def = """class Bar: | ||||
__custom_documentations__ = { | ||||
"prop" : "cdoc for prop", | ||||
"non_exist" : "cdoc for non_exist", | ||||
} | ||||
@property | ||||
def prop(self): | ||||
''' | ||||
Docstring for prop | ||||
''' | ||||
return self._prop | ||||
@prop.setter | ||||
def prop(self, v): | ||||
self._prop = v | ||||
""" | ||||
ip.run_cell(obj_def) | ||||
ip.run_cell("b = Bar()") | ||||
with AssertPrints("Docstring: cdoc for prop"): | ||||
ip.run_line_magic("pinfo", "b.prop") | ||||
with AssertPrints("Docstring: cdoc for non_exist"): | ||||
ip.run_line_magic("pinfo", "b.non_exist") | ||||
with AssertPrints("Docstring: cdoc for prop"): | ||||
ip.run_cell("b.prop?") | ||||
with AssertPrints("Docstring: cdoc for non_exist"): | ||||
ip.run_cell("b.non_exist?") | ||||
with AssertPrints("Docstring: <no docstring>"): | ||||
ip.run_cell("b.undefined?") | ||||
|
r20699 | def test_pinfo_magic(): | ||
|
r28195 | with AssertPrints("Docstring:"): | ||
ip._inspect("pinfo", "lsmagic", detail_level=0) | ||||
|
r20699 | |||
|
r28195 | with AssertPrints("Source:"): | ||
ip._inspect("pinfo", "lsmagic", detail_level=1) | ||||
|
r22539 | |||
def test_init_colors(): | ||||
# ensure colors are not present in signature info | ||||
info = inspector.info(HasSignature) | ||||
|
r26904 | init_def = info["init_definition"] | ||
assert "[0m" not in init_def | ||||
|
r22539 | |||
def test_builtin_init(): | ||||
info = inspector.info(list) | ||||
init_def = info['init_definition'] | ||||
|
r26904 | assert init_def is not None | ||
|
r22539 | |||
|
r24881 | |||
def test_render_signature_short(): | ||||
|
r24882 | def short_fun(a=1): pass | ||
|
r24881 | sig = oinspect._render_signature( | ||
signature(short_fun), | ||||
short_fun.__name__, | ||||
) | ||||
|
r26724 | assert sig == "short_fun(a=1)" | ||
|
r24881 | |||
def test_render_signature_long(): | ||||
from typing import Optional | ||||
def long_function( | ||||
a_really_long_parameter: int, | ||||
and_another_long_one: bool = False, | ||||
let_us_make_sure_this_is_looong: Optional[str] = None, | ||||
) -> bool: pass | ||||
sig = oinspect._render_signature( | ||||
signature(long_function), | ||||
long_function.__name__, | ||||
) | ||||
|
r28219 | expected = """\ | ||
|
r25719 | long_function( | ||
a_really_long_parameter: int, | ||||
and_another_long_one: bool = False, | ||||
let_us_make_sure_this_is_looong: Optional[str] = None, | ||||
) -> bool\ | ||||
|
r28176 | """ | ||
|
r28219 | |||
|
r28176 | assert sig == expected | ||