在标准项目目录结构中调用脚本(Python bin 子目录的路径)

Calling script in standard project directory structure (Python path for bin subdirectory)

我正在尝试将我的 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 安装它时效果很好。但是如果我直接从源目录 运行 脚本,就像我在对 library/script 开发新的更改时所做的那样,我会得到这个错误:

ImportError: No module named 'mylib'

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

什么是在开发包时使 运行 脚本变得容易的永久性好方法?

这是一个relevant other question(特别是对第一个答案的评论)。

到目前为止我找到的解决方案分为三类,但其中 none 是理想的:

  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。他们避免为每个小编辑重新 "install",但您仍然需要为您从未打算完全安装的软件包创建一个 setup.py,并且与 PyCharm 不能很好地配合使用(它有一个 运行 develop 命令的菜单条目,但没有简单的方法 运行 它复制到虚拟环境的脚本。
  3. 将脚本移动到项目的根目录(即在 mylibsrc/ 而不是 mylibsrc/bin/ 中)。
    • 呸!这是不得已的办法,但不幸的是,这似乎是目前唯一可行的选择

最简单的方法是在你的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

无论哪种方式,都会为您创建新的 Python 脚本(不带 .py 后缀的可执行脚本),指向您在 setup.py 中描述的入口点。通常,它们位于 Python 解释器的 "directory where executable binaries should be" 概念中,这通常已经在您的 PATH 中。如果你使用的是虚拟环境,那么 virtualenv 会欺骗 Python 解释器认为这个目录是 bin/ 在你定义的 virtualenv 应该在的任何地方。按照上面的示例,在 virtualenv 中,运行ning 前面的命令应该导致:

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

运行 模块作为脚本

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

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

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

PyCharm 不以这种方式支持 运行ning 模块(参见 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 可编辑模式

命令行有一个替代方法,虽然不那么简单,但仍然值得了解:

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