test_oinspect.py
452 lines
| 12.6 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 | from __future__ import print_function | ||
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 | ||
Thomas Kluyver
|
r15335 | from IPython.testing.decorators import skipif | ||
Thomas Kluyver
|
r20699 | from IPython.testing.tools import AssertPrints | ||
Thomas Kluyver
|
r20578 | from IPython.utils.path import compress_user | ||
Thomas Kluyver
|
r4760 | from IPython.utils import py3compat | ||
Min RK
|
r22171 | from IPython.utils.signatures import Signature, Parameter | ||
Fernando Perez
|
r3051 | |||
Fernando Perez
|
r7432 | |||
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. | ||||
Min RK
|
r22171 | THIS_LINE_NUMBER = 43 # Put here the actual number of this line | ||
Fernando Perez
|
r7290 | def test_find_source_lines(): | ||
nt.assert_equal(oinspect.find_source_lines(test_find_source_lines), | ||||
THIS_LINE_NUMBER+1) | ||||
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): | ||||
def wrapper(): | ||||
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 | def check_calltip(obj, name, call, docstring): | ||
"""Generic check pattern all calltip tests will use""" | ||||
info = inspector.info(obj, name) | ||||
call_line, ds = oinspect.call_tip(info) | ||||
nt.assert_equal(call_line, call) | ||||
Bernardo B. Marques
|
r4872 | nt.assert_equal(ds, docstring) | ||
Fernando Perez
|
r3051 | |||
#----------------------------------------------------------------------------- | ||||
# Tests | ||||
#----------------------------------------------------------------------------- | ||||
def test_calltip_class(): | ||||
check_calltip(Call, 'Call', 'Call(x, y=1)', Call.__init__.__doc__) | ||||
def test_calltip_instance(): | ||||
c = Call(1) | ||||
check_calltip(c, 'c', 'c(*a, **kw)', c.__call__.__doc__) | ||||
def test_calltip_method(): | ||||
c = Call(1) | ||||
check_calltip(c.method, 'c.method', 'c.method(x, z=2)', c.method.__doc__) | ||||
def test_calltip_function(): | ||||
check_calltip(f, 'f', 'f(x, y=2, *a, **kw)', f.__doc__) | ||||
def test_calltip_function2(): | ||||
check_calltip(g, 'g', 'g(y, z=3, *a, **kw)', '<no docstring>') | ||||
Thomas Kluyver
|
r21313 | @skipif(sys.version_info >= (3, 5)) | ||
Fernando Perez
|
r3051 | def test_calltip_builtin(): | ||
check_calltip(sum, 'sum', None, sum.__doc__) | ||||
Bernardo B. Marques
|
r4872 | |||
Fernando Perez
|
r6995 | |||
def test_calltip_line_magic(): | ||||
check_calltip(lmagic, 'lmagic', 'lmagic(line)', "A line magic") | ||||
def test_calltip_cell_magic(): | ||||
check_calltip(cmagic, 'cmagic', 'cmagic(line, cell)', "A cell magic") | ||||
Thomas Kluyver
|
r11124 | def test_calltip_line_cell_magic(): | ||
Fernando Perez
|
r6995 | check_calltip(lcmagic, 'lcmagic', 'lcmagic(line, cell=None)', | ||
"A line/cell magic") | ||||
def test_class_magics(): | ||||
cm = SimpleMagics(ip) | ||||
ip.register_magics(cm) | ||||
check_calltip(cm.Clmagic, 'Clmagic', 'Clmagic(cline)', | ||||
"A class-based line magic") | ||||
check_calltip(cm.Ccmagic, 'Ccmagic', 'Ccmagic(cline, ccell)', | ||||
"A class-based cell magic") | ||||
check_calltip(cm.Clcmagic, 'Clcmagic', 'Clcmagic(cline, ccell=None)', | ||||
"A class-based line/cell magic") | ||||
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) | ||||
Matthias Bussonnier
|
r22456 | if sys.version_info > (3,): | ||
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']) | ||
Min RK
|
r22177 | _self_py2 = '' if py3compat.PY3 else 'self, ' | ||
Matthias Bussonnier
|
r22536 | nt.assert_equal(i['init_definition'], "Call(%sx, y=1)" % _self_py2) | ||
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 | |||
Thomas Kluyver
|
r3867 | # Test old-style classes, which for example may not have an __init__ method. | ||
Thomas Kluyver
|
r4760 | if not py3compat.PY3: | ||
i = inspector.info(OldStyle) | ||||
nt.assert_equal(i['type_name'], 'classobj') | ||||
Bernardo B. Marques
|
r4872 | |||
Thomas Kluyver
|
r4760 | i = inspector.info(OldStyle()) | ||
nt.assert_equal(i['type_name'], 'instance') | ||||
nt.assert_equal(i['docstring'], OldStyle.__doc__) | ||||
Thomas Kluyver
|
r5538 | |||
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] | ||||
i = inspector.info(SerialLiar(fib_tracker)) | ||||
# 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) | ||||
Thomas Kluyver
|
r15335 | if py3compat.PY3: | ||
exec("def f_kwarg(pos, *, kwonly): pass") | ||||
@skipif(not py3compat.PY3) | ||||
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(): | ||||
import zlib | ||||
class A(object): | ||||
@property | ||||
def foo(self): | ||||
return 'bar' | ||||
foo = foo.setter(lambda self, v: setattr(self, 'bar', v)) | ||||
id = property(id) | ||||
compress = property(zlib.compress) | ||||
i = inspector.info(A.foo, detail_level=1) | ||||
nt.assert_in('def foo(self):', i['source']) | ||||
nt.assert_in('lambda self, v:', i['source']) | ||||
i = inspector.info(A.id, detail_level=1) | ||||
nt.assert_in('fget = <function id>', i['source']) | ||||
i = inspector.info(A.compress, detail_level=1) | ||||
nt.assert_in('fget = <function zlib.compress>', i['source']) | ||||
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() | ||||
nt.assert_equals( | ||||
'This is `foobar` property.', | ||||
ip.object_inspect('a_obj.foobar', detail_level=0)['docstring']) | ||||
ip.user_ns['a_cls'] = A | ||||
nt.assert_equals( | ||||
'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 | |||
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'] | ||||
# Python < 3.4 can't get init definition from builtins, | ||||
# but still exercise the inspection in case of error-raising bugs. | ||||
if sys.version_info >= (3,4): | ||||
nt.assert_is_not_none(init_def) | ||||