需求背景
现需要实现一个工具类,功能为验证给定路径是否为有效的 Python 解释器可执行文件(不一定是主程序所使用的解释器),并获取该解释器版本信息、是否安装某模块/包等信息。该工具类将赋予主程序类似 PyCharm 中选取 Python 解释器的功能。
快速编写了百余行代码完成基本设计需求,记录如下,旨在抛砖引玉。
总体结构
设计名为 InterpreterVailidator 的类,主要实现如下几个方法:
import subprocess
from pathlib import Path
class InterpreterValidator:
"""
验证给定的可执行文件是否为有效的Python解释器,并获取该解释器相关信息
"""
def __init__(self, path):
"""
:param path: 可执行文件路径
"""
self._itp_path = Path(path)
self._itp_validated = False
self.validate_itp()
def validate_itp(self) -> bool:
"""
验证解释器是否有效
:return: 解释器是否有效
"""
pass
@classmethod
def validate(cls, path) -> bool:
"""
验证path是否指向有效的Python解释器
:return: 是否有效
"""
pass
def module_installed(self, module) -> bool:
"""
验证该解释器环境中是否已安装某个模块
:param module: 模块名,要求为import语句中使用的名称
:return: 未安装该模块或解释器无效时返回False
"""
pass
def itp_info(self):
"""
获取解释器的相关信息
"""
pass
仅使用标准库即可完成以上需求,下面简单介绍一下将会用到的标准库。
相关标准库简介
pathlib
pathlib 为面向对象的文件系统路径模块。这个从3.4新引入的标准库,相比 os.path 提供了更高级更面向对象的路径操作。用字符串或 os.PathLike 类型的路径创建 pathlib.Path 对象,后续可以很方便地获取该路径对象是否存在、是否为文件、绝对路径……等。
subprocess
subprocess 是子进程管理模块,可以在 Python 主进程中以子进程的形式创建并运行一个外部命令/程序。对于一般使用,直接调用 subprocess.run() 函数即可,并可获取子进程退出码等信息。
重要函数方法设计
validate_itp()
在 self.__init__() 中已经将待判断的路径转为 pathlib.Path 对象并保存在实例属性 self._itp_path 中,直接处理该对象即可。此处使用的若干种判断方式按顺序逐渐严格:
- 该路径是否存在
- 该路径指向的是否为文件(不是目录)
- 该文件的文件名是否以
python 起始 - 该名为
python 的文件是否可以作为可执行程序运行测试代码 import os
前三条直接使用 pathlib 提供的相关接口判断即可,可以依顺序写在同一条 if 语句中,注意完成判断后需要返回值和修改 self._itp_validated 属性值:
def validate_itp(self) -> bool:
"""
验证解释器是否有效 \n
:return: 解释器是否有效
"""
if (
self._itp_path.exists()
and self._itp_path.is_file()
and self._itp_path.name.startswith("python")
):
self._itp_validated = True
return True
else:
self._itp_validated = False
return False
至此,只能保证路径指向一个名为 python* 的现有文件,但不能保证该文件就是可执行的解释器。
通过尝试使用该文件执行 python -c 'import os' 命令来进一步验证。python -c 模式会将后面的命令行参数作为 Python 代码执行。对于任一个有效的 Python 解释器,在启动时其实已经运行过了一次 import os 命令(而在日常编程中还需要显式导入一次,应该是出于命名空间之考虑),因此使用该命令进行验证,是额外开销很低而又无需 stdio 的理想方式。
使用 subprocess.run() 来尝试运行 python -c 'import os' ,如果顺利运行而返回码为 0,则有非常大的把握确认该文件是一个有效的 Python 解释器可执行文件。validate_itp() 方法扩展为:
import subprocess
from pathlib import Path
def validate_itp(self) -> bool:
"""
验证解释器是否有效 \n
:return: 解释器是否有效
"""
if (
self._itp_path.exists()
and self._itp_path.is_file()
and self._itp_path.name.startswith("python")
):
subprocess_args = [
str(self._itp_path.resolve()),
"-c",
"import os",
]
result = subprocess.run(
args=subprocess_args,
timeout=300,
)
if result.returncode == 0:
self._itp_validated = True
return True
else:
self._itp_validated = False
return False
else:
self._itp_validated = False
return False
然而这个版本还有些缺陷——在随意创建的一个假解释器文件 python4 上调用此方法时,由于子进程错误,最终引发了 OSError 并导致主程序也崩溃。所以最终版本还需加入一点异常处理:
import subprocess
from pathlib import Path
def validate_itp(self) -> bool:
"""
验证解释器是否有效 \n
:return: 解释器是否有效
"""
if (
self._itp_path.exists()
and self._itp_path.is_file()
and self._itp_path.name.startswith("python")
):
try:
subprocess_args = [
str(self._itp_path.resolve()),
"-c",
"import os",
]
result = subprocess.run(
args=subprocess_args,
timeout=300,
)
if result.returncode == 0:
self._itp_validated = True
return True
else:
self._itp_validated = False
return False
except OSError:
self._itp_validated = False
return False
else:
self._itp_validated = False
return False
(末尾的三条完全一样的返回语句看起来并不优雅,暂时还没想到该如何解决。)
validate() 类方法
使用 InterpreterVailidator 类的用户可能只需简单快速判断某个文件路径是否指向有效的 Python 解释器,而并不关心其他细节信息。所以提供一个类方法是非常必要的。简单来说,使用 @classclassmethod 修饰类中的某个函数,并以 cls (这代表这个类本身)作为首个参数,即可将其变成类方法。
@classmethod
def validate(cls, path) -> bool:
"""
验证path是否指向有效的Python解释器 \n
:return: 是否有效
"""
return InterpreterValidator(path).validate_itp()
这里采用了偷懒的写法:偷偷实例化一个 InterpreterValidator 并调用其 validate_itp() 判断方法,最后返回。无需担心——得益于引用计数,此处实例化的对象很快就会被自动销毁掉。而有了这个类方法,调用过程简化了:
if __name__ == "__main__":
iv = InterpreterValidator("/usr/bin/python")
result = iv.validate_itp()
result = InterpreterValidator.validate("/usr/bin/python")
module_installed()
假设已经确认该路径确实是一个有效的 Python 解释器,那么接下来很可能关心的一个问题是该解释器环境是否安装了某个(某些)模块/包。有了上面的思路,编写这个方法并不难,继续使用 python -c 子进程就好:
def module_installed(self, module) -> bool:
"""
验证该解释器环境中是否已安装某个模块 \n
:param module: 模块名,要求为import语句中使用的名称
:return: 未安装该模块或解释器无效时返回False
"""
if self._itp_validated:
subprocess_arg_list = [
str(self._itp_path.resolve()),
"-c",
f"import {module}",
]
result = subprocess.run(
args=subprocess_arg_list,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=300,
)
if result.returncode != 0 and b"ModuleNotFoundError" in result.stderr:
return False
else:
return True
else:
return False
有几条需要注意的地方:
- 参数中的module模块名必须是用于
import 导入的名称,有些库的常用名和导入名并不相同(比如 PyTorch 的导入名为 torch ); - 调用
subprocess.run() 时需要将 stderr 重定向至 PIPE 以便于捕捉,在result.stderr中即为字节串形式的标准错误; - 而同样也需将 stdout 重定向,以防某些模块的
__init__ 中有输出而对主程序控制台造成干扰;
完整代码
最终版本的完整代码如下,使用 Black 与 isort 工具进行了代码格式化、加入类型注解并能够通过 Mypy 静态检查:
import os
import subprocess
from pathlib import Path
from typing import Union
class InterpreterValidator:
"""
验证给定的可执行文件是否为有效的Python解释器,并获取该解释器相关信息
"""
def __init__(self, path: Union[str, os.PathLike[str]]) -> None:
"""
:param path: 可执行文件路径
"""
self._itp_path: Path = Path(path)
self._itp_validated: bool = False
self.validate_itp()
def validate_itp(self) -> bool:
"""
验证解释器是否有效 \n
:return: 解释器是否有效
"""
if (
self._itp_path.exists()
and self._itp_path.is_file()
and self._itp_path.name.startswith("python")
):
try:
subprocess_args = [
str(self._itp_path.resolve()),
"-c",
"import os",
]
result = subprocess.run(
args=subprocess_args,
timeout=300,
)
if result.returncode == 0:
self._itp_validated = True
return True
else:
self._itp_validated = False
return False
except OSError:
self._itp_validated = False
return False
else:
self._itp_validated = False
return False
def itp_info(self):
"""
获取解释器的相关信息 \n
"""
if self._itp_validated:
pass
def module_installed(self, module: str) -> bool:
"""
验证该解释器环境中是否已安装某个模块 \n
:param module: 模块名,要求为import语句中使用的名称
:return: 未安装该模块或解释器无效时返回False
"""
if self._itp_validated:
subprocess_arg_list = [
str(self._itp_path.resolve()),
"-c",
f"import {module}",
]
result = subprocess.run(
args=subprocess_arg_list,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=300,
)
if result.returncode != 0 and b"ModuleNotFoundError" in result.stderr:
return False
else:
return True
else:
return False
@classmethod
def validate(cls, path: Union[str, os.PathLike[str]]) -> bool:
"""
验证path是否指向有效的Python解释器 \n
:return: 是否有效
"""
return InterpreterValidator(path).validate_itp()
if __name__ == "__main__":
iv = InterpreterValidator("/usr/bin/python")
print(iv.module_installed("PySide6"))
print(iv.module_installed("black"))
|