簡體   English   中英

cython:不允許超出主包的相對 cimport

[英]cython: relative cimport beyond main package is not allowed

我正在嘗試在 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/ 我試過在test目錄中運行setup.py (適當調整include_dirs )。 他們都給出了同樣的錯誤。

如果我執行cimport other並刪除. 它可以工作,但這是一個玩具示例,我需要相對導入,以便其他文件夾可以正確導入。 這是我能為這個錯誤找到的唯一例子,我非常有信心我的問題是不同的。

其他唯一的例子我能找到這個錯誤是hal.pyx中的machinekit項目 我非常確信這是一個不同的錯誤,但今天我意識到在錯誤解決后,機器人工作正在運行,這意味着顯式相對導入必須有效。 他們的setup.py文件是指linuxcnc ,它不在目錄樹中,但我想是在編譯期間的某個時刻創建的。 重要的是include_dirs包括父目錄而不是子目錄。

轉換為我的項目結構,這意味着我將myProject放在include_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},
)

現在一切正常!

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語句更改為文件example_package/driver.pyx的絕對cimport (該包使用此替代方法構建和安裝,但不導入,因為還需要更改Extension ):

     from . import other from example_package cimport other
  4. 添加文件example_package/__init__.py改變cimport語句絕對cimport里面的文件example_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名稱的更改對於導入已安裝的子模塊是必要的(另外在第四個備選方案中傳遞參數和關鍵字參數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已成為命名空間包(CPython 詞匯表條目):

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

(另外,我在我使用的setup.py文件中省略了setuptools.setup的參數include_dirs ,我在下面包含了它。)

構建和安裝包以及導入擴展模塊需要這些更改。 用於從 Python導入已安裝的包,以防它不包含任何擴展模塊(因此沒有成為命名空間包):

  • 需要在目錄example_package/ (在問題中是目錄Test/ )中添加一個文件__init__.py ,並且
  • 關鍵字參數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

為了統一起見,我在運行setup.py時實際上也將目錄重命名為Test/使用這個包名,但我目前不在區分大小寫的文件系統上,所以我不知道一個名為test/的目錄是否在一起使用關鍵字參數name='Test',setup.py ,如問題所示,會導致區分大小寫的文件系統出現問題。

所以:

  • 使用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的包:

test包包含 Python 的所有回歸測試以及模塊test.supporttest.regrtest

因此,名稱test不能用於打算在安裝后導入的示例包(盡管該包確實已構建和安裝,甚至可以通過pip uninstall -y test ,很好)。

另一個細節是, from test cimport other實際上是錯誤的,即使它編譯,因為如果構建的test包實際上以某種方式神奇地導入(在 CPython 的test包的存在下),在運行時這個cimport語句將默認為 CPython 的test包. 盡管如此,Cython 的翻譯可能會將此cimport轉換為某種其他形式,該形式實際上是從構建包的test.other導入的。 由於在CPython的test包存在的情況下,安裝的test包的導入似乎是不可能的,所以不知道這個cimport是否會引發運行時錯誤。

另外,請注意:

注意: test包僅供 Python 內部使用。 為了 Python 的核心開發人員的利益,它被記錄下來。 不鼓勵在 Python 標准庫之外使用此包,因為此處提到的代碼可能會在 Python 版本之間更改或刪除,恕不另行通知。

在所有實驗之間,我運行rm -rf build dist *.egg-info test/*.c 所以在將文件排列更改為之前顯示的文件排列之前,我使用的與問題相同。

將包重命名為example_package

我將包的名稱更改為example_package ,假設test/包含要安裝的實際包,基於問題的文件setup.py中參數name=的參數。

這種重命名的動機是“test”或“tests”通常用於命名 Python 包附帶的測試目錄。 對於此類目錄以及如何使用測試,有多種安排。 在下一節中,我將討論我對安排測試的建議。

關於可能性,除我在下一節中描述的之外的其他安排已被廣泛使用,包括將測試放在包本身的目錄 鑒於問題寫的是myProject/ ,並且有一個文件myProject/__init__.py ,我不確定這個問題是否真的使用了這樣的安排。

但是,在這種情況下, driverother實際上是測試模塊。 雖然將測試安裝為一個單獨的包(在問題中稱為Test ),這是myProject/setup.py模塊所做的,表明driverother是主包的模塊,因此主包被稱為“測試” .

如果不是,即如果driverother實際上是測試模塊,並且setup.py不是主包的安裝腳本,而是構建和安裝“輔助”包的安裝腳本,該包僅用於測試主包(其中在這種情況下,可能被命名為“myProject”,在包含問題的目錄myProject/的目錄中存在setup.py ),那么我將Test重命名為example_package/將不對應於這是主包。 (有一個包含 Cython 代碼的測試工具包也很有趣,因此需要編譯 - 並可能安裝。)

在這種情況下,也許可以將Test重命名為tests_of_example_package 換句話說,在這種情況下,在包的名稱中包含“test”這個詞是相關的,盡管將包限定為example_package輔助似乎是明確的。 顯式優於隱式 ( PEP 20 )。

(測試有時被安排為包(使用__init__.py ),即使不將其安裝為輔助 Python 包(僅作為它附帶的主要 Python 包的測試工具)也是如此。動機是啟用導入通用模塊由多個測試模塊使用的測試套件,但它們本身不是由測試運行器直接運行的模塊。)

如果這是主包,那么我假設使用“Test”是為了在示例中編寫示例。 如果是這樣,那么我重命名(小寫除外)的唯一原因是將主包本身與其測試區分開來。

PEP 8強制要求 Python 包的小寫名稱:

Python 包也應該有簡短的、全小寫的名稱,但不鼓勵使用下划線。

example_package的下划線僅用於示例。

安排測試

測試可能放置在test/目錄中,該目錄與包含 Python 包的目錄位於同一目錄中,並以包命名。 我強烈推薦這種方法,例如(這棵樹是用程序tree創建的):

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

為了測試不會意外地從源目錄導入包example_package ,而是從它的安裝位置(通常在site-packages/ ),我建議在所有情況下,運行任何測試之前首先cd到目錄tests/ 這是最可靠的測試方法,不依賴於每個測試框架如何工作,測試框架的各種配置選項如何工作,選項如何相互交互,也不依賴於測試框架本身的錯誤如何影響測試。

這樣,包源就可以放在目錄example_package ,沒有任何理由使用任何其他目錄排列。

Python 模塊中的 Shebangs

*.pyx文件中的shebang可以刪除,因為它沒有效果。 Shebang行被Cython視為 Python 注釋行,該行被移動到 C 注釋內的某個地方,稍后在 Cython 從*.pyx文件生成的*.c文件中。 所以它沒有效果。 我不知道在 C 源代碼中使用任何旨在通過直接調用gcc (或另一個 C 編譯器)編譯的 shebang 行,就像 Cython 所做的那樣(Cython 調用gcc還是另一個編譯器取決於系統、環境路徑、環境變量和其他信息)。

此外,shebang 僅與可能作為可執行文件執行的 Python 模塊相關。 對於 Python 包中的模塊而言,情況並非如此,因此幾乎從不使用 shebang 行。

一個例外可能是在開發過程中很少直接運行的包模塊,例如,用於實驗或調試目的。 盡管如此,這樣的模塊應該有一個__main__節。

所以與 shebang 相關的 Python 模塊通常也有一個__main__節。

為了完整setup.pysetup.py旨在作為__main__運行,並且確實有一個__main__節,但是安裝腳本的運行方式(強烈建議不使用pip --using pip )是由python setup.py ,所以有在setup.py不需要 shebang(問題中沒有 shebang 出現——我只是為了完整性才提到這一點)。

setup.py導入setuptools而不是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”,對問題示例的唯一更改是添加__init__.py__init__.pxd ,以及絕對cimportdriver.pyxExtensions的重命名中。

刪除文件__init__.py

在最終版本中,我刪除了與setup.py位於同一目錄中的文件__init__.py 我的理解是這個文件在這個例子中沒有作用。 如果示例打算將test/作為主包的目錄,那么任何__init__.py都會出現在test/

如果test/實際上是主包的輔助測試包,那么__init__.py將是主包的一部分,與test/包無關。 但是,在這種情況下,似乎myProject/上方會有一個setup.py文件,它負責構建主包和測試工具包。

使用絕對導入

cython < 3.0.0的默認language_level是 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) ,或安裝預發布的cython == 3.0.0a8 ),前兩個解決方案(使用相對導入)確實有效。

盡管如此, PEP 8 建議使用絕對導入

建議使用絕對導入,因為如果導入系統配置不正確,它們通常更具可讀性並且往往表現更好(或至少給出更好的錯誤消息)...

絕對導入對於重構包的結構也很健壯。 它們是顯式的,顯式優於隱式( PEP 20 )。

此更改后生成的模塊driver.pyx將是:

from example_package import other
from example_package cimport other

此答案中的setup.py代碼基於我在Python 包dd的文件download.py中編寫的download.py

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM