python自动使用虚拟环境和安装依赖
代码如下,Windows环境测试通过(1. 判断是否在虚拟环境里;2. 判断当前目录下是否有venv文件夹;3. 如果都没有则通过python -m venv venv来创建;4. 然后调用venv里的pip来安装模块)
1 import platform 2 import re 3 import subprocess 4 import sys 5 from contextlib import AbstractContextManager 6 from pathlib import Path 7 8 9 class EnsureImport(AbstractContextManager): 10 """Auto install modules if import error. 11 12 Usage:: 13 >>> for _ range(EnsureImport.retry): 14 ... with EnsureImport( 15 ... multipart='python-multipart', dotenv='python-dotenv' 16 ... ) as _m: 17 ... import six 18 ... import multipart 19 ... from dotenv import load_dotenv 20 ... # more imports ... 21 ... if _m.ok: 22 ... break 23 ... 24 """ 25 26 mapping = { 27 "multipart": "python-multipart", 28 "dotenv": "python-dotenv", 29 "snap7": "python-snap7", 30 } 31 retry = 30 32 33 def __init__(self, **kwargs): 34 self.exception = None 35 self._success = True 36 self.package_mapping = dict(self.mapping, **kwargs) 37 38 @property 39 def ok(self) -> bool: 40 return self._success 41 42 def __exit__(self, exc_type, exc_value, traceback): 43 if isinstance(exc_value, (ImportError, ModuleNotFoundError)): 44 self.exception = exc_value 45 self._success = False 46 self.run() 47 return True 48 49 def run(self): 50 e = self.exception 51 modules = re.findall(r"'([a-zA-Z][0-9a-zA-Z_-]+)'", str(e)) 52 if "--no-install" in sys.argv or not modules: 53 raise e 54 ms = (self.package_mapping.get(i, i) for i in modules) 55 rc = self.install_and_extend_sys_path(*ms) 56 if rc: 57 sys.exit(rc) 58 59 @staticmethod 60 def is_venv() -> bool: 61 """Whether in a virtual environment(also work for poetry)""" 62 return hasattr(sys, "real_prefix") or ( 63 hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix 64 ) 65 66 @staticmethod 67 def run_and_echo(cmd: str) -> int: 68 print("-->\n", cmd, flush=True) 69 return subprocess.call(cmd, shell=True) 70 71 @staticmethod 72 def log_error(action: str) -> None: 73 print(f"ERROR: failed to {action}") 74 75 @classmethod 76 def install_and_extend_sys_path(cls, *packages) -> int: 77 py = Path(sys.executable) 78 depends = " ".join(packages) 79 if not cls.is_venv(): 80 p = Path.cwd() / "venv" 81 if not p.exists(): 82 if cls.run_and_echo(f"{py} -m venv venv"): 83 cls.log_error(f"create virtual environment for {py}") 84 return 1 85 if platform.platform().lower().startswith("win"): 86 py = p / "Scripts" / "python.exe" 87 else: 88 py = p / "bin/python" 89 cls.run_and_echo(f"{py} -m pip install --upgrade pip") 90 lib = list(p.rglob("site-packages"))[0] 91 sys.path.append(lib.as_posix()) 92 if cls.run_and_echo(f"{py} -m pip install {depends}"): 93 cls.log_error(f"install {depends}") 94 return 2 95 return 0 96 97 98 for _ in range(EnsureImport.retry): 99 with EnsureImport(dotenv='python-dotenv') as _f: 100 from dotenv import load_dotenv 101 if _f.ok: 102 break 103 104 105 load_dotenv()
#######################
以下为旧版代码,可以不用看
#######################
import os import platform import re import sys from pathlib import Path def is_venv() -> bool: """判断是否处于虚拟环境(也适用于poetry的)""" if hasattr(sys, "real_prefix"): return True return hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix def run_and_echo(cmd) -> int: print("-->\n", cmd, flush=True) return os.system(cmd) def install_and_rerun(*packages): py = Path(sys.executable) if not is_venv(): if not (p := Path("venv")).exists(): if run_and_echo(f"{py} -m venv venv"): return 1 if platform.platform().lower().startswith("win"): py = p / "Scripts" / "python.exe" else: py = p / "bin/python" if run_and_echo(f"{py} -m pip install {' '.join(packages)}"): return 2 cmd = f"{py} {sys.argv[0]} --no-install {' '.join(sys.argv[1:])}" return run_and_echo(cmd) try: import kivy except ImportError as e: if "--no-install" in sys.argv: raise e modules = re.findall(r"'([a-zA-Z_-]+)'", str(e)) sys.exit(install_and_rerun(*modules)) def main(): pass if __name__ == '__main__': main()
运行时,如果import失败,会判断是否处于虚拟环境,是的话,直接pip install报错的缺失包,然后自动重新执行脚本;
否则,判断当前路径是否有venv文件,有的话使用venv/*/python,否则使用python -m venv venv创建它
优化成class以便PyCharm可以折叠
import os import platform import random import re import sys from pathlib import Path class EnsureImport: def __init__(self, e): self.exception = e def run(self): e = self.exception if "--no-install" in sys.argv: raise e modules = re.findall(r"'([a-zA-Z_-]+)'", str(e)) sys.exit(self.install_and_rerun(*modules)) @staticmethod def is_venv() -> bool: """判断是否处于虚拟环境(也适用于poetry的)""" if hasattr(sys, "real_prefix"): return True return hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix @staticmethod def run_and_echo(cmd) -> int: print("-->\n", cmd, flush=True) return os.system(cmd) @classmethod def install_and_rerun(cls, *packages): py = Path(sys.executable) command = " ".join(sys.argv) if not cls.is_venv(): if not (p := Path("venv")).exists(): if cls.run_and_echo(f"{py} -m venv venv"): return 1
else:
cls.run_and_echo(f'{py} -m pip install -U pip') if platform.platform().lower().startswith("win"): py = p / "Scripts" / "python.exe" else: py = p / "bin/python" return cls.run_and_echo(f"{py} {command}") depends = " ".join(packages) if cls.run_and_echo(f"{py} -m pip install {depends}"): return 2 cmd = f"{py} {command} --no-install" return cls.run_and_echo(cmd) try: import dearpygui.dearpygui as dpg except ImportError as e: EnsureImport(e).run() def main(): print(dpg) if __name__ == "__main__": main()
使用了isort+black+ruff进行代码格式化
进一步优化成with风格:
import os import platform import re import sys from pathlib import Path class EnsureImport: """Auto install module if import error. Usage:: >>> with EnsureImport(): ... import six """ def __init__(self): self.exception = None def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): if exc_type is not None: if isinstance(exc_value, (ImportError, ModuleNotFoundError)): self.exception = exc_value self.run() def run(self): e = self.exception if "--no-install" in sys.argv: raise e modules = re.findall(r"'([a-zA-Z_-]+)'", str(e)) sys.exit(self.install_and_rerun(*modules)) @staticmethod def is_venv() -> bool: """判断是否处于虚拟环境(也适用于poetry的)""" if hasattr(sys, "real_prefix"): return True return hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix @staticmethod def run_and_echo(cmd) -> int: print("-->\n", cmd, flush=True) return os.system(cmd) @classmethod def install_and_rerun(cls, *packages): py = Path(sys.executable) command = " ".join(sys.argv) if not cls.is_venv(): if not (p := Path("venv")).exists(): if cls.run_and_echo(f"{py} -m venv venv"): return 1 if platform.platform().lower().startswith("win"): py = p / "Scripts" / "python.exe" else: py = p / "bin/python" return cls.run_and_echo(f"{py} {command}") depends = " ".join(packages) if cls.run_and_echo(f"{py} -m pip install {depends}"): return 2 cmd = f"{py} {command} --no-install" return cls.run_and_echo(cmd) with EnsureImport(): import six def main(): print(six.__file__) if __name__ == "__main__": main()
进一步优化成支持多import且只run一次的:
import platform import re import subprocess import sys from contextlib import AbstractContextManager from pathlib import Path class EnsureImport(AbstractContextManager): """Auto install modules if import error. Usage:: >>> for _ range(EnsureImport.retry): ... with EnsureImport( ... multipart='python-multipart', dotenv='python-dotenv' ... ) as _m: ... import six ... import multipart ... from dotenv import load_dotenv ... # more imports ... ... if _m.ok: ... break ... """ retry = 10 def __init__(self, **kwargs): self.exception = None self._success = True self.package_mapping = kwargs @property def ok(self) -> bool: return self._success def __exit__(self, exc_type, exc_value, traceback): if isinstance(exc_value, (ImportError, ModuleNotFoundError)): self.exception = exc_value self._success = False self.run() return True def run(self): e = self.exception if "--no-install" in sys.argv: raise e modules = re.findall(r"'([a-zA-Z_-]+)'", str(e)) if mp := self.package_mapping: modules = [mp.get(i, i) for i in modules] if rc := self.install_and_extend_sys_path(*modules): sys.exit(rc) @staticmethod def is_venv() -> bool: """判断是否处于虚拟环境(也适用于poetry的)""" if hasattr(sys, "real_prefix"): return True return hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix @staticmethod def run_and_echo(cmd: str) -> int: print("-->\n", cmd, flush=True) return subprocess.call(cmd, shell=True) @staticmethod def log_error(action: str) -> None: print(f"ERROR: failed to {action}") @classmethod def install_and_extend_sys_path(cls, *packages) -> int: py = Path(sys.executable) depends = " ".join(packages) if not cls.is_venv(): if not (p := Path("venv")).exists():
if depends.lower() == 'dearpygui' and sys.version_info >= (3, 11):
py = Path('python3.10') if cls.run_and_echo(f"{py} -m venv venv"): cls.log_error(f"create virtual environment for {py}") return 1 if platform.platform().lower().startswith("win"): py = p / "Scripts" / "python.exe" else: py = p / "bin/python" lib = list(p.rglob("site-packages"))[0] sys.path.append(lib.as_posix()) if cls.run_and_echo(f"{py} -m pip install {depends}"): cls.log_error(f"install {depends}") return 2 return 0 for _ in range(EnsureImport.retry): with EnsureImport(multipart="python-multipart", dotenv="python-dotenv") as _m: import multipart import six from dotenv import load_dotenv if _m.ok: break def main(): print(six.__file__) print(multipart.__file__) load_dotenv() if __name__ == "__main__": main()