如何在 Python 项目中正确构建内部脚本?
How to properly structure internal scripts in a Python project?
考虑以下 Python 项目框架:
proj/
├── foo
│ └── __init__.py
├── README.md
└── scripts
└── run.py
在这种情况下 foo
包含主要项目文件,例如
# foo/__init__.py
class Foo():
def run(self):
print('Running...')
和scripts
包含需要从foo
导入文件的辅助脚本,然后通过以下方式调用:
[~/proj]$ python scripts/run.py
有两种导入方法 Foo
都失败了:
- 如果尝试相对导入
from ..foo import Foo
,则错误为ValueError: attempted relative import beyond top-level package
- 如果尝试 绝对 导入
from foo import Foo
则错误为 ModuleNotFoundError: No module named 'foo'
我目前的解决方法是将 运行 路径附加到 sys.path
:
import sys
sys.path.append('.')
from foo import Foo
Foo().run()
但这感觉像是一个 hack,必须添加到 scripts/
中的每个新脚本中。
是否有更好的方法来构建此类项目中的脚本?
您需要将 __init__.py
个文件添加到 scripts
和 proj
文件夹,以便将这些文件视为 Python 包,以便您能够从这些文件中导入.
一种常用的方法是将 foo
和 scripts
文件夹放入 proj/src
文件夹,然后该文件夹有一个 __init__.py
文件,并且因此是一个 Python 包。
有两种方法可以解决这个问题。
(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"],
)
创建 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
在 virtualenv 中,foo
表现为已安装的软件包,可通过 import foo
.
导入
因此您可以在脚本中使用绝对导入。
要从任何地方制作它们 运行,无需激活 virtualenv,您可以将路径指定为 shebang。
在scripts/run.py
中(第一行很重要):
#!/path/to/proj/virtualenv/bin/python
import foo
print(foo.callfunc())
(2) 使脚本成为 foo
包的一部分
而不是单独的子目录scripts
,制作一个子包。在 proj/foo/commands/run.py
:
from .. import callfunc()
def main():
print(callfunc())
if __name__ == "__main__":
main()
然后从 top-level proj/
目录执行脚本:
python -m foo.commands.run
如果您将它与 (1) 结合起来并安装您的软件包,那么您可以从任何地方 运行 python -m foo.commands.run
。
Python 在 sys.path
中列出的目录中查找 packages/modules。有几种方法可以确保您感兴趣的目录(在本例中为 proj
)是这些目录之一:
- 将您的脚本移动到
proj
目录。 Python 将包含输入脚本的目录添加到 sys.path
.
- 将目录
proj
放入PYTHONPATH环境变量的内容
- 将模块作为可安装包的一部分进行安装,无论是否在虚拟环境中。
- 在运行时,动态添加目录
proj
到sys.path
。
选项 1 是最合乎逻辑的,不需要更改源代码。 如果您担心这可能会破坏某些东西,您也许可以使 scripts
成为符号 link 指向 proj
?
如果你不愿意那样做,那么...
您可能认为它是黑客攻击,但我建议您做修改您的脚本以在运行时间更新sys.path
。而是附加一个绝对路径,这样无论当前目录是什么,脚本都可以执行。在您的例子中,目录 proj
是脚本所在的目录 scripts
的父目录,因此:
import sys
import os.path
parent_directory = os.path.split(os.path.dirname(__file__))[0]
if parent_directory not in sys.path:
#sys.path.insert(0, parent_directory) # the first entry is directory of the running script, so maybe insert after that at index 1
sys.append(parent_directory)
解决方案
有多种方法可以实现这一点。两者都需要通过添加 setup.py(基于@matejcik 的回答)来创建 python 包。
选项 1(推荐): entry_point
+ console_scripts
在您的项目中注册一个函数作为脚本执行的入口点(即:proj:foo:cli:run
).
选项 2:scripts
: 在 setup()
方法中使用此关键字参数来引用脚本的路径(即:`bin/script.py).
备注
我建议使用像 Click 这样的 CLI library/framework,这样您的代码库只关心维护特定于应用程序的业务逻辑,而不是 CLI 健壮的框架功能逻辑。另外,由于cross-platform兼容性,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/
如果你喜欢简单,并且对你的要求没有额外的限制,添加一个 __init__.py
到 scripts
文件夹,以及任何其他同级文件夹,将它们打包,然后总是使用绝对导入形式,正如您所说,您不希望 proj
作为它们的父包,因此那里没有 __init__.py
,然后从 [=12] 内部调用您的脚本(而不是) =] 文件夹:
python -m scripts.run
或您为 run.py
以外的其他脚本指定的任何名称
这类似于@matejcik 回答的选项 2,但更简单。
另一个解决方案是在 Python 目录中添加一个 pth 文件
并写入以下内容,
# your.pth
#↓ input the directory of proj
C:\...\proj
完成
# scripts.py
from foo import Foo
Foo().run()
它会很好用。
.. 注意:: 如果你的 IDE 是 PyCharm, then you can use the Source roots 也可以帮助你。
最佳做法? 在根目录中放置一个 entry-point
我知道这可能听起来很荒谬,如果您有很多脚本想要执行...但它实际上是最简洁的选项,也是大型 Python 中最常用的选项例如,Django 中的 magage.py
之类的项目。它也不需要是一项艰巨的任务。更重要的是,拥有一个入口点总是 比几个较小的入口点更安全。
proj/
├── run.py
├── foo
│ └── __init__.py
├── README.md
└── scripts
└── my_script.py
当 run.py
位于根目录时,它可以非常轻量级...基本上只是一个包装器,用于从 my_scripts.py 调用您需要的函数。它只是将所有内容联系在一起,所以现在您所有的导入都可以正常工作。
请记住,您的入口点是您的根。根的父级不存在。因此,将您的入口点放在根目录中,然后相对于根目录导入包,也就是 import foo
from scripts
.
但是如何调用多个脚本!?
如果您需要能够调用多个脚本,这是一个很好的论据……好吧……arguments!将 run.py
作为您的单个 entrypoint/command,并利用子命令将功能传递给您关心的脚本。
重新发明轮子?
一般来说,框架已经为你添加自己的子命令做了架构,比如 Django,为了占用空间更小,Flask。
不过,正如我所说明的那样,您可以在没有帮助的情况下轻松完成一个小项目。
安全
没有人希望他们的代码在使用几年后更少可重构。没有人希望他们的代码库具有 更少 的安全性。一般来说,随着我们转向更安全的系统,创建一些看门人脚本来确定什么是安全操作,什么不是安全操作以及由谁执行是有意义的。将代码移动到基于 LDAP 的系统,并需要按组锁定事情?没问题。您可以更改单个文件或在您的代码库中添加 LDAP 安全性,甚至创建您自己的内部 API.
对于分布式脚本,安全选项的灵活性要低得多,维护起来也更难,而且一个漏洞就可能让您大开方便之门。
奖金优势
您正在为脚本库添加抽象。如果您想更改代码库的结构(也许您希望 scripts
具有更多组织的子文件夹),you/your 用户不需要对任何依赖项进行任何重构,或将路径更改为更长、更详细的名称。您的包裹是 self-contained,用户唯一需要触摸的就是您的 proj/run.py
entry-point.
而且,显然,您不需要那么多地使用 Python 路径!
考虑以下 Python 项目框架:
proj/
├── foo
│ └── __init__.py
├── README.md
└── scripts
└── run.py
在这种情况下 foo
包含主要项目文件,例如
# foo/__init__.py
class Foo():
def run(self):
print('Running...')
和scripts
包含需要从foo
导入文件的辅助脚本,然后通过以下方式调用:
[~/proj]$ python scripts/run.py
有两种导入方法 Foo
都失败了:
- 如果尝试相对导入
from ..foo import Foo
,则错误为ValueError: attempted relative import beyond top-level package
- 如果尝试 绝对 导入
from foo import Foo
则错误为ModuleNotFoundError: No module named 'foo'
我目前的解决方法是将 运行 路径附加到 sys.path
:
import sys
sys.path.append('.')
from foo import Foo
Foo().run()
但这感觉像是一个 hack,必须添加到 scripts/
中的每个新脚本中。
是否有更好的方法来构建此类项目中的脚本?
您需要将 __init__.py
个文件添加到 scripts
和 proj
文件夹,以便将这些文件视为 Python 包,以便您能够从这些文件中导入.
一种常用的方法是将 foo
和 scripts
文件夹放入 proj/src
文件夹,然后该文件夹有一个 __init__.py
文件,并且因此是一个 Python 包。
有两种方法可以解决这个问题。
(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"],
)
创建 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
在 virtualenv 中,foo
表现为已安装的软件包,可通过 import foo
.
因此您可以在脚本中使用绝对导入。
要从任何地方制作它们 运行,无需激活 virtualenv,您可以将路径指定为 shebang。
在scripts/run.py
中(第一行很重要):
#!/path/to/proj/virtualenv/bin/python
import foo
print(foo.callfunc())
(2) 使脚本成为 foo
包的一部分
而不是单独的子目录scripts
,制作一个子包。在 proj/foo/commands/run.py
:
from .. import callfunc()
def main():
print(callfunc())
if __name__ == "__main__":
main()
然后从 top-level proj/
目录执行脚本:
python -m foo.commands.run
如果您将它与 (1) 结合起来并安装您的软件包,那么您可以从任何地方 运行 python -m foo.commands.run
。
Python 在 sys.path
中列出的目录中查找 packages/modules。有几种方法可以确保您感兴趣的目录(在本例中为 proj
)是这些目录之一:
- 将您的脚本移动到
proj
目录。 Python 将包含输入脚本的目录添加到sys.path
. - 将目录
proj
放入PYTHONPATH环境变量的内容 - 将模块作为可安装包的一部分进行安装,无论是否在虚拟环境中。
- 在运行时,动态添加目录
proj
到sys.path
。
选项 1 是最合乎逻辑的,不需要更改源代码。 如果您担心这可能会破坏某些东西,您也许可以使 scripts
成为符号 link 指向 proj
?
如果你不愿意那样做,那么...
您可能认为它是黑客攻击,但我建议您做修改您的脚本以在运行时间更新sys.path
。而是附加一个绝对路径,这样无论当前目录是什么,脚本都可以执行。在您的例子中,目录 proj
是脚本所在的目录 scripts
的父目录,因此:
import sys
import os.path
parent_directory = os.path.split(os.path.dirname(__file__))[0]
if parent_directory not in sys.path:
#sys.path.insert(0, parent_directory) # the first entry is directory of the running script, so maybe insert after that at index 1
sys.append(parent_directory)
解决方案
有多种方法可以实现这一点。两者都需要通过添加 setup.py(基于@matejcik 的回答)来创建 python 包。
选项 1(推荐): entry_point
+ console_scripts
在您的项目中注册一个函数作为脚本执行的入口点(即:proj:foo:cli:run
).
选项 2:scripts
: 在 setup()
方法中使用此关键字参数来引用脚本的路径(即:`bin/script.py).
备注
我建议使用像 Click 这样的 CLI library/framework,这样您的代码库只关心维护特定于应用程序的业务逻辑,而不是 CLI 健壮的框架功能逻辑。另外,由于cross-platform兼容性,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/
如果你喜欢简单,并且对你的要求没有额外的限制,添加一个 __init__.py
到 scripts
文件夹,以及任何其他同级文件夹,将它们打包,然后总是使用绝对导入形式,正如您所说,您不希望 proj
作为它们的父包,因此那里没有 __init__.py
,然后从 [=12] 内部调用您的脚本(而不是) =] 文件夹:
python -m scripts.run
或您为 run.py
这类似于@matejcik 回答的选项 2,但更简单。
另一个解决方案是在 Python 目录中添加一个 pth 文件
并写入以下内容,
# your.pth
#↓ input the directory of proj
C:\...\proj
完成
# scripts.py
from foo import Foo
Foo().run()
它会很好用。
.. 注意:: 如果你的 IDE 是 PyCharm, then you can use the Source roots 也可以帮助你。
最佳做法? 在根目录中放置一个 entry-point
我知道这可能听起来很荒谬,如果您有很多脚本想要执行...但它实际上是最简洁的选项,也是大型 Python 中最常用的选项例如,Django 中的 magage.py
之类的项目。它也不需要是一项艰巨的任务。更重要的是,拥有一个入口点总是 比几个较小的入口点更安全。
proj/
├── run.py
├── foo
│ └── __init__.py
├── README.md
└── scripts
└── my_script.py
当 run.py
位于根目录时,它可以非常轻量级...基本上只是一个包装器,用于从 my_scripts.py 调用您需要的函数。它只是将所有内容联系在一起,所以现在您所有的导入都可以正常工作。
请记住,您的入口点是您的根。根的父级不存在。因此,将您的入口点放在根目录中,然后相对于根目录导入包,也就是 import foo
from scripts
.
但是如何调用多个脚本!?
如果您需要能够调用多个脚本,这是一个很好的论据……好吧……arguments!将 run.py
作为您的单个 entrypoint/command,并利用子命令将功能传递给您关心的脚本。
重新发明轮子?
一般来说,框架已经为你添加自己的子命令做了架构,比如 Django,为了占用空间更小,Flask。
不过,正如我所说明的那样,您可以在没有帮助的情况下轻松完成一个小项目。
安全
没有人希望他们的代码在使用几年后更少可重构。没有人希望他们的代码库具有 更少 的安全性。一般来说,随着我们转向更安全的系统,创建一些看门人脚本来确定什么是安全操作,什么不是安全操作以及由谁执行是有意义的。将代码移动到基于 LDAP 的系统,并需要按组锁定事情?没问题。您可以更改单个文件或在您的代码库中添加 LDAP 安全性,甚至创建您自己的内部 API.
对于分布式脚本,安全选项的灵活性要低得多,维护起来也更难,而且一个漏洞就可能让您大开方便之门。
奖金优势
您正在为脚本库添加抽象。如果您想更改代码库的结构(也许您希望 scripts
具有更多组织的子文件夹),you/your 用户不需要对任何依赖项进行任何重构,或将路径更改为更长、更详细的名称。您的包裹是 self-contained,用户唯一需要触摸的就是您的 proj/run.py
entry-point.
而且,显然,您不需要那么多地使用 Python 路径!