Кошмар с относительным импортом, как работает pep 366?
У меня есть "каноническая файловая структура" (я даю разумные имена, чтобы облегчить чтение):
mainpack/
__main__.py
__init__.py
- helpers/
__init__.py
path.py
- network/
__init__.py
clientlib.py
server.py
- gui/
__init__.py
mainwindow.py
controllers.py
В этой структуре, например, модули, содержащиеся в каждом пакете, могут иметь доступ к утилитам helpers
через относительный импорт в виде:
# network/clientlib.py
from ..helpers.path import create_dir
Программа запускается "как script" с помощью файла __main__.py
следующим образом:
python mainpack/
Попытка следовать PEP 366 Я ввел __main__.py
следующие строки:
___package___ = "mainpack"
from .network.clientlib import helloclient
Но при запуске:
$ python mainpack
Traceback (most recent call last):
File "/usr/lib/python2.6/runpy.py", line 122, in _run_module_as_main
"__main__", fname, loader, pkg_name)
File "/usr/lib/python2.6/runpy.py", line 34, in _run_code
exec code in run_globals
File "path/mainpack/__main__.py", line 2, in <module>
from .network.clientlib import helloclient
SystemError: Parent module 'mainpack' not loaded, cannot perform relative import
Что случилось? Каков правильный способ обработки и эффективного использования относительного импорта?
Я также попытался добавить текущий каталог в PYTHONPATH, ничего не изменилось.
Ответы
Ответ 1
Код загрузки выглядит как this:
try:
return sys.modules[pkgname]
except KeyError:
if level < 1:
warn("Parent module '%s' not found while handling "
"absolute import" % pkgname, RuntimeWarning, 1)
return None
else:
raise SystemError, ("Parent module '%s' not loaded, cannot "
"perform relative import" % pkgname)
что заставляет меня думать, что, возможно, ваш модуль не находится на sys.path. Если вы запустите Python (обычно) и просто введите "import mainpack" в подсказке, что он делает? Он должен быть в состоянии найти его.
Я сам пробовал и получил ту же ошибку. После некоторого чтения я нашел следующее решение:
# foo/__main__.py
import sys
mod = __import__('foo')
sys.modules["foo"]=mod
__package__='foo'
from .bar import hello
hello()
Мне кажется, это немного хаки, но это действительно работает. Кажется, что трюк делает загрузку пакета foo
, поэтому импорт может быть относительным.
Ответ 2
"Шаблон", указанный в PEP 366, кажется неполным. Хотя он устанавливает переменную __package__
, она фактически не импортирует пакет, который также необходим, чтобы разрешить относительный импорт. Решение extraneon находится на правильном пути.
Обратите внимание, что недостаточно просто иметь каталог, содержащий модуль в sys.path
, соответствующий пакет должен быть явно импортирован. Следующее выглядит как лучший шаблон, чем то, что было дано в PEP 366, чтобы гарантировать, что модуль python может быть выполнен независимо от того, как он вызывается (через обычный import
, или с помощью python -m
или с python
, из любого места):
# boilerplate to allow running as script directly
if __name__ == "__main__" and __package__ is None:
import sys, os
# The following assumes the script is in the top level of the package
# directory. We use dirname() to help get the parent directory to add to
# sys.path, so that we can import the current package. This is necessary
# since when invoked directly, the 'current' package is not automatically
# imported.
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(1, parent_dir)
import mypackage
__package__ = str("mypackage")
del sys, os
# now you can use relative imports here that will work regardless of how this
# python file was accessed (either through 'import', through 'python -m', or
# directly.
Если script не находится на верхнем уровне каталога пакета, и вам нужно импортировать модуль под верхним уровнем, то os.path.dirname
необходимо повторить, пока parent_dir
не будет содержать каталог, содержащий верхний уровень.
Ответ 3
Вдохновленный ответами extraneon и taherh здесь приведен код, который запускает дерево файлов до тех пор, пока не закончится файл __init__.py
, чтобы создать полное имя пакета. Это определенно взломанно, но, похоже, работает независимо от глубины файла в дереве каталогов. Кажется, что абсолютный импорт сильно рекомендуется.
import os, sys
if __name__ == "__main__" and __package__ is None:
d,f = os.path.split(os.path.abspath(__file__))
f = os.path.splitext(f)[0]
__package__ = [f] #__package__ will be a reversed list of package name parts
while os.path.exists(os.path.join(d,'__init__.py')): #go up until we run out of __init__.py files
d,name = os.path.split(d) #pull of a lowest level directory name
__package__.append(name) #add it to the package parts list
__package__ = ".".join(reversed(__package__)) #create the full package name
mod = __import__(__package__) #this assumes the top level package is in your $PYTHONPATH
sys.modules[__package__] = mod #add to modules
Ответ 4
Это минимальная настройка, основанная на большинстве других ответов, протестированных на python 2.7 с таким компоновкой пакетов. Это также имеет то преимущество, что вы можете вызывать runme.py
script из любого места, и кажется, что он делает правильные вещи - я еще не тестировал его в более сложной настройке, поэтому caveat emptor... и т.д.
Это в основном ответ Брэда выше с вставкой в sys.path, о котором другие рассказывали.
packagetest/
__init__.py # Empty
mylib/
__init__.py # Empty
utils.py # def times2(x): return x*2
scripts/
__init__.py # Empty
runme.py # See below (executable)
runme.py
выглядит следующим образом:
#!/usr/bin/env python
if __name__ == '__main__' and __package__ is None:
from os import sys, path
d = path.dirname(path.abspath(__file__))
__package__ = []
while path.exists(path.join(d, '__init__.py')):
d, name = path.split(d)
__package__.append(name)
__package__ = ".".join(reversed(__package__))
sys.path.insert(1, d)
mod = __import__(__package__)
sys.modules[__package__] = mod
from ..mylib.utils import times2
print times2(4)