[英]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
):
添加文件example_package/__init__.pxd
並將正在構建的Extension
的名稱更改為正在構建的模塊的子模塊,即example_package.other
和example_package.driver
(在問題中這些將是Test.other
和Test.driver
)。
無論如何,此更改對於導入已安裝的子模塊driver
和other
是必要的,如下所述。 請注意,在這種情況下,由於缺少關鍵字參數和參數packages=['example_package']
,安裝的包實際上是一個命名空間包,如下所述。
添加文件example_package/__init__.py
並將正在構建的Extension
的名稱更改為正在構建的模塊的子模塊,即example_package.other
和example_package.driver
。 即使在這種情況下,存在__init__.py
,安裝的包example_package
也將是一個命名空間包。 將其轉換為常規包需要將packages=['example_package']
傳遞給函數setuptools.setup
。
與添加__init__.pxd
,此更改對於導入已安裝的子模塊是必要的。
添加文件example_package/__init__.pxd
並將cimport
語句更改為文件example_package/driver.pyx
的絕對cimport
(該包使用此替代方法構建和安裝,但不導入,因為還需要更改Extension
):
from . import other from example_package cimport other
添加文件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
導入,但不是其子模塊driver
和other
)。
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__.pxd
或example_package/__init__.py
是不夠的,並且Extension
名是不夠的,並且cimport
語句更改為from example_package cimport other
是不夠的。構建和安裝需要同時進行這些更改中的兩個,即前面列出的四個備選方案之一。
為了能夠導入從 Cython 源driver.pyx
和other.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.support
和test.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
,我不確定這個問題是否真的使用了這樣的安排。
但是,在這種情況下, driver
和other
實際上是測試模塊。 雖然將測試安裝為一個單獨的包(在問題中稱為Test
),這是myProject/setup.py
模塊所做的,表明driver
和other
是主包的模塊,因此主包被稱為“測試” .
如果不是,即如果driver
和other
實際上是測試模塊,並且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
,沒有任何理由使用任何其他目錄排列。
*.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.py
, setup.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 中刪除。
.c
,而不是.pyx
這符合Cython 的建議:
強烈建議您分發生成的
.c
文件以及 Cython 源,以便用戶可以安裝您的模塊,而無需使用 Cython。
setup.py
中保持不變的模塊范圍變量的大寫名稱(“常量”)旨在用作常量的模塊范圍 Python 變量,即在初始賦值后保持不變, PEP 8強制要求使用大寫帶下划線的標識符:
常量通常在模塊級別定義,並以所有大寫字母書寫,下划線分隔單詞。 示例包括
MAX_OVERFLOW
和TOTAL
。
因此標識符PACKAGE_NAME
。
我使用了格式化字符串文字,它需要 Python >= 3.6。
setup.py
中將代碼安排為函數這通常是一個很好的做法,通過函數名稱來命名不同的代碼部分,僅在作為__main__
運行時執行代碼,通過包含__main__
節,從而啟用導入setup.py
並使用可能與外部代碼相關的特定功能(例如,安裝框架)而不必運行所有代碼——例如,無需運行函數setuptools.setup
。
該問題提供了一個最小的工作示例,因此一個小的setup.py
與該問題相關。 我寫這個部分是作為建議在實際包中做什么,而不是在問題中。
同樣的觀察適用於setup.py
模塊和函數文檔字符串。
另外,我推薦函數的自頂向下排列:調用者高於被調用者,因為這種布局更具可讀性。
我使用os.extsep
作為通用性,盡管使用我認為仍然可以使用的點,並且更具可讀性。
正如我之前提到的,為了避免構建錯誤“不允許超出主包的相對 cimport”,對問題示例的唯一更改是添加__init__.py
或__init__.pxd
,以及絕對cimport
在driver.pyx
或Extensions
的重命名中。
__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.