我很难让我的测试框架适用于Python2和Python3的C扩展模块。我喜欢通过doctest
运行我的文档字符串,以确保我没有向用户提供错误的信息,所以我想在我的测试中运行doctest
。
我不相信我的问题的根源是文档字符串本身,而是doctest
模块试图读取我的扩展模块的方式。如果我用Python2运行doctest
(在针对Python2编译的模块上),我得到了我期望的输出:
$ python -m doctest myext.so -v
...
1 items passed all tests:
98 tests in myext.so
98 tests in 1 items.
98 passed and 0 failed.
Test passed.
然而,当我使用Python3时,我得到UnicodeDecodeError
:
$ python3 -m doctest myext3.so -v
Traceback (most recent call last):
...
File "/usr/local/Cellar/python3/3.3.3/Frameworks/Python.framework/Versions/3.3/lib/python3.3/doctest.py", line 223, in _load_testfile
return f.read(), filename
File "/usr/local/Cellar/python3/3.3.3/Frameworks/Python.framework/Versions/3.3/lib/python3.3/codecs.py", line 301, in decode
(result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xcf in position 0: invalid continuation byte
为了获得更多信息,我通过pytest
运行了完整的追溯:
$ python3 -m pytest --doctest-glob "*.so" --full-trace
...
self = <encodings.utf_8.IncrementalDecoder object at 0x102ff5110>
input = b'\xcf\xfa\xed\xfe\x07\x00\x00\x01\x03\x00\x00\x00\x08\x00\x00\x00\r\x00\x00\x00\xd0\x05\x00\x00\x85\x00\x00\x00\x00\x...edString\x00_PyUnicode_FromString\x00_Py_BuildValue\x00__Py_FalseStruct\x00__Py_TrueStruct\x00dyld_stub_binder\x00\x00'
final = True
def decode(self, input, final=False):
# decode input (taking the buffer into account)
data = self.buffer + input
> (result, consumed) = self._buffer_decode(data, self.errors, final)
E UnicodeDecodeError: 'utf-8' codec can't decode byte 0xcf in position 0: invalid continuation byte
/usr/local/Cellar/python3/3.3.3/Frameworks/Python.framework/Versions/3.3/lib/python3.3/codecs.py:301: UnicodeDecodeError
看起来doctest
实际上正在阅读 .so
文件以获取文档字符串(而不是导入模块),但Python3不知道如何解码输入。我可以通过尝试自己读取.so
文件来复制字节字符串和回溯来确认这一点:
$ python3
Python 3.3.3 (default, Dec 10 2013, 20:13:18)
[GCC 4.2.1 Compatible Apple LLVM 5.0 (clang-500.2.79)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> open('myext3.so').read()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/Cellar/python3/3.3.3/Frameworks/Python.framework/Versions/3.3/lib/python3.3/codecs.py", line 301, in decode
(result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xcf in position 0: invalid continuation byte
>>> open('myext3.so', 'rb').read()
b'\xcf\xfa\xed\xfe\x07\x00\x00\x01\x03\x00\x00\x00\x08\x00\x00\x00\r\x00\x00\x00\xd0\x05...'
之前是否有其他人遇到此问题?是否有标准(或不那么标准)的方法让doctest
在python3上对C扩展模块执行测试?
更新:我还应该补充一点,我在Travis-CI(see here)上得到相同的结果,所以它并不特定于我的本地构建。
答案 0 :(得分:2)
我找到了解决这个问题的方法,所以我会发布它,但我发现它相当不满意。我仍然在寻找更优雅/更少hacky的解决方案。
doctest.py
存在三个问题需要克服才能使其发挥作用:
1)获取doctest以将.so文件视为python模块。
如果查看doctest.py
源代码,您会在测试运行器中注意到一个与此类似的块(取决于您运行的python版本):
if filename.endswith(".py"):
# It is a module -- insert its dir into sys.path and try to
# import it. If it is part of a package, that possibly
# won't work because of package imports.
dirname, filename = os.path.split(filename)
sys.path.insert(0, dirname)
m = __import__(filename[:-3])
del sys.path[0]
failures, _ = testmod(m)
else:
failures, _ = testfile(filename, module_relative=False)
这里发生的是doctest.py
正在检查“.py”扩展名,如果是,则将文件作为python模块加载,否则将文件读取为文本(如README文件) .rst可能是)。我们需要让doctest.py
确认扩展名为“.so”的文件是python模块。要执行此操作,只需添加对“.so”扩展名的检查,方法是修改此if
块以进行读取
if filename.endswith(".py") or filename.endswith(".so"):
...
2)获取doctest以识别C扩展模块中的功能
doctest.py
使用inspect.isfunction函数来确定在递归搜索模块对象中的文档字符串时哪些对象是函数。这个函数的问题是它只识别用python编写的函数,而不是C语言(python将C-extension函数识别为内置函数)。因此,为了在递归模块时识别我们的函数,我们需要使用inspect.isbuiltin代替。
要解决此问题,我们需要在DocTestFinder._find
中找到doctest.py
方法并更改其查找函数的方式。我转换了
# Recurse to functions & classes.
if ((inspect.isfunction(val) or inspect.isclass(val)) and
self._from_module(module, val)):
self._find(tests, val, valname, module, source_lines,
globs, seen)
到
# Recurse to functions & classes.
if ((inspect.isbuiltin(val) or inspect.isclass(val)) and
self._from_module(module, val)):
self._find(tests, val, valname, module, source_lines,
globs, seen)
3)正确删除.so文件中的版本标记(仅限Python3)。
在Python3上,可以使用版本标识符标记C扩展名(即“myext.cpython-3mu.so”,请参阅PEP 3149)。我们需要知道在doctest.py
测试运行器中进行初始导入时如何删除它。
为此,我转换了行
m = __import__(filename[:-3])
到
from sysconfig import get_config_var
m = __import__(filename[:-3] if filename.endswith(".py") else filename.replace(get_config_var("EXT_SUFFIX"), ""))
这仅适用于Python3。
进行这些修改后,我可以让doctest在Python2和Python3上按预期工作。由于这些修改相当烦人,我已经创建了一个patch_doctest.py
脚本自动执行此操作并将修补后的doctest.py
放在当前目录中。如果要使用此文件,可以获取此文件here。然后,您可以像这样
$ python2 patch_doctest.py
$ python2 -m doctest myext2.so
$ rm doctest.py
$ python3 patch_doctest.py
$ python3 -m doctest myext3.so
作为证据证明这是有效的,here are the new Travis-CI results。