Skip to content

Commit

Permalink
[FEATURE] Add command line upgrader. Add PIP util to project.
Browse files Browse the repository at this point in the history
  • Loading branch information
tomvlk committed Apr 14, 2018
1 parent 7740757 commit dc4f7d9
Show file tree
Hide file tree
Showing 3 changed files with 230 additions and 0 deletions.
54 changes: 54 additions & 0 deletions pyplanet/core/management/commands/upgrade.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from pyplanet.utils.pip import Pip
from pyplanet.core.management import BaseCommand


class Command(BaseCommand): # pragma: no cover
help = 'Upgrade PyPlanet within the current PIP environment.'

requires_system_checks = False
requires_migrations_checks = False

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.pip = Pip()

def add_arguments(self, parser):
parser.add_argument(
'--to-version', type=str, default=None,
help='Upgrade to specific version given, leave empty to upgrade to latest'
)

def handle(self, *args, **options):
version = options.get('to_version', None)
print(options)

if self.pip.is_supported:
print('PIP: Found pip command: {}'.format(self.pip.command))
print('PIP: ==> Supported!')
else:
print('PIP: ==>! Unsupported! Please manually upgrade your installation.')
return

print('')
print('===================================================================================')
print('! WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING !')
print('The automatic update system can be unstable and can result in a broken installation')
print('When the installation of PyPlanet is broken you have to use the manuals on the site')
print('http://pypla.net')
print('===================================================================================')

user = input('Are you sure you want to upgrade PyPlanet to version: \'{}\'? [y/N]: '.format(
version or 'latest'
))
if not user or not (user.lower() == 'y' or user.lower() == 'yes'):
print('Cancelled!')
return

code, out, err = self.pip.install('pyplanet', target_version=version)

if code == 0:
print('Upgrade complete!')
else:
print('Error from PIP:')
print(err.decode())
128 changes: 128 additions & 0 deletions pyplanet/utils/pip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""
The PIP module contains utilities to upgrade packages in the installation of PyPlanet from the application itself.
"""
import logging
import os
import site
import subprocess
import sys
import tempfile


class Pip:
def __init__(self):
self.command = None
self.virtual_env = None
self.install_dir = None
self.pyplanet_dir = None
self.writable = False
self.user_flag = False
self.can_use_user_flag = False

self.is_supported = False

self.investigate_environment()

@classmethod
def autodetect_pip(cls):
commands = [[sys.executable, "-m", "pip"],
[os.path.join(os.path.dirname(sys.executable), "pip.exe" if sys.platform == "win32" else "pip")],

# this should be our last resort since it might fail thanks to using pip programmatically like
# that is not officially supported or sanctioned by the pip developers
[sys.executable, "-c", "import sys; sys.argv = ['pip'] + sys.argv[1:]; import pip; pip.main()"]]

for command in commands:
p = subprocess.Popen(command + ['--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
_, _ = p.communicate()
if p.returncode == 0:
logging.getLogger(__name__).info("Using \"{}\" as command to invoke pip".format(" ".join(command)))
return command

return None

def investigate_environment(self):
"""
This method investigates the environment of the project (checks for virtualenv/pyenv/--user or system wide installation).
"""
self.command = Pip.autodetect_pip()
if not self.command:
return
self.pyplanet_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))

# Checking PIP installation and compatibility...
test_package = os.path.join(os.path.realpath(os.path.dirname(__file__)), 'pip_test_pkg')
temp_file = tempfile.NamedTemporaryFile()
data = dict()

try:
p = subprocess.Popen(
self.command + ['install', '.'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
cwd=test_package,
env=dict(
TESTBALLOON_OUTPUT=temp_file.name
)
)
_, _ = p.communicate()

with open(temp_file.name) as f:
for line in f:
key, value = line.split("=", 2)
data[key] = value
except Exception as e:
logging.getLogger(__name__).error("Failed to check if PIP is supported. Failed to investigate PIP installation.")
logging.getLogger(__name__).exception(e)
return

install_dir_str = data.get("PIP_INSTALL_DIR", None)
virtual_env_str = data.get("PIP_VIRTUAL_ENV", None)
writable_str = data.get("PIP_WRITABLE", None)

if install_dir_str is not None and virtual_env_str is not None and writable_str is not None:
self.install_dir = install_dir_str.strip()
self.virtual_env = virtual_env_str.strip() == "True"
self.writable = writable_str.strip() == "True"

self.can_use_user_flag = not self.virtual_env and site.ENABLE_USER_SITE

self.is_supported = self.writable or self.can_use_user_flag
self.user_flag = not self.writable and self.can_use_user_flag

logging.getLogger(__name__).info(
"pip installs to {} (writable -> {}), --user flag needed -> {}, virtual env -> {}".format(
self.install_dir,
"yes" if self.writable else "no",
"yes" if self.user_flag else "no",
"yes" if self.virtual_env else "no"
)
)
logging.getLogger(__name__).info("==> pip ok -> {}".format("yes" if self.is_supported else "NO!"))
else:
logging.getLogger(__name__).error(
"Could not detect desired output from pip_test_pkg install, got this instead: {!r}".format(data)
)

def install(self, package, target_version=None):
"""
Install (or upgrade) package to target version given or to latest if no target version given.
:param package: Package name
:param target_version: Target version
:return: Return code from PIP, stdout and stderr
"""
if not self.is_supported:
raise Exception('Pip environment is not supported!')

command = self.command + ['install', '-U', '{}{}'.format(
package, '=={}'.format(target_version) if target_version else ''
)]

p = subprocess.Popen(
command,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
cwd=self.pyplanet_dir,
)
stdout, stderr = p.communicate()

return p.returncode, stdout, stderr
48 changes: 48 additions & 0 deletions pyplanet/utils/pip_test_pkg/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import os
import sys

"""
This "python package" doesn't actually install. This is intentional. It is merely
used to figure out some information about the environment a specific pip call
is running under (installation dir, whether it belongs to a virtual environment,
whether the install location is writable by the current user), and for that it
only needs to be invoked by pip, the pip call doesn't have to be successful
however.
If an environment variable "TESTBALLOON_OUTPUT" is set, it will be used as location
to write a file with the figured out data to. Simply writing to stdout (the default
behaviour if no such environment variable is set) is sadly not going to work out
with versions of pip > 8.0.0, which capture all stdout output regardless of used
--verbose or --log flags.
"""


def produce_output(stream):
from distutils.command.install import install as cmd_install
from distutils.dist import Distribution

cmd = cmd_install(Distribution())
cmd.finalize_options()

install_dir = cmd.install_lib
virtual_env = hasattr(sys, "real_prefix")
writable = os.access(install_dir, os.W_OK)

print("PIP_INSTALL_DIR={}".format(install_dir), file=stream)
print("PIP_VIRTUAL_ENV={}".format(virtual_env), file=stream)
print("PIP_WRITABLE={}".format(writable), file=stream)
stream.flush()


path = os.environ.get("TESTBALLOON_OUTPUT", None)
if path is not None:
# environment variable set, write to a log
path = os.path.abspath(path)
with open(path, mode="w+") as output:
produce_output(output)
else:
# write to stdout
produce_output(sys.stdout)

# fail intentionally
sys.exit(-1)

0 comments on commit dc4f7d9

Please sign in to comment.