-
-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[FEATURE] Add command line upgrader. Add PIP util to project.
- Loading branch information
Showing
3 changed files
with
230 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |