兄弟包导入

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

结构如下:

├── 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

examplestests目录中的脚本如何从 api模块,并从命令行运行?< / p > 另外,我想避免对每个文件都使用丑陋的sys.path.insert攻击。肯定 这可以在Python中完成,对吧?< / p >

186835 次浏览

您需要查看import语句是如何在相关代码中编写的。如果examples/example_one.py使用以下import语句:

import api.api

...然后,它期望项目的根目录位于系统路径中。

不需要任何hack(如你所说)就能支持它的最简单的方法就是从顶级目录运行这些例子,就像这样:

PYTHONPATH=$PYTHONPATH:. python examples/example_one.py

首先,您应该避免使用与模块本身同名的文件。它可能会破坏其他导入。

当你导入一个文件时,解释器首先检查当前目录,然后搜索全局目录。

examplestests中,你可以调用:

from ..api import api

七年之后

由于我在下面写下了答案,修改sys.path仍然是一个快速而肮脏的技巧,适用于私有脚本,但有几个改进

  • 安装包(在virtualenv中或不在)将给你你想要的东西,尽管我建议使用pip来做,而不是直接使用setuptools(并使用setup.cfg存储元数据)
  • 使用-m标志并作为一个包运行也可以(但是如果你想将你的工作目录转换为一个可安装的包,这会变得有点尴尬)。
  • 具体来说,对于测试,pytest能够在这种情况下找到api包,并为你处理sys.path hacks

所以这取决于你想做什么。不过,在你的情况下,因为你的目标似乎是在某个时候制作一个合适的包,所以通过pip -e安装可能是你最好的选择,即使它还不完美。

旧的答案

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

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

(在这里)

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

# 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

这里path[0]是运行脚本的父文件夹,dir(path[0])是顶层文件夹。

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

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

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

我还没有对python学有必要的理解,以了解在不相关的项目之间共享代码的预期方式,而没有兄弟姐妹/亲戚导入黑客。在那一天到来之前,这就是我的解决方案。对于examplestests..\api导入东西,它看起来像这样:

import sys.path
import os.path
# Import from sibling directory ..\api
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/..")
import api.api
import api.api_key

为了防止在Eclipse上使用Pydev的人在这里结束:你可以使用项目- >属性添加兄弟的父路径(因此调用模块的父路径)作为外部库文件夹,并在左侧菜单Pydev-PYTHONPATH下设置外部库。然后你可以从你的兄弟导入,例如from sibling import some_class

你不需要也不应该破解sys.path,除非它是必要的,在这种情况下,它不是。使用:

import api.api_key # in tests, examples

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

你可能应该将tests(如果它们是api的单元测试)移动到api中,并运行python -m api.test来运行所有测试(假设有__main__.py)或python -m api.test.test_one来运行test_one

你也可以从examples中删除__init__.py(它不是一个Python包),并在安装了api的virtualenv中运行示例,例如,如果你有适当的setup.py, virtualenv中的pip install -e .将安装在api包中。

对于兄弟包导入,你可以使用[sys.path] [2]模块的插入附加方法:

if __name__ == '__main__' and if __package__ is None:
import sys
from os import path
sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
import api

如果你启动你的脚本,这将工作如下:

python examples/example_one.py
python tests/test_one.py

另一方面,你也可以使用相对导入:

if __name__ == '__main__' and if __package__ is not None:
import ..api.api

在这种情况下,你必须使用“- m”的论点启动你的脚本(注意,在这种情况下,你不能给出. py的扩展名):

python -m packageName.examples.example_one
python -m packageName.tests.test_one

当然,你可以混合使用这两种方法,这样你的脚本无论如何调用都能正常工作:

if __name__ == '__main__':
if __package__ is None:
import sys
from os import path
sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
import api
else:
import ..api.api

厌倦了系统。路径黑客吗?

有很多sys.path.append -hacks可用,但我发现了另一种解决问题的方法。

总结

  • 将代码打包到一个文件夹中(例如packaged_stuff)
  • 创建setup.py脚本,其中使用setuptools.setup ()。(参见下面的minimal setup.py)
  • pip install -e <myproject_folder>将包安装在可编辑状态
  • 使用from packaged_stuff.modulename import function_name导入

设置

起始点是您提供的文件结构,包装在名为myproject的文件夹中。

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

我将把.称为根文件夹,在我的示例中,它位于C:\tmp\test_imports\

api.py

作为测试用例,让我们使用下面的./api/api.py

def function_from_api():
return 'I am the return value from api.api!'

test_one.py

from api.api import function_from_api


def test_function():
print(function_from_api())


if __name__ == '__main__':
test_function()

尝试运行test_one:

PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
Traceback (most recent call last):
File ".\myproject\tests\test_one.py", line 1, in <module>
from api.api import function_from_api
ModuleNotFoundError: No module named 'api'

尝试相对导入也不会起作用:

使用from ..api.api import function_from_api会得到

PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
Traceback (most recent call last):
File ".\tests\test_one.py", line 1, in <module>
from ..api.api import function_from_api
ValueError: attempted relative import beyond top-level package

步骤

  1. 创建一个setup.py文件到根目录

setup.py的内容是*

from setuptools import setup, find_packages


setup(name='myproject', version='1.0', packages=find_packages())
  1. 使用虚拟环境

使用虚拟环境不是绝对所必需的,但它们将在长期内帮助你(当你有多个正在进行的项目..)。最基本的步骤是(在根文件夹中运行)

  • 创建虚拟环境
    • python -m venv venv
  • 激活虚拟环境
    • source ./venv/bin/activate (Linux, macOS)或./venv/Scripts/activate (Win)

要了解更多信息,请点击谷歌out“python virtual env tutorial"或类似的。除了创建、激活和取消激活之外,您可能永远不需要任何其他命令。

创建并激活虚拟环境后,控制台应该在括号中给出虚拟环境的名称

PS C:\tmp\test_imports> python -m venv venv
PS C:\tmp\test_imports> .\venv\Scripts\activate
(venv) PS C:\tmp\test_imports>

你的文件夹树应该看起来像这样**

.
├── myproject
│   ├── api
│   │   ├── api_key.py
│   │   ├── api.py
│   │   └── __init__.py
│   ├── examples
│   │   ├── example_one.py
│   │   ├── example_two.py
│   │   └── __init__.py
│   ├── LICENCE.md
│   ├── README.md
│   └── tests
│       ├── __init__.py
│       └── test_one.py
├── setup.py
└── venv
├── Include
├── Lib
├── pyvenv.cfg
└── Scripts [87 entries exceeds filelimit, not opening dir]
  1. PIP在可编辑状态下安装项目

使用pip安装顶层包myproject。诀窍是在安装时使用-e标志。通过这种方式,它被安装在可编辑状态下,对.py文件的所有编辑都将自动包含在已安装的包中。

在根目录下运行

pip install -e .(注意点,它代表"当前目录")

你也可以看到它是通过使用pip freeze安装的

(venv) PS C:\tmp\test_imports> pip install -e .
Obtaining file:///C:/tmp/test_imports
Installing collected packages: myproject
Running setup.py develop for myproject
Successfully installed myproject
(venv) PS C:\tmp\test_imports> pip freeze
myproject==1.0
  1. myproject.添加到导入中

注意,你只能将myproject.添加到导入中,否则导入无效。没有setup.py &pip install将工作仍然工作正常。请参见下面的示例。


测试解决方案

现在,让我们使用上面定义的api.py和下面定义的test_one.py来测试解决方案。

test_one.py

from myproject.api.api import function_from_api


def test_function():
print(function_from_api())


if __name__ == '__main__':
test_function()

运行测试

(venv) PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
I am the return value from api.api!

*参见setuptools文档获取更详细的setup.py示例。

**在现实中,您可以将虚拟环境放在硬盘上的任何地方。

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/callsib.py中定义的那样,即使sib2/callsib.py是通过sib1/call.py调用的。然而,如果直接调用sib1/call.py(在对导入进行适当更改之后),则会抛出异常。即使它在其父目录中被脚本调用时可以工作,但如果它认为自己位于包的顶层,它就不能工作。

我做了一个示例项目来演示如何处理这个问题,这实际上是另一个系统。路径hack如上所示。Python兄弟导入示例,它依赖于:

if __name__ == '__main__': import os import sys sys.path.append(os.getcwd())

只要你的工作目录保持在Python项目的根目录,这似乎是非常有效的。

  1. 项目

1.1 用户

1.1.1 about.py

1.1.2 < >强init < /强> . py

1.2 科技

1.2.1 info.py

1.1.2 < >强init < /强> . py

现在,如果你想从技术方案中的info.py 模块访问用户包中的about.py 模块,那么你必须将cmd(在windows中)路径带到项目中,即。 **C:\Users\Personal\Desktop\Project>**如上面的包示例所示。你必须从这个路径进入,python -m Package_name.module_name 例如,对于上面的Package,我们必须执行

c:\用户\个人\ desktop\ Project>

小鬼点

  1. 不要在信息模块后使用.py扩展名,即python -m Tech.info.py
  2. 输入this,其中兄弟包位于同一级别。
  3. -m是标志,要检查它,你可以从cmd python——帮助中输入

对于主要问题:

调用兄弟文件夹作为模块:

从. .进口siblingfolder

从兄弟文件夹调用a_file.py作为模块:

从. .兄弟文件夹导入a_file

在兄弟文件夹中的文件中调用a_function作为模块:

从. . siblingmodule。A_file import func_name_exists_in_a_file

最简单的方法。

进入lib/site-packages文件夹。

如果存在“easy_install.pth”文件,只需编辑它并添加您有脚本的目录,将其作为模块。

如果不存在,就把它变成一个…把你想要的文件夹放在那里

添加之后…, python会自动将该文件夹视为类似site-packages的文件夹,您可以将该文件夹或子文件夹中的每个脚本作为模块调用。

这是我用手机写的,很难设置成让每个人都能舒服地阅读。

我想对np8提供的解决方案进行评论,但我没有足够的声誉,所以我只想提到,你可以完全按照他们的建议创建一个setup.py文件,然后从项目根目录执行pipenv install --dev -e .,将其转化为可编辑的依赖项。然后你的绝对导入就可以工作了,比如from api.api import foo,你就不必在系统范围的安装上瞎折腾了。

文档

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

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

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

这是你的箱子mainScriptDepth = "../../"。然后,您可以通过从项目的根指定路径(from api.api import *)来导入。

如果使用pytest, pytest文档描述了如何从单独的测试包引用源包的方法。

建议的项目目录结构为:

setup.py
src/
mypkg/
__init__.py
app.py
view.py
tests/
__init__.py
foo/
__init__.py
test_view.py
bar/
__init__.py
test_view.py

setup.py文件的内容:

from setuptools import setup, find_packages


setup(name="PACKAGENAME", packages=find_packages())

可编辑的模式安装包:

pip install -e .

pytest文章引用这篇博文由Ionel Cristian matrie什撰写

对于2021年的读者:如果你对pip install -e 没有信心:

根据Python 3中的相对导入给出的答案,考虑这个层次结构:

MyProject
├── src
│   ├── bot
│   │   ├── __init__.py
│   │   ├── main.py
│   │   └── sib1.py
│   └── mod
│       ├── __init__.py
│       └── module1.py
└── main.py


main.py的内容,它是起点,我们在这里使用绝对的进口(没有前导点):

from src.bot import main




if __name__ == '__main__':
main.magic_tricks()

bot/main.py的内容,它利用了显式相对导入:

from .sib1 import my_drink                # Both are explicit-relative-imports.
from ..mod.module1 import relative_magic


def magic_tricks():
# Using sub-magic
relative_magic(in=["newbie", "pain"], advice="cheer_up")
    

my_drink()
# Do your work
...

原因如下:

  • 当执行python MyProject/main.py时,path/to/MyProject会被添加到sys.path中。
  • 绝对导入import src.bot将读取它。
  • from ..mod 部分意味着它将上升一层到MyProject/src
    • 我们能看看吗?是的,因为path/to/MyProject被添加到sys.path中。

所以重点是:

我们应该把主脚本旁边 MyProject/src,因为在进行相对引用时,我们不会会离开src,而绝对导入import src. 为我们提供了刚好合适作用域:src/作用域。

参见:ModuleNotFoundError:没有名为“sib1”的模块

存在的问题:

你根本不能让import mypackagetest.py中工作。你需要将editable install更改为path,或者更改为__name__path

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中导入之前执行上述操作。这是它。你现在可以import mypackage

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

为什么有效:

sys.path包含了在试图导入时,如果在已安装的站点包中没有找到包时,按顺序查找包的位置。当你运行test.py时,sys.path中的第一项将类似于/mnt/c/Users/username/Desktop/demo/dev,即:你运行文件的地方。联机程序将简单地将兄弟文件夹添加到路径和一切工作。你不必担心Windows和Linux的文件路径,因为我们正在只有编辑最后的文件夹名,而不是其他的。如果你的存储库的项目结构已经固定不变,我们也可以合理地使用神奇的数字3来切掉dev,取而代之的是src