简体   繁体   English

相对进口的噩梦,pep 366如何运作?

[英]nightmare with relative imports, how does pep 366 work?

I have a "canonical file structure" like that (I'm giving sensible names to ease the reading): 我有一个“规范的文件结构”(我给出了明智的名称来简化阅读):

mainpack/

  __main__.py
  __init__.py 

  - helpers/
     __init__.py
     path.py

  - network/
     __init__.py
     clientlib.py
     server.py

  - gui/
     __init__.py
     mainwindow.py
     controllers.py

In this structure, for example modules contained in each package may want to access the helpers utilities through relative imports in something like: 在此结构中,例如,每个包中包含的模块可能希望通过相对导入来访问helpers程序实用程序,例如:

# network/clientlib.py
from ..helpers.path import create_dir

The program is runned "as a script" using the __main__.py file in this way: 该程序以这种方式使用__main__.py文件“作为脚本”运行:

python mainpack/

Trying to follow the PEP 366 I've put in __main__.py these lines: 试图按照PEP 366我放入__main__.py这些行:

___package___ = "mainpack"
from .network.clientlib import helloclient 

But when running: 但是在跑步时:

$ 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

What's wrong? 怎么了? What is the correct way to handle and effectively use relative imports? 处理和有效使用相对进口的正确方法是什么?

I've tried also to add the current directory to the PYTHONPATH, nothing changes. 我也尝试将当前目录添加到PYTHONPATH,没有任何变化。

The "boilerplate" given in PEP 366 seems incomplete. PEP 366中给出的“样板”似乎不完整。 Although it sets the __package__ variable, it doesn't actually import the package, which is also needed to allow relative imports to work. 虽然它设置了__package__变量,但它实际上并不导入包,这也是允许相对导入工作所必需的。 extraneon 's solution is on the right track. extraneon的解决方案正在走上正轨。

Note that it is not enough to simply have the directory containing the module in sys.path , the corresponding package needs to be explicitly imported. 请注意,仅仅在sys.path包含包含模块的目录是不够的,需要显式导入相应的包。 The following seems like a better boilerplate than what was given in PEP 366 for ensuring that a python module can be executed regardless of how it is invoked (through a regular import , or with python -m , or with python , from any location): 以下似乎是比PEP 366中给出的更好的样板,以确保无论如何调用python模块(通过常规import ,或使用python -m ,或使用python ,从任何位置)都可以执行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.

If the script is not at the top level of the package directory and you need to import a module below the top level, then the os.path.dirname has to be repeated until the parent_dir is the directory containing the top level. 如果脚本不在包目录的顶层,并且您需要在顶层下面导入模块,则必须重复os.path.dirname ,直到parent_dir是包含顶层的目录。

The loading code seems to be something like 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)

which makes me think that maybe your module is not on sys.path. 这让我觉得你的模块可能不在sys.path上。 If you start Python (normally) and just type "import mainpack" on the prompt, what does it do? 如果您启动Python(通常)并在提示符下键入“import mainpack”,它会做什么? It should be able to find it. 应该能够找到它。

I have tried it myself and got the same error. 我自己尝试过并得到了同样的错误。 After reading a bit I found the following solution: 读了一下后,我找到了以下解决方案:

# foo/__main__.py
import sys
mod = __import__('foo')
sys.modules["foo"]=mod

__package__='foo'
from .bar import hello

hello()

It seems a bit hackish to me but it does work. 这对我来说似乎有些神圣,但确实有效。 The trick seems to be making sure package foo is loaded so the import can be relative. 诀窍似乎是确保加载包foo ,因此导入可以是相对的。

Inspired by extraneon's and taherh's answers here is some code that runs up the file tree until it runs out of __init__.py files to build the full package name. 灵感来自extraneon和taherh的答案,这里有一些代码运行文件树,直到用完__init__.py文件来构建完整的软件包名称。 This is definitely hacky, but does seem to work regardless of the depth of the file in your directory tree. 这绝对是hacky,但无论目录树中文件的深度如何,它似乎都能正常工作。 It seems absolute imports are heavily encouraged. 这似乎是鼓励大量进口绝对。

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 

This is a minimal setup based on most of the other answers, tested on python 2.7 with a package layout like so. 这是基于大多数其他答案的最小设置,在python 2.7上测试,包装布局如此。 It also has the advantage that you can call the runme.py script from anywhere and it seems like it's doing the right thing - I haven't yet tested it in a more complex setup, so caveat emptor... etc. 它还有一个优点,你可以从任何地方调用runme.py脚本,它似乎正在做正确的事情 - 我还没有在更复杂的设置中测试它,所以需要注意的事情......等等。

This is basically Brad's answer above with the insert into sys.path others have described. 这基本上是Brad在上面的回答,其中插入了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 looks like this: 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)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM