diff --git a/IPython/core/oinspect.py b/IPython/core/oinspect.py index 253fc26..e6434a5 100644 --- a/IPython/core/oinspect.py +++ b/IPython/core/oinspect.py @@ -18,6 +18,7 @@ __all__ = ['Inspector','InspectColors'] import inspect import linecache import os +from textwrap import dedent import types import io as stdlib_io @@ -28,6 +29,7 @@ except ImportError: # IPython's own from IPython.core import page +from IPython.lib.pretty import pretty from IPython.testing.skipdoctest import skip_doctest_py3 from IPython.utils import PyColorize from IPython.utils import io @@ -43,7 +45,8 @@ from IPython.utils.py3compat import cast_unicode, string_types, PY3 _func_call_docstring = types.FunctionType.__call__.__doc__ _object_init_docstring = object.__init__.__doc__ _builtin_type_docstrings = { - t.__doc__ for t in (types.ModuleType, types.MethodType, types.FunctionType) + t.__doc__ for t in (types.ModuleType, types.MethodType, types.FunctionType, + property) } _builtin_func_type = type(all) @@ -150,33 +153,68 @@ def getdoc(obj): return None -def getsource(obj,is_binary=False): +def getsource(obj, oname=''): """Wrapper around inspect.getsource. This can be modified by other projects to provide customized source extraction. - Inputs: + Parameters + ---------- + obj : object + an object whose source code we will attempt to extract + oname : str + (optional) a name under which the object is known - - obj: an object whose source code we will attempt to extract. + Returns + ------- + src : unicode or None - Optional inputs: + """ - - is_binary: whether the object is known to come from a binary source. - This implementation will skip returning any output for binary objects, but - custom extractors may know how to meaningfully process them.""" + if isinstance(obj, property): + sources = [] + for attrname in ['fget', 'fset', 'fdel']: + fn = getattr(obj, attrname) + if fn is not None: + encoding = get_encoding(fn) + oname_prefix = ('%s.' % oname) if oname else '' + sources.append(cast_unicode( + ''.join(('# ', oname_prefix, attrname)), + encoding=encoding)) + if inspect.isfunction(fn): + sources.append(dedent(getsource(fn))) + else: + # Default str/repr only prints function name, + # pretty.pretty prints module name too. + sources.append(cast_unicode( + '%s%s = %s\n' % ( + oname_prefix, attrname, pretty(fn)), + encoding=encoding)) + if sources: + return '\n'.join(sources) + else: + return None - if is_binary: - return None else: - # get source if obj was decorated with @decorator - if hasattr(obj,"__wrapped__"): + # Get source for non-property objects. + + # '__wrapped__' attribute is used by some decorators (e.g. ones defined + # functools) to provide access to the decorated function. + if hasattr(obj, "__wrapped__"): obj = obj.__wrapped__ + try: src = inspect.getsource(obj) except TypeError: - if hasattr(obj,'__class__'): - src = inspect.getsource(obj.__class__) + # The object itself provided no meaningful source, try looking for + # its class definition instead. + if hasattr(obj, '__class__'): + try: + src = inspect.getsource(obj.__class__) + except TypeError: + return None + encoding = get_encoding(obj) return cast_unicode(src, encoding=encoding) @@ -457,15 +495,18 @@ class Inspector: else: page.page('\n'.join(lines)) - def psource(self,obj,oname=''): + def psource(self, obj, oname=''): """Print the source code for an object.""" # Flush the source cache because inspect can return out-of-date source linecache.checkcache() try: - src = getsource(obj) - except: - self.noinfo('source',oname) + src = getsource(obj, oname=oname) + except Exception: + src = None + + if src is None: + self.noinfo('source', oname) else: page.page(self.format(src)) @@ -696,32 +737,25 @@ class Inspector: elif fname.endswith(''): fname = 'Dynamically generated function. No source code available.' out['file'] = fname - - # Docstrings only in detail 0 mode, since source contains them (we - # avoid repetitions). If source fails, we add them back, see below. - if ds and detail_level == 0: - out['docstring'] = ds - # Original source code for any callable + # Original source code for a callable, class or property. if detail_level: # Flush the source cache because inspect can return out-of-date # source linecache.checkcache() - source = None try: - try: - source = getsource(obj, binary_file) - except TypeError: - if hasattr(obj, '__class__'): - source = getsource(obj.__class__, binary_file) - if source is not None: - out['source'] = source.rstrip() + if isinstance(obj, property) or not binary_file: + src = getsource(obj, oname) + if src is not None: + src = src.rstrip() + out['source'] = src + except Exception: pass - if ds and source is None: - out['docstring'] = ds - + # Add docstring only if no source is to be shown (avoid repetitions). + if ds and out.get('source', None) is None: + out['docstring'] = ds # Constructor docstring for classes if inspect.isclass(obj): @@ -824,7 +858,6 @@ class Inspector: return object_info(**out) - def psearch(self,pattern,ns_table,ns_search=[], ignore_case=False,show_all=False): """Search namespaces with wildcards for objects. diff --git a/IPython/core/tests/test_oinspect.py b/IPython/core/tests/test_oinspect.py index eb11459..73cb2d2 100644 --- a/IPython/core/tests/test_oinspect.py +++ b/IPython/core/tests/test_oinspect.py @@ -313,6 +313,54 @@ def test_getdoc(): nt.assert_equal(oinspect.getdoc(b), "custom docstring") nt.assert_equal(oinspect.getdoc(c), "standard docstring") + +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 = ', i['source']) + + i = inspector.info(A.compress, detail_level=1) + nt.assert_in('fget = ', i['source']) + + +def test_property_docstring_is_in_info_for_detail_level_0(): + class A(object): + @property + def foobar(): + """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']) + + def test_pdef(): # See gh-1914 def foo(): pass diff --git a/docs/source/whatsnew/pr/incompat-oinspect-getsource.rst b/docs/source/whatsnew/pr/incompat-oinspect-getsource.rst new file mode 100644 index 0000000..453377b --- /dev/null +++ b/docs/source/whatsnew/pr/incompat-oinspect-getsource.rst @@ -0,0 +1,5 @@ +* :func:`IPython.core.oinspect.getsource` call specification has changed: + + * `oname` keyword argument has been added for property source formatting + * `is_binary` keyword argument has been dropped, passing ``True`` had + previously short-circuited the function to return ``None`` unconditionally