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

How do you organise a python project that contains multiple packages so that each file in a package can still be run individually?

DR

下面是一个示例存储库,它的设置如第一个图表(如下)中所述:https://github.com/poddster/package_problems

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

1
2
3
4
5
6
7
8
9
10
11
$ 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__"来做有用的事情。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.
├── config.txt
├── docs/
│&nbsp;&nbsp; ├── ...
├── my_tool.py
├── a.py
├── b.py
├── c.py
├── d.py
├── e.py
├── README.md
├── tests
│&nbsp;&nbsp; ├── __init__.py
│&nbsp;&nbsp; ├── a.py
│ &nbsp; ├── b.py
│&nbsp;&nbsp; ├── c.py
│&nbsp;&nbsp; ├── d.py
│&nbsp;&nbsp; └── e.py
└── resources
    ├── ...

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

"my-tool.py"是利用所有其他脚本的主脚本。

我想发生的事

不过,我想改变项目的组织方式。项目本身代表了一个可供用户使用的完整程序,并将按此方式分发,但我知道它的某些部分在以后的不同项目中会很有用,因此我希望尝试将当前文件封装到一个包中。在不久的将来,我还将向同一个项目添加其他包。

为了促进这一点,我决定将该项目重新组织成如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.
├── config.txt
├── docs/
│&nbsp;&nbsp; ├── ...
├── my_tool
│&nbsp;&nbsp; ├── __init__.py
│&nbsp;&nbsp; ├── my_tool.py
│&nbsp;&nbsp; ├── a.py
│&nbsp;&nbsp; ├── b.py
│&nbsp;&nbsp; ├── c.py
│&nbsp;&nbsp; ├── d.py
│&nbsp;&nbsp; ├── e.py
│&nbsp;&nbsp; └── tests
│&nbsp;&nbsp;     ├── __init__.py
│&nbsp;&nbsp;     ├── a.py
│&nbsp;&nbsp;   &nbsp; ├── b.py
│&nbsp;&nbsp;     ├── c.py
│&nbsp;&nbsp;     ├── d.py
│&nbsp;&nbsp;     └── e.py
├── package2
│&nbsp;&nbsp; ├── __init__.py
│&nbsp;&nbsp; ├── my_second_package.py
|   ├── ...
├── README.md
└── resources
    ├── ...

但是,我无法确定一个符合以下标准的项目组织:

  • 所有脚本都可以在命令行上调用(作为my_tool\a.pycd my_tool && a.py)
  • 测试实际运行:)
  • 包2中的文件可以执行import my_tool
  • 主要问题是包和测试使用的导入语句。

    目前,所有的软件包,包括测试,都只是简单地执行import ,并且它得到了正确的解决。但是,当周围的东西摇摆不定的时候,它就不起作用了。

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

    我所做的努力,以及灾难性的结果一

    如果按上面所示移动文件,但保留所有导入语句的当前状态:

  • $ ./my_tool/*.py工作正常
  • 从顶层目录运行$ nosetests不起作用。测试无法导入包脚本。
  • pycharm在编辑这些文件时以红色突出显示导入语句:(
  • 如果我随后将测试脚本更改为:

    1
    from my_tool import x
  • $ ./my_tool/*.py仍能正常运行。
  • 从顶层目录运行$ nosetests不起作用。然后测试可以导入正确的脚本,但是当测试脚本导入脚本时,脚本本身中的导入会失败。
  • pycharm在主脚本中以红色突出显示import语句:(
  • 如果我保持相同的结构,将所有内容都改为from my_tool import,那么:

  • $ ./my_tool/*.py导致ImportErrors
  • $ nosetests运行正常。
  • Pycharm什么都不抱怨
  • 例如1。

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

    我也尝试过from . import x,但最终还是直接运行脚本。

    看看其他答案:

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

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

    我试过了:

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

    但没用。

    我也尝试过:

    1
    2
    __package__ ="my_tool"
    from . import b

    但收到:

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

    from . import b之前加上import my_tool,结果与ImportError: cannot import name b相反。

    修理?

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


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

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

  • 创建my_tool/__init__.py。(您似乎已经这样做了,但我想完整地介绍一下。)

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

    1
    2
    3
    4
    import c
    import d
    import k
    import s

    到:

    1
    2
    3
    4
    from . import c
    from . import d
    from . import k
    from . import s

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

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

    1
    import my_tool

    到:

    1
    from .. import my_tool

    与所有其他测试文件类似。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    $ 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|

    我可以运行测试:

    1
    2
    3
    4
    5
    6
    $ nosetests
    ........
    ----------------------------------------------------------------------
    Ran 8 tests in 0.006s

    OK

    请注意,我可以用python 2.7和python 3来运行上面的代码。

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

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

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

    你有:

    1
    2
    3
    4
    5
    6
    def main():
        print("my_tool main!")
        print(do_something())

    if __name__ =="__main__":
        main()
  • 创建一个包含正确的entry_pointssetup.py文件。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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相同的输出,我在上面已经显示了这一点。


  • 第1点

    我相信这是可行的,所以我不作评论。

    第2点

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

    1
    2
    3
    4
    import os
    import sys

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

    第3点

    在my_second_package.py中,在顶部执行此操作(在导入我的_工具之前)

    1
    2
    3
    4
    5
    6
    import os
    import sys

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

    最好的问候,

    JM


    要从命令行运行它,并像库一样运行它,同时允许nosetest以标准方式运行,我相信您必须对导入进行双重处理。

    例如,python文件需要:

    1
    2
    3
    4
    try:
        import f
    except ImportError:
        import tools.f as f

    我在Github上做了一个公关,你链接到所有的测试用例。

    https://github.com/poddster/package_问题/pull/1

    编辑:忘记了__init__.py中的导入可以在其他包中正常使用,补充说。现在应该能够做到:

    1
    2
    import tools
    tools.c.do_something()