Skip to content

Commit 807267c

Browse files
Wizard1209Wizard1209
andauthored
Backport: install script improvments (#841)
Co-authored-by: Wizard1209 <[email protected]>
1 parent 35fa34b commit 807267c

File tree

2 files changed

+113
-68
lines changed

2 files changed

+113
-68
lines changed

src/dipdup/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ async def install(
575575
"""Install DipDup for the current user."""
576576
import dipdup.install
577577

578-
dipdup.install.install(quiet, force, ref, path)
578+
dipdup.install.install(quiet, force, None, ref, path)
579579

580580

581581
@cli.command()
@@ -605,4 +605,4 @@ async def update(
605605
"""Update DipDup for the current user."""
606606
import dipdup.install
607607

608-
dipdup.install.install(quiet, force, None, None)
608+
dipdup.install.install(quiet, force, None, None, None)

src/dipdup/install.py

Lines changed: 111 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,37 @@
1414
from shutil import rmtree
1515
from shutil import which
1616
from typing import Any
17-
from typing import Dict
1817
from typing import NoReturn
19-
from typing import Optional
20-
from typing import Set
18+
from typing import cast
2119

2220
GITHUB = 'https://github.com/dipdup-io/dipdup.git'
2321
WHICH_CMDS = (
22+
'python3.10',
2423
'python3',
2524
'pipx',
2625
'dipdup',
2726
'datamodel-codegen',
2827
'poetry',
28+
'pdm',
2929
'pyvenv',
3030
'pyenv',
3131
)
3232

33+
WELCOME_ASCII = """\0
34+
____ _ ____
35+
/ __ \ (_)____ / __ \ __ __ ____
36+
/ / / // // __ \ / / / // / / // __ \\
37+
/ /_/ // // /_/ // /_/ // /_/ // /_/ /
38+
/_____//_// .___//_____/ \__,_// .___/
39+
/_/ /_/
40+
"""
41+
EPILOG = """\0
42+
Documentation: https://dipdup.io/docs
43+
GitHub: https://github.com/dipdup-io/dipdup
44+
Discord: https://discord.gg/aG8XKuwsQd
45+
"""
46+
DIPDUP_LTS_VERSION = '6.5.12'
47+
3348

3449
class Colors:
3550
"""ANSI color codes"""
@@ -55,16 +70,16 @@ def done(msg: str) -> NoReturn:
5570
sys.exit(0)
5671

5772

58-
def ask(msg: str, default: bool, quiet: bool) -> bool:
59-
msg += ' [Y/n]' if default else ' [y/N]'
60-
echo(msg, Colors.YELLOW)
61-
62-
if quiet:
63-
return default
64-
if default:
65-
return input().lower() not in ('n', 'no')
66-
else:
67-
return input().lower() in ('y', 'yes')
73+
def ask(question: str, default: bool) -> bool:
74+
"""Ask user a yes/no question"""
75+
while True:
76+
answer = input(question + (' [Y/n] ' if default else ' [y/N] ')).lower().strip()
77+
if not answer:
78+
return default
79+
if answer in ('n', 'no'):
80+
return False
81+
if answer in ('y', 'yes'):
82+
return True
6883

6984

7085
# NOTE: DipDup has `tabulate` dep, don't use this one elsewhere
@@ -73,42 +88,40 @@ def _tab(text: str, indent: int = 20) -> str:
7388

7489

7590
class DipDupEnvironment:
76-
def __init__(self, quiet: bool = False) -> None:
91+
def __init__(self) -> None:
7792
self._os = os.uname().sysname
7893
self._arch = os.uname().machine
79-
self._quiet = quiet
80-
self._commands: Dict[str, Optional[str]] = {}
81-
self._pipx_packages: Set[str] = set()
94+
self._commands: dict[str, str | None] = {}
95+
self._pipx_packages: set[str] = set()
8296

8397
def refresh(self) -> None:
84-
if not self._quiet and not self._commands:
85-
print()
86-
print(_tab('OS:') + self._os)
87-
print(_tab('Arch:') + self._arch)
88-
print(_tab('Python:') + sys.version)
89-
print(_tab('PATH:') + os.environ['PATH'])
90-
print()
91-
9298
for command in WHICH_CMDS:
9399
old, new = self._commands.get(command), which(command)
94100
if old == new:
95101
continue
96102
self._commands[command] = new
97-
self._quiet or print(_tab(command) + (new or ''))
98103

104+
def print(self) -> None:
105+
print()
106+
print(WELCOME_ASCII)
107+
print(EPILOG)
99108
print()
109+
print(_tab('OS:') + f'{self._os} ({self._arch})')
110+
print(_tab('Python:') + sys.version)
111+
print(_tab('PATH:') + os.environ['PATH'])
112+
print(_tab('PYTHONPATH:') + os.environ.get('PYTHONPATH', ''))
113+
print()
114+
for command, path in self._commands.items():
115+
print(_tab(f'{command}:') + (path or ''))
116+
print(_tab('pipx packages:') + ', '.join(self._pipx_packages) + '\n')
100117

101118
def refresh_pipx(self) -> None:
102119
"""Get installed pipx packages"""
103120
self.ensure_pipx()
104121
pipx_packages_raw = self.run_cmd('pipx', 'list', '--short', capture_output=True).stdout
105122
self._pipx_packages = {p.split()[0].decode() for p in pipx_packages_raw.splitlines()}
106-
self._quiet or print(_tab('pipx packages:') + ', '.join(self._pipx_packages) + '\n')
107-
108-
def check(self) -> None:
109-
if not sys.version.startswith('3.10'):
110-
fail('DipDup requires Python 3.10')
111123

124+
def prepare(self) -> None:
112125
# NOTE: Show warning if user is root
113126
if os.geteuid() == 0:
114127
echo('WARNING: Running as root, this is not generally recommended', Colors.YELLOW)
@@ -120,109 +133,136 @@ def check(self) -> None:
120133
self.refresh()
121134
self.refresh_pipx()
122135

123-
if self._commands.get('pyenv'):
124-
echo('WARNING: pyenv is installed, this may cause issues', Colors.YELLOW)
125-
126136
def run_cmd(self, cmd: str, *args: Any, **kwargs: Any) -> subprocess.CompletedProcess[bytes]:
127137
"""Run command safely (relatively lol)"""
128138
if (found_cmd := self._commands.get(cmd)) is None:
129139
fail(f'Command not found: {cmd}')
130-
args = (found_cmd,) + tuple(a for a in args if a)
140+
args = (found_cmd, *tuple(a for a in args if a))
131141
try:
132142
return subprocess.run(
133143
args,
134144
**kwargs,
135145
check=True,
136146
)
137147
except subprocess.CalledProcessError as e:
138-
self._quiet or fail(f'{cmd} failed: {e.cmd} {e.returncode}')
139-
raise
148+
fail(f'{cmd} failed: {e.cmd} {e.returncode}')
140149

141150
def ensure_pipx(self) -> None:
151+
if not sys.version.startswith('3.10'):
152+
fail('DipDup 6 requires Python 3.10')
153+
142154
"""Ensure pipx is installed for current user"""
143155
if self._commands.get('pipx'):
144156
return
145157

146-
if sys.prefix != sys.base_prefix:
147-
fail("pipx can't be installed in virtualenv, run `deactivate` and try again")
148-
149158
echo('Installing pipx')
150-
self.run_cmd('python3', '-m', 'pip', 'install', '--user', '-q', 'pipx')
151-
self.run_cmd('python3', '-m', 'pipx', 'ensurepath')
152-
os.environ['PATH'] = os.environ['PATH'] + ':' + str(Path.home() / '.local' / 'bin')
153-
os.execv(sys.executable, [sys.executable] + sys.argv)
159+
if sys.base_prefix != sys.prefix:
160+
self.run_cmd('python3.10', '-m', 'pip', 'install', '-q', 'pipx')
161+
else:
162+
self.run_cmd('python3.10', '-m', 'pip', 'install', '--user', '-q', 'pipx')
163+
self.run_cmd('python3.10', '-m', 'pipx', 'ensurepath')
164+
pipx_path = str(Path.home() / '.local' / 'bin')
165+
os.environ['PATH'] = pipx_path + os.pathsep + os.environ['PATH']
166+
self._commands['pipx'] = which('pipx')
154167

155168

156169
def install(
157170
quiet: bool,
158171
force: bool,
172+
version: str | None,
159173
ref: str | None,
160174
path: str | None,
161175
) -> None:
162176
"""Install DipDup and its dependencies with pipx"""
163177
if ref and path:
164178
fail('Specify either ref or path, not both')
179+
pkg = 'dipdup' if not version else f'dipdup=={version}'
180+
181+
if not any((version, ref, path)):
182+
version = DIPDUP_LTS_VERSION
165183

166184
env = DipDupEnvironment()
167-
env.check()
185+
env.prepare()
186+
if not quiet:
187+
env.print()
168188

169189
force_str = '--force' if force else ''
170190
pipx_packages = env._pipx_packages
171191
pipx_dipdup = 'dipdup' in pipx_packages
172192
pipx_datamodel_codegen = 'datamodel-code-generator' in pipx_packages
173193
pipx_poetry = 'poetry' in pipx_packages
174194

175-
if pipx_dipdup:
176-
echo('Updating DipDup')
177-
env.run_cmd('pipx', 'upgrade', 'dipdup', force_str)
195+
python_inter_pipx = cast(str, which('python3.10'))
196+
if 'pyenv' in python_inter_pipx:
197+
python_inter_pipx = (
198+
subprocess.run(
199+
['pyenv', 'which', 'python3.10'],
200+
capture_output=True,
201+
text=True,
202+
)
203+
.stdout.strip()
204+
.split('\n')[0]
205+
)
206+
207+
if pipx_dipdup and not force:
208+
if quiet or ask(f'Reinstall dipdup to {version}', False):
209+
env.run_cmd('pipx', 'uninstall', 'dipdup')
210+
env.run_cmd('pipx', 'install', '--python', python_inter_pipx, pkg, force_str)
178211
else:
179212
if path:
180213
echo(f'Installing DipDup from `{path}`')
181-
env.run_cmd('pipx', 'install', path, force_str)
214+
env.run_cmd('pipx', 'install', '--python', python_inter_pipx, path, force_str)
182215
elif ref:
183-
echo(f'Installing DipDup from `{ref}`')
184-
env.run_cmd('pipx', 'install', f'git+{GITHUB}@{ref}', force_str)
216+
url = f'git+{GITHUB}@{ref}'
217+
echo(f'Installing DipDup from `{url}`')
218+
env.run_cmd('pipx', 'install', '--python', python_inter_pipx, url, force_str)
185219
else:
186220
echo('Installing DipDup from PyPI')
187-
env.run_cmd('pipx', 'install', 'dipdup', force_str)
221+
env.run_cmd('pipx', 'install', '--python', python_inter_pipx, pkg, force_str)
188222

189223
if pipx_datamodel_codegen:
190224
env.run_cmd('pipx', 'upgrade', 'datamodel-code-generator', force_str)
191225
else:
192-
env.run_cmd('pipx', 'install', 'datamodel-code-generator', force_str)
226+
env.run_cmd('pipx', 'install', '--python', python_inter_pipx, 'datamodel-code-generator', force_str)
193227

194228
if (legacy_poetry := Path(Path.home(), '.poetry')).exists():
195229
rmtree(legacy_poetry, ignore_errors=True)
196230
env.run_cmd('pipx', 'install', 'poetry', force_str)
197231
elif pipx_poetry:
198232
echo('Updating Poetry')
199233
env.run_cmd('pipx', 'upgrade', 'poetry', force_str)
200-
elif ask('Install poetry? Optional for `dipdup new` command', True, quiet):
234+
elif quiet or ask('Install poetry? Optional for `dipdup new` command', True):
201235
echo('Installing poetry')
202236
env.run_cmd('pipx', 'install', 'poetry', force_str)
203237
env._commands['poetry'] = which('poetry')
204238
pipx_poetry = True
205239

206240
done(
207-
'Done! DipDup is ready to use.\nRun `dipdup new` to create a new project or `dipdup` to see all available commands.'
241+
'Done! DipDup is ready to use.\nRun `dipdup new` to create a new project or `dipdup` to see all available'
242+
' commands.'
208243
)
209244

210245

211246
def uninstall(quiet: bool) -> NoReturn:
212247
"""Uninstall DipDup and its dependencies with pipx"""
213248
env = DipDupEnvironment()
214-
env.check()
215-
216-
pipx_packages = env._pipx_packages
217-
218-
if 'dipdup' in pipx_packages:
219-
echo('Uninstalling DipDup')
220-
env.run_cmd('pipx', 'uninstall', 'dipdup')
249+
env.prepare()
250+
if not quiet:
251+
env.print()
252+
253+
packages = (
254+
('dipdup', True),
255+
('datamodel-code-generator', False),
256+
('poetry', False),
257+
)
258+
for package, default in packages:
259+
if package not in env._pipx_packages:
260+
continue
261+
if not quiet and not ask(f'Uninstall {package}?', default):
262+
continue
221263

222-
if 'datamodel-code-generator' in pipx_packages:
223-
if ask('Uninstall datamodel-code-generator?', True, quiet):
224-
echo('Uninstalling datamodel-code-generator')
225-
env.run_cmd('pipx', 'uninstall', 'datamodel-code-generator')
264+
echo(f'Uninstalling {package}')
265+
env.run_cmd('pipx', 'uninstall', package)
226266

227267
done('Done! DipDup is uninstalled.')
228268

@@ -233,17 +273,22 @@ def cli() -> None:
233273
parser = argparse.ArgumentParser()
234274
parser.add_argument('-q', '--quiet', action='store_true', help='Use default answers for all questions')
235275
parser.add_argument('-f', '--force', action='store_true', help='Force reinstall')
276+
parser.add_argument('-v', '--version', help='Install DipDup from a specific version')
236277
parser.add_argument('-r', '--ref', help='Install DipDup from a specific git ref')
237278
parser.add_argument('-p', '--path', help='Install DipDup from a local path')
238279
parser.add_argument('-u', '--uninstall', action='store_true', help='Uninstall DipDup')
239280
args = parser.parse_args()
240281

282+
if not args.quiet:
283+
sys.stdin = open('/dev/tty') # noqa: PTH123
284+
241285
if args.uninstall:
242286
uninstall(args.quiet)
243287
else:
244288
install(
245289
quiet=args.quiet,
246290
force=args.force,
291+
version=args.version.strip() if args.version else None,
247292
ref=args.ref.strip() if args.ref else None,
248293
path=args.path.strip() if args.path else None,
249294
)

0 commit comments

Comments
 (0)