如果在包根目录中,则无法导入 C++ 扩展

Cannot import C++ extension if in the package root directory

注意:自从问了这个问题,后来发现python -m pip install -e .会把扩展安装到cmod.venv/lib/python3.8/site-packages/hello-c-extension.egg-link指向当前目录中的项目。我也在以后的提交中切换到 src 布局,并发现 https://pythonwheels.com/ 是分发轮子的高质量包的一个很好的参考。但是,我仍然很想知道 setup.py 子命令的行为。


作为对 manylinux 的一些研究的一部分,我正在使用一个玩具项目为 C++ 扩展模块构建不同的平台轮子。

好像在本地构建安装的时候,如果我的当前目录是项目根目录,我无法导入C++扩展模块。这让我无法运行宁单元测试,等等。我相信这样做的原因是 . 成为 sys.path 的第一个组件,因此选择了纯 Python 版本,而编译扩展没有。

我该如何解决这个问题?我 运行 本地 build/install 正确吗?

包结构如下所示:

$ tree hello-c-extension/
hello-c-extension/
├── LICENSE
├── Makefile
├── README.md
├── cmod
│   ├── __init__.py
│   ├── _cmodule.cc
│   └── pymod.py
├── setup.py
└── tests
    ├── __init__.py
    └── test_cext.py

我在GitHub上也有这个项目;我从 29fef5b.

开始问这个问题

至build/install我使用:

cd hello-c-extension
python -m venv .venv
source ./.venv/bin/activate
python -m pip install -U pip wheel setuptools
python setup.py build install

现在,我可以从当前目录导入 Python 模块,但不能导入相应的扩展模块。 Python 模块被选为当前目录中的模块,而不是 site-packages:

中的模块
$ python -c 'from cmod import pymod; print(pymod)'
<module 'cmod.pymod' from '/Users/brad/Scripts/python/projects/bsolomon1124/hello-c-extension/cmod/pymod.py'>
$ python -c 'from cmod import _cmod; print(_cmod)'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ImportError: cannot import name '_cmod' from 'cmod' (/Users/brad/Scripts/python/projects/bsolomon1124/hello-c-extension/cmod/__init__.py)

hackishly 删除 sys.path 的 PWD 元素修复了这个问题:

>>> import sys
>>> sys.path
['', '/Users/brad/.pyenv/versions/3.8.1/lib/python38.zip', '/Users/brad/.pyenv/versions/3.8.1/lib/python3.8', '/Users/brad/.pyenv/versions/3.8.1/lib/python3.8/lib-dynload', '/Users/brad/Scripts/python/projects/bsolomon1124/hello-c-extension/.venv/lib/python3.8/site-packages', '/Users/brad/Scripts/python/projects/bsolomon1124/hello-c-extension/.venv/lib/python3.8/site-packages/hello_c_extension-0.4-py3.8-macosx-10.15-x86_64.egg']
>>> del sys.path[0]
>>> from cmod import _cmod; print(_cmod)
<module 'cmod._cmod' from '/Users/brad/Scripts/python/projects/bsolomon1124/hello-c-extension/.venv/lib/python3.8/site-packages/hello_c_extension-0.4-py3.8-macosx-10.15-x86_64.egg/cmod/_cmod.cpython-38-darwin.so'>

最后,更改目录也会使问题消失:

$ cd ..
$ python -c 'from cmod import _cmod; print(_cmod)'
<module 'cmod._cmod' from '/Users/brad/Scripts/python/projects/bsolomon1124/hello-c-extension/.venv/lib/python3.8/site-packages/hello_c_extension-0.4-py3.8-macosx-10.15-x86_64.egg/cmod/_cmod.cpython-38-darwin.so'>

这真的......它应该如何工作?在这种情况下,运行 对扩展模块进行单元测试的正确方法是什么?

系统信息:

$ python -V
Python 3.8.1
$ uname -mrsv
Darwin 19.4.0 Darwin Kernel Version 19.4.0: Wed Mar  4 22:28:40 PST 2020; root:xnu-6153.101.6~15/RELEASE_X86_64 x86_64

问题的原因

Is this really ... how it's supposed to work? What would be the proper way to run unit tests for the extension module in this case?

Python 从 sys.path 导入。这意味着它将在每个目录中搜索导入,从索引 0sys.path 中的最后一项。

请注意,一种古怪的修复方法是在导入之前添加以下内容:sys.path.append(sys.path.pop(0))。这仍然允许本地导入,但意味着首先搜索 site-packages 和标准库。

How can I fix this? Am I running the local build/install correctly?

我将在下面回答第一个问题。

是的,您的 build/install 没问题,但要解决导入问题,您需要稍微调整一下。

解决问题

切换到 src/cmod 布局。一个很棒的 link - 这是您自己在问题评论中指出的 - 是 here.

评论中讨论了这种方法,自从提出问题以来,您实际上已经实现了这一点。 (60093f1).

我现在将引用那篇文章的部分内容;它比我能更好地解释这一点。

The src directory is a better approach because:

  • You get import parity. The current directory is implicitly included in sys.path; but not so when installing & importing from site-packages. Users will never have the same current working directory as you do.

This constraint has beneficial implications in both testing and packaging:

  • You will be forced to test the installed code (e.g.: by installing in a virtualenv). This will ensure that the deployed code works (it's packaged correctly) - otherwise your tests will fail. Early. Before you can publish a broken distribution.

  • You will be forced to install the distribution. If you ever uploaded a distribution on PyPI with missing modules or broken dependencies it's because you didn't test the installation. Just beeing able to successfuly build the sdist doesn't guarantee it will actually install!

并且 pip install -e . 不再乱七八糟