诗歌 + Cython + 测试 (Nosetests)

3
我使用Poetry来构建包含Cython扩展的软件包。现在,我想为其编写测试(最好使用nosetest)。问题是我需要预编译二进制文件,通常使用setup.py build_clib build_ext --inplace完成。
对我而言,最好的解决方案是在不创建额外的.py.sh文件的情况下运行测试,因为我已经有了build.py。在虚拟环境中安装软件包后运行测试也可以,就像在readthedocs服务器上实现的那样。
我还熟悉taskipy,因此在我的pyproject.toml中使用一些bash命令也可以。欢迎使用与pyproject.toml兼容的任何其他软件包。
也许有任何钩子适用于Poetry,因为它在创建.whl分布文件时进行Cython化和编译。
非常感谢您的任何帮助。
更新:Tox似乎是一个合适的工具,但它无法在目录中查看pyproject.toml。非常欢迎提供使用Tox和包含Cython的软件包或教程的存储库链接。
2个回答

2
如果扩展程序是分发的一部分,除了运行poetry install之外,您不需要做任何事情-poetry将在可编辑安装项目的过程中就地构建扩展程序。
在其他情况下,您可以将调用distutils命令嵌入到测试中,作为套件设置/拆卸的一部分。我对nose不是很熟悉,但这里有一个简单的示例。想象一下,我有一个fib.pyx(这是来自Cython书籍的示例):
def fib(long n):
    '''Returns the nth Fibonacci number.'''
    cdef long a=0, b=1, i
    for i in range(n):
        a, b = a + b, a
    return a

一个test_fib.py模块,它构建fib库并在测试成功后删除它:

from distutils.dist import Distribution
from distutils.core import Extension
from pathlib import Path
from Cython.Build import cythonize


fib_source = Path('fib.pyx')

# distutils magic. This is essentially the same as calling
# python setup.py build_ext --inplace
dist = Distribution(attrs={'ext_modules': cythonize(fib_source.name)})
build_ext_cmd = dist.get_command_obj('build_ext')
build_ext_cmd.ensure_finalized()
build_ext_cmd.inplace = 1
build_ext_cmd.run()

fib_obj = Path(build_ext_cmd.get_ext_fullpath(fib_source.stem))

# the lib was built, so the import will succeed now
from fib import fib


def teardown_module():
    # remove built library
    fib_obj.unlink()

    # if you also want to clean the build dir:
    from distutils.dir_util import remove_tree
    remove_tree(build_ext_cmd.build_lib)
    remove_tree(build_ext_cmd.build_temp)


# sample tests

def test_zero():
    assert fib(0) == 0


def test_ten():
    assert fib(10) == 55

您可能正在定制自定义 build.py 中的 setup_kwargs。要重用此代码,请调整 dist 初始化,例如:

from build import build

setup_kwargs = {}
build(setup_kwargs)
dist = Distribution(attrs=setup_kwargs)
...

pytest示例

使用pytest可以更方便地组织测试,创建名为conftest.py的文件,并将设置/拆卸代码提取到钩子中:

# conftest.py

from distutils.core import Extension
from distutils.dist import Distribution
from distutils.dir_util import remove_tree
from pathlib import Path
from Cython.Build import cythonize


def pytest_sessionstart(session):
    fib_source = Path('fib.pyx')
    dist = Distribution(attrs={'ext_modules': cythonize(fib_source.name)})
    build_ext_cmd = dist.get_command_obj('build_ext')
    build_ext_cmd.ensure_finalized()
    build_ext_cmd.inplace = 1
    build_ext_cmd.run()
    session.fib_obj = Path(build_ext_cmd.get_ext_fullpath(fib_source.stem))


def pytest_sessionfinish(session):
    session.fib_obj.unlink()

现在测试变得更加清晰,设置代码仅会在整个测试会话中运行一次。以上测试示例已更新:

from fib import fib


def test_zero():
    assert fib(0) == 0


def test_ten():
    assert fib(10) == 55

很棒的解决方案,使用pytest一切正常。Nosetests具有相同的功能,当我在__init__.py中使用setup_module函数而不是在conftest.py中使用pytest_sessionstart时,它也可以构建二进制文件,但无法导入并出现访问被拒绝的错误。你有什么想法可能是错的吗? - Ivan Mishalkin
我怀疑 setup_module 执行得太晚了(或者如果您愿意,导入过早)。 我猜测 nose 在收集时会导入测试模块,因此如果在测试模块级别上有 import fib,则将执行导入。 仅在此之后才会执行固定装置(顺便说一下,pytest也是这样做的-这就是为什么我将init代码放在钩子中而不是在固定装置中的原因)。 - hoefling
解决方法可能包括:在模块级别上执行 setup_module(在 __init__.py 中显式调用 setup_module()),或者将 setup_module 中的代码提取出来在模块级别上运行(如我在答案中的示例)。或者另一种方式:延迟库导入,将它们从模块级别移动到测试内部。但是,这些方法都不太优雅。 - hoefling
我可能是错的 - 如果这不是问题,你能把错误追踪添加到问题中吗? - hoefling

0
以下是已接受答案(pytest示例)的轻微修改,现在也适用于将源代码和测试代码分开放置在不同目录中的情况。
例如:
.
|_ src/
    |_ your_package/
        |_ some_cython_module.pyx
|_ test/
    |_ conftest.py
    |_ some_cython_module_test.py

# conftest.py

from distutils.dist import Distribution
from pathlib import Path

from Cython.Build import cythonize


def pytest_sessionstart(session):
    difflib_source = Path('src/your_package/some_cython_module.pyx')
    dist = Distribution(attrs={'ext_modules': cythonize(str(difflib_source))})
    build_ext_cmd = dist.get_command_obj('build_ext')
    build_ext_cmd.ensure_finalized()
    build_ext_cmd.inplace = 1
    build_ext_cmd.run()
    session.fib_obj = Path(build_ext_cmd.get_ext_fullpath(
        str(difflib_source.parents[0]) + '/' + difflib_source.stem
    ))


def pytest_sessionfinish(session):
    session.fib_obj.unlink()


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接