兄弟姐妹包进口

时间:2011-06-12 18:42:51

标签: python packages python-import siblings

我试过阅读有关兄弟进口甚至是兄弟姐妹的问题 package documentation,但我还没有找到答案。

具有以下结构:

├── 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模块并从命令行运行?

另外,我想避免每个文件的丑陋sys.path.insert黑客攻击。一定 这可以在Python中完成,对吧?

16 个答案:

答案 0 :(得分:62)

厌倦了sys.path hacks?

有很多sys.path.append - 可用,但我找到了解决问题的另一种方法:setuptools。我不确定是否存在与此无关的边缘情况。以下是使用Python 3.6.5(Anaconda,conda 4.5.1),Windows 10计算机进行测试的。

设置

起点是您提供的文件结构,包含在名为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())

2)使用虚拟环境

如果您熟悉虚拟环境,请激活一个,然后跳到下一步。虚拟环境的使用不是绝对,但它们将真的帮助你从长远来看(当你有超过1个项目正在进行..)。最基本的步骤是(在根文件夹中运行)

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

要了解有关此内容的更多信息,请参阅Google的&#34; python虚拟环境教程&#34;或类似的。除了创建,激活和停用之外,您可能永远不需要任何其他命令。

制作并激活虚拟环境后,您的控制台应在括号中提供虚拟环境的名称

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]

3)将项目安装在可编辑状态

使用myproject安装顶级包pip。诀窍是在安装时使用-e标志。这样,它以可编辑状态安装,对.py文件所做的所有编辑将自动包含在已安装的包中。

在根目录中,运行

pip install -e .(注意点,它代表&#34;当前目录&#34;)

您还可以使用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

4)将myproject.添加到您的导入

请注意,您必须仅将myproject.添加到无法正常工作的导入中。没有setup.py&amp; 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!

*有关详细的setup.py示例,请参阅setuptools docs

**实际上,您可以将虚拟环境放在硬盘上的任何位置。

答案 1 :(得分:44)

后七年

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

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

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

旧答案

正如其他地方已经说过的那样,可怕的事实是你必须做丑陋的黑客攻击才能允许从__main__模块中导入兄弟模块或父包。该问题详见PEP 366PEP 3122试图以更合理的方式处理进口,但Guido拒绝了

的说法
  

唯一的用例似乎是运行脚本   住在模块的目录中,我一直认为这是一个目录   反模式。

here

尽管如此,我会定期使用此模式

# 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的父文件夹中)。

答案 2 :(得分:38)

这是我在tests文件夹中的Python文件顶部插入的另一种选择:

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

答案 3 :(得分:23)

除非有必要,否则你不需要也不应该攻击sys.path,在这种情况下它不是。使用:

import api.api_key # in tests, examples

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

您应该在tests内移动api(如果它们是api的单元测试)并运行python -m api.test以运行所有测试(假设有__main__.py)或{{1而是运行python -m api.test.test_one

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

答案 4 :(得分:7)

我还没有必要理解Pythonology,以便在没有兄弟/相对导入黑客的情况下看到在不相关项目之间共享代码的预期方式。直到那天,这是我的解决方案。要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

答案 5 :(得分:3)

对于兄弟姐妹包导入,您可以使用 [sys.path] [2] 模块的 insert 追加方法:

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' argument启动脚本(请注意,在这种情况下,您不得提供'。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

答案 6 :(得分:1)

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

import api.api

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

最简单的方法是在没有任何黑客攻击的情况下支持(如你所说)将从顶级目录运行示例,如下所示:

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

答案 7 :(得分:1)

以防有人在Eclipse上使用Pydev结束:你可以使用 Project-&gt ;属性并在左侧菜单 Pydev-PYTHONPATH 下设置外部库。然后你可以从兄弟姐妹那里导入,例如。 G。 from sibling import some_class

答案 8 :(得分:1)

TLDR

此方法不需要setuptools,path hacks,其他命令行参数或在项目的每个文件中指定软件包的顶层。

只需在要调用的__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 / callsib.py包含:

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

if __name__ == '__main__':
    CallSib()

如果重现此示例,则将注意到,即使Main.py是通过{{1}进行调用的,调用sib2/callsib.py也会导致按照sib2/callsib.py的定义打印“ Got Called”。 }。但是,如果要直接调用sib1/call.py(在对导入进行适当更改之后),它将引发异常。即使该脚本在其父目录中的脚本调用时有效,但如果它认为自己位于程序包的顶层,则它将无法正常工作。

答案 9 :(得分:1)

我制作了一个示例项目来演示如何处理此问题,这确实是如上所述的另一个sys.path hack。 Python Sibling Import Example,取决于:

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

这似乎很有效,只要您的工作目录保留在Python项目的根目录下即可。如果有人将其部署在实际的生产环境中,那么很高兴知道它在这里也可以正常工作。

答案 10 :(得分:1)

如果您使用的是 pytest,那么 pytest docs 描述了一种如何从单独的测试包中引用源包的方法。

建议的项目目录结构是:

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 文章引用了 this blog post by Ionel Cristian Mărieș

答案 11 :(得分:0)

我想评论np8提供的solution,但是我没有足够的声誉,因此我只想提一下,您可以按照他们的建议完全创建setup.py文件,然后执行{{ 1}},将其转换为可编辑的依赖项。然后您的绝对导入将起作用例如pipenv install --dev -e .,您不必搞乱系统范围的安装。

Documentation

答案 12 :(得分:-1)

  1. 项目

1.1 用户

1.1.1 about.py

1.1.2 初始化 .py

1.2 技术

1.2.1 info.py

1.1.2 初始化 .py

现在,如果要访问用户包中的 about.py 模块,请从 info.py 技术包中的em> 模块,那么您必须将cmd(在Windows中)路径带到Project中,即 ** C:\ Users \ Personal \ Desktop \ Project> **,如上述Package示例所示。在此路径中,您必须输入 python -m Package_name.module_name 例如,对于上述包,我们必须要做的

C:\ Users \ Personal \ Desktop \ Project> python -m Tech.info

展示点

  1. info模块之后不要使用.py扩展名,即python -m Tech.info.py
  2. 在同级软件包位于同一级别的情况下输入此内容。
  3. -m是标志,要检查它,您可以从cmd python --help
  4. 中键入

答案 13 :(得分:-1)

主要问题:

将同级文件夹作为模块:

从..导入同级文件夹

从同级文件夹中调用a_file.py作为模块:

从..siblingfolder导入a_file

在同级文件夹中的文件内调用a_function作为模块:

from..siblingmodule.a_file导入func_name_exists_in_a_file

最简单的方法。

转到lib / site-packages文件夹。

如果存在“ easy_install.pth”文件,则只需对其进行编辑并添加目录,即可将其作为模块添加到脚本中。

如果不存在,则将其设置为一个...然后将所需的文件夹放在那里

添加后...,python将自动将其视为类似于站点包的文件夹,并且您可以将该文件夹或子文件夹中的每个脚本作为模块调用。

我是用手机写的,很难设置它以使每个人都容易阅读。

答案 14 :(得分:-1)

在您的主文件中添加:

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

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

这是您的情况 mainScriptDepth = "../../"。然后,您可以通过指定项目根目录的路径 (from api.api import * ) 进行导入。

答案 15 :(得分:-3)

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

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

examplestests内,您可以致电:

from ..api import api