
Werkzeug 的重载器必须确定要使用哪些参数来启动新进程,包括 Python 可执行文件、脚本、是否使用 -m 运行以及传递给脚本的参数。
遗憾的是,Python 并不会暴露它以 python -m module 身份运行的事实 it replaces -m module with the path to the module in sys.argv[2]。此外,Python 对 python -m module 和 python file.py 的行为也不同:运行文件会将其目录添加到 sys.path。当使用 python -m werkzeug.serving 运行重载器时,它会在重新加载时调用 python /path/to/werkzeug/serving.py,从而添加了 /path/to/werkzeug to sys.path。这导致与 Werkzeug 中的模块名称发生导入冲突,例如 http,从而覆盖了内置函数。
经过一番使用调试器深入研究内部机制后,我发现可以检测到 Python 是否以 -m 的方式运行,并相应地调整 sys.argv。这花了我几个星期的时间进行调查,因为我发现了越来越多的脚本安装方法和各种特殊情况。
为了确定是否使用了 -m,我查看了 __main__.__package__。__main__[3] 是分配给已运行脚本的模块名称,无论脚本名称是什么,也无论它是否以 -m 的方式运行。就我能想到的所有情况而言,如果 __package__[4] 不为 None,则它是以 -m 的方式运行的;否则,它是以文件的方式运行的。如果我们以 -m 的方式运行,那么可以通过将 sys.argv[0] 中的文件名附加到 __main__.__package__ 来重建模块名称。
除此之外,还有一些其他因素会影响该值。如果脚本是 Windows 系统上通过 pip 安装的入口点,则它是 exe,但不包含 sys.argv 中的扩展名。Python -m a.b 可能指的是模块 a/b.py 或包 a/b/__init__.py,也可能是 a/b/__main__.py。此外,许多 IDE 调试器的后端 pydevd 会错误地将 -m script 重写为 script 而不是 path/to/script.py。
def _get_args_for_reloading() -> list[str]:
"""Determine how the script was executed, and return the args needed
to execute it again in a new process.
"""
rv = [sys.executable]
py_script = sys.argv[0]
args = sys.argv[1:]
# Need to look at main module to determine how it was executed.
__main__ = sys.modules["__main__"]
# The value of __package__ indicates how Python was called. It may
# not exist if a setuptools script is installed as an egg. It may be
# set incorrectly for entry points created with pip on Windows.
if getattr(__main__, "__package__", None) is None or (
os.name == "nt"
and __main__.__package__ == ""
and not os.path.exists(py_script)
and os.path.exists(f"{py_script}.exe")
):
# Executed a file, like "python app.py".
py_script = os.path.abspath(py_script)
if os.name == "nt":
# Windows entry points have ".exe" extension and should be
# called directly.
if not os.path.exists(py_script) and os.path.exists(f"{py_script}.exe"):
py_script += ".exe"
if (
os.path.splitext(sys.executable)[1] == ".exe"
and os.path.splitext(py_script)[1] == ".exe"
):
rv.pop(0)
rv.append(py_script)
else:
# Executed a module, like "python -m werkzeug.serving".
if os.path.isfile(py_script):
# Rewritten by Python from "-m script" to "/path/to/script.py".
py_module = t.cast(str, __main__.__package__)
name = os.path.splitext(os.path.basename(py_script))[0]
if name != "__main__":
py_module += f".{name}"
else:
# Incorrectly rewritten by pydevd debugger from "-m script" to "script".
py_module = py_script
rv.extend(("-m", py_module.lstrip(".")))
rv.extend(args)
return rv虽然“这个脚本是如何被调用的”这个问题的答案看似简单,“只需查看sys.argv”,但实际情况却远比这复杂得多。而且上面的代码仍然不完整,它除了-m之外,并没有捕获arguments that were passed to python[5],例如-u、-X等等。这就是Python 3.10添加sys.orig_argv[6]的原因。它准确地捕获了在任何重写操作之前传递给python的内容。以上所有代码都可以用以下代码更准确地替换:
args = [sys.executable, *sys.orig_argv[1:]]https://davidism.com/python-args/
[1] 我2018年为Werkzeug提交的第一个*大型*PR: https://github.com/pallets/werkzeug/pull/1416[2] it replaces `-m module` with the path to the module in `sys.argv`: https://docs.python.org/3/using/cmdline.html#cmdoption-m[3] `__main__`: https://docs.python.org/3/library/__main__.html#module-__main__[4] `__package__`: https://docs.python.org/3/reference/datamodel.html#module.__package__[5] arguments that were passed to `python`: https://docs.python.org/3/using/cmdline.html[6] `sys.orig_argv`: https://docs.python.org/3/library/sys.html#sys.orig_argv