如何在 Python 项目中正确构建内部脚本?

Yuv*_*dam 12 python pythonpath python-3.x

考虑以下 Python 项目框架:

proj/
??? foo
?   ??? __init__.py
??? README.md
??? scripts
    ??? run.py
Run Code Online (Sandbox Code Playgroud)

在这种情况下foo保存主项目文件,例如

# foo/__init__.py
class Foo():
    def run(self):
        print('Running...')
Run Code Online (Sandbox Code Playgroud)

scripts保存需要从 导入文件的辅助脚本,foo然后通过以下方式调用:

[~/proj]$ python scripts/run.py
Run Code Online (Sandbox Code Playgroud)

有两种导入方式Foo都失败了:

  1. 如果尝试进行相对导入,from ..foo import Foo则错误为ValueError: attempted relative import beyond top-level package
  2. 如果尝试绝对导入,from foo import Foo则错误为ModuleNotFoundError: No module named 'foo'

我目前的解决方法是将运行路径附加到sys.path

import sys
sys.path.append('.')

from foo import Foo
Foo().run()
Run Code Online (Sandbox Code Playgroud)

但这感觉就像一个黑客,必须添加到scripts/.

有没有更好的方法来构建此类项目中的脚本?

mat*_*cik 9

有两种方法可以解决这个问题。

(1) 把你的项目变成一个可安装的包

添加一个proj/setup.py包含以下内容的文件:

import setuptools

setuptools.setup(
    name="my-project",
    version="1.0.0",
    author="You",
    author_email="you@example.com",
    description="This is my project",
    packages=["foo"],
)
Run Code Online (Sandbox Code Playgroud)

创建一个virtualenv

python3 -m venv virtualenv  # this creates a directory "virtualenv" in your project
source ./virtualenv/bin/activate  # this switches you into the new environment
python setup.py develop  # this places your "foo" package in the environment
Run Code Online (Sandbox Code Playgroud)

在 virtualenv 中,foo作为一个已安装的包运行,并且可以通过import foo.

因此,您可以在脚本中使用绝对导入。

为了让它们从任何地方运行,而无需激活 virtualenv,您可以将路径指定为 shebang。

scripts/run.py(第一行很重要):

#!/path/to/proj/virtualenv/bin/python

import foo

print(foo.callfunc())
Run Code Online (Sandbox Code Playgroud)

(2) 使脚本成为foo包的一部分

scripts制作一个子包,而不是单独的子目录。在proj/foo/commands/run.py

from .. import callfunc()

def main():
    print(callfunc())

if __name__ == "__main__":
    main()
Run Code Online (Sandbox Code Playgroud)

然后从顶级proj/目录执行脚本:

python -m foo.commands.run
Run Code Online (Sandbox Code Playgroud)

如果您将其与 (1) 结合起来并安装您的软件包,您就可以python -m foo.commands.run从任何地方运行。


smc*_*nes 8

最佳实践?在根目录中放置一个入口点

\n

我知道这可能听起来很荒谬,如果你有很多想要执行的脚本...但它实际上是最干净的选项,并且是大型 Python 项目中最常使用的选项,magage.py例如以姜戈为例。它也不需要是一项艰巨的任务。更重要的是,拥有一个入口点总是比拥有几个较小的入口点更安全。

\n
proj/\n\xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 run.py\n\xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 foo\n\xe2\x94\x82   \xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 __init__.py\n\xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 README.md\n\xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 scripts\n    \xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 my_script.py\n
Run Code Online (Sandbox Code Playgroud)\n

run.py位于根目录中时,它可以非常轻量级...基本上只是一个包装器,用于从 my_scripts.py 调用您需要的函数。它只是将所有内容联系在一起,因此现在您所有的导入都可以正常工作。

\n

请记住,您的入口点是您的根。根的父级不存在。因此,将入口点放在根目录中,然后导入相对于根目录的包,即import foofrom scripts

\n

但是我如何调用多个脚本!?

\n

如果您需要能够调用多个脚本,那么这是一个很好的论据...好吧...论据!保持run.py作为您的单个入口点/命令,并利用子命令将功能传递给您关心的脚本。

\n

重新发明轮子?

\n

一般来说,框架已经完成了架构,供您添加自己的子命令,例如 Django,以及为了占用空间更小的Flask

\n

不过,正如我所说明的,您可以在没有这种帮助的情况下轻松完成一个小项目。

\n

安全

\n

没有人希望自己的代码在使用几年后可重构性降低。没有人希望自己的代码库更少。一般来说,当我们转向更安全的系统时,创建一些看门人脚本来确定什么是安全操作以及由谁执行\xe2\x80\x99t 是有意义的。将代码移至基于 LDAP 的系统,并且需要按组锁定内容?没问题。您可以更改单个文件或在代码库中添加 LDAP 安全性,甚至可以创建自己的内部 API。

\n

对于分布式脚本,安全选项的灵活性要差得多,维护起来也困难得多,而且单个漏洞可能会让您很容易被利用。

\n

额外优势\n您正在向脚本库添加抽象。如果您想要更改代码库的结构(也许您希望scripts拥有具有更多组织的子文件夹),您/您的用户不需要对任何依赖项进行任何重构,或将路径更改为更长、更详细的名称。您的包是独立的,用户唯一需要触摸的就是您的包proj/run.py入口点。

\n

而且,显然,您不需要过多地使用 Python 路径!

\n


pyg*_*eek 5

解决方案

有多种方法可以实现这一点。两者都需要通过添加 setup.py(基于@matejcik 的答案)来创建一个 python 包。

选项 1(推荐): entry_point +console_scripts在您的项目中注册一个函数作为脚本执行的入口点(即:)proj:foo:cli:run

选项2: scripts使用在该关键字参数setup()方法来引用路径到您的脚本(即:`斌/ script.py)。

笔记

我建议使用像Click这样的 CLI 库/框架,以便您的代码库只关心维护特定于应用程序的业务逻辑,而不是 CLI 健壮的框架功能逻辑。此外,由于跨平台兼容性,click 建议使用entry_point+console_scripts脚本集成方法。

设置工具 - 自动脚本创建:https : //setuptools.readthedocs.io/en/latest/setuptools.html#automatic-script-creation

设置工具 - 关键字参数:https : //setuptools.readthedocs.io/en/latest/setuptools.html#new-and-changed-setup-keywords

点击GitHub:https : //github.com/pallets/click/

单击 Setuptools 集成:https ://click.palletsprojects.com/en/master/setuptools/