Doctests терпит неудачу с UnicodeDecodeError на C-расширение и Python3

Я испытываю трудности с тем, чтобы заставить мою среду тестирования работать для модуля C-extension для Python2 и Python3. Я люблю пробежаться по моим документам doctest чтобы убедиться, что я не передаю своим пользователям плохую информацию, поэтому я хочу запустить doctest как часть моего тестирования.

Я не верю, что источником моей проблемы являются сами строки документации, а скорее как doctest Модуль пытается прочитать мой модуль расширения. Если я бегу doctest с Python2 (в модуле, скомпилированном для 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 выполнить тесты модулей расширения C на python3?

Обновление: я должен также добавить, что я получаю идентичные результаты на Travis-CI ( см. Здесь), так что это не характерно для моей локальной сборки.

1 ответ

Я нашел обходной путь к этой проблеме, поэтому я опубликую его, но считаю его довольно неудовлетворительным. Я все еще ищу более изящные / менее хакерские решения для этого.


Есть три проблемы с 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-extension

doctest.py использует функцию inspect.isfunction, чтобы определить, какие объекты являются функциями, при рекурсивном поиске строк документации в объекте модуля. Проблема с этой функцией заключается в том, что она идентифицирует только функции, написанные на python, а не на C (python определяет функции расширения C как встроенные). Итак, чтобы идентифицировать наши функции при повторении через модуль, нам нужно использовать 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 в вашем текущем каталоге. Вы можете получить этот файл здесь, если хотите его использовать. Затем вы можете запустить тесты на модули расширения, как это

$ python2 patch_doctest.py
$ python2 -m doctest myext2.so
$ rm doctest.py
$ python3 patch_doctest.py
$ python3 -m doctest myext3.so

В качестве доказательства того, что это работает, вот новые результаты Travis-CI.

Другие вопросы по тегам