我试着通读关于兄弟姐妹导入的问题,甚至 软件包文档,但我还没找到答案。

结构如下:

├── LICENSE.md
├── README.md
├── api
│   ├── __init__.py
│   ├── api.py
│   └── api_key.py
├── examples
│   ├── __init__.py
│   ├── example_one.py
│   └── example_two.py
└── tests
│   ├── __init__.py
│   └── test_one.py

示例和测试目录中的脚本如何从 API模块和从命令行运行?

另外,我希望避免对每个文件都使用难看的sys.path.insert。肯定 这可以在Python中完成,对吧?


当前回答

七年之后

由于我把答案写在下面,修改sys.;Path仍然是一种快速而肮脏的技巧,适用于私有脚本,但是已经有了一些改进

安装包(在virtualenv中或不在virtualenv中)会给你想要的,尽管我建议使用pip来完成,而不是直接使用setuptools(并使用setup.cfg存储元数据) 使用-m标志并作为包运行也可以(但是如果您想将工作目录转换为可安装包,则会有点尴尬)。 具体来说,对于测试,pytest能够在这种情况下找到api包,并负责sys. exe。给你的路径技巧

所以这取决于你想做什么。但是,在您的情况下,由于您的目标似乎是在某种程度上制作一个适当的包,因此通过pip -e安装可能是您的最佳选择,即使它还不完美。

旧的答案

正如在其他地方已经说过的,可怕的事实是,你必须做一些丑陋的hack来允许从__main__模块的兄弟模块或父包导入。该问题在PEP 366中有详细说明。PEP 3122试图以更合理的方式处理导入,但Guido拒绝了它

唯一的用例似乎是运行发生的脚本 在一个模块的目录中,我总是把它看作一个 反模式。

(在这里)

不过,我经常使用这种模式

# Ugly hack to allow absolute import from the root folder
# whatever its name is. Please forgive the heresy.
if __name__ == "__main__" and __package__ is None:
    from sys import path
    from os.path import dirname as dir

    path.append(dir(path[0]))
    __package__ = "examples"

import api

这里的路径[0]是运行脚本的父文件夹,目录(路径[0])是顶级文件夹。

虽然我还不能使用相对导入,但它确实允许从顶层(在示例api的父文件夹中)进行绝对导入。

其他回答

TLDR

这种方法不需要setuptools、路径hack、额外的命令行参数,也不需要在项目的每个文件中指定包的顶层。

只要在你调用的__main__的父目录中创建一个脚本,并从那里运行所有内容。进一步的解释请继续阅读。

解释

这可以在不使用新路径、额外的命令行参数或向每个程序添加代码来识别其兄弟程序的情况下完成。

这个失败的原因,我相信之前已经提到过,是被调用的程序将它们的__name__设置为__main__。当这种情况发生时,被调用的脚本接受自己位于包的顶层,并拒绝识别同级目录中的脚本。

但是,目录顶层下的所有内容仍然可以识别顶层下的任何其他内容。这意味着要让同级目录中的文件相互识别/利用,惟一需要做的就是从父目录中的脚本调用它们。

概念证明 在具有以下结构的目录中:

.
|__Main.py
|
|__Siblings
   |
   |___sib1
   |   |
   |   |__call.py
   |
   |___sib2
       |
       |__callsib.py

Main.py包含以下代码:

import sib1.call as call


def main():
    call.Call()


if __name__ == '__main__':
    main()

sib1 / call.py包含:

import sib2.callsib as callsib


def Call():
    callsib.CallSib()


if __name__ == '__main__':
    Call()

而sib2/ callsibb .py包含:

def CallSib():
    print("Got Called")

if __name__ == '__main__':
    CallSib()

如果你重现这个例子,你会注意到调用Main.py会导致“Got Called”被打印出来,就像在sib2/ callsibb .py中定义的那样,即使sib2/ callsibb .py是通过sib1/call.py调用的。但是,如果直接调用sib1/call.py(在对导入进行适当更改之后),则会抛出异常。即使它在其父目录中被脚本调用时可以工作,但如果它认为自己位于包的顶层,它就不能工作。

下面是我在tests文件夹的Python文件顶部插入的另一个替代方案:

# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))

存在的问题:

你只是不能让import mypackage在test.py中工作。你将需要一个可编辑的安装,改变到路径,或改变__name__和路径

demo
├── dev
│   └── test.py
└── src
    └── mypackage
        ├── __init__.py
        └── module_of_mypackage.py

--------------------------------------------------------------
ValueError: attempted relative import beyond top-level package

解决方案:

import sys; sys.path += [sys.path[0][:-3]+"src"]

在尝试在test.py中导入之前,请将上述内容放在上面。这是它。你现在可以导入mypackage了。

这在Windows和Linux上都可以工作。它也不会关心从哪个路径运行脚本。它足够短,可以拍打任何你需要它的地方。

为什么有效:

The sys.path contains the places, in order, where to look for packages when attempting imports if they are not found in installed site packages. When you run test.py the first item in sys.path will be something like /mnt/c/Users/username/Desktop/demo/dev i.e.: where you ran your file. The oneliner will simply add the sibling folder to path and everything works. You will not have to worry about Windows vs Linux file paths since we are only editing the last folder name and nothing else. If you project structure is already set in stone for your repository we can also reasonably just use the magic number 3 to slice away dev and substitute src

在主文件中添加以下内容:

import sys
import os 
sys.path.append(os.path.abspath(os.path.join(__file__,mainScriptDepth)))

mainScriptDepth =主文件从项目根目录的深度。

这是你的案例mainScriptDepth = "../../"。然后,您可以通过指定路径(从api。API import *)从你的项目的根。

你不需要也不应该侵入系统。路径,除非它是必要的,在这种情况下,它不是。使用:

import api.api_key # in tests, examples

从项目目录运行:python -m tests.test_one。

你可能应该把测试(如果它们是api的单元测试)移动到api内部并运行python -m api。测试运行所有测试(假设有__main__.py)或python -m api.test。Test_one来运行Test_one。

你也可以从示例中删除__init__.py(它不是Python包),并在安装api的virtualenv中运行示例,例如pip install -e。在virtualenv中,如果你有正确的setup.py,就会安装API包。