【问题标题】:Doctests fail with UnicodeDecodeError on C-extension and Python3Doctests 在 C 扩展和 Python3 上因 UnicodeDecodeError 而失败
【发布时间】:2014-08-05 05:18:08
【问题描述】:

我很难让我的测试框架同时适用于 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) 上得到了相同的结果,所以它并不特定于我的本地构建。

【问题讨论】:

  • 也许您需要为 Python 3 编译不同版本的 doctest
  • 对于 python3,我正在使用来自 std 库的 doctest 模块。有没有其他推荐的版本? doctest不是纯python模块吗,还需要编译吗?
  • 对不起,我以前从未使用过doctest,没有意识到它是一个内置模块。也许您的 PYTHONPATH 将第 2 版模块放在第 3 版之前?
  • 我的 PYTHONPATH 是空的...我完全依赖 sys.path 中的硬编码路径。此外,在第一个回溯中,您可以看到 doctest.py 文件位于 python 3.3 标准库位置,所以我认为这不是问题所在。感谢您的建议,请继续提供!

标签: python python-3.x doctest python-c-extension


【解决方案1】:

我找到了解决这个问题的方法,所以我会发布它,但我觉得它相当不令人满意。我仍在寻找更优雅/更少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 模块。为此,只需将 if 块修改为读取以添加对“.so”扩展名的检查

if filename.endswith(".py") or filename.endswith(".so"):
    ...

2) 获取doctest来识别C-extension模块中的函数

doctest.py 使用inspect.isfunction 函数来确定在递归搜索模块对象中的文档字符串时哪些对象是函数。这个函数的问题在于它只识别用 python 编写的函数,而不是用 C 编写的函数(python 将 C 扩展函数识别为内置函数)。所以,为了在模块递归时识别我们的函数,我们需要使用inspect.isbuiltin来代替。

为了纠正这个问题,我们需要在doctest.py 中找到DocTestFinder._find 方法并更改它查找函数的方式。我转换了

# 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

【讨论】:

  • 考虑到它不需要 太多 来让它工作,我想知道这是否是 python 开发人员可能感兴趣的增强......
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-04-17
  • 1970-01-01
  • 1970-01-01
  • 2018-07-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多