cython:不允许超出主包的相对 cimport

cython: relative cimport beyond main package is not allowed

我正在尝试在 cython 中使用显式相对导入。从 release notes 看来相对导入应该在 cython 0.23 之后工作,我使用的是 0.23.4 和 python 3.5。但是我遇到了这个奇怪的错误,我找不到很多参考资料。错误仅来自 cimport:

driver.pyx:4:0: relative cimport beyond main package is not allowed

目录结构为:

    myProject/
        setup.py
        __init__.py
        test/
            driver.pyx
            other.pyx
            other.pxd

看来我可能在 setup.py 中搞砸了,所以我包含了下面的所有文件。

setup.py

from setuptools import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

ext_modules = [
    Extension('other', ['test/other.pyx'],),
    Extension('driver', ['test/driver.pyx'],),
]

setup(
    name='Test',
    ext_modules=ext_modules,
    include_dirs=["test/"],
    cmdclass={'build_ext': build_ext},
)

driver.pyx

#!/usr/bin/env python
from . import other
from . cimport other

other.pyx

#!/usr/bin/env python

HI = "Hello"

cdef class Other:
    def __init__(self):
        self.name = "Test"

    cdef get_name(self):
        return self.name

other.pxd

cdef class Other:
    cdef get_name(self)

我试过将 __init__.py 移动到 test/。我已经在 test 目录中尝试 运行 setup.py(适当调整 include_dirs)。他们都给出了相同的错误。

如果我执行 cimport other 并删除 . 它可以工作,但这是一个玩具示例,我需要相对导入以便其他文件夹可以正确导入。这是我能找到的唯一一个 example 这个错误,我非常有信心我的问题是不同的。

唯一的另一个example I could find of this error was in hal.pyx of the machinekit project。我非常有信心这是一个不同的错误,但今天我意识到在解决该错误后,machinekit 正在工作,这意味着显式相对导入必须工作。他们的 setup.py 文件引用了 linuxcnc ,它不在目录树中,但我猜是在编译期间的某个时候创建​​的。重要的是 include_dirs 包括父目录而不是子目录。

转换为我的项目结构意味着我将 myProject 放在 include_dirs 而不是 test/ 中。第二次阅读此 guide 后,我终于开始了解 python 如何看待包。问题是 include_dirs 是子目录。似乎这有效地使 cython 将其视为单个平面目录,在这种情况下不允许相对导入?像这样的错误可能使它更清楚:

ValueError: Attempted relative import in non-package

我仍然没有足够深刻的理解来确切知道发生了什么,但幸运的是解决方案相对简单。我只是更改了 include_dirs 以使 cython 识别嵌套文件结构:

from setuptools import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

ext_modules = [
    Extension('other', ['test/other.pyx'],),
    Extension('driver', ['test/driver.pyx'],),
]

setup(
    name='Test',
    ext_modules=ext_modules,
    include_dirs=["."],
    cmdclass={'build_ext': build_ext},
)

现在一切正常!

Cythonization错误至少有四种解决方案(这些结果与cython == 0.29.24):

  1. 添加文件 example_package/__init__.pxd 将正在构建的 Extension 的名称更改为正在构建的模块的子模块,即 example_package.otherexample_package.driver(在问题中这些将是 Test.otherTest.driver)。

    无论如何,此更改对于导入已安装的子模块 driverother 都是必需的,如下所述。请注意,在这种情况下,由于缺少关键字参数和参数 packages=['example_package'],安装的包实际上是一个 命名空间包 ,如下所述。

  2. 添加文件 example_package/__init__.py 将正在构建的 Extension 的名称更改为正在构建的模块的子模块,即 example_package.otherexample_package.driver。即使在这种情况下,如果存在 __init__.py,安装的包 example_package 也将是命名空间包。将其变成常规包需要将 packages=['example_package'] 传递给函数 setuptools.setup.

    与添加 __init__.pxd 类似,此更改对于导入已安装的子模块是必要的。

  3. 添加文件 example_package/__init__.pxd cimport 语句更改为文件中的绝对 cimport example_package/driver.pyx(该包使用此替代方案构建和安装,但不导入,因为还需要更改 Extensions 的名称):

     from . import other
     from example_package cimport other
    
  4. 添加文件 example_package/__init__.py cimport 语句更改为文件中的绝对 cimport example_package/driver.pyx,和上一项一样。该软件包使用它构建和安装,但不导入。

问题明确要求 relative imports,因此从这个意义上说,前两个备选方案是问题的答案,因为它们确实适用于相对导入。

上面列出的四个更改中的任何一个都避免了以下错误:

Error compiling Cython file:
------------------------------------------------------------
...
from . import other
from . cimport other
^
------------------------------------------------------------

example_package/driver.pyx:2:0: relative cimport beyond main package is not allowed

但正如上面已经提到的,以及下面讨论的,第一个或第二个备选方案的 Extension 名称的更改对于导入已安装的子模块是必要的(另外传递参数和关键字参数 packages=[PACKAGE_NAME] 在第四个选项中允许导入 Python 包 example_package,但不允许导入其子模块 driverother).

已修改setup.py

我推荐的文件 setup.py 以及所有其他更改(不仅是上面列出的构建和安装所需的更改)是:

"""Installation script."""
import os
import setuptools


try:
    from Cython.Build import cythonize
    cy_ext = f'{os.extsep}pyx'
except ImportError:
    # this case is intended for use when installing from
    # a source distribution (produced with `sdist`),
    # which, as recommended by Cython documentation,
    # should include the generated `*.c` files,
    # in order to enable installation in absence of `cython`
    print('`import cython` failed')
    cy_ext = f'{os.extsep}c'


PACKAGE_NAME = 'example_package'


def run_setup():
    """Build and install package."""
    ext_modules = extensions()
    setuptools.setup(
        name=PACKAGE_NAME,
        ext_modules=ext_modules,
        packages=[PACKAGE_NAME],
        package_dir={PACKAGE_NAME: PACKAGE_NAME})


def extensions():
    """Return C extensions, cythonize as needed."""
    extensions = dict(
        other=setuptools.extension.Extension(
            f'{PACKAGE_NAME}.other',
            sources=[f'{PACKAGE_NAME}/other{cy_ext}'],),
        driver=setuptools.extension.Extension(
            f'{PACKAGE_NAME}.driver',
            sources=[f'{PACKAGE_NAME}/driver{cy_ext}'],))
    if cy_ext == f'{os.extsep}pyx':
        ext_modules = list()
        for k, v in extensions.items():
            c = cythonize(
                [v],
                # show_all_warnings=True  # this line requires `cython >= 3.0`
                )
            ext_modules.append(c[0])
    else:
        ext_modules = list(extensions.values())
    return ext_modules


if __name__ == '__main__':
    run_setup()

其他变化

此答案中的其他更改对于成功构建和安装包不是必需的,但出于其他原因推荐。对于其他一些更改,我在下面描述了动机。

注意:

  • 只加example_package/__init__.pxdexample_package/__init__.py是不够的,
  • 只改Extension个名字是不够的,
  • 仅将 cimport 语句更改为 from example_package cimport other 是不够的。

构建和安装需要同时进行其中两项更改,即前面列出的四种备选方案之一。

为了能够 导入 从 Cython 源 driver.pyxother.pyx 构建的扩展模块,还需要更改扩展到:

  • Extension('example_package.other', ...)
  • Extension('example_package.driver', ...)

请注意,这会使 import 起作用,因为现在 example_package 已变成 namespace package (CPython glossary entry):

>>> 
<module 'example_package' (namespace)>
>>> import example_package.driver
>>> import example_package.other

(此外,我在我使用的 setup.py 文件中省略了 setuptools.setup 的参数 include_dirs,我将其包含在下面。)

构建和安装包以及导入扩展模块需要进行这些更改。对于 从 Python 导入 安装的包,以防它不包含任何扩展模块(因此没有成为命名空间包):

  • 需要在example_package/目录下添加一个文件__init__.py(题目中是Test/目录),
  • 关键字参数packages=[example_package],需要传递给函数setuptools.setup.

否则,语句 import example_package 将引发 ModuleNotFoundError。添加 __init__.py 文件对于使包成为 regular package (CPython glossary entry) 也是必要的,这通常是预期的,而不是命名空间包。

是否使用__init__.pxd

一个普通的 Python 包包含一个 __init__.py 文件。 __init__.pxd 文件仅在其他包需要 *.pxd headers 的情况下才相关。如果不是这种情况,似乎文件 example_package/__init__.py 就足够了,因为上面的四个解决方案本质上是两个解决方案,每个解决方案都有 __init__.py__init__.pxd 作为替代方案。

所以我对文件及其排列的建议是:

.
├── example_package
│   ├── __init__.py
│   ├── driver.pyx
│   ├── other.pxd
│   └── other.pyx
└── setup.py

需要进行两项更改

仅添加 __init__.pxd 文件会引发 cythonization 错误:

Error compiling Cython file:
------------------------------------------------------------
...
from . import other
from . cimport other
^
------------------------------------------------------------

example_package/driver.pyx:3:0: relative cimport beyond main package is not allowed

并且仅更改 cimport 语句(没有 __init__.pxd)会引发 cythonization 错误:

Error compiling Cython file:
------------------------------------------------------------
...
#!/usr/bin/env python
from . import other
from example_package cimport other
^
------------------------------------------------------------

example_package/driver.pyx:3:0: 'example_package.pxd' not found

Error compiling Cython file:
------------------------------------------------------------
...
#!/usr/bin/env python
from . import other
from example_package cimport other
^
------------------------------------------------------------

example_package/driver.pyx:3:0: 'example_package/other.pxd' not found

南ng 包

上面我写 example_package 作为包的名称,虽然我确实构建和安装了示例也使用名称 Test/ 因为它在问题中被命名,以确保这确实有效,因此所需的最小更改是 __init__.pxd 文件和 from example_package cimport other.

为了统一,其实我在运行ning setup.py的时候也把目录重命名为Test/用这个名字的包,但是我不在case-sensitive目前的文件系统,所以我不知道名为 test/ 的目录和 setup.py 中的关键字参数 name='Test', 是否会导致 [=493] 上的问题=] 文件系统。

所以:

  • 使用 Test 作为包名和 Test 作为目录名对我来说构建和安装有效,并且
  • 使用 test 作为包名和 test 作为目录名对我来说构建和安装很有效。

我建议使用另一个包名。此外,由于下述原因:

  • 当包名为 Test 时导入是使用语句 import Test 完成的。写入 import test 将导入 另一个 包(见下文)。
  • 使用 test 作为包名不会导入已安装的 test 包,原因如下所述,即使添加了 __init__.py 文件也是如此。

无论如何,出于下面解释的原因,我的建议是更改包名称,即使它是一个辅助包,仅用作主包的测试工具。

此外,PEP 8强制要求小写包命名,因此导致test,这可能被理解为测试目录,如果这实际上是为了以主包为例。

构建和安装后,当包和目录被命名为test时发生的错误是(点...是编辑实际的结果输出):

>>> import test
>>> test
<module 'test' from '.../lib/python3.9/test/__init__.py'>

换句话说,CPython 包含一个名为 test:

的包

The test package contains all regression tests for Python as well as the modules test.support and test.regrtest.

因此,名称 test 不能用于要在安装后导入的示例包(尽管该包确实已构建和安装,甚至被 pip uninstall -y test 卸载,很好).

另一个细节是 from test cimport other 实际上是错误的,即使它编译了,因为构建的 test 包实际上以某种方式神奇地导入了(在 CPython 存在的情况下) s test 包),在 运行 时,此 cimport 语句将默认为 CPython 的 test 包。尽管如此,Cython 的翻译可能会将此 cimport 转换为实际从构建包的 test.other 导入的其他形式。由于在 CPython 的 test 包存在的情况下导入已安装的 test 包似乎是不可能的,因此不知道这个 cimport 是否会引发 运行时间错误。

另外,请注意:

Note: The test package is meant for internal use by Python only. It is documented for the benefit of the core developers of Python. Any use of this package outside of Python’s standard library is discouraged as code mentioned here can change or be removed without notice between releases of Python.

在所有实验之间,我运行rm -rf build dist *.egg-info test/*.c。 所以在将文件排列更改为之前显示的排列之前,我使用的与问题相同

正在将包重命名为 example_package

我将包的名称更改为 example_package,假设 test/ 包含要安装的实际包,基于文件中给参数 name= 的参数setup.py 的问题。

此重命名的动机是“测试”或“测试”通常用于命名 Python 包附带的测试目录。此类目录以及如何使用测试有多种安排。在下一节中,我将讨论我对安排测试的建议。

关于可能性,除了我在下一节中描述的安排外,一般都使用了其他安排,包括将测试放在 目录中 包本身。鉴于题目写的是myProject/,并且有一个文件myProject/__init__.py,我不确定题目是否真的使用了这样的安排。

不过,在那种情况下,driverother 实际上是测试模块。尽管将测试安装为单独的包(在问题中称为 Test),这是模块 myProject/setup.py 所做的,但表明 driverother 是主包的模块,因此主包被称为“测试”。

如果不是,即如果 driverother 实际上是测试模块,而 setup.py 而不是 主包的安装脚本,而是一个构建和安装“辅助”包的安装脚本,该包仅用于测试主包(在这种情况下可能被命名为“myProject”,在包含该目录的目录中存在 setup.py myProject/ 的问题),那么我将 Test 重命名为 example_package/ 将不对应于这是主包。 (有一个包含 Cython cod 的 test-harness 包也很有趣因此需要编译——并且可能需要安装。)

在那种情况下,也许 Test 可以重命名为 tests_of_example_package。换句话说,在那种情况下,在包的名称中包含“test”一词是相关的,尽管似乎将包限定为 example_package 的辅助是明确的。显式优于隐式 (PEP 20).

(测试有时被安排为包(使用 __init__.py),即使没有将其作为辅助 Python 包安装(仅作为主 Python 的测试工具)将其打包。其动机是允许导入测试套件的公共模块,这些模块被多个测试模块使用,但它们本身不是由测试 运行 直接 运行 的模块。)

如果这是主包,那么我假设“测试”是为了在示例中编写示例。如果是这样,那么我重命名(除了小写字母)的唯一原因是将主包本身与其测试区分开来。

Python 软件包的小写名称由 PEP 8:

强制要求

Python packages should also have short, all-lowercase names, although the use of underscores is discouraged.

example_package中的下划线只是为了举例。

安排测试

测试可能放在 test/ 目录中,该目录与包含 Python 包的目录位于同一目录中,并以包命名。我强烈推荐这种方法,例如(这棵树是用程序 tree 创建的):

.
├── example_package
│   └── __init__.py
├── setup.py
└── tests
    └── module_name_test.py

为了测试不会意外地从其源目录导入包 example_package,而是从它的安装位置(通常在 site-packages/ 下),我建议在所有情况下首先 cding在tests/ 目录之前 运行进行任何测试。这是最可靠的测试方法,不依赖于每个测试框架如何工作,测试框架的各种配置选项如何工作,选项如何相互交互,也不依赖于测试框架本身的错误如何影响测试。

这样,包源码就可以放在目录example_package里面,没有任何理由使用其他目录安排。

Python 模块中的 Shebangs

shebang inside the *.pyx files can be removed, because it has no effect. The shebang line is treated by Cython as a Python comment line that is moved to inside a C comment somewhere later inside the *.c files that Cython generates from the *.pyx files. So it has no effect. I am not aware of any use of shebang lines in C sources that are intended to be compiled by directly calling gcc(或其他C编译器),与Cython一样(Cython是否调用gcc,或其他编译器,取决于系统、环境路径、环境变量等信息)。

此外,shebang 仅与 Python 可能作为可执行文件执行的模块相关。对于 Python 包内的模块,情况并非如此,因此那里几乎从不使用 shebang 行。

一个例外可能是在开发过程中很少直接 运行 的包模块,例如,用于实验或调试目的。尽管如此,这样的模块应该有一个 __main__ 节。

所以 Python 与 shebang 相关的模块通常也有一个 __main__ 节。

为了完整起见,setup.py 旨在 运行 作为 __main__,并且确实有一个 __main__ 节,但是设置脚本的方式是 运行(不使用 pip 时——强烈建议使用 pip)由 python setup.py 提供,因此 setup.py 中不需要 shebang(没有 shebang 出现在那里这个问题——为了完整起见,我只是提到这个)。

setup.py 中导入 setuptools,而不是 distutils

distutils 模块]() 是 deprecated as of Python 3.10, as specified in PEP 632,将在 Python 3.12 中删除。

当 Cython 不存在时切换到扩展 .c,而不是 .pyx

这符合Cython recommendations:

It is strongly recommended that you distribute the generated .c files as well as your Cython sources, so that users can install your module without needing to have Cython available.

setup.py 中 module-scope 变量的大写名称保持不变(“常量”)

Module-scope Python 旨在用作常量的变量,即在初始赋值后保持不变的变量,由 PEP 8 强制要求具有大写带下划线的标识符:

Constants are usually defined on a module level and written in all capital letters with underscores separating words. Examples include MAX_OVERFLOW and TOTAL.

因此标识符 PACKAGE_NAME

格式化字符串

我用的是formatted string literals,需要Python >= 3.6.

将代码安排为模块中的函数 setup.py

这通常是一个很好的做法,允许通过函数名称命名不同的代码部分,仅当 运行 为 __main__ 时执行代码,包括一个 __main__ 节,并且从而启用导入 setup.py 并使用可能与外部代码相关的特定功能(例如,安装框架),而不必 运行ning 所有代码——例如,没有 运行ning 函数 setuptools.setup.

该问题提供了一个最小的工作示例,因此小 setup.py 与该问题相关。我写这部分是为了建议在实际包中做什么,而不是在问题中。

相同的观察结果适用于 setup.py 内的模块和函数文档字符串。

此外,我推荐 top-down 函数排列:调用者在被调用者之上,因为 t布局是否更具可读性。

我使用 os.extsep 是为了通用,尽管我认为使用点仍然有效,并且更具可读性。

套餐安排

正如我之前提到的,为避免构建错误“不允许超出主包的相对 cimport”,对问题示例的唯一更改是添加 __init__.py__init__.pxd,以及 driver.pyx 内的绝对 cimportExtensions.

的重命名

正在删除文件__init__.py

在最终版本中,我删除了与setup.py同一目录下的文件__init__.py。我的理解是这个文件在这个例子中没有作用。如果该示例打算将 test/ 作为主包的目录,那么任何 __init__.py 都会出现在 test/.

如果test/实际上是主包的辅助测试包,那么__init__.py将是主包的一部分,与test/包无关。但是,在那种情况下,myProject/ 上方似乎会有一个 setup.py 文件,它负责构建主包和 test-harness 包。

使用绝对导入

The default language_level in cython < 3.0.0 is 2,即使在 Python 3:

language_level (2/3/3str) Globally set the Python language level to be used for module compilation. Default is compatibility with Python 2. To enable Python 3 source code semantics, set this to 3 (or 3str) at the start of a module or pass the "-3" or "--3str" command line options to the compiler.

题目使用了Python3.5和cython == 0.23.4,所以是这样的

默认的 Cython 语义是 changing in cython >= 3.0.0:

The default language level was changed to 3str, i.e. Python 3 semantics, ...

同时使用Python 2 和Python 3 语义(传递compiler_directives=dict(language_level=3),或安装pre-release cython == 3.0.0a8),前两个解决方案(使用相对进口)做的工作。

尽管如此,absolute imports are recommended by PEP 8:

Absolute imports are recommended, as they are usually more readable and tend to be better behaved (or at least give better error messages) if the import system is incorrectly configured ...

绝对导入对于重构包的结构也很稳健。它们是显式的,显式优于隐式 (PEP 20)。

此更改后生成的模块 driver.pyx 将是:

from example_package import other
from example_package cimport other

此答案中的 setup.py 代码基于我在文件 download.py of the Python package dd.

中编写的内容