如何从 python 脚本调用 pip 并使其在本地安装到该脚本?

How to call pip from a python script and make it install locally to that script?

我维护的源代码存储库中有一个 python 脚本。假设它在位置

scripts/python/make_salad/make_salad.py

它按原样签入存储库。该脚本的用户只想在 windows 资源管理器中单击该脚本,然后就可以离开了。他们拒绝使用命令行。但是,该脚本还依赖于我必须安装的许多外部包。我已经使用 following trick 来安装用户第一次运行脚本时所需的任何包。就像

def install(package):
    # This is an evil little function
    # that installs packages via pip.
    # This means the script can install
    # it's own dependencies.
    try:
        __import__(package)
    except:
        import subprocess
        subprocess.call([sys.executable, "-m", "pip", "install", package])

install("colorama")
install("pathlib")
install("iterfzf")
install("prompt_toolkit")
install("munch")
install("appdirs")
install("art")
install("fire")

import os
import tkFileDialog
import getpass
import json
import shutil
import subprocess
import sys
import pprint
import art

# <snip> out all my business logic
print("Making Salad")

但是我不喜欢这样,因为它会将包安装到全局包存储库中。我想要的是,如果所有软件包都安装了

scripts/python/make_salad/make_salad.py
                         /__packages__
                                      /colorama
                                      /pathlib
                                      /iterfzf
                                      ...
                                      /fire

然后在调用导入时,此目录将位于搜索路径的第一位。是否可以破解上面的脚本,所以上面的是可能的?

注意要求

  1. 只有单个脚本存储在存储库中
  2. 用户必须在 windows 资源管理器
  3. 中单击脚本
  4. 需要从脚本中 pip 安装外部包
  5. 包不应该污染全局包

也许您可以将 --prefix 选项与 pip 一起使用,修改 sys.path 以包含该前缀 (+ lib/python2.7/site-packages/)。我认为 --prefix 甚至可以使用相对路径:

def install(package):
    try:
        __import__(package)
    except:
        import subprocess
        subprocess.call([sys.executable, "-m", "pip", "install", package, "--prefix="packages"])
.
.
.
sys.path.append("packages/lib/python2.7/site-packages/")
.
.
.
import art
.
.
.

(未经测试)

事实证明,直接从脚本中使用 virtualenv 来解决这个问题并不太难。我写了一个小帮手class.

import sys
import subprocess

class App:
    def __init__(self, virtual_dir):
        self.virtual_dir = virtual_dir
        self.virtual_python = os.path.join(self.virtual_dir, "Scripts", "python.exe")

    def install_virtual_env(self):
        self.pip_install("virtualenv")
        if not os.path.exists(self.virtual_python):
            import subprocess
            subprocess.call([sys.executable, "-m", "virtualenv", self.virtual_dir])
        else:
            print("found virtual python: " + self.virtual_python)

    def is_venv(self):
        return sys.prefix==self.virtual_dir

    def restart_under_venv(self):
        print("Restarting under virtual environment " + self.virtual_dir)
        subprocess.call([self.virtual_python, __file__] + sys.argv[1:])
        exit(0)

    def pip_install(self, package):
        try:
            __import__(package)
        except:
            subprocess.call([sys.executable, "-m", "pip", "install", package, "--upgrade"])

    def run(self):
        if not self.is_venv():
            self.install_virtual_env()
            self.restart_under_venv()
        else:
            print("Running under virtual environment")

并且可以像这样从主脚本的顶部使用它make_salad.py

pathToScriptDir = os.path.dirname(os.path.realpath(__file__))

app = App(os.path.join(pathToScriptDir, "make_salad_virtual_env"))

app.run()

app.install("colorama")
app.install("pathlib")
app.install("iterfzf")
app.install("prompt_toolkit")
app.install("munch")
app.install("appdirs")
app.install("art")
app.install("fire")
app.install("appdirs")

print "making salad"

第一次 运行ning 它将安装虚拟环境和所有必需的包。第二次它只是运行虚拟环境下的脚本。

found virtual python: C:\workspace\make_salad_virtualenv\Scripts\python.exe
Restarting under virtual environment C:\workspace\make_salad_virtualenv\Scripts
Running under virtual environment
Making Salad