test_oinspect.py
474 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 | ||
Fernando Perez
|
r7290 | import os | ||
Fernando Perez
|
r7434 | import re | ||
Thomas Kluyver
|
r21314 | import sys | ||
Fernando Perez
|
r3051 | |||
import nose.tools as nt | ||||
from .. import oinspect | ||||
Fernando Perez
|
r6995 | from IPython.core.magic import (Magics, magics_class, line_magic, | ||
cell_magic, line_cell_magic, | ||||
register_line_magic, register_cell_magic, | ||||
register_line_cell_magic) | ||||
MinRK
|
r20813 | from decorator import decorator | ||
Min RK
|
r23020 | from IPython import get_ipython | ||
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 | ||||
#----------------------------------------------------------------------------- | ||||
inspector = oinspect.Inspector() | ||||
Fernando Perez
|
r6995 | ip = get_ipython() | ||
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. | ||||
Hugo
|
r24010 | THIS_LINE_NUMBER = 41 # Put here the actual number of this line | ||
Matthias Bussonnier
|
r22911 | |||
from unittest import TestCase | ||||
class Test(TestCase): | ||||
def test_find_source_lines(self): | ||||
Hugo
|
r24010 | self.assertEqual(oinspect.find_source_lines(Test.test_find_source_lines), | ||
Matthias Bussonnier
|
r22911 | THIS_LINE_NUMBER+6) | ||
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): | ||||
Jörgen Stenarson
|
r7452 | nt.assert_equal(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__)) | ||
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" | ||||
Fernando Perez
|
r7434 | match_pyfiles(oinspect.find_file(f), os.path.abspath(__file__)) | ||
Fernando Perez
|
r7432 | nt.assert_equal(f.__doc__, "My docstring") | ||
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" | ||||
Fernando Perez
|
r7434 | match_pyfiles(oinspect.find_file(f), os.path.abspath(__file__)) | ||
Fernando Perez
|
r7432 | nt.assert_equal(f.__doc__, "My docstring 2") | ||
Fernando Perez
|
r7290 | def test_find_file_magic(): | ||
run = ip.find_line_magic('run') | ||||
nt.assert_not_equal(oinspect.find_file(run), None) | ||||
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
|
r3867 | class OldStyle: | ||
"""An old-style class for testing.""" | ||||
pass | ||||
Fernando Perez
|
r3051 | |||
Fernando Perez
|
r6995 | |||
Fernando Perez
|
r3051 | def f(x, y=2, *a, **kw): | ||
"""A simple function.""" | ||||
Fernando Perez
|
r6995 | |||
Fernando Perez
|
r3051 | def g(y, z=3, *a, **kw): | ||
pass # no docstring | ||||
Fernando Perez
|
r6995 | @register_line_magic | ||
def lmagic(line): | ||||
"A line magic" | ||||
@register_cell_magic | ||||
def cmagic(line, cell): | ||||
"A cell magic" | ||||
@register_line_cell_magic | ||||
def lcmagic(line, cell=None): | ||||
"A line/cell magic" | ||||
@magics_class | ||||
class SimpleMagics(Magics): | ||||
@line_magic | ||||
def Clmagic(self, cline): | ||||
"A class-based line magic" | ||||
@cell_magic | ||||
def Ccmagic(self, cline, ccell): | ||||
"A class-based cell magic" | ||||
@line_cell_magic | ||||
def Clcmagic(self, cline, ccell=None): | ||||
"A class-based line/cell magic" | ||||
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." | ||||
i = inspector.info(Call, oname='Call') | ||||
nt.assert_equal(i['type_name'], 'type') | ||||
Thomas Kluyver
|
r4760 | expted_class = str(type(type)) # <class 'type'> (Python 3) or <type 'type'> | ||
nt.assert_equal(i['base_class'], expted_class) | ||||
Paul Ivanov
|
r22959 | nt.assert_regex(i['string_form'], "<class 'IPython.core.tests.test_oinspect.Call'( at 0x[0-9a-f]{1,9})?>") | ||
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: | ||||
Min RK
|
r20826 | nt.assert_equal(i['file'].lower(), compress_user(fname).lower()) | ||
MinRK
|
r15711 | nt.assert_equal(i['definition'], None) | ||
Thomas Kluyver
|
r3859 | nt.assert_equal(i['docstring'], Call.__doc__) | ||
Thomas Kluyver
|
r3869 | nt.assert_equal(i['source'], None) | ||
Thomas Kluyver
|
r3859 | nt.assert_true(i['isclass']) | ||
Srinivas Reddy Thatiparthy
|
r23096 | nt.assert_equal(i['init_definition'], "Call(x, y=1)") | ||
Thomas Kluyver
|
r3859 | nt.assert_equal(i['init_docstring'], Call.__init__.__doc__) | ||
Bernardo B. Marques
|
r4872 | |||
Thomas Kluyver
|
r3859 | i = inspector.info(Call, detail_level=1) | ||
Thomas Kluyver
|
r3869 | nt.assert_not_equal(i['source'], None) | ||
nt.assert_equal(i['docstring'], None) | ||||
Bernardo B. Marques
|
r4872 | |||
Thomas Kluyver
|
r3859 | c = Call(1) | ||
c.__doc__ = "Modified instance docstring" | ||||
i = inspector.info(c) | ||||
nt.assert_equal(i['type_name'], 'Call') | ||||
nt.assert_equal(i['docstring'], "Modified instance docstring") | ||||
nt.assert_equal(i['class_docstring'], Call.__doc__) | ||||
nt.assert_equal(i['init_docstring'], Call.__init__.__doc__) | ||||
MinRK
|
r15711 | nt.assert_equal(i['call_docstring'], Call.__call__.__doc__) | ||
Bernardo B. Marques
|
r4872 | |||
Min RK
|
r22171 | def test_class_signature(): | ||
info = inspector.info(HasSignature, 'HasSignature') | ||||
Matthias Bussonnier
|
r22536 | nt.assert_equal(info['init_definition'], "HasSignature(test)") | ||
Min RK
|
r22171 | nt.assert_equal(info['init_docstring'], HasSignature.__init__.__doc__) | ||
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 | ||||
nt.assert_less(fib_tracker[0], 9000) | ||||
Thomas Kluyver
|
r15362 | def test_calldef_none(): | ||
# We should ignore __call__ for all of these. | ||||
for obj in [f, SimpleClass().method, any, str.upper]: | ||||
print(obj) | ||||
i = inspector.info(obj) | ||||
nt.assert_is(i['call_def'], None) | ||||
Matthias Bussonnier
|
r22911 | def f_kwarg(pos, *, kwonly): | ||
pass | ||||
Thomas Kluyver
|
r15335 | |||
def test_definition_kwonlyargs(): | ||||
i = inspector.info(f_kwarg, oname='f_kwarg') # analysis:ignore | ||||
Sylvain Corlay
|
r22460 | nt.assert_equal(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" | ||||
Thomas Kluyver
|
r5573 | class C(object): | ||
"""standard docstring""" | ||||
def getdoc(self): | ||||
return None | ||||
Thomas Kluyver
|
r5538 | a = A() | ||
b = B() | ||||
Thomas Kluyver
|
r5573 | c = C() | ||
Thomas Kluyver
|
r5538 | |||
nt.assert_equal(oinspect.getdoc(a), "standard docstring") | ||||
nt.assert_equal(oinspect.getdoc(b), "custom docstring") | ||||
Thomas Kluyver
|
r5573 | nt.assert_equal(oinspect.getdoc(c), "standard docstring") | ||
Thomas Kluyver
|
r7460 | |||
immerrr
|
r17023 | |||
def test_empty_property_has_no_source(): | ||||
i = inspector.info(property(), detail_level=1) | ||||
nt.assert_is(i['source'], None) | ||||
def test_property_sources(): | ||||
madhu94
|
r24026 | import posixpath | ||
# A simple adder whose source and signature stays | ||||
# the same across Python distributions | ||||
def simple_add(a, b): | ||||
"Adds two numbers" | ||||
return a + b | ||||
immerrr
|
r17023 | class A(object): | ||
@property | ||||
def foo(self): | ||||
return 'bar' | ||||
foo = foo.setter(lambda self, v: setattr(self, 'bar', v)) | ||||
madhu94
|
r24026 | dname = property(posixpath.dirname) | ||
adder = property(simple_add) | ||||
immerrr
|
r17023 | |||
i = inspector.info(A.foo, detail_level=1) | ||||
nt.assert_in('def foo(self):', i['source']) | ||||
nt.assert_in('lambda self, v:', i['source']) | ||||
madhu94
|
r24026 | i = inspector.info(A.dname, detail_level=1) | ||
nt.assert_in('def dirname(p)', i['source']) | ||||
i = inspector.info(A.adder, detail_level=1) | ||||
nt.assert_in('def simple_add(a, b)', 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 | ||||
ip.user_ns['a_obj'] = A() | ||||
Min RK
|
r23021 | nt.assert_equal( | ||
immerrr
|
r17023 | 'This is `foobar` property.', | ||
ip.object_inspect('a_obj.foobar', detail_level=0)['docstring']) | ||||
ip.user_ns['a_cls'] = A | ||||
Min RK
|
r23021 | nt.assert_equal( | ||
immerrr
|
r17023 | 'This is `foobar` property.', | ||
ip.object_inspect('a_cls.foobar', detail_level=0)['docstring']) | ||||
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 | |||
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 | ||||
''' | ||||
ip.run_cell(obj_def) | ||||
ip.run_cell('foo = Foo()') | ||||
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) | ||||
init_def = info['init_definition'] | ||||
nt.assert_not_in('[0m', init_def) | ||||
def test_builtin_init(): | ||||
info = inspector.info(list) | ||||
init_def = info['init_definition'] | ||||
Hugo
|
r24260 | nt.assert_is_not_none(init_def) | ||
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__, | ||||
) | ||||
Philipp A
|
r24882 | nt.assert_equal(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__, | ||||
) | ||||
Philipp A
|
r24882 | nt.assert_in(sig, [ | ||
# 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\ | ||||
''', | ||||
]) | ||||