通过iPython和伪控制台运行doctests
我有一个比较基础的可测试文件:
class Foo():
"""
>>> 3+2
5
"""
if __name__ in ("__main__", "__console__"):
import doctest
doctest.testmod(verbose=True)
直接通过python运行时,它的表现是正常的。
但是在iPython中,我得到了
1 items had no tests:
__main__
0 tests in 1 items.
0 passed and 0 failed.
Test passed.
因为这是一个Django项目的一部分,需要访问manage.py设置的所有相关变量,所以我可以通过一个修改过的命令来运行它,这个命令使用了code.InteractiveConsole,结果之一是__name__
被设置为'__console__
'。
用上面的代码,我得到的结果和在iPython中是一样的。我尝试把最后一行改成这样:
this = __import__(__name__)
doctest.testmod(this, verbose=True)
结果我在__console__
上遇到了一个导入错误,这也算是可以理解的。这个错误对python和ipython都没有影响。
所以,我希望能通过这三种方法成功运行可测试,特别是InteractiveConsole那种,因为我预计很快就会需要Django的一些魔法功能。
为了更清楚,这就是我期待的结果:
Trying:
3+2
Expecting:
5
ok
1 items had no tests:
__main__
1 items passed all tests:
1 tests in __main__.Foo
1 tests in 2 items.
1 passed and 0 failed.
Test passed.
2 个回答
根本问题在于,ipython
对 __main__
做了一些奇怪的处理(通过它自己的 FakeModule
模块),导致在 doctest
检查这个“假模块”的 __dict__
时,Foo
并不存在——所以 doctest
不会深入到里面去。
这里有一个解决方案:
class Foo():
"""
>>> 3+2
5
"""
if __name__ in ("__main__", "__console__"):
import doctest, inspect, sys
m = sys.modules['__main__']
m.__test__ = dict((n,v) for (n,v) in globals().items()
if inspect.isclass(v))
doctest.testmod(verbose=True)
这样做确实能如你所愿地产生结果:
$ ipython dot.py
Trying:
3+2
Expecting:
5
ok
1 items had no tests:
__main__
1 items passed all tests:
1 tests in __main__.__test__.Foo
1 tests in 2 items.
1 passed and 0 failed.
Test passed.
Python 2.5.1 (r251:54863, Feb 6 2009, 19:02:12)
[[ snip snip ]]
In [1]:
仅仅设置全局的 __test__
是不够的,因为把它设置为你认为的 __main__
的全局变量,并不会真正放到通过 m = sys.modules['__main__']
恢复的实际对象的 __dict__
中,而后者正是 doctest
内部使用的表达式(实际上它使用的是 sys.modules.get
,但这里不需要额外的防范,因为我们知道 __main__
在 sys.modules
中存在……只是它并不是你想象中的那个对象!)。
另外,直接设置 m.__test__ = globals()
也不行,原因不同:doctest
会检查 __test__
中的值是否是字符串、函数、类或模块,如果不进行选择,你无法保证 globals()
会满足这个条件(实际上它不会)。在这里,我只选择了类,如果你还想要函数等,可以在 dict
调用中的生成表达式的 if
条件里使用 or
。
我不太清楚你是如何运行一个能够执行你脚本的 Django shell 的(因为我认为 python manage.py shell
不接受参数,你一定是在做其他事情,我也猜不出具体是什么!),但类似的方法应该会有帮助(无论你的 Django shell 是使用 ipython,还是在可用时使用的默认设置,或者是普通的 Python):在你通过 sys.modules['__main__']
获取的对象中适当地设置 __test__
(或者如果你传递给 doctest.testmod
的是 __console__
,我想这样做也应该有效),因为这模仿了 doctest
内部查找你的测试字符串的方式。
最后,关于设计、架构、简单性、透明性和“黑魔法”的一些哲学思考……:
所有这些努力基本上是为了对抗 ipython
(也许还有 Django,尽管它可能只是把这部分委托给了 ipython)为你“方便”所做的“黑魔法”……当两个框架(或者更多;-))各自独立地施展自己的“黑魔法”时,互操作性可能会突然变得非常困难,变得一点也不方便;-)。
我并不是说这些便利是可以在没有“黑魔法”、反射、假模块等情况下提供的;这些框架的设计者和维护者都是优秀的工程师,我相信他们已经做了充分的功课,并且只进行了交付他们认为需要的用户便利所必需的最小量的“黑魔法”。然而,即使在这种情况下,一旦你想做一些框架作者没有考虑到的事情,“黑魔法”就会从便利的梦想变成调试的噩梦。
好吧,也许在这个案例中不算噩梦,但我注意到这个问题已经开放了一段时间,即使有悬赏也没有得到很多答案——不过你现在确实有两个答案可以选择,我的答案使用了 doctest
的 __test__
特殊功能,@codeape 的答案使用了 ironpython 的特殊 __IP.magic_run
功能。我更喜欢我的答案,因为它不依赖于任何内部或未记录的内容——__test__
是 doctest
的一个文档功能,而 __IP
那两个前导下划线让我觉得“深层内部,别碰”;-)……如果在下一个版本发布时它坏了我一点也不会惊讶。不过,这也是个人喜好——那个答案可以说是更“方便”。
但是,这正是我的观点:便利可能会以放弃简单性、透明性和/或避免内部/未记录/不稳定特性为代价;因此,作为我们所有人的教训,尽可能少用“黑魔法”等(即使在某些地方放弃一点便利),从长远来看我们都会更快乐(而且我们也会让未来需要利用我们当前努力的其他开发者更快乐)。
下面的代码可以正常工作:
$ ipython
...
In [1]: %run file.py
Trying:
3+2
Expecting:
5
ok
1 items had no tests:
__main__
1 items passed all tests:
1 tests in __main__.Foo
1 tests in 2 items.
1 passed and 0 failed.
Test passed.
In [2]:
我不知道为什么 ipython file.py
不能正常运行。不过上面的办法至少可以解决问题。
编辑:
我找到了它不工作的原因,其实很简单:
- 如果你在
doctest.testmod()
中没有指定要测试的模块,它会默认认为你想测试__main__
模块。 - 当 IPython 执行你在命令行中传给它的文件时,
__main__
模块是 IPython 自己的__main__
,而不是你的模块。所以 doctest 会尝试在 IPython 的入口脚本中执行 doctests。
下面的代码可以工作,但感觉有点奇怪:
if __name__ == '__main__':
import doctest
import the_current_module
doctest.testmod(the_current_module)
所以基本上这个模块是自我导入的(这就是“感觉有点奇怪”的部分)。不过它确实有效。我不太喜欢这种方法的地方是,每个模块都需要在代码中包含自己的名字。
编辑 2:
下面的脚本 ipython_doctest
可以让 IPython 按你想要的方式运行:
#! /usr/bin/env bash
echo "__IP.magic_run(\"$1\")" > __ipython_run.py
ipython __ipython_run.py
这个脚本会创建一个 Python 脚本,在 IPython 中执行 %run argname
。
示例:
$ ./ipython_doctest file.py
Trying:
3+2
Expecting:
5
ok
1 items had no tests:
__main__
1 items passed all tests:
1 tests in __main__.Foo
1 tests in 2 items.
1 passed and 0 failed.
Test passed.
Python 2.5 (r25:51908, Mar 7 2008, 03:27:42)
Type "copyright", "credits" or "license" for more information.
IPython 0.9.1 -- An enhanced Interactive Python.
? -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help -> Python's own help system.
object? -> Details about 'object'. ?object also works, ?? prints more.
In [1]: