Doctests and decorated methods don't get along

Discussion in 'Python' started by Steven D'Aprano, Feb 6, 2010.

  1. It seems that doctest doesn't discover or execute doctests in methods
    which have been decorated.


    Here is my test file:

    # ======
    class MyStaticMethod(object):
    """Emulate built-in staticmethod descriptor."""
    def __init__(self, f):
    self.f = f
    def __get__(self, obj, objtype=None):
    return self.f

    class Test(object):
    def method1(self):
    """Doc string

    >>> print 'x'

    x
    """

    @staticmethod
    def method2():
    """Doc string

    >>> print 'y'

    y
    """

    @MyStaticMethod
    def method3():
    """Doc string

    >>> print '***' # This test should fail.

    z
    """

    if __name__ == '__main__':
    import doctest
    doctest.testmod(verbose=True)
    # ======


    and here is the output using Python 2.6.4:

    [steve@sylar python]$ python2.6 test_doctests.py
    Trying:
    print 'x'
    Expecting:
    x
    ok
    Trying:
    print 'y'
    Expecting:
    y
    ok
    5 items had no tests:
    __main__
    __main__.MyStaticMethod
    __main__.MyStaticMethod.__get__
    __main__.MyStaticMethod.__init__
    __main__.Test
    2 items passed all tests:
    1 tests in __main__.Test.method1
    1 tests in __main__.Test.method2
    2 tests in 7 items.
    2 passed and 0 failed.
    Test passed.


    It doesn't even see method3, let alone run the tests.


    This looks like bug to me. Have I missed anything?

    Any work arounds?



    --
    Steven
     
    Steven D'Aprano, Feb 6, 2010
    #1
    1. Advertising

  2. Steven D'Aprano

    John Posner Guest

    On 2/6/2010 6:48 AM, Steven D'Aprano wrote:
    > class MyStaticMethod(object):
    > """Emulate built-in staticmethod descriptor."""
    > def __init__(self, f):
    > self.f = f
    > def __get__(self, obj, objtype=None):
    > return self.f


    How about using a function, instead of a class, to implement the decorator:

    import functools
    def MyStaticMethod(f):
    @functools.wraps(f)
    def _wrapper(*args, **kwargs):
    return f(*args, **kwargs)
    return _wrapper


    For me, this produces the hoped-for doctest failure in
    __main__.Test.method3.

    I tried making the class-based implementation work by using
    functools.wraps(), but I couldn't manage it. Also, doesn't the decorator
    protocol want the class to implement a __call__() method, not a
    __get__() method? Or am I getting hung up (not the first time) at the
    nexus of the descriptor and decorator protocols?

    -John
     
    John Posner, Feb 6, 2010
    #2
    1. Advertising

  3. Steven D'Aprano

    Terry Reedy Guest

    On 2/6/2010 6:48 AM, Steven D'Aprano wrote:
    > It seems that doctest doesn't discover or execute doctests in methods
    > which have been decorated.
    >
    >
    > Here is my test file:
    >
    > # ======
    > class MyStaticMethod(object):
    > """Emulate built-in staticmethod descriptor."""
    > def __init__(self, f):
    > self.f = f
    > def __get__(self, obj, objtype=None):
    > return self.f
    >
    > class Test(object):
    > def method1(self):
    > """Doc string
    >
    > >>> print 'x'

    > x
    > """
    >
    > @staticmethod
    > def method2():
    > """Doc string
    >
    > >>> print 'y'

    > y
    > """
    >
    > @MyStaticMethod
    > def method3():
    > """Doc string
    >
    > >>> print '***' # This test should fail.

    > z
    > """
    >
    > if __name__ == '__main__':
    > import doctest
    > doctest.testmod(verbose=True)
    > # ======
    >
    >
    > and here is the output using Python 2.6.4:
    >
    > [steve@sylar python]$ python2.6 test_doctests.py
    > Trying:
    > print 'x'
    > Expecting:
    > x
    > ok
    > Trying:
    > print 'y'
    > Expecting:
    > y
    > ok
    > 5 items had no tests:
    > __main__
    > __main__.MyStaticMethod
    > __main__.MyStaticMethod.__get__
    > __main__.MyStaticMethod.__init__
    > __main__.Test
    > 2 items passed all tests:
    > 1 tests in __main__.Test.method1
    > 1 tests in __main__.Test.method2
    > 2 tests in 7 items.
    > 2 passed and 0 failed.
    > Test passed.
    >
    >
    > It doesn't even see method3, let alone run the tests.


    Method3 is wrapped as an instance of MyStaticMethod and doctest does not
    see that as function. Even if it did, you would might have to copy the
    docstring (self.__doc__ = f.__doc__) to have the test run.

    Note the following: (3.1)

    >>> Test.method3

    <function method3 at 0x00F5F7C8>
    >>> Test.__dict__['method3']

    <__main__.MyStaticMethod object at 0x00F5CDF0>
    >>> Test.__dict__['method2']

    <staticmethod object at 0x00F5C290>

    Doctest has to scan the dict, so it does not see the attribute-lookup
    result.

    > This looks like bug to me. Have I missed anything?


    I would call it a limitation as I do not see the doc as promising
    otherwise. I believe doctest predates the descripter protocol, at least
    in its current form, so it may not be completely descriptor-aware. You
    certainly could file a feature request for improved recognition and
    subsequent calling of __get__. An initial patch would improve chances of
    action.

    > Any workarounds?


    Perhaps look at the doctest source to see why staticmethod instances are
    recognized as functions.

    If doctest directly special-cases their recognition, then use the
    standard nested function approach for decorators, making sure to copy
    the doc string. Look at functools.wraps.

    If doctest recognizes them by some characteristic that you have not
    emulated, try to improve that.

    Terry Jan Reedy


    Terry Jan Reedy
     
    Terry Reedy, Feb 6, 2010
    #3
  4. On Sat, 06 Feb 2010 14:00:28 -0500, John Posner wrote:

    > On 2/6/2010 6:48 AM, Steven D'Aprano wrote:
    >> class MyStaticMethod(object):
    >> """Emulate built-in staticmethod descriptor."""
    >> def __init__(self, f):
    >> self.f = f
    >> def __get__(self, obj, objtype=None):
    >> return self.f

    >
    > How about using a function, instead of a class, to implement the
    > decorator:


    I've had problems with function decorators and doctest in the past. If I
    remember the problem correctly, functools.wraps changing the __module__
    of the wrapped function, making it look like it came from the functool
    module, and so doctest would not run the tests. But it seems that has
    been fixed in 2.6: I can confirm your version works as expected.

    Nevertheless, I think it is a limitation of doctest that it doesn't see
    methods using the descriptor protocol. Even if I could work around my
    immediate issue by using a function decorator (and I don't think I can)
    I'd still want to fix this problem.


    > I tried making the class-based implementation work by using
    > functools.wraps(), but I couldn't manage it.


    Me neither. I think the problem is that the doctest.DocTestFinder class
    doesn't find descriptor-based methods. It special cases staticmethod,
    classmethod and property.



    > Also, doesn't the decorator
    > protocol want the class to implement a __call__() method, not a
    > __get__() method? Or am I getting hung up (not the first time) at the
    > nexus of the descriptor and decorator protocols?


    In this case, I'm actually using a descriptor as the decorator.

    The descriptor is called at attribute lookup, before the method is
    called. __get__ returns a function, which has a __call__ method, and it
    should all just work.



    --
    Steven
     
    Steven D'Aprano, Feb 6, 2010
    #4
  5. On Sat, 06 Feb 2010 14:39:00 -0500, Terry Reedy wrote:

    > On 2/6/2010 6:48 AM, Steven D'Aprano wrote:
    >> It seems that doctest doesn't discover or execute doctests in methods
    >> which have been decorated.


    [...]

    > Doctest has to scan the dict, so it does not see the attribute-lookup
    > result.


    Ahaha, now we're getting somewhere. If I do this:

    >>> import inspect
    >>> import test_doctests
    >>> method3 = test_doctests.Test.__dict__['method3']
    >>> inspect.isfunction(method3)

    False
    >>> inspect.ismethod(method3)

    False

    But:

    >>> inspect.ismethoddescriptor(method3)

    True

    which is what I want to see. Presumably doctest.DocTestFinder needs a
    patch to call methoddescriptor's __get__ method.

    (BTW, classmethod, staticmethod and property are all special-cased by
    DocTestFinder.)



    --
    Steven
     
    Steven D'Aprano, Feb 6, 2010
    #5
  6. On Sat, 06 Feb 2010 14:39:00 -0500, Terry Reedy wrote:

    > On 2/6/2010 6:48 AM, Steven D'Aprano wrote:
    >> It seems that doctest doesn't discover or execute doctests in methods
    >> which have been decorated [by method descriptors].


    [...]

    >> This looks like bug to me. Have I missed anything?

    >
    > I would call it a limitation as I do not see the doc as promising
    > otherwise. I believe doctest predates the descripter protocol, at least
    > in its current form, so it may not be completely descriptor-aware. You
    > certainly could file a feature request for improved recognition and
    > subsequent calling of __get__. An initial patch would improve chances of
    > action.


    I have found an existing report for this issue:

    http://bugs.python.org/issue4037

    The original poster's suggested fix was on the right track, but not
    complete. I've added a patch which works according to my tests.

    Since this report is currently unassigned, is there a protocol for
    raising its profile and having somebody check my patch for inclusion?
    Should I just send an email to python-dev?


    --
    Steven
     
    Steven D'Aprano, Feb 7, 2010
    #6
  7. Steven D'Aprano

    Terry Reedy Guest

    On 2/6/2010 7:51 PM, Steven D'Aprano wrote:

    > I have found an existing report for this issue:
    >
    > http://bugs.python.org/issue4037
    >
    > The original poster's suggested fix was on the right track, but not
    > complete. I've added a patch which works according to my tests.
    >
    > Since this report is currently unassigned, is there a protocol for
    > raising its profile and having somebody check my patch for inclusion?
    > Should I just send an email to python-dev?


    At one time, I believe this would have been assigned to Tim Peters, but
    he seems not to be very active in Python development currently. Given
    that the patch is fairly trivial and the module possible orphaned, and
    that you do not regularly bug python-dev, I suspect it would be ok to
    enquire, once you have a complete patch (with testcase). The latter
    would let a committer/reviewer quickly check that it does what you
    claim. You might be asked to review some other issues in exchange. (Or
    the OP of this thread or the tracker item might help ;-).

    Terry Jan Reedy
     
    Terry Reedy, Feb 7, 2010
    #7
  8. Am 06.02.10 12:48, schrieb Steven D'Aprano:
    > It seems that doctest doesn't discover or execute doctests in methods
    > which have been decorated.
    >
    >
    > Here is my test file:
    >
    > # ======
    > class MyStaticMethod(object):
    > """Emulate built-in staticmethod descriptor."""
    > def __init__(self, f):
    > self.f = f
    > def __get__(self, obj, objtype=None):
    > return self.f
    >
    > class Test(object):
    > def method1(self):
    > """Doc string
    >
    > >>> print 'x'

    > x
    > """
    >
    > @staticmethod
    > def method2():
    > """Doc string
    >
    > >>> print 'y'

    > y
    > """
    >
    > @MyStaticMethod
    > def method3():
    > """Doc string
    >
    > >>> print '***' # This test should fail.

    > z
    > """
    >
    > if __name__ == '__main__':
    > import doctest
    > doctest.testmod(verbose=True)
    > # ======
    >
    >
    > and here is the output using Python 2.6.4:
    >
    > [steve@sylar python]$ python2.6 test_doctests.py
    > Trying:
    > print 'x'
    > Expecting:
    > x
    > ok
    > Trying:
    > print 'y'
    > Expecting:
    > y
    > ok
    > 5 items had no tests:
    > __main__
    > __main__.MyStaticMethod
    > __main__.MyStaticMethod.__get__
    > __main__.MyStaticMethod.__init__
    > __main__.Test
    > 2 items passed all tests:
    > 1 tests in __main__.Test.method1
    > 1 tests in __main__.Test.method2
    > 2 tests in 7 items.
    > 2 passed and 0 failed.
    > Test passed.
    >
    >
    > It doesn't even see method3, let alone run the tests.
    >
    >
    > This looks like bug to me. Have I missed anything?
    >
    > Any work arounds?



    Use functools.wraps to preserve some essential parts of the function
    declaration.

    def my_static(f):
    @wraps(f)
    def _d(*args, **kwargs):
    return f(*args, **kwargs)
    return _d


    Diez
     
    Diez B. Roggisch, Feb 9, 2010
    #8
    1. Advertising

Want to reply to this thread or ask your own question?

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. Replies:
    1
    Views:
    1,897
    Roedy Green
    Sep 17, 2005
  2. Wolfgang Draxinger

    What are decorated functions?

    Wolfgang Draxinger, Aug 22, 2006, in forum: Python
    Replies:
    4
    Views:
    349
    Richard Jones
    Aug 23, 2006
  3. Zaza
    Replies:
    0
    Views:
    656
  4. Lorenzo

    Stored Query and DATASETs don't get along ?

    Lorenzo, Jul 6, 2005, in forum: ASP .Net Datagrid Control
    Replies:
    2
    Views:
    154
    Lorenzo
    Jul 14, 2005
  5. Roy Smith

    PIL and requests don't get along

    Roy Smith, Oct 23, 2012, in forum: Python
    Replies:
    3
    Views:
    232
    Roy Smith
    Oct 24, 2012
Loading...

Share This Page