【问题标题】:Calling script in standard project directory structure (Python path for bin subdirectory)标准项目目录结构中的调用脚本(bin 子目录的 Python 路径)
【发布时间】:2016-10-12 16:03:37
【问题描述】:

我正在尝试将我的 Python 代码放入用于使用 setup.py 和 PyPI 进行部署的标准目录结构中。对于名为 mylib 的 Python 库,它会是这样的:

mylibsrc/
  README.rst
  setup.py
  bin/
    some_script.py
  mylib/
    __init.py__
    foo.py

通常还有一个test/ 子目录,但我还没有尝试编写单元测试。在official Python packaging documentation 中可以找到将脚本放在bin/ 子目录中的建议。

当然,脚本开头的代码如下所示:

#!/usr/bin/env python
from mylib.foo import something
something("bar")

这在最终部署脚本(例如到 devpi)然后使用 pip 安装时效果很好。但是,如果我直接从源目录运行脚本,就像在开发库/脚本的新更改时那样,我会收到以下错误:

ImportError: No module named 'mylib'

即使当前工作目录是根 mylibsrc/ 并且我通过键入 ./bin/some_script.py 运行脚本也是如此。这是因为 Python 开始在正在运行的脚本的目录(即来自 bin/)中搜索包,而不是当前工作目录。

在开发包时,有什么好的、永久的方法可以让脚本运行变得容易?

这是relevant other question(尤其是第一个答案的cmets)。

到目前为止,我发现的解决方案分为三类,但没有一个是理想的:

  1. 在运行脚本之前以某种方式手动修复 Python 的模块搜索路径。
    • 您可以手动将mylibsrc 添加到我的PYTHONPATH 环境变量中。这似乎是最官方的(Pythonic?)解决方案,但这意味着每次我签出一个项目时,我都必须记住手动更改我的环境,然后才能在其中运行任何代码。
    • . 添加到我的PYTHONPATH 环境变量的开头。据我了解,这可能存在一些安全问题。如果我是唯一一个使用我的代码的人,这实际上是我最喜欢的伎俩,但我不是,而且我不想要求其他人这样做。
    • 在互联网上查看答案时,对于 test/ 目录中的文件,我看到建议它们都(间接)包含一行代码 sys.path.insert(0, os.path.abspath('..'))(例如在 structuring your project 中)。呸!对于仅用于测试的文件,而不是那些将与软件包一起安装的文件,这似乎是一个可以忍受的 hack。
    • 编辑:我后来找到了一个替代方案,结果证明属于这一类:通过使用 Python 的 -m 脚本运行脚本,搜索路径从工作目录而不是 bin/ 目录开始。有关详细信息,请参阅下面的答案。
  2. 使用 setup.py(直接运行或使用 pip)在使用前将包安装到虚拟环境。
    • 如果我只是在测试一个我甚至不确定在语法上是否正确的更改,这似乎有点过头了。我正在处理的一些项目甚至不打算作为包安装,但我想对所有内容使用相同的目录结构,这意味着编写一个 setup.py 以便我可以测试它们!
    • 编辑:下面的答案中讨论了这两个有趣的变体:logc 的答案中的setup.py develop 命令和我的pip install -e。他们避免了每次小编辑都必须重新“安装”,但您仍然需要为您从未打算完全安装的软件包创建一个setup.py,并且与 PyCharm(它有一个菜单项可以运行develop 命令,但没有简单的方法来运行它复制到虚拟环境的脚本)。
  3. 将脚本移动到项目的根目录(即在mylibsrc/ 中而不是mylibsrc/bin/ 中)。
    • 糟糕!这是最后的手段但不幸的是,这似乎是目前唯一可行的选择

【问题讨论】:

    标签: python setup.py


    【解决方案1】:

    将模块作为脚本运行

    自从我发布这个问题以来,我了解到您可以使用 Python 的 -m 命令行开关(我原以为仅适用于包)运行一个模块,就像它是一个脚本一样。

    所以我认为最好的解决方案是:

    • 不要在 bin 子目录中编写包装脚本,而是将大部分逻辑放在模块中(无论如何都应该这样做),并像在脚本中那样放在相关模块的末尾 if __name__ == "__main__": main()
    • 要在命令行上运行脚本,直接调用模块如下:python -m pkg_name.module_name
    • 如果您有 setup.py,正如 Alik 所说,您可以在安装时生成包装脚本,这样您的用户就不需要以这种有趣的方式运行它们。

    PyCharm 不支持以这种方式运行模块(请参阅this request)。但是,您可以像往常一样运行模块(以及 bin 中的脚本),因为 PyCharm 会自动将项目根添加到 PYTHONPATH,因此导入语句无需任何进一步的努力即可解决。不过,有一些陷阱:

    • 主要问题是工作目录不正确,因此无法打开数据文件。不幸的是,没有快速解决办法。第一次运行每个脚本时,您必须停止它并更改其配置的工作目录(请参阅this link)。
    • 如果你的包目录不直接在项目根目录下,则需要在项目结构设置页面中将其父目录标记为源目录。
    • 相对导入不起作用,即您可以使用from pkg_name.other_module import fn,但不能使用from .other_module import fn。无论如何,相对导入通常都是糟糕的风格,但它们对于单元测试很有用。
    • 如果一个模块有循环依赖并且你直接运行它,它最终会被导入两次(一次是pkg_name.module_name,一次是__main__)。但无论如何,你不应该有循环依赖。

    额外的命令行乐趣:

    • 如果您仍想将一些脚本放在 bin/ 中,可以使用 python -m bin.scriptname 调用它们(但在 Python 2 中,您需要将 __init__.py 放在 bin 目录中)。
    • 你甚至可以运行整个包,如果它有一个__main__.py,像这样:python -m pkg_name

    点子可编辑模式

    命令行有一个替代方案,虽然没那么简单,但还是值得了解的:

    • 使用 pip 的可编辑模式,documented here
    • 要使用它,请创建一个 setup.py,然后使用以下命令将包安装到您的虚拟环境中:pip install -e .
    • 注意尾随点,它指的是当前目录。
    • 这会将 setup.py 生成的脚本放在虚拟环境的 bin 目录中,并链接到您的包源代码,以便您无需重新运行 pip 即可对其进行编辑和调试。
    • 完成后,您可以运行pip uninstall pkg_name
    • 这类似于setup.pydevelop 命令,但卸载似乎效果更好。

    【讨论】:

    • 我不明白这个解决方案是如何工作的,你有一个最终结构的例子,你应该让代码在 PyCharm 中工作并尊重bin 文件夹项目结构?
    • @RobertoLeinardi 对于项目结构,我使用我在问题中所述的结构。正如我在这个答案中所说,PyCharm 默认应该找到包,因为它默认将项目目录添加到PYTHONPATH(只需右键单击脚本并选择“运行”或“调试”)。如果你的 PyCharm 项目是 Python 项目的父目录(例如,你可以同时看到几个不同的项目),你可以进入设置对话框,在“项目结构”页面,右键单击项目目录并单击"sources" 将其标记为源根目录。
    【解决方案2】:

    最简单的方法是在您的setup.py 脚本中使用setuptools,并使用entry_points 关键字,请参阅Automatic Script Creation 的文档。

    更详细地说:您创建一个看起来像这样的setup.py

    from setuptools import setup
    
    setup(
        # other arguments here...
        entry_points={
            'console_scripts': [
                'foo = my_package.some_module:main_func',
                'bar = other_module:some_func',
            ],
            'gui_scripts': [
                'baz = my_package_gui:start_func',
            ]
        }
    )
    

    然后在 setup.py 所在的目录下创建其他 Python 包和模块,例如按照上面的例子:

    .
    ├── my_package
    │   ├── __init__.py
    │   └── some_module.py
    ├── my_package_gui
    │   └── __init__.py
    ├── other_module.py
    └── setup.py
    

    然后运行

    $ python setup.py install
    

    $ python setup.py develop
    

    无论哪种方式,都会为您创建指向您在setup.py 中描述的入口点的新 Python 脚本(不带 .py 后缀的可执行脚本)。通常,它们处于 Python 解释器的“可执行二进制文件应该所在的目录”的概念中,这通常已经在您的 PATH 上。如果您使用的是虚拟环境,那么virtualenv 会诱使 Python 解释器认为该目录是 bin/,在您定义虚拟环境的任何位置下。按照上面的示例,在 virtualenv 中,运行前面的命令应该会导致:

    bin
    ├── bar
    ├── baz
    └── foo
    

    【讨论】:

    • 感谢您的详细回答。关于entry_points 的内容很有趣,但忽略了重点:我不想安装一个软件包只是为了尝试每一个微小的实验性更改(实际上它使情况变得更糟!)。但是您在传递一个真正的解决方案时提到了:setup.py develop 命令。这适用于entry_points 和常规脚本。遗憾的是,它要求您创建一个setup.py,即使是您从未打算正确安装的软件包,并且不容易从像 PyCharm 这样的 IDE 访问。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-08-05
    • 2016-01-31
    • 1970-01-01
    • 2020-12-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多