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

Pod*_*Pod 28 python import packages pycharm python-2.7

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)
Run Code Online (Sandbox Code Playgroud)

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


现在的情况

我有一堆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
    ??? ...
Run Code Online (Sandbox Code Playgroud)

一些脚本的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
    ??? ...
Run Code Online (Sandbox Code Playgroud)

但是,我无法找出满足以下条件的项目组织:

  1. 所有脚本都可以在命令行上调用(或者作为my_tool\a.pycd my_tool && a.py)
  2. 测试实际运行:)
  3. package2中的文件可以做 import my_tool

主要问题是包和测试使用的import语句.

目前,包括测试在内的所有软件包都import <module>可以正常完成并正确解析.但是当它周围的东西摇摆不起作用时.

请注意,支持py2.7是必需的,因此所有文件都from __future__ import absolute_import, ...位于顶部.

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

1

如果我如上所示移动文件,但保留当前所有的import语句:

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

2

如果我然后更改测试脚本:

from my_tool import x
Run Code Online (Sandbox Code Playgroud)
  1. $ ./my_tool/*.py 仍然有效,他们都运行正常
  2. $ nosetests从顶级目录运行不起作用.然后测试可以导入正确的脚本,但是当测试脚本导入时,脚本中的导入本身会失败.
  3. pycharm仍然在主脚本中突出显示红色的import语句:(

3

如果我保持相同的结构并改变一切,from my_tool import那么:

  1. $ ./my_tool/*.py导致ImportErrors
  2. $ nosetests 运行一切都好.
  3. 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
Run Code Online (Sandbox Code Playgroud)

4

我也试过from . import x但最终只能ValueError: Attempted relative import in non-package直接运行脚本.

看看其他一些SO答案:

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

a)我没有 .py.我想我可以买一个?
b)我希望能够运行所有脚本,而不仅仅是main?

我试过了:

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

但它没有帮助.

我也尝试过:

__package__ = "my_tool"
from . import b
Run Code Online (Sandbox Code Playgroud)

但收到:

SystemError: Parent module 'loading_tool' not loaded, cannot perform relative import
Run Code Online (Sandbox Code Playgroud)

import my_toolfrom . import b最终回来之前添加ImportError: cannot import name b

固定?

什么是正确的魔法咒语和目录布局,以使所有这些工作?

Lou*_*uis 14

移动到所需配置后,您用于加载特定于my_tool不再有效的模块的绝对导入.

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

  1. 创造my_tool/__init__.py.(你似乎已经这样做但我想提及它的完整性.)

  2. 在直接位于的文件中my_tool:更改import语句以从当前包加载模块.所以my_tool.py变化:

    import c
    import d
    import k
    import s
    
    Run Code Online (Sandbox Code Playgroud)

    至:

    from . import c
    from . import d
    from . import k
    from . import s
    
    Run Code Online (Sandbox Code Playgroud)

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

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

    import my_tool
    
    Run Code Online (Sandbox Code Playgroud)

    至:

    from .. import my_tool
    
    Run Code Online (Sandbox Code Playgroud)

    同样适用于所有其他测试文件.

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

$ 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|
Run Code Online (Sandbox Code Playgroud)

我可以运行测试:

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

OK
Run Code Online (Sandbox Code Playgroud)

请注意,我可以使用Python 2.7和Python 3运行上述两者.


my_tool我建议使用适当的setup.py文件来声明入口点,然后setup.py在安装软件包时创建这些入口点,而不是使各个模块可以直接执行.由于您打算分发此代码,因此您应该使用a setup.py来正式打包它.

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

    if __name__ == "__main__":
        print("my_tool main!")
        print(do_something())
    
    Run Code Online (Sandbox Code Playgroud)

    你有:

    def main():
        print("my_tool main!")
        print(do_something())
    
    if __name__ == "__main__":
        main()
    
    Run Code Online (Sandbox Code Playgroud)
  2. 创建setup.py包含正确的文件entry_points.例如:

    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=[
        ],
    )
    
    Run Code Online (Sandbox Code Playgroud)

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

  • 我认为这是正确的做法.我想添加如何将软件包安装为可编辑:`pip -e path/to/SomeProject` [link](https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs) .这允许保留您的目录结构,但您仍然可以像任何其他模块一样导入模块. (2认同)