Coverage for devshell.py: 34%
72 statements
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-06 19:50 +0100
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-06 19:50 +0100
1#!/usr/bin/env python3
3"""
4 developer shell
5 ~~~~~~~~~~~~~~~
7 Just call this file, and the magic happens ;)
9 This file is from: https://pypi.org/project/dev-shell/
10 Source: https://github.com/jedie/dev-shell/blob/main/devshell.py
12 :copyleft: 2021 by Jens Diemer
13 :license: GNU GPL v3 or above
14"""
16import argparse
17import hashlib
18import signal
19import subprocess
20import sys
21import venv
22from pathlib import Path
25try:
26 import ensurepip # noqa
27except ModuleNotFoundError as err:
28 print(err)
29 print('-' * 100)
30 print('Error: Pip not available!')
31 print('Hint: "apt-get install python3-venv"\n')
32 raise
35assert sys.version_info >= (3, 7), 'Python version is too old!'
38if sys.platform == 'win32': # wtf 38 ↛ 40line 38 didn't jump to line 40, because the condition on line 38 was never true
39 # Files under Windows, e.g.: .../.venv/Scripts/python.exe
40 BIN_NAME = 'Scripts'
41 FILE_EXT = '.exe'
42else:
43 # Files under Linux/Mac and all other than Windows, e.g.: .../.venv/bin/python
44 BIN_NAME = 'bin'
45 FILE_EXT = ''
47BASE_PATH = Path(__file__).parent
48VENV_PATH = BASE_PATH / '.venv'
49BIN_PATH = VENV_PATH / BIN_NAME
50PYTHON_PATH = BIN_PATH / f'python{FILE_EXT}'
51PIP_PATH = BIN_PATH / f'pip{FILE_EXT}'
52POETRY_PATH = BIN_PATH / f'poetry{FILE_EXT}'
54DEP_LOCK_PATH = BASE_PATH / 'poetry.lock'
55DEP_HASH_PATH = VENV_PATH / '.dep_hash'
57# script file defined in pyproject.toml as [tool.poetry.scripts]
58# (Under Windows: ".exe" not added!)
59PROJECT_SHELL_SCRIPT = BIN_PATH / 'devshell'
62def get_dep_hash():
63 """Get SHA512 hash from poetry.lock content."""
64 return hashlib.sha512(DEP_LOCK_PATH.read_bytes()).hexdigest()
67def store_dep_hash():
68 """Generate /.venv/.dep_hash"""
69 DEP_HASH_PATH.write_text(get_dep_hash())
72def venv_up2date():
73 """Is existing .venv is up-to-date?"""
74 if DEP_HASH_PATH.is_file():
75 return DEP_HASH_PATH.read_text() == get_dep_hash()
76 return False
79def verbose_check_call(*popen_args):
80 popen_args = [str(arg) for arg in popen_args] # e.g.: Path() -> str for python 3.7
81 print(f'\n+ {" ".join(popen_args)}\n')
82 return subprocess.check_call(popen_args)
85def noop_signal_handler(signal_num, frame):
86 """
87 Signal handler that does nothing: Used to ignore "Ctrl-C" signals
88 """
89 pass
92def main(argv):
93 if len(argv) == 2 and argv[1] in ('--update', '--help'):
94 parser = argparse.ArgumentParser(
95 prog=Path(__file__).name, description='Developer shell', epilog='...live long and prosper...'
96 )
97 parser.add_argument(
98 '--update', default=False, action='store_true', help='Force create/upgrade virtual environment'
99 )
100 parser.add_argument(
101 'command_args',
102 nargs=argparse.ZERO_OR_MORE,
103 help='arguments to pass to dev-setup shell/cli',
104 )
105 options = parser.parse_args(argv)
106 force_update = options.update
107 extra_args = argv[2:]
108 else:
109 force_update = False
110 extra_args = argv[1:]
112 # Create virtual env in ".../.venv/":
113 if not PYTHON_PATH.is_file() or force_update:
114 print('Create virtual env here:', VENV_PATH.absolute())
115 builder = venv.EnvBuilder(symlinks=True, upgrade=True, with_pip=True)
116 builder.create(env_dir=VENV_PATH)
118 # install/update "pip" and "poetry":
119 if not POETRY_PATH.is_file() or force_update:
120 # Note: Under Windows pip.exe can't replace this own .exe file, so use the module way:
121 verbose_check_call(PYTHON_PATH, '-m', 'pip', 'install', '-U', 'pip', 'setuptools')
122 verbose_check_call(PIP_PATH, 'install', 'poetry')
124 if not DEP_LOCK_PATH.is_file():
125 verbose_check_call(POETRY_PATH, 'update')
127 # install via poetry, if:
128 # 1. .venv not exists
129 # 2. "--update" used
130 # 3. poetry.lock file was changed
131 if not PROJECT_SHELL_SCRIPT.is_file() or force_update or not venv_up2date():
132 verbose_check_call(POETRY_PATH, 'install')
133 store_dep_hash()
135 # The cmd2 shell should not abort on Ctrl-C => ignore "Interrupt from keyboard" signal:
136 signal.signal(signal.SIGINT, noop_signal_handler)
138 # Run project cmd shell via "setup.py" entrypoint:
139 # (Call it via python, because Windows sucks calling the file direct)
140 try:
141 verbose_check_call(PYTHON_PATH, PROJECT_SHELL_SCRIPT, *extra_args)
142 except subprocess.CalledProcessError as err:
143 sys.exit(err.returncode)
146if __name__ == '__main__':
147 main(sys.argv)