кошмар с относительным импортом, как работает pep 366? - PullRequest
31 голосов
/ 31 мая 2010

У меня есть такая "каноническая файловая структура" (я даю разумные имена, чтобы облегчить чтение):

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

Программа запускается "как скрипт", используя файл __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, ничего не меняется.

Ответы [ 4 ]

41 голосов
/ 11 июля 2011

«Образец», приведенный в 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.

Если скрипт не находится на верхнем уровне каталога пакета, и вам необходимо импортировать модуль ниже верхнего уровня, то os.path.dirname необходимо повторять до тех пор, пока parent_dir не будет каталогом, содержащим верхний уровень.

7 голосов
/ 31 мая 2010

Код загрузки выглядит примерно так: 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 загружен, поэтому импорт может быть относительным.

6 голосов
/ 20 марта 2013

Вдохновленный ответами 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 
0 голосов
/ 08 декабря 2015

Это минимальная настройка, основанная на большинстве других ответов, протестированная на python 2.7 с такой компоновкой пакета. Он также имеет то преимущество, что вы можете вызывать скрипт runme.py из любого места, и он кажется , как будто он делает правильные вещи - я еще не тестировал его в более сложной установке, так что будьте внимательны с 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)
...