通过iPython和伪控制台运行doctests

2 投票
2 回答
2373 浏览
提问于 2025-04-15 13:53

我有一个比较基础的可测试文件:

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 个回答

8

根本问题在于,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 那两个前导下划线让我觉得“深层内部,别碰”;-)……如果在下一个版本发布时它坏了我一点也不会惊讶。不过,这也是个人喜好——那个答案可以说是更“方便”。

但是,这正是我的观点:便利可能会以放弃简单性、透明性和/或避免内部/未记录/不稳定特性为代价;因此,作为我们所有人的教训,尽可能少用“黑魔法”等(即使在某些地方放弃一点便利),从长远来看我们都会更快乐(而且我们也会让未来需要利用我们当前努力的其他开发者更快乐)。

2

下面的代码可以正常工作:

$ 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]:

撰写回答