7

我正在尝试在 cython 中使用显式相对导入。从发行说明看来,相对导入应该在 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/. 我试过setup.pytest目录中运行(include_dirs适当调整)。他们都给出了同样的错误。

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

4

2 回答 2

5

我能找到的关于此错误的唯一其他示例是在machinekit项目的hal.pyx 中。我非常确信这是一个不同的错误,但今天我意识到在解决该错误后 machinekit 正在工作,这意味着显式相对导入必须工作。他们的setup.py文件指的linuxcnc是不在目录树中的文件,但我猜是在编译期间的某个时间点创建的。重要的是include_dirs包含父目录而不是子目录。

转换为我的项目结构,这意味着我放入myProjectinclude_dirs不是test/. 在第二次阅读本指南后,我终于开始了解一点 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},
)

现在一切正常!

于 2015-11-06T17:36:52.767 回答
2

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

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

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

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

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

  3. 添加文件example_package/__init__.pxd 并将语句更改为文件内cimport的绝对值(使用此替代方法构建和安装包,但不导入,因为还需要更改s 的名称):cimportexample_package/driver.pyxExtension

     from . import other
     from example_package cimport other
    
  4. 添加文件example_package/__init__.py 并将语句更改为文件内cimport的绝对值,如上一项中所做的那样。该软件包以此构建和安装,但不导入。cimportexample_package/driver.pyx

问题是明确要求相对进口,所以从这个意义上说,前两个备选方案是问题的答案,因为它们确实适用于相对进口。

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

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传递参数和关键字参数允许 Python 包导入,但不是它的子模块和)。packages=[PACKAGE_NAME]example_packagedriverother

修改的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已成为命名空间包(CPython词汇表条目):

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

(另外,我省略了我使用的文件中的参数,我将其包含在下面。include_dirssetuptools.setupsetup.py

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

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

否则,该语句import example_package将引发ModuleNotFoundError. 添加__init__.py文件也是必要的,以使包成为常规包(CPython词汇表条目),这通常是预期的,而不是命名空间包。

是否使用__init__.pxd

一个常规的 Python 包包含一个__init__.py文件。文件仅在其他包需要标头的__init__.pxd情况下才相关*.pxd。如果不是这种情况,文件似乎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

命名包

上面我写example_package的是包的名称,尽管我确实使用Test/问题中命名的名称构建和安装了示例,以确保这确实有效,并且所需的最小更改是__init__.pxd文件和from example_package cimport other.

为了统一起见,我其实在使用这个包名Test/运行的时候也将目录重命名为在问题中,会导致区分大小写的文件系统出现问题。setup.pytest/name='Test',setup.py

所以:

  • 使用Test作为包名称和Test目录名称为我构建和安装工作,并且
  • 使用test作为包名称和test目录名称为我构建和安装工作。

我建议使用另一个包名称。此外,出于以下原因:

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

在任何情况下,出于下面解释的原因,我的建议是更改包名称,即使它旨在成为仅用作主包的测试工具的辅助包。

此外,PEP 8强制要求小写包命名,因此导致test,它可以被理解为测试目录,如果这实际上是作为主包的示例,则情况并非如此。

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

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

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

test包包含 Python 的所有回归测试以及模块test.supporttest.regrtest.

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

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

另外,请注意:

注意:test包仅供 Python 内部使用。它的文档是为了 Python 的核心开发人员的利益。不鼓励在 Python 标准库之外使用此包,因为此处提到的代码可能会在 Python 版本之间更改或删除,恕不另行通知。

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

将包重命名为example_package

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

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

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

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

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

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

(测试有时被安排为包(使用__init__.py),即使没有将其安装为辅助 Python 包(仅作为它随附的主要 Python 包的测试工具)。动机是启用导入测试套件的通用模块被多个测试模块使用,但它们本身并不是由测试运行器直接运行的模块。)

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

PEP 8要求 Python 包的小写名称:

Python 包也应该有简短的全小写名称,尽管不鼓励使用下划线。

中的下划线example_package仅用于示例。

安排测试

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

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

为了在不意外从源目录导入包的情况下进行测试,但从example_package的安装位置(通常在. 这是最可靠的测试方法,不依赖于每个测试框架如何工作,测试框架的各种配置选项如何工作,选项如何相互交互,也不依赖于测试框架本身的错误如何影响测试。site-packages/cdtests/

这样,包源就可以放在目录里面了example_package,没有任何理由使用任何其他的目录排列方式。

Python 模块中的 Shebangs

文件里面的shebang可以*.pyx去掉,因为没有效果。shebang 行被Cython*.c视为 Python 注释行,稍后移到 Cython 从文件生成的文件中的某个 C 注释内*.pyx。所以它没有效果。我不知道在 C 源代码中使用 shebang 行,这些代码旨在通过直接调用gcc(或另一个 C 编译器)进行编译,就像 Cython 所做的那样(无论是 Cython 调用gcc,还是另一个编译器取决于系统、环境路径、环境变量和其他信息)。

此外,shebang 仅与可能作为可执行文件执行的 Python 模块相关。这不适用于 Python 包中的模块,因此几乎从不使用 shebang 行。

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

因此,与 shebang 相关的 Python 模块通常也有一个__main__节。

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

导入setuptools,setup.py而不是distutils

distutils模块]()自Python 3.10 起已弃用,如PEP 632中所述,并将在 Python 3.12 中删除。

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

这符合Cython 的建议

强烈建议您分发生成的.c文件以及 Cython 源代码,以便用户可以安装您的模块,而无需 Cython 可用。

模块范围变量的大写名称setup.py保持不变(“常量”)

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

常量通常在模块级别定义,并以全大写字母书写,并用下划线分隔单词。示例包括MAX_OVERFLOWTOTAL

因此标识符PACKAGE_NAME

格式化字符串

我使用了格式化字符串文字,这需要 Python >= 3.6。

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

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

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

同样的观察适用于内部的模块和函数文档字符串setup.py

另外,我推荐函数的自顶向下排列:调用者在被调用者之上,因为这种布局更具可读性。

我用于os.extsep一般性,虽然使用点我认为仍然可以工作,并且更具可读性。

包裹安排

正如我之前提到的,为避免构建错误“不允许超出主包的相对 cimport”所需的问题示例的唯一更改是添加 an__init__.py或 an __init__.pxd,以及绝对cimport内部driver.pyx或重命名Extensions.

删除文件__init__.py

在最终版本中,我删除了__init__.pysetup.py. 我的理解是这个文件在这个例子中没有任何作用。如果该示例旨在test/作为主包的目录,那么 any__init__.py将出现在test/.

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

使用绝对导入

language_levelin的默认cython < 3.0.0值为 2,即使在 Python 3 上也是如此:

language_level (2/3/3str) 全局设置用于模块编译的 Python 语言级别。默认与 Python 2 兼容。要启用 Python 3 源代码语义,请在模块开头将其设置为 3(或 3str)或将“-3”或“--3str”命令行选项传递给编译器。

该问题使用 Python 3.5 和cython == 0.23.4,所以情况就是这样。

默认的 Cython 语义正在发生变化cython >= 3.0.0

默认语言级别更改为3str,即 Python 3 语义,...

使用 Python 2 和 Python 3 语义(传递compiler_directives=dict(language_level=3)或安装 pre-release cython == 3.0.0a8),前两个解决方案(使用相对导入)确实有效。

尽管如此,PEP 8 建议绝对导入

建议使用绝对导入,因为如果导入系统配置不正确,它们通常更具可读性并且往往表现得更好(或至少给出更好的错误消息)......

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

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

from example_package import other
from example_package cimport other

此答案中的setup.py代码基于我在Python 包download.py文件中编写的内容。dd

于 2021-07-15T05:11:58.510 回答