test_oinspect.py
475 lines
| 13.1 KiB
| text/x-python
|
PythonLexer
Fernando Perez
|
r3051 | """Tests for the object inspection functionality. | |
""" | |||
Min RK
|
r22171 | # Copyright (c) IPython Development Team. | |
# Distributed under the terms of the Modified BSD License. | |||
Fernando Perez
|
r3051 | ||
Philipp A
|
r24881 | from inspect import signature, Signature, Parameter | |
Ben Greiner
|
r27346 | import inspect | |
Fernando Perez
|
r7290 | import os | |
Ben Greiner
|
r27346 | import pytest | |
Fernando Perez
|
r7434 | import re | |
Ben Greiner
|
r27346 | import sys | |
Fernando Perez
|
r3051 | ||
from .. import oinspect | |||
Matthias Bussonnier
|
r25079 | ||
MinRK
|
r20813 | from decorator import decorator | |
Matthias Bussonnier
|
r25079 | ||
Min RK
|
r23021 | from IPython.testing.tools import AssertPrints, AssertNotPrints | |
Thomas Kluyver
|
r20578 | from IPython.utils.path import compress_user | |
Fernando Perez
|
r3051 | ||
Philipp A
|
r24882 | ||
Fernando Perez
|
r3051 | #----------------------------------------------------------------------------- | |
# Globals and constants | |||
#----------------------------------------------------------------------------- | |||
Matthias Bussonnier
|
r25117 | inspector = None | |
def setup_module(): | |||
global inspector | |||
inspector = oinspect.Inspector() | |||
Fernando Perez
|
r3051 | ||
Ben Greiner
|
r27346 | class SourceModuleMainTest: | |
__module__ = "__main__" | |||
Fernando Perez
|
r3051 | #----------------------------------------------------------------------------- | |
# Local utilities | |||
#----------------------------------------------------------------------------- | |||
Fernando Perez
|
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. | |||
Ben Greiner
|
r27346 | THIS_LINE_NUMBER = 46 # Put here the actual number of this line | |
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 | |||
Matthias Bussonnier
|
r22911 | ||
Ben Greiner
|
r27346 | def test_getsource(): | |
assert oinspect.getsource(type) is None | |||
assert oinspect.getsource(SourceModuleMainTest) is None | |||
assert oinspect.getsource(SourceModuleMainTest()) is None | |||
Matthias Bussonnier
|
r22911 | ||
Ben Greiner
|
r27346 | def test_inspect_getfile_raises_exception(): | |
"""Check oinspect.find_file/getsource/find_source_lines expectations""" | |||
with pytest.raises(TypeError): | |||
inspect.getfile(type) | |||
with pytest.raises(OSError if sys.version_info >= (3, 10) else TypeError): | |||
inspect.getfile(SourceModuleMainTest) | |||
Fernando Perez
|
r7290 | ||
Fernando Perez
|
r7434 | # A couple of utilities to ensure these tests work the same from a source or a | |
# binary install | |||
def pyfile(fname): | |||
Jörgen Stenarson
|
r7452 | return os.path.normcase(re.sub('.py[co]$', '.py', fname)) | |
Fernando Perez
|
r7434 | ||
def match_pyfiles(f1, f2): | |||
Ben Greiner
|
r27346 | assert pyfile(f1) == pyfile(f2) | |
Fernando Perez
|
r7434 | ||
Fernando Perez
|
r7290 | def test_find_file(): | |
Fernando Perez
|
r7434 | match_pyfiles(oinspect.find_file(test_find_file), os.path.abspath(__file__)) | |
Ben Greiner
|
r27346 | assert oinspect.find_file(type) is None | |
assert oinspect.find_file(SourceModuleMainTest) is None | |||
assert oinspect.find_file(SourceModuleMainTest()) is None | |||
Fernando Perez
|
r7290 | ||
Fernando Perez
|
r7432 | def test_find_file_decorated1(): | |
@decorator | |||
def noop1(f): | |||
Min RK
|
r23020 | def wrapper(*a, **kw): | |
Fernando Perez
|
r7432 | return f(*a, **kw) | |
return wrapper | |||
@noop1 | |||
def f(x): | |||
"My docstring" | |||
Ben Greiner
|
r27346 | ||
Fernando Perez
|
r7434 | match_pyfiles(oinspect.find_file(f), os.path.abspath(__file__)) | |
Ben Greiner
|
r27346 | assert f.__doc__ == "My docstring" | |
Fernando Perez
|
r7432 | ||
def test_find_file_decorated2(): | |||
@decorator | |||
def noop2(f, *a, **kw): | |||
return f(*a, **kw) | |||
@noop2 | |||
Min RK
|
r21505 | @noop2 | |
@noop2 | |||
Fernando Perez
|
r7432 | def f(x): | |
"My docstring 2" | |||
Ben Greiner
|
r27346 | ||
Fernando Perez
|
r7434 | match_pyfiles(oinspect.find_file(f), os.path.abspath(__file__)) | |
Ben Greiner
|
r27346 | assert f.__doc__ == "My docstring 2" | |
Fernando Perez
|
r7432 | ||
Fernando Perez
|
r7290 | def test_find_file_magic(): | |
run = ip.find_line_magic('run') | |||
Ben Greiner
|
r27346 | assert oinspect.find_file(run) is not None | |
Fernando Perez
|
r7290 | ||
Fernando Perez
|
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""" | |||
Bernardo B. Marques
|
r4872 | ||
Min RK
|
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""" | |||
Thomas Kluyver
|
r15362 | class SimpleClass(object): | |
def method(self, x, z=2): | |||
"""Some method's docstring""" | |||
Fernando Perez
|
r6995 | ||
Thomas Kluyver
|
r11058 | class Awkward(object): | |
def __getattr__(self, name): | |||
raise Exception(name) | |||
Pierre Gerold
|
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') | |||
Thomas Kluyver
|
r11058 | ||
Thomas Kluyver
|
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) | |||
Fernando Perez
|
r3051 | #----------------------------------------------------------------------------- | |
# Tests | |||
#----------------------------------------------------------------------------- | |||
Thomas Kluyver
|
r3859 | def test_info(): | |
"Check that Inspector.info fills out various fields as expected." | |||
Ben Greiner
|
r27346 | i = inspector.info(Call, oname="Call") | |
assert i["type_name"] == "type" | |||
expected_class = str(type(type)) # <class 'type'> (Python 3) or <type 'type'> | |||
assert i["base_class"] == expected_class | |||
assert re.search( | |||
"<class 'IPython.core.tests.test_oinspect.Call'( at 0x[0-9a-f]{1,9})?>", | |||
i["string_form"], | |||
) | |||
Thomas Kluyver
|
r3859 | fname = __file__ | |
if fname.endswith(".pyc"): | |||
fname = fname[:-1] | |||
Min RK
|
r4105 | # case-insensitive comparison needed on some filesystems | |
# e.g. Windows: | |||
Ben Greiner
|
r27346 | assert i["file"].lower() == compress_user(fname).lower() | |
assert i["definition"] == None | |||
assert i["docstring"] == Call.__doc__ | |||
assert i["source"] == None | |||
assert i["isclass"] is True | |||
assert i["init_definition"] == "Call(x, y=1)" | |||
assert i["init_docstring"] == Call.__init__.__doc__ | |||
Bernardo B. Marques
|
r4872 | ||
Thomas Kluyver
|
r3859 | i = inspector.info(Call, detail_level=1) | |
Ben Greiner
|
r27346 | assert i["source"] is not None | |
assert i["docstring"] == None | |||
Bernardo B. Marques
|
r4872 | ||
Thomas Kluyver
|
r3859 | c = Call(1) | |
c.__doc__ = "Modified instance docstring" | |||
i = inspector.info(c) | |||
Ben Greiner
|
r27346 | 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__ | |||
Bernardo B. Marques
|
r4872 | ||
Min RK
|
r22171 | def test_class_signature(): | |
Ben Greiner
|
r27346 | info = inspector.info(HasSignature, "HasSignature") | |
assert info["init_definition"] == "HasSignature(test)" | |||
assert info["init_docstring"] == HasSignature.__init__.__doc__ | |||
Min RK
|
r22171 | ||
Thomas Kluyver
|
r11058 | def test_info_awkward(): | |
# Just test that this doesn't throw an error. | |||
Matthias Bussonnier
|
r22536 | inspector.info(Awkward()) | |
Thomas Kluyver
|
r11058 | ||
Pierre Gerold
|
r21886 | def test_bool_raise(): | |
inspector.info(NoBoolCall()) | |||
Thomas Kluyver
|
r21906 | def test_info_serialliar(): | |
fib_tracker = [0] | |||
Matthias Bussonnier
|
r22911 | inspector.info(SerialLiar(fib_tracker)) | |
Thomas Kluyver
|
r21906 | ||
# Nested attribute access should be cut off at 100 levels deep to avoid | |||
# infinite loops: https://github.com/ipython/ipython/issues/9122 | |||
Ben Greiner
|
r27346 | assert fib_tracker[0] < 9000 | |
Thomas Kluyver
|
r21906 | ||
Matthias Bussonnier
|
r25081 | def support_function_one(x, y=2, *a, **kw): | |
"""A simple function.""" | |||
Thomas Kluyver
|
r15362 | def test_calldef_none(): | |
# We should ignore __call__ for all of these. | |||
Matthias Bussonnier
|
r25081 | for obj in [support_function_one, SimpleClass().method, any, str.upper]: | |
Thomas Kluyver
|
r15362 | i = inspector.info(obj) | |
Ben Greiner
|
r27346 | assert i["call_def"] is None | |
Thomas Kluyver
|
r15362 | ||
Matthias Bussonnier
|
r22911 | def f_kwarg(pos, *, kwonly): | |
pass | |||
Thomas Kluyver
|
r15335 | ||
def test_definition_kwonlyargs(): | |||
Ben Greiner
|
r27346 | i = inspector.info(f_kwarg, oname="f_kwarg") # analysis:ignore | |
assert i["definition"] == "f_kwarg(pos, *, kwonly)" | |||
Thomas Kluyver
|
r15335 | ||
Thomas Kluyver
|
r5538 | def test_getdoc(): | |
class A(object): | |||
"""standard docstring""" | |||
pass | |||
Sylvain Corlay
|
r22460 | ||
Thomas Kluyver
|
r5538 | class B(object): | |
"""standard docstring""" | |||
def getdoc(self): | |||
return "custom docstring" | |||
Ben Greiner
|
r27346 | ||
Thomas Kluyver
|
r5573 | class C(object): | |
"""standard docstring""" | |||
def getdoc(self): | |||
return None | |||
Ben Greiner
|
r27346 | ||
Thomas Kluyver
|
r5538 | a = A() | |
b = B() | |||
Thomas Kluyver
|
r5573 | c = C() | |
Ben Greiner
|
r27346 | ||
assert oinspect.getdoc(a) == "standard docstring" | |||
assert oinspect.getdoc(b) == "custom docstring" | |||
assert oinspect.getdoc(c) == "standard docstring" | |||
Thomas Kluyver
|
r7460 | ||
immerrr
|
r17023 | ||
def test_empty_property_has_no_source(): | |||
i = inspector.info(property(), detail_level=1) | |||
Ben Greiner
|
r27346 | assert i["source"] is None | |
immerrr
|
r17023 | ||
def test_property_sources(): | |||
madhu94
|
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 | |||
Ben Greiner
|
r27346 | ||
immerrr
|
r17023 | class A(object): | |
@property | |||
def foo(self): | |||
return 'bar' | |||
foo = foo.setter(lambda self, v: setattr(self, 'bar', v)) | |||
Ben Greiner
|
r27346 | dname = property(oinspect.getdoc) | |
adder = property(simple_add) | |||
immerrr
|
r17023 | ||
i = inspector.info(A.foo, detail_level=1) | |||
Ben Greiner
|
r27346 | assert "def foo(self):" in i["source"] | |
assert "lambda self, v:" in i["source"] | |||
immerrr
|
r17023 | ||
madhu94
|
r24026 | i = inspector.info(A.dname, detail_level=1) | |
Ben Greiner
|
r27346 | assert "def getdoc(obj)" in i["source"] | |
madhu94
|
r24026 | i = inspector.info(A.adder, detail_level=1) | |
Ben Greiner
|
r27346 | assert "def simple_add(a, b)" in i["source"] | |
immerrr
|
r17023 | ||
def test_property_docstring_is_in_info_for_detail_level_0(): | |||
class A(object): | |||
@property | |||
Matthias Bussonnier
|
r22354 | def foobar(self): | |
immerrr
|
r17023 | """This is `foobar` property.""" | |
pass | |||
Ben Greiner
|
r27346 | ip.user_ns["a_obj"] = A() | |
assert ( | |||
"This is `foobar` property." | |||
== ip.object_inspect("a_obj.foobar", detail_level=0)["docstring"] | |||
) | |||
immerrr
|
r17023 | ||
Ben Greiner
|
r27346 | ip.user_ns["a_cls"] = A | |
assert ( | |||
"This is `foobar` property." | |||
== ip.object_inspect("a_cls.foobar", detail_level=0)["docstring"] | |||
) | |||
immerrr
|
r17023 | ||
Thomas Kluyver
|
r7460 | def test_pdef(): | |
# See gh-1914 | |||
def foo(): pass | |||
inspector.pdef(foo, 'foo') | |||
Thomas Kluyver
|
r11067 | ||
Min RK
|
r22539 | ||
Thomas Kluyver
|
r11067 | def test_pinfo_nonascii(): | |
# See gh-1177 | |||
from . import nonascii2 | |||
ip.user_ns['nonascii2'] = nonascii2 | |||
ip._inspect('pinfo', 'nonascii2', detail_level=1) | |||
Thomas Kluyver
|
r20699 | ||
Matthias Bussonnier
|
r24946 | def test_pinfo_type(): | |
""" | |||
type can fail in various edge case, for example `type.__subclass__()` | |||
""" | |||
ip._inspect('pinfo', 'type') | |||
Min RK
|
r22539 | ||
Min RK
|
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) | |||
ryan thielke
|
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 | |||
Ben Greiner
|
r27346 | ''' | |
ryan thielke
|
r23660 | ip.run_cell(obj_def) | |
ip.run_cell('foo = Foo()') | |||
Ben Greiner
|
r27346 | ||
ryan thielke
|
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) | |||
Thomas Kluyver
|
r20699 | def test_pinfo_magic(): | |
with AssertPrints('Docstring:'): | |||
ip._inspect('pinfo', 'lsmagic', detail_level=0) | |||
with AssertPrints('Source:'): | |||
ip._inspect('pinfo', 'lsmagic', detail_level=1) | |||
Min RK
|
r22539 | ||
def test_init_colors(): | |||
# ensure colors are not present in signature info | |||
info = inspector.info(HasSignature) | |||
Ben Greiner
|
r27346 | init_def = info["init_definition"] | |
assert "[0m" not in init_def | |||
Min RK
|
r22539 | ||
def test_builtin_init(): | |||
info = inspector.info(list) | |||
init_def = info['init_definition'] | |||
Ben Greiner
|
r27346 | assert init_def is not None | |
Min RK
|
r22539 | ||
Philipp A
|
r24881 | ||
def test_render_signature_short(): | |||
Philipp A
|
r24882 | def short_fun(a=1): pass | |
Philipp A
|
r24881 | sig = oinspect._render_signature( | |
signature(short_fun), | |||
short_fun.__name__, | |||
) | |||
Ben Greiner
|
r27346 | assert sig == "short_fun(a=1)" | |
Philipp A
|
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__, | |||
) | |||
Ben Greiner
|
r27346 | assert sig in [ | |
Matthias Bussonnier
|
r25722 | # Python >=3.9 | |
'''\ | |||
long_function( | |||
a_really_long_parameter: int, | |||
and_another_long_one: bool = False, | |||
let_us_make_sure_this_is_looong: Optional[str] = None, | |||
) -> bool\ | |||
''', | |||
Philipp A
|
r24882 | # Python >=3.7 | |
'''\ | |||
Philipp A
|
r24881 | long_function( | |
a_really_long_parameter: int, | |||
and_another_long_one: bool = False, | |||
let_us_make_sure_this_is_looong: Union[str, NoneType] = None, | |||
) -> bool\ | |||
Philipp A
|
r24882 | ''', # Python <=3.6 | |
'''\ | |||
long_function( | |||
a_really_long_parameter:int, | |||
and_another_long_one:bool=False, | |||
let_us_make_sure_this_is_looong:Union[str, NoneType]=None, | |||
) -> bool\ | |||
''', | |||
Ben Greiner
|
r27346 | ] |