Doctests and decorated methods don't get along

  • Thread starter Steven D'Aprano
  • Start date
S

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
x
"""

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

@MyStaticMethod
def method3():
"""Doc string
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?
 
J

John Posner

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
 
T

Terry Reedy

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
x
"""

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

@MyStaticMethod
def method3():
"""Doc string
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
>>> Test.__dict__['method3']
>>> 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
 
S

Steven D'Aprano

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.
 
S

Steven D'Aprano

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:
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.)
 
S

Steven D'Aprano

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?
 
T

Terry Reedy

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
 
D

Diez B. Roggisch

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
x
"""

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

@MyStaticMethod
def method3():
"""Doc string
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
 

Ask a Question

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

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,576
Members
45,054
Latest member
LucyCarper

Latest Threads

Top