如何组织一个包含多个包的 python 项目,以便包中的每个文件仍然可以单独运行?

Posted

技术标签:

【中文标题】如何组织一个包含多个包的 python 项目,以便包中的每个文件仍然可以单独运行?【英文标题】:How do you organise a python project that contains multiple packages so that each file in a package can still be run individually? 【发布时间】:2017-01-24 12:29:21 【问题描述】:

TL;DR

这是一个示例存储库,其设置如第一张图(下图)所述:https://github.com/Poddster/package_problems

如果您可以让它在项目组织方面看起来像第二张图并且仍然可以运行以下命令,那么您已经回答了这个问题:

$ git clone https://github.com/Poddster/package_problems.git
$ cd package_problems
<do your magic here>

$ nosetests

$ ./my_tool/my_tool.py
$ ./my_tool/t.py
$ ./my_tool/d.py

 (or for the above commands, $ cd ./my_tool/ && ./my_tool.py is also acceptable)

或者:给我一个不同的项目结构,允许我将相关文件(“包”)组合在一起,单独运行所有文件,将文件导入同一包中的其他文件,并将包/文件导入到其他包的文件。


现状

我有一堆 python 文件。它们中的大多数在可从命令行调用时很有用,即它们都使用 argparse 和 if __name__ == "__main__" 来做有用的事情。

目前我有这个目录结构,一切正常:

.
├── config.txt
├── docs/
│   ├── ...
├── my_tool.py
├── a.py
├── b.py
├── c.py
├── d.py
├── e.py
├── README.md
├── tests
│   ├── __init__.py
│   ├── a.py
│   ├── b.py
│   ├── c.py
│   ├── d.py
│   └── e.py
└── resources
    ├── ...

一些脚本import 来自其他脚本的东西来完成它们的工作。但是没有脚本仅仅是一个库,它们都是可调用的。例如我可以调用./my_tool.py./a.by./b.py./c.py 等,它们会为用户做有用的事情。

“my_tool.py”是利用所有其他脚本的主脚本。

我想要发生的事情

但是我想改变项目的组织方式。该项目本身代表了一个可供用户使用的整个程序,并将按原样分发,但我知道它的一部分将在以后的不同项目中有用,所以我想尝试将当前文件封装到一个包中。在不久的将来,我还将在同一个项目中添加其他包。

为了促进这一点,我决定将项目重新组织为以下内容:

.
├── config.txt
├── docs/
│   ├── ...
├── my_tool
│   ├── __init__.py
│   ├── my_tool.py
│   ├── a.py
│   ├── b.py
│   ├── c.py
│   ├── d.py
│   ├── e.py
│   └── tests
│       ├── __init__.py
│       ├── a.py
│       ├── b.py
│       ├── c.py
│       ├── d.py
│       └── e.py
├── package2
│   ├── __init__.py
│   ├── my_second_package.py
|   ├── ...
├── README.md
└── resources
    ├── ...

但是,我想不出满足以下条件的项目组织:

    所有脚本都可以在命令行中调用(my_tool\a.pycd my_tool &amp;&amp; a.py) 测试实际运行:) package2 中的文件可以做import my_tool

主要问题在于包和测试使用的导入语句。

目前,包括测试在内的所有包都只需执行import &lt;module&gt; 即可正确解析。但是,当在它周围晃动东西时,它就不起作用了。

请注意,支持 py2.7 是一项要求,因此所有文件的顶部都有 from __future__ import absolute_import, ...

我的尝试,以及灾难性的结果

1

如果我如上所示移动文件,但将所有导入语句保留为当前状态:

    $ ./my_tool/*.py 工作正常,它们都运行正常 $ nosetests 从顶层目录运行不起作用。测试无法导入包脚本。 pycharm 在编辑这些文件时以红色突出显示导入语句 :(

2

如果我将测试脚本更改为:

from my_tool import x
    $ ./my_tool/*.py 仍然可以正常运行 $ nosetests 从顶层目录运行不起作用。然后测试可以导入正确的脚本,但是当测试脚本导入它们时,脚本本身的导入会失败。 pycharm 在主脚本中仍以红色突出显示导入语句 :(

3

如果我保持相同的结构并将 everything 更改为 from my_tool import 那么:

    $ ./my_tool/*.py 导致 ImportErrors $ nosetests 一切正常。 pycharm 不会抱怨任何事情

例如1 个:

Traceback (most recent call last):
  File "./my_tool/a.py", line 34, in <module>
    from my_tool import b
ImportError: cannot import name b

4

我也尝试过from . import x,但最终还是使用ValueError: Attempted relative import in non-package 直接运行脚本。

查看其他一些 SO 答案:

我不能只使用python -m pkg.tests.core_test作为

a) 我没有 ma​​in.py。我想我可以拥有一个? b) 我希望能够运行所有脚本,而不仅仅是主脚本?

我试过了:

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

但这没有帮助。

我也试过了:

__package__ = "my_tool"
from . import b

但收到了:

SystemError: Parent module 'loading_tool' not loaded, cannot perform relative import

from . import b 之前添加import my_tool 会以ImportError: cannot import name b 结束

修复?

什么是正确的魔法咒语和目录布局来完成所有这些工作?

【问题讨论】:

如果你在包内阅读,你应该使用 .module_name 让解释器(和用户)知道它是本地的。如果要在资源中导入 my_tool,请添加 from .my_tool import submodule_name my_second_package.py 这个名称是否具有误导性,因为 my_second_package 是一个模块?也许my_second_package.py 应该命名为another_mod.py。然后您可以创建另一个目录和文件,例如:package2/my_third_package/__init__.py,这实际上是package2 包中的第三个包。 【参考方案1】:

一旦您移至所需的配置,您用于加载特定于 my_tool 的模块的绝对导入将不再起作用。

创建my_tool 子目录并将文件移入其中后,您需要进行三处修改:

    创建my_tool/__init__.py。 (您似乎已经这样做了,但为了完整起见,我想提一下。)

    my_tool 直接下的文件中:更改import 语句以从当前包中加载模块。所以在my_tool.py改:

    import c
    import d
    import k
    import s
    

    到:

    from . import c
    from . import d
    from . import k
    from . import s
    

    您需要对所有其他文件进行类似的更改。 (您提到尝试设置__package__,然后进行相对导入,但不需要设置__package__。)

    在位于my_tool/tests 的文件中:将导入要测试的代码的import 语句更改为从层次结构中的一个包向上加载的相对导入。所以在test_my_tool.py改:

    import my_tool
    

    到:

    from .. import my_tool
    

    对于所有其他测试文件也是如此。

通过上面的修改,我可以直接运行模块了:

$ python -m my_tool.my_tool
C!
D!
F!
V!
K!
T!
S!
my_tool!
my_tool main!
|main tool!||detected||tar edit!||installed||keys||LOL||ssl connect||parse ASN.1||config|

$ python -m my_tool.k
F!
V!
K!
K main!
|keys||LOL||ssl connect||parse ASN.1|

我可以运行测试:

$ nosetests 
........
----------------------------------------------------------------------
Ran 8 tests in 0.006s

OK

请注意,我可以同时使用 Python 2.7 和 Python 3 运行上述程序。


我建议不要让my_tool下的各个模块直接可执行,我建议使用适当的setup.py文件来声明入口点,并让setup.py在安装包时创建这些入口点。由于您打算分发此代码,因此无论如何您都应该使用setup.py 来正式打包它。

    修改可以从命令行调用的模块,以my_tool/my_tool.py为例,而不是这样:

    if __name__ == "__main__":
        print("my_tool main!")
        print(do_something())
    

    你有:

    def main():
        print("my_tool main!")
        print(do_something())
    
    if __name__ == "__main__":
        main()
    

    创建一个包含正确entry_pointssetup.py 文件。例如:

    from setuptools import setup, find_packages
    
    setup(
        name="my_tool",
        version="0.1.0",
        packages=find_packages(),
        entry_points=
            'console_scripts': [
                'my_tool = my_tool.my_tool:main'
            ],
        ,
        author="",
        author_email="",
        description="Does stuff.",
        license="MIT",
        keywords=[],
        url="",
        classifiers=[
        ],
    )
    

    上面的文件指示setup.py 创建一个名为my_tool 的脚本,该脚本将调用模块my_tool.my_tool 中的main 方法。在我的系统上,一旦安装了软件包,就会有一个位于/usr/local/bin/my_tool 的脚本调用my_tool.my_tool 中的main 方法。它产生与运行python -m my_tool.my_tool 相同的输出,如上所示。

【讨论】:

谢谢,这符合所有要求。我不热衷于将其调用为 python -m my_tool.my_tool,因为它不是 shell 友好的(即没有制表符完成,没有遵守 +x 权限)。你没有提到它,但它也适用于同一项目中加载my_tool 的其他包,假设它们也被python -m my_second_package.blah 所激发。您对更适合 shell 的版本有什么建议吗?如果没有,我可能会从@CasualDemon 的答案中借用一点,即在直接调用它们时捕获 ImportError,或者只是制作一个前端脚本,将脚本作为 arg 运行。 我已经编辑了我的答案以包含一种解决您问题的方法。 我认为这是正确的做法。我想添加如何将软件包安装为可编辑:pip -e path/to/SomeProjectlink。这允许您根据需要保持目录结构,但您仍然可以将模块作为任何其他模块导入。 我想我正在做的事情有点疯狂,但这是最接近的,而且这样做的方式不会让人觉得我在做淘气的事情。谢谢:)【参考方案2】:

要从命令行运行它并像库一样运行它,同时允许nosetest 以标准方式运行,我相信您必须对Imports 采取双重方法。

例如,Python 文件将需要:

try:
    import f
except ImportError:
    import tools.f as f

我在你链接的所有测试用例工作的 github 上做了一个 PR。

https://github.com/Poddster/package_problems/pull/1

编辑:忘记了 __init__.py 中的导入,以便在其他包中正确使用,添加。现在应该可以做到:

import tools
tools.c.do_something()

【讨论】:

不确定否决票的用途:它不符合我要求的标准,包括被调用为tool/blah.py。你甚至使用了我制作的测试仓库!但是它没有解决从其他包导入 my_tool 的问题。 (即如果 tools/ 和 other_pkg/ 是兄弟姐妹,我希望能够在 other_pkg 中 import tools。虽然它显示在第二张图中,但我并没有在我的 OP 中真正指定) 哎呀,忘记了 __init__.py 中的导入,已修复并更新了 PR。 @pod 你能用编辑重新测试吗?有什么问题吗? 我刚刚尝试过它们,但恐怕它们没有帮助。执行my_other_package/whatever.py 会导致导入错误。但是,如果我从另一个答案中借用并拒绝使用“。”的导入路径。然后就可以了。【参考方案3】:

第 1 点

我相信它有效,所以我不评论它。

第 2 点

我总是使用与 my_tool 相同级别的测试,而不是低于它,但如果您在每个测试文件的顶部执行此操作(在导入 my_tool 或同一目录中的任何其他 py 文件之前),它们应该可以工作

import os
import sys

sys.path.insert(0, os.path.abspath(__file__).rsplit(os.sep, 2)[0])

第 3 点

在 my_second_package.py 的顶部执行此操作(在导入 my_tool 之前)

import os
import sys

sys.path.insert(0,
                os.path.abspath(__file__).rsplit(os.sep, 2)[0] + os.sep
                + 'my_tool')

最好的问候,

JM

【讨论】:

它可以工作,尽管 pycharm 仍然认为模块不存在,即使在修改了它的项目结构配置之后也是如此。注意:我仍然打算通过import my_tool.whatever而不是简单的import whatever将my_tool导入my_second_package,但我所要做的就是不将“/my_tool”添加到路径中,因此point3与point 2共享相同的代码。跨度> 这充其量只是 hacky - 不要乱用 os.path

以上是关于如何组织一个包含多个包的 python 项目,以便包中的每个文件仍然可以单独运行?的主要内容,如果未能解决你的问题,请参考以下文章

组织模块和包的 Pythonic 方式

包含多个项目发布版本的 Git (EGit) 存储库的组织

如何在不同文件夹中组织的多个模块的项目中在 python 中进行导入?

如何在 conda 包中设置环境变量,以便在激活包含该包的环境时设置它们?

python模块布局

如何组织一个有计划的冲刺项目?