【问题标题】:Using py.test with compiled library code将 py.test 与已编译的库代码一起使用
【发布时间】:2016-05-18 18:20:15
【问题描述】:

我有一个具有以下存储库结构的 python 库:

repobase
 |- mylibrary
 |  |- __init__.py
 |- tests
    |- test_mylibrary.py

到目前为止,只需在 repobase 目录中调用 py.test 即可运行测试。 test_mylibrary.py 中的 import mylibrary 然后使用 repobase/mylibrary 中的本地代码。

现在,我已扩展库以使用已编译的代码。因此,repobase/mylibrary 中的源代码本身不能正常工作。我必须做一个setup.py build。这将创建 repobase/build/lib.linux-x86_64-2.7/mylibrary

有没有合理的方法让 py.test 使用这个目录来导入 mylibrary?鉴于这些限制:

  1. 我不想在 test_mylibrary.py 中包含任何 sys.path / import 魔法,因为这可能会破坏其他环境中的测试。

  2. 我不想放弃从 repobase 运行 py.test 的可能性。因此修改 PYTHONPATH 没有帮助,因为 . 仍将是 sys.path 中的第一个。因此 repobase/mylibrary 会比 repobase/build/lib.linux-x86_64-2.7/mylibrary 更受青睐。

如果不是,测试需要构建的 python 库的标准方法是什么?

【问题讨论】:

  • 不清楚您所说的“...我已经扩展库以使用编译代码...”是什么意思,即编译版本是否提供与 Python 版本相同的接口,或者确实Python 版本import 编译后的版本?如果是前者,那么您实际上是在测试两种不同的东西,所以即使测试套件相同,它们也应该有不同的名称,q.v. Python 的 picklecPickle。如果是后者,那么它们肯定应该有不同的名称。一个常见的 Python 习惯用法是在编译部分前加下划线 q.v。 Python 的 socket_socket
  • (续)无论哪种方式,让两个不同的模块实现共享相同的名称都是自找麻烦。即使您的解决方案适用于今天所有可能的运行时案例,您也无法预测所有可能的未来运行时案例,您最终可能会在没有意识到的情况下导入或测试错误的版本。
  • @Aya 我只有一个实现。编译后的代码替换一些以前的 python 代码。正如我所写的“因此,repobase/mylibrary 的源代码本身不能正常工作。”。我需要编译后的代码才能运行测试。
  • 所以如果编译后的代码应该替换 Python 代码,那么你能不能不直接从存储库中删除 Python 代码?如果答案是“否”,因为它只替换了“部分”功能,那么它不是真正的替换,而是扩展。
  • python代码被淘汰。说,我有一个函数do_something(args),它是用python实现的。现在,python 实现被对一些编译的 c 代码的内部调用所取代。 do_something 的 API 没有改变,我仍然想执行相同的测试。在测试之前,只需运行来自repobase/mylibrary 的原始代码。更改后,这是不可能的,因为原始 python 代码本身不再起作用。相反,我必须先setup.py build,然后使用build 子目录中的代码。问题是如何让 py.test 使用该代码。

标签: python build pytest


【解决方案1】:

我认为您的问题只是 py.test 没有将构建的共享对象复制到存储库的根目录。

我刚刚尝试在使用 py.test 测试 C 扩展时直接从 Python wiki 运行 UT,如下所示:

python setup.py build
py.test test/examp_unittest.py

AssertionError: No module named examp 失败。

但是,当我按照 wiki 进行操作时(并改为运行 python setup.py test),我注意到它会将 .so 复制到根目录(注意它开始运行测试之前的最后一行):

running test
running egg_info
writing examp.egg-info/PKG-INFO
writing top-level names to examp.egg-info/top_level.txt
writing dependency_links to examp.egg-info/dependency_links.txt
reading manifest file 'examp.egg-info/SOURCES.txt'
writing manifest file 'examp.egg-info/SOURCES.txt'
running build_ext
copying build/lib.linux-x86_64-2.6/examp.so ->
runTest (test.examp_unittest.DeviceTest) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

在我的系统上运行之后,我现在可以在相同的代码库上非常愉快地运行 py.test - 如下所示。

============================= test session starts ==============================
platform linux2 -- Python 2.7.3, pytest-2.9.2, py-1.4.31, pluggy-0.3.1
rootdir: /tmp/sotest, inifile: 
collected 1 items 

test/examp_unittest.py .

=========================== 1 passed in 0.01 seconds ===========================

因此,解决方案是将共享对象复制到存储库的根目录。

为了确保我从头开始运行整个程序,只需构建扩展、复制共享对象然后运行 ​​py.test。这一切都按预期工作。

【讨论】:

  • 我真的不能说,因为我还没有时间尝试(明天回家)。我从你的例子中得出结论,我必须运行python setup.py test。但这将首先运行标准单元测试(这是过时的,因为它不会发现和/或正确处理我的所有测试)。只是需要额外的时间。也许那时我应该将 pytest 与 setuptools (pytest.org/latest/…) 集成。
  • 每次都必须运行所有测试仍然感觉有些不对劲。我经常想更改一些代码,然后构建(因为我必须为已编译的东西)然后运行单个测试,例如py.test tests/test_mylibrary.py::test_do_something。也许我必须适应python setup.py build 或制作一个自定义python setup.py build_test,它只复制所需的.so 文件。我真的很想知道为什么我在那个方向上找不到任何东西,因为我认为这是您在使用 py.test 和编译代码时遇到的标准问题。
  • @TimHoffmann 您不需要每次都运行所有测试。只需运行构建并自己复制文件。如果你和我一样不喜欢打字,你可以把它放在一个 bash 脚本中......
  • @TimHoffmann 再想一想...如果您的共享对象与包名冲突也有问题,请改用内部名称。例如,许多项目使用_mylibrary 作为mylibrary 的C 扩展。
【解决方案2】:

从聊天中的讨论来看,C 实现似乎只提供了 Python 实现功能的一个子集。

一种常见的解决方案是拆分模块,使得需要优化实现的部分存在于单独的模块中。

考虑一个需要在不同图像格式之间转换的库的更具体示例。

让我们假设你的布局看起来像这样......

repobase
 |- image
 |  |- __init__.py
 |  |- pyJPEG.py
 |- build
 |  |- lib.linux-x86_64-2.7
 |     |- cJPEG.so
 |- tests
    |- test_image.py

...您的PYTHONPATH 包括/path/to/repobase:/path/to/repobase/build/lib.linux-x86_64-2.7,您的cJPEG.so 导出符号jpeg_decompressjpeg_compress,您的文件看起来像这样...

图像/__init__.py

# Load the C implementation if we have it, otherwise fall back to
# a pure Python implementation
try:
    from cJPEG import jpeg_decompress, jpeg_compress
except ImportError:
    from pyJPEG import jpeg_decompress, jpeg_compress

def load_image(filename):
    data = open(filename, 'rb').read()
    if filename.endswidth('.jpg'):
        return jpeg_decompress(data)
    else:
        raise NotImplementedError

def save_image(data, filename, filetype='JPEG'):
    if filetype == 'JPEG':
        data = jpeg_compress(data)
    else:
        raise NotImplementedError
    open(filename, 'wb').write(data)

image/pyJPEG.py

def jpeg_decompress(data):
    # A pure Python implementation of a JPEG decoder

def jpeg_compress(data):
    # A pure Python implementation of a JPEG encoder

使用这种布局,测试套件无需关心库是否已构建 - 您可以在两种情况下使用相同的套件,cJPEG.so 的存在(或不存在)将决定测试哪个版本.

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-06-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-15
    • 2020-11-05
    相关资源
    最近更新 更多