Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Print sources for fget/fset/fdel property methods #4827

Merged
merged 3 commits into from

6 participants

@immerrr

This is a work-in-progress fixing issue #1555 which has been bugging me for quite a while. Here's what it is capable of now:

In [1]: def foo(x): return id(x)

In [2]: p = property(foo, lambda x,val: None, id)

In [3]: p?
Type:       property
String Form:<property object at 0x2c52c00>
File:       /home/immerrr/sources/<ipython-input-1-8d2e34e1a2b1>
Docstring:  <no docstring>

In [4]: p??
Type:       property
String Form:<property object at 0x2c52c00>
File:       /home/immerrr/sources/<ipython-input-1-8d2e34e1a2b1>
Getter:     def foo(x): return id(x)

Setter:     p = property(foo, lambda x,val: None, id)

Deleter:    <function id>

In [5]: class Foo(object):
   ...:     @property
   ...:     def bar(self):
   ...:         return None
   ...:     

In [6]: f = Foo()

In [7]: f.bar?
Type:       property
String Form:<property object at 0x2f84aa0>
File:       /home/immerrr/sources/<ipython-input-5-64d3245b5460>
Docstring:  <no docstring>

In [8]: f.bar??
Type:       property
String Form:<property object at 0x2f84aa0>
File:       /home/immerrr/sources/<ipython-input-5-64d3245b5460>
Getter:
    @property
    def bar(self):
        return None

Admittedly, this is not perfect, so ideas about what's missing are welcome. What I think is missing:

  • tests (didn't look how testing is done in IPython, nor do I have an idea how to test this properly)
  • get rid of newlines that still appear after one-liner source snippets
  • stop invoking property getter when querying as it may cause unintended side-effects and/or raise an error rendering the property inaccessible
@ellisonbg ellisonbg was assigned
@takluyver
Owner

This changes the info dict so the 'source' field can be a list of 2-tuples. I think the info dict also gets sent to frontends on an object_info_request, so changes to its structure might break frontends. We're also trying to take it in the direction of being more language agnostic, which might need some more thought. Do you want to join our next online dev meeting (Thursday, 10am PST) to discuss this more?

@immerrr

I've checked that hack with "regular" frontends and it seemed to work, but I agree that interface concerns are perfectly reasonable.

In that case it may be structurally cleaner to embrace the fact that there can be multiple "kinds" of sources for objects and add those additional "kinds" of sources as separate fields into "info" dictionary, e.g. descriptor protocol sources (properties being a special case of that), places where constants of built-in types are set and so on. I thought that these things should be figured out by now in libraries like rope or jedi (at least to some extent), so relying on code introspection libraries to do actual code introspection may be a win-win. I'll think in that direction.

@immerrr

As for the meeting, yeah, sure, why not. I'm not sure if I qualify as an "IPython dev" as this is probably the first time I got my hands dirty with its internals, but yeah. As a matter of fact I've had this idea of implementing something IPython-ish* for Lua someday, but the language-agnostic direction you've brought up may make this someday a lot closer.

  • disclaimer: I'm maintaining lua-mode for Emacs in spare time and the original intention was to make something like SLIME, but I guess not all languages as introspectable as Common Lisp.
@Carreau
Owner

That one of the reason we like to have people from non-python world, to try to abstract what is needed for all programming languages.

As far as your concern to qualify as an "IPython dev", you submitted a PR, which I suppose is enough. IPython don't have users, only dev :-). After you have the "core dev" who have commit right, but this is just when you start contributing regularly and we just can't keep up with the review, so we give you commit right :-)

As far as the Meeting they are live (and archived) on youtube, so yo can look at them whenever you want.
Just tell us you like to hangout live, and we'll send you an invitation by mail when the hangout start.
We filter people on live hangout because google have difficulties handeling more than 10.

@immerrr

@Carreau, Ok, let's try then, where do I sign? :)

@Carreau
Owner

@Carreau, Ok, let's try then, where do I sign? :)

Meetings are live on youtube (google hangout) every thurday 10am PST, usually with a chat open here and agenda just request an invite (ie we need your email) if you want to join on a specific session and @minrk will send you a google hangout invite a few minutes before we start.

@immerrr

Ok, not sure if I'll be back home in time, but I'll request an invite if I do. Thanks.

@Carreau
Owner

Ok, not sure if I'll be back home in time, but I'll request an invite if I do. Thanks.

It's every week, no hurry. But today /tomorow, we might speek of completer already.

@immerrr

Ok, I have:

  • rebased this onto master
  • rewritten it to put all sources into info['source'] field
  • patched ofind to avoid invoking the last attribute lookup if it's a property
  • added tests

It now looks something like that:

In [6]: class A(object):
    id = property(id)
    @id.setter
    def id(self, val):
        raise NotImplementedError("You cannot set object's id")

In [10]: a.id??
Type:       property
String Form:<property object at 0x1b1e0a8>
Source:
# a.id.fget
a.id.fget = <function id>

# a.id.fset
@id.setter
def id(self, val):
    raise NotImplementedError("You cannot set object's id")

Is there a possibility that this gets merged before 3.0? :)

@takluyver
Owner

Yes, I imagine this will be merged before 3.0, but that's still some time away - we're aiming for 2.0 in the next few weeks.

@ellisonbg
Owner

Needs rebase.

@immerrr

Done.

@immerrr

Rebased once more and fixed tests on Py3.

@minrk
Owner

Sorry for the long silence. Now that the msg spec changes are merged, the object_info_reply implications of this PR are no longer an issue. I don't see anything more to do here. Is this okay to merge, @takluyver?

@jdfreder
Owner

bump just in case @takluyver hasn't seen this since he was away for a while there.

@takluyver takluyver commented on the diff
IPython/core/interactiveshell.py
((6 lines not shown))
try:
parent = obj
- obj = getattr(obj,part)
+ # The last part is looked up in a special way to avoid
+ # descriptor invocation as it may raise or have side
+ # effects.
+ if idx == len(oname_rest) - 1:
+ obj = self._getattr_property(obj, part)
+ else:
+ obj = getattr(obj, part)
@takluyver Owner

Using a loop and checking if we're on the last iteration seems icky. Why not split oname up into oname_head, oname_rest and oname_tail, leave the loop as it is, and do the last bit outside the loop:

obj = self._getattr_property(obj, oname_tail)
@immerrr
immerrr added a note

It appears that the whole loop must be duplicated outside (carpet catch, parent preservation) and for: ... else: ... block can no longer be used to check if the lookup succeeded, so an extra variable must be introduced. Does it look icky enough for that or did you have anything else in mind?

@takluyver Owner

I guess you're right, the approach you've got is better than the alternative.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
IPython/core/oinspect.py
@@ -705,11 +708,33 @@ def info(self, obj, oname='', formatter=None, info=None, detail_level=0):
linecache.checkcache()
source = None
try:
- try:
- source = getsource(obj, binary_file)
- except TypeError:
- if hasattr(obj, '__class__'):
- source = getsource(obj.__class__, binary_file)
+ if isinstance(obj, property):
+ sources = []
@takluyver Owner

This whole section is only active when we do A.b?? to see the source. Shouldn't we be looking up property docstrings when we do single-question-mark A.b? introspection? At least for the getter, as the most common use of properties is getter-only.

Also, a comment explaining what's going on would be good here - the whole of oinspect can get pretty hard to follow.

@immerrr
immerrr added a note

re: single-? — I'm pretty sure I had given this a thought when implementing, but can't remember what was the rationale. Will fix that.

re: comment — ok.

@immerrr
immerrr added a note

re: single-? — Apparently, inspect.getdoc(property(...)) already looks up proper docstring and so, as long as property objects are found via ofind, it should work. It doesn't currently work for other descriptors, like slot attributes, though:

In [66]: ar = np.array([1])

In [67]: ar.shape?
Type:        tuple
String form: (1,)
Length:      1
Docstring:
tuple() -> empty tuple
tuple(iterable) -> tuple initialized from iterable's items

If the argument is a tuple, the return value is the same object.

In [68]: np.ndarray.shape?
Type:        getset_descriptor
String form: <attribute 'shape' of 'numpy.ndarray' objects>
Docstring:
Tuple of array dimensions.

Notes
-----
May be used to "reshape" the array, as long as this would not
require a change in the total number of elements

Examples
--------
>>> x = np.array([1, 2, 3, 4])
>>> x.shape
(4,)
>>> y = np.zeros((2, 3, 4))
>>> y.shape
(2, 3, 4)
>>> y.shape = (3, 8)
>>> y
array([[ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.]])
>>> y.shape = (3, 6)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: total size of new array must be unchanged

@takluyver Owner

OK, it sounds like that's good enough for now. If you want to drop those couple of extra comments in here, I think this is ready to merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
IPython/core/oinspect.py
((16 lines not shown))
+ 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:
+ source = '\n'.join(sources)
+ else:
+ try:
@takluyver Owner

Likewise a comment here pointing out that this section gets source for objects other than properties would be useful.

@immerrr
immerrr added a note

ok, I'll refactor/comment this bit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@takluyver
Owner

Thanks for your patience, @immerrr - this basically looks good, I just spotted a couple of things in review. The most important one is that I think we should show some property docstring details with the single-question-mark introspection that doesn't show source.

@immerrr

I've rebased/refactored the code a bit.

Now it's tested that info contains property docstrings as expected and the property-specific source lookup code is put into oinspect.getsource so that %psource command could work the same way.

I couldn't resist a bit of refactoring of getsource on the way, though:

  • binary_file parameter was passed in only to short-circuit and exit from it, so I've moved it outside
  • I've also added an optional oname parameter to make property formatter work as earlier

Two further considerations.

  • Now it seems that binary_file flag in info method can be removed, too: if obj comes from a binary file, getsource should gracefully return None (or at least it could be made to do so).

  • Also, while investigating the getset_descriptor objects in numpy, I've run into a funny file: numpy/numpy@numpy/add_newdocs.py which kind of shows that there exists at least one sane use case when docstrings may be attached to a live object but not present in its immediate source. Maybe the logic that drops docstrings if the source is shown should do some duplication detection to handle that.

@takluyver takluyver commented on the diff
IPython/core/oinspect.py
@@ -150,33 +153,68 @@ def getdoc(obj):
return None
-def getsource(obj,is_binary=False):
+def getsource(obj, oname=''):
@takluyver Owner

This is a backwards incompatible change, so please write a note in docs/source/whatsnew/pr/incompat-oinspect-getsource.rst (the name doesn't matter, so long as it starts with incompat-).

@immerrr
immerrr added a note

Done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
IPython/core/oinspect.py
((17 lines not shown))
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:
+ out['source'] = getsource(obj, oname).rstrip()
@takluyver Owner

getsource can still return None, in which case rstrip() will fail.

@immerrr
immerrr added a note

try-catch around it wouldn't allow that to do any harm, but still a good catch, will fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@immerrr

I've revisited property lookup a bit after going through Python reference: it turns out properties (class-level data descriptors) take precedence over instance-level attributes, so property-aware look up doesn't have to peek into object's dictionary if its class is known to have a property by the sought name.

docs/source/whatsnew/pr/incompat-oinspect-getsource.rst
@@ -0,0 +1,4 @@
+* :func:`core.oinspect.getsource` call specification has changed:
@takluyver Owner

I think this needs the fully qualified name, i.e. starting with IPython., for Sphinx crosslinking to work. I may be wrong about that, though.

@immerrr
immerrr added a note

Yup, you're right.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@takluyver
Owner

Thanks @immerrr , I think this is ready to go now. Are you happy with it?

@takluyver
Owner

(The Travis failure is an unrelated thing in a JS test)

immerrr added some commits
@immerrr immerrr Print sources for property fget/fset/fdel methods
This commit incorporates a backward-incompatible change:
`core.oinspect.getsource` will no longer accept `is_binary` kwarg.
783754c
@immerrr immerrr core.interactiveshell.ofind: don't evaluate property.fget (it may raise) 02f9be0
@immerrr immerrr Use inspect.getdoc when populating _builtin_type_docstrings
IPython.core.oinspect.getdoc uses inspect.getdoc which
strips some whitespace internally and because of that

    inspect.getdoc(property) != property.__doc__
1fd9014
@immerrr

Are you happy with it?

Yeah, sure.

@takluyver
Owner

Great, merging once we get the green light from Travis.

@takluyver takluyver merged commit 620860b into ipython:master
@immerrr

Awesome, thanks for your support.

@immerrr immerrr deleted the immerrr:print-property-getter-setter-deleter-sources branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jun 18, 2014
  1. @immerrr

    Print sources for property fget/fset/fdel methods

    immerrr authored
    This commit incorporates a backward-incompatible change:
    `core.oinspect.getsource` will no longer accept `is_binary` kwarg.
  2. @immerrr
  3. @immerrr

    Use inspect.getdoc when populating _builtin_type_docstrings

    immerrr authored
    IPython.core.oinspect.getdoc uses inspect.getdoc which
    strips some whitespace internally and because of that
    
        inspect.getdoc(property) != property.__doc__
This page is out of date. Refresh to see the latest.
View
73 IPython/core/interactiveshell.py
@@ -1443,10 +1443,16 @@ def _ofind(self, oname, namespaces=None):
continue
else:
#print 'oname_rest:', oname_rest # dbg
- for part in oname_rest:
+ for idx, part in enumerate(oname_rest):
try:
parent = obj
- obj = getattr(obj,part)
+ # The last part is looked up in a special way to avoid
+ # descriptor invocation as it may raise or have side
+ # effects.
+ if idx == len(oname_rest) - 1:
+ obj = self._getattr_property(obj, part)
+ else:
+ obj = getattr(obj, part)
@takluyver Owner

Using a loop and checking if we're on the last iteration seems icky. Why not split oname up into oname_head, oname_rest and oname_tail, leave the loop as it is, and do the last bit outside the loop:

obj = self._getattr_property(obj, oname_tail)
@immerrr
immerrr added a note

It appears that the whole loop must be duplicated outside (carpet catch, parent preservation) and for: ... else: ... block can no longer be used to check if the lookup succeeded, so an extra variable must be introduced. Does it look icky enough for that or did you have anything else in mind?

@takluyver Owner

I guess you're right, the approach you've got is better than the alternative.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
except:
# Blanket except b/c some badly implemented objects
# allow __getattr__ to raise exceptions other than
@@ -1486,33 +1492,48 @@ def _ofind(self, oname, namespaces=None):
return {'found':found, 'obj':obj, 'namespace':ospace,
'ismagic':ismagic, 'isalias':isalias, 'parent':parent}
- def _ofind_property(self, oname, info):
- """Second part of object finding, to look for property details."""
- if info.found:
- # Get the docstring of the class property if it exists.
- path = oname.split('.')
- root = '.'.join(path[:-1])
- if info.parent is not None:
- try:
- target = getattr(info.parent, '__class__')
- # The object belongs to a class instance.
- try:
- target = getattr(target, path[-1])
- # The class defines the object.
- if isinstance(target, property):
- oname = root + '.__class__.' + path[-1]
- info = Struct(self._ofind(oname))
- except AttributeError: pass
- except AttributeError: pass
-
- # We return either the new info or the unmodified input if the object
- # hadn't been found
- return info
+ @staticmethod
+ def _getattr_property(obj, attrname):
+ """Property-aware getattr to use in object finding.
+
+ If attrname represents a property, return it unevaluated (in case it has
+ side effects or raises an error.
+
+ """
+ if not isinstance(obj, type):
+ try:
+ # `getattr(type(obj), attrname)` is not guaranteed to return
+ # `obj`, but does so for property:
+ #
+ # property.__get__(self, None, cls) -> self
+ #
+ # The universal alternative is to traverse the mro manually
+ # searching for attrname in class dicts.
+ attr = getattr(type(obj), attrname)
+ except AttributeError:
+ pass
+ else:
+ # This relies on the fact that data descriptors (with both
+ # __get__ & __set__ magic methods) take precedence over
+ # instance-level attributes:
+ #
+ # class A(object):
+ # @property
+ # def foobar(self): return 123
+ # a = A()
+ # a.__dict__['foobar'] = 345
+ # a.foobar # == 123
+ #
+ # So, a property may be returned right away.
+ if isinstance(attr, property):
+ return attr
+
+ # Nothing helped, fall back.
+ return getattr(obj, attrname)
def _object_find(self, oname, namespaces=None):
"""Find an object and return a struct with info about it."""
- inf = Struct(self._ofind(oname, namespaces))
- return Struct(self._ofind_property(oname, inf))
+ return Struct(self._ofind(oname, namespaces))
def _inspect(self, meth, oname, namespaces=None, **kw):
"""Generic interface to the inspector system.
View
105 IPython/core/oinspect.py
@@ -18,6 +18,7 @@
import inspect
import linecache
import os
+from textwrap import dedent
import types
import io as stdlib_io
@@ -28,6 +29,7 @@
# 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 @@
_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)
+ inspect.getdoc(t) 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=''):
@takluyver Owner

This is a backwards incompatible change, so please write a note in docs/source/whatsnew/pr/incompat-oinspect-getsource.rst (the name doesn't matter, so long as it starts with incompat-).

@immerrr
immerrr added a note

Done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
"""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 @@ def pdoc(self,obj,oname='',formatter = None):
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 @@ def info(self, obj, oname='', formatter=None, info=None, detail_level=0):
elif fname.endswith('<string>'):
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 @@ def info(self, obj, oname='', formatter=None, info=None, detail_level=0):
return object_info(**out)
-
def psearch(self,pattern,ns_table,ns_search=[],
ignore_case=False,show_all=False):
"""Search namespaces with wildcards for objects.
View
57 IPython/core/tests/test_interactiveshell.py
@@ -375,7 +375,62 @@ def cmagic(line, cell):
namespace = 'IPython internal', obj= cmagic.__wrapped__,
parent = None)
nt.assert_equal(find, info)
-
+
+ def test_ofind_property_with_error(self):
+ class A(object):
+ @property
+ def foo(self):
+ raise NotImplementedError()
+ a = A()
+
+ found = ip._ofind('a.foo', [('locals', locals())])
+ info = dict(found=True, isalias=False, ismagic=False,
+ namespace='locals', obj=A.foo, parent=a)
+ nt.assert_equal(found, info)
+
+ def test_ofind_multiple_attribute_lookups(self):
+ class A(object):
+ @property
+ def foo(self):
+ raise NotImplementedError()
+
+ a = A()
+ a.a = A()
+ a.a.a = A()
+
+ found = ip._ofind('a.a.a.foo', [('locals', locals())])
+ info = dict(found=True, isalias=False, ismagic=False,
+ namespace='locals', obj=A.foo, parent=a.a.a)
+ nt.assert_equal(found, info)
+
+ def test_ofind_slotted_attributes(self):
+ class A(object):
+ __slots__ = ['foo']
+ def __init__(self):
+ self.foo = 'bar'
+
+ a = A()
+ found = ip._ofind('a.foo', [('locals', locals())])
+ info = dict(found=True, isalias=False, ismagic=False,
+ namespace='locals', obj=a.foo, parent=a)
+ nt.assert_equal(found, info)
+
+ found = ip._ofind('a.bar', [('locals', locals())])
+ info = dict(found=False, isalias=False, ismagic=False,
+ namespace=None, obj=None, parent=a)
+ nt.assert_equal(found, info)
+
+ def test_ofind_prefers_property_to_instance_level_attribute(self):
+ class A(object):
+ @property
+ def foo(self):
+ return 'bar'
+ a = A()
+ a.__dict__['foo'] = 'baz'
+ nt.assert_equal(a.foo, 'bar')
+ found = ip._ofind('a.foo', [('locals', locals())])
+ nt.assert_is(found['obj'], A.foo)
+
def test_custom_exception(self):
called = []
def my_handler(shell, etype, value, tb, tb_offset=None):
View
48 IPython/core/tests/test_oinspect.py
@@ -313,6 +313,54 @@ def getdoc(self):
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 = <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
+ 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
View
5 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
Something went wrong with that request. Please try again.