1414from shutil import rmtree
1515from shutil import which
1616from typing import Any
17- from typing import Dict
1817from typing import NoReturn
19- from typing import Optional
20- from typing import Set
18+ from typing import cast
2119
2220GITHUB = 'https://github.com/dipdup-io/dipdup.git'
2321WHICH_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
3449class 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
7590class 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
156169def 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.\n Run `dipdup new` to create a new project or `dipdup` to see all available commands.'
241+ 'Done! DipDup is ready to use.\n Run `dipdup new` to create a new project or `dipdup` to see all available'
242+ ' commands.'
208243 )
209244
210245
211246def 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