Skip to content
This repository has been archived by the owner on Jan 12, 2021. It is now read-only.

Commit

Permalink
Merge pull request #365 from dephell/improve-installer
Browse files Browse the repository at this point in the history
use laziness in installer
  • Loading branch information
orsinium committed Jan 27, 2020
2 parents c80f8fa + 1690afe commit 4d55150
Showing 1 changed file with 178 additions and 119 deletions.
297 changes: 178 additions & 119 deletions install.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,67 +5,85 @@
from os import environ, name as os_name, pathsep
from pathlib import Path
from shutil import rmtree
from typing import Tuple
from venv import create


POST_MESSAGE = """DepHell is installed now. Great!
To get started you need DepHell's bin directory ({dephell_home_bin}) in your `PATH`
environment variable. Future applications will automatically have the
correct environment, but you may need to restart your current shell.
DepHell was added in your PATH. Please, restart your shell.
"""

POST_MESSAGE_NO_MODIFY_PATH = """DepHell is installed now. Great!
To get started you need DepHell's bin directory ({dephell_home_bin}) in your `PATH`
environment variable. This has not been done automatically.
Please, add DepHell's bin directory ({dephell_home_bin}) in your `PATH`
environment variable.
"""


def get_platform_names() -> Tuple[str, str]:
"""Get platform-dependent executable names"""
windows = sys.platform.startswith('win') or (sys.platform == 'cli' and os_name == 'nt')
names = 'dephell', 'python3'
if windows:
names = 'dephell.exe', 'python.exe'
return names


# parse CLI arguments
parser = ArgumentParser()
parser.add_argument('--branch', help='install dephell from git from given branch')
parser.add_argument('--version', help='install specified version')
parser.add_argument('--slug', default='dephell/dephell',
help='repository slug to use when installing from Github')
args = parser.parse_args()


# install pip
try:
import pip # noQA: F401
except ImportError:
print('install pip')
from ensurepip import bootstrap
bootstrap()


# get dephell's jail path
def get_data_dir() -> Path:
try:
from appdirs import user_data_dir
except ImportError:

# linux
path = Path.home() / '.local' / 'share'
if path.exists():
return path / 'dephell'

# mac os
path = Path.home() / 'Library' / 'Application Support'
if path.exists():
return path / 'dephell'
# https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76
# https://github.com/pydanny/cached-property/blob/master/cached_property.py
class cached_property(object): # noqa: N801
"""
A property that is only computed once per instance and then replaces itself
with an ordinary attribute. Deleting the attribute resets the property.
"""

def __init__(self, func):
self.__doc__ = func.__doc__
self.func = func

def __get__(self, obj, cls):
if obj is None:
return self
value = obj.__dict__[self.func.__name__] = self.func(obj)
return value


class Context:
@cached_property
def is_windows(self) -> bool:
if sys.platform.startswith('win'):
return True
if sys.platform == 'cli' and os_name == 'nt':
return True
return False

@cached_property
def dephell_name(self) -> str:
if self.is_windows:
return 'dephell.exe'
return 'dephell'

@cached_property
def python_name(self) -> str:
if self.is_windows:
return 'python.exe'
return 'python3'

@cached_property
def parser(self):
parser = ArgumentParser()
parser.add_argument('--branch', help='install dephell from git from given branch')
parser.add_argument('--version', help='install specified version')
parser.add_argument('--slug', default='dephell/dephell',
help='repository slug to use when installing from Github')
return parser

@cached_property
def args(self):
return self.parser.parse_args()

@cached_property
def pip_main(self):
# install
try:
import pip # noQA: F401
except ImportError:
print('install pip')
from ensurepip import bootstrap
bootstrap()

# import
try:
from pip._internal.main import main
except ImportError:
Expand All @@ -74,78 +92,119 @@ def get_data_dir() -> Path:
except ImportError:
from pip import main

main(['install', 'appdirs'])
from appdirs import user_data_dir

return Path(user_data_dir('dephell'))


print('make venv')
path = get_data_dir() / 'venvs' / 'dephell'
if path.exists():
rmtree(str(path))
create(str(path), with_pip=True)


dephell_name, python_name = get_platform_names()

print('update pip')
python_path = list(path.glob(f'*/{python_name}'))[0]
command = [str(python_path), '-m', 'pip', 'install', '-U', 'pip']
result = subprocess.run(command)
if result.returncode != 0:
# try again, pip is nightly
result = subprocess.run(command)
if result.returncode != 0:
exit(result.returncode)


print('install dephell')
if args.branch:
name = 'git+https://github.com/{slug}.git@{branch}#egg=dephell[full]'
name = name.format(slug=args.slug, branch=args.version or args.branch)
elif args.version:
name = 'dephell[full]=={version}'.format(version=args.version)
else:
name = 'dephell[full]'
result = subprocess.run([str(python_path), '-m', 'pip', 'install', name])
if result.returncode != 0:
exit(result.returncode)
return main

@cached_property
def data_dir(self) -> Path:
try:
from appdirs import user_data_dir
except ImportError:

def get_bin_dir() -> Path:
path = Path.home() / '.local' / 'bin'
if path.exists():
return path
paths = [Path(path) for path in environ.get('PATH', '').split(pathsep)]
for path in paths:
if path.exists() and '.local' in path.parts:
return path
for path in paths:
# linux
path = Path.home() / '.local' / 'share'
if path.exists():
return path / 'dephell'

# mac os
path = Path.home() / 'Library' / 'Application Support'
if path.exists():
return path / 'dephell'

self.pip_main(['install', 'appdirs'])
from appdirs import user_data_dir

return Path(user_data_dir('dephell'))

@cached_property
def venv_path(self):
return self.data_dir / 'venvs' / 'dephell'

@cached_property
def python_path(self):
"""path to python in venv
"""
pythons = self.venv_path.glob('*/{}'.format(self.python_name))
return list(pythons)[0]

@cached_property
def bin_dir(self) -> Path:
"""Global directory from PATH to simlink dephell's binary
"""
path = Path.home() / '.local' / 'bin'
if path.exists():
return path
raise LookupError('cannot find place to install binary', paths)


print('copy binary dephell')
dephell_home_bin = python_path.parent
local_path = dephell_home_bin / dephell_name
if not local_path.exists():
print('DepHell binary not found')
exit(1)
global_path = get_bin_dir() / dephell_name
modified_path = True
if global_path.exists() or global_path.is_symlink():
global_path.unlink()
try:
global_path.symlink_to(local_path)
except OSError:
modified_path = False

kwargs = {
'dephell_home_bin': dephell_home_bin,
}
if modified_path:
print(POST_MESSAGE.format(**kwargs))
else:
print(POST_MESSAGE_NO_MODIFY_PATH.format(**kwargs))
paths = [Path(path) for path in environ.get('PATH', '').split(pathsep)]
for path in paths:
if path.exists() and '.local' in path.parts:
return path
for path in paths:
if path.exists():
return path
raise LookupError('cannot find place to install binary', paths)

# actions

def make_venv(self) -> None:
self.pip_main # install pip before all to have it in the venv
if self.venv_path.exists():
rmtree(str(self.venv_path))
create(str(self.venv_path), with_pip=True)

def upgrade_pip(self) -> None:
command = [str(self.python_path), '-m', 'pip', 'install', '-U', 'pip']
result = subprocess.run(command)
if result.returncode != 0:
# try again, pip is nightly
result = subprocess.run(command)
if result.returncode != 0:
exit(result.returncode)

def install_dephell(self):
if self.args.branch:
name = 'git+https://github.com/{slug}.git@{branch}#egg=dephell[full]'
name = name.format(
slug=self.args.slug,
branch=self.args.version or self.args.branch,
)
elif self.args.version:
name = 'dephell[full]=={version}'.format(version=self.args.version)
else:
name = 'dephell[full]'
result = subprocess.run([str(self.python_path), '-m', 'pip', 'install', name])
if result.returncode != 0:
exit(result.returncode)

def copy_binary(self):
dephell_home_bin = self.python_path.parent
local_path = dephell_home_bin / self.dephell_name
if not local_path.exists():
print('DepHell binary not found')
exit(1)
global_path = self.bin_dir / self.dephell_name
if global_path.exists() or global_path.is_symlink():
global_path.unlink()
try:
global_path.symlink_to(local_path)
except OSError:
return False
return True


if __name__ == '__main__':
context = Context()
print('make venv')
context.make_venv()
print('upgrade pip')
context.upgrade_pip()
print('install dephell')
context.install_dephell()
print('copy binary dephell')
modified_path = context.copy_binary()

kwargs = {
'dephell_home_bin': context.python_path.parent,
}
if modified_path:
print(POST_MESSAGE.format(**kwargs))
else:
print(POST_MESSAGE_NO_MODIFY_PATH.format(**kwargs))

0 comments on commit 4d55150

Please sign in to comment.