pyenv-virtualenv for Windows 1.2
A 'pyenv' plugin to manage Python virtual environments, depending on different Python versions, for various Python projects.
Loading...
Searching...
No Matches
pyenv-virtualenv.py
Go to the documentation of this file.
1##
2# @package pyenv-virtualenv
3# @file pyenv-virtualenv.py
4# @author Michael Paul Korthals
5# @date 2025-07-10
6# @version 1.0.0
7# @copyright © 2025 Michael Paul Korthals. All rights reserved.
8# See License details in the documentation.
9#
10# Utility application to create a new named virtual environment,
11# which depends on an installed Python version.
12#
13
14# --- IMPORTS ----------------------------------------------------------
15
16# Python
17import argparse
18import os
19import shutil
20import subprocess
21import sys
22
23# Avoid colored output problems
24os.system('')
25
26# Community
27try:
28 import virtualenv
29except ImportError():
30 print(
31 '\x1b[101mCRITICAL %s\x1b[0m'
32 %
33 'Cannot find package "%s".'
34 %
35 'virtualenv'
36 )
37 print(
38 '\x1b[37mINFO %s\x1b[0m'
39 %
40 'Install it using "pip". Then try again.')
41 import virtualenv
42
43# My
44import lib.log as log
45import lib.hlp as hlp
46
47# --- RUN ---------------------------------------------------------------
48
49## Sub routine to run the application.
50#
51# @param args Parsed command line arguments of this application.
52# @return RC = 0 or other values in case of error.
53def run(args: argparse.Namespace) -> int:
54 cwd = os.getcwd()
55 # noinspection PyBroadException
56 try:
57 while True:
58 # Force to select the "Version" argument,
59 # even if it is not given.
60 try:
61 # Use the given "version" argument
62 version: str = args.version
63 realm = 'argument'
64 except AttributeError:
65 # Use the "pyenv" python version
66 version, realm = hlp.getPythonVersion()
67 if version == '':
68 log.error('Cannot determine the version number of the "pyenv" Python version.')
69 log.info('Trying next, set the version number explicitly. See --help.')
70 rc = 1
71 break
72 # Optimize arguments
73 version = version.strip()
74 name: str = hlp.fName(args.name)
75 props: bool = args.props
76 # Output actual arguments
77 log.info('Creating Python virtual environment in "pyenv":')
78 log.info(f' * Version: {version} ({realm})')
79 log.info(f' * Name: {name}')
80 log.info(f' * Set project properties: {props}')
81 # Filter/select best virtual environment capable version
82 selected_version_dir, rc = hlp.selectVersionDir(
83 version,
84 realm,
85 venv_capable=True
86 )
87 if rc != 0:
88 break
89 selected_version = os.path.basename(selected_version_dir)
90 # Check and make directories
91 envs_dir = os.path.join(selected_version_dir, 'envs')
92 if not os.path.isdir(envs_dir):
93 log.verbose(f'Creating "{envs_dir}" folder ...')
94 os.mkdir(envs_dir)
95 # Create Python virtual environment
96 venv_dir = os.path.join(envs_dir, name)
97 if os.path.isdir(venv_dir):
98 # Remove existing directory
99 log.verbose(f'Overriding folder "{venv_dir}" ...')
100 shutil.rmtree(venv_dir)
101 else:
102 log.verbose(f'Creating folder "{venv_dir}" ...')
103 # Get executable path for the selected Python version
104 python_exe_path = os.path.join(
105 selected_version_dir,
106 'python.exe'
107 )
108 # Generate Python virtual environment
109 cmd = [
110 python_exe_path,
111 '-m',
112 'venv',
113 venv_dir
114 ]
115 log.verbose(f'Execute: {python_exe_path}')
116 log.info('This will take some seconds ...')
117 # noinspection SpellCheckingInspection
118 cp = subprocess.run(
119 cmd,
120 stdout=subprocess.DEVNULL,
121 stderr=subprocess.DEVNULL,
122 shell=True
123 )
124 rc = cp.returncode
125 if rc != 0:
126 log.error(
127 'Unexpectedly cannot create ' +
128 'Python virtual environment.'
129 )
130 log.info('Check/repair Python and "pyenv". Then try again.')
131 rc = 1
132 break
133 # Create junction to virtual environment directory
134 # within the Python versions directory.
135 venv_version_dir = os.path.join(
136 os.environ['PYENV_ROOT'],
137 'versions',
138 f'{name}-{selected_version}'
139 )
140 if hlp.isJunction(venv_version_dir):
141 # Remove existing junction
143 'Overriding junction "{}" ...'.format(
144 venv_version_dir
145 )
146 )
147 os.remove(venv_version_dir)
148 else:
150 'Creating junction "{}" ...'.format(
151 venv_version_dir
152 )
153 )
154 # noinspection SpellCheckingInspection
155 cmd = [
156 'mklink',
157 '/J',
158 venv_version_dir,
159 venv_dir
160 ]
161 log.verbose(f'Execute: {cmd}')
162 cp = subprocess.run(
163 cmd,
164 stdout=subprocess.DEVNULL,
165 stderr=subprocess.DEVNULL,
166 shell=True
167 )
168 rc = cp.returncode
169 if rc != 0:
170 log.error(
171 'Unexpectedly cannot create junction to ' +
172 'Python virtual environment directory.'
173 )
174 log.info(
175 'Check/repair security properties ' +
176 'of the junction. Then try again.'
177 )
178 rc = 1
179 break
180 if props:
181 log.verbose('Setting project properties ...')
182 rc = hlp.setProjectProperties(selected_version, name)
183 if rc == 0:
184 log.verbose('Project properties successfully set.')
185 else:
186 log.error('Cannot set project properties.')
187 # Log success message
189 f'Virtual environment "{name}" is installed ' +
190 f'in "pyenv", depending on "Python {selected_version}".'
191 )
192 # Go on
193 break
194 # End while
195 except:
196 log.error(sys.exc_info())
197 rc = 1
198 finally:
199 os.chdir(cwd)
200 return rc
201
202
203# --- MAIN --------------------------------------------------------------
204
205## Parse CLI arguments for this application.<br>
206# <br>
207# Implement this as required, but don't touch the interface definition
208# for input and output.
209#
210# @return A tuple of:
211# * Namespace to read arguments in "dot" notation or None
212# in case of help or error.
213# * RC = 0 or another value in case of error.
214def parseCliArguments1() -> tuple[(argparse.Namespace, None), int]:
215 rc: int = 0
216 # noinspection PyBroadException
217 try:
218 parser = argparse.ArgumentParser(
219# --- BEGIN CHANGE -----------------------------------------------------
220 prog='pyenv virtualenv',
221 description='Create a version-assigned and named ' +
222 'Python virtual environment in "pyenv".'
223 )
224 # Add flag
225 parser.add_argument(
226 '-p', '--props',
227 dest='props',
228 action=argparse.BooleanOptionalAction,
229 help='As project properties, add the files ' +
230 '`.python-version` and `.python-env` to CWD. Default: --no_props.'
231 )
232 # Add positional str argument
233 parser.add_argument(
234 'name',
235 help='Short name of the new Python virtual environment.'
236 )
237# --- END CHANGE -------------------------------------------------------
238 return parser.parse_args(), rc
239 except SystemExit:
240 return None, 0
241 except:
242 log.error(sys.exc_info())
243 return None, 1
244
245## Parse CLI arguments for this application.<br>
246# <br>
247# Implement this as required, but don't touch the interface definition
248# for input and output.
249#
250# @return A tuple of:
251# * Namespace to read arguments in "dot" notation or None
252# in case of help or error.
253# * RC = 0 or another value in case of error.
254def parseCliArguments2() -> tuple[(argparse.Namespace, None), int]:
255 rc: int = 0
256 # noinspection PyBroadException
257 try:
258 parser = argparse.ArgumentParser(
259# --- BEGIN CHANGE -----------------------------------------------------
260 prog='pyenv virtualenv',
261 description='Create a version-assigned and named ' +
262 'Python virtual environment in "pyenv".'
263 )
264 # Add flag
265 parser.add_argument(
266 '-p', '--props',
267 dest='props',
268 action=argparse.BooleanOptionalAction,
269 help='As project properties, add the files ' +
270 '`.python-version` and `.python-env` to CWD. Default: --no_props.'
271 )
272 # Add positional str argument
273 parser.add_argument(
274 'version',
275 help='Python version, which must be already installed ' +
276 'in "pyenv".'
277 )
278 # Add positional str argument
279 parser.add_argument(
280 'name',
281 help='Short name of the new Python virtual environment.'
282 )
283# --- END CHANGE -------------------------------------------------------
284 return parser.parse_args(), rc
285 except SystemExit:
286 return None, 0
287 except:
288 log.error(sys.exc_info())
289 return None, 1
290
291## Display the version number and generating procedure for "pyenv-virtualenv" for Windows.
292#
293# @return RC = 0 or another value in case of error
295 # noinspection PyBroadException
296 try:
297 cmd = os.path.join(
298 os.environ['PYENV_ROOT'],
299 'plugins',
300 'pyenv-virtualenv',
301 'docs',
302 'open_doxygen_docs.bat'
303 )
304 log.verbose(f'Execute: {cmd}')
305 cp = subprocess.run(
306 cmd,
307 shell=True
308 )
309 rc = cp.returncode
310 if rc != 0:
311 log.error(f'Displaying Doxygen documentation failed (RC = {rc}.')
312 log.info('Check/reinstall "pyenv-virtualenv". Then try again. ')
313 log.notice('CANCELED.')
314 rc = 130
315 except:
316 log.error(sys.exc_info())
317 rc = 1
318 return rc
319
320## Display the version number and generating procedure for "pyenv-virtualenv" for Windows.
321#
322# @return RC = 0 or another value in case of error
324 # noinspection PyBroadException
325 try:
326 file_path = os.path.join(
327 os.environ['PYENV_ROOT'],
328 'plugins',
329 'pyenv-virtualenv',
330 '.version'
331 )
332 if not os.path.isfile(file_path):
333 log.error('Cannot display version number.')
334 log.info(f'Reinstall the missing file "{file_path}".')
335 log.notice('CANCELED.')
336 return 130
337 with open(file_path, 'r') as f:
338 version = f.read().strip()
339 # noinspection SpellCheckingInspection
340 print(f'"\x1b[92mpyenv-virtualenv\x1b[0m" for Windows \x1b[96m{version}\x1b[0m (\x1b[93mpython -m venv\x1b[0m).')
341 return 0
342 except:
343 log.error(sys.exc_info())
344 return 1
345
346## Main routine of the application.
347#
348# @return RC = 0 or other values in case of error.
349def main() -> int:
350 # noinspection PyBroadException
351 try:
352 while True:
353 # Audit the operating system platform
354 rc = hlp.auditPlatform('Windows')
355 if rc != 0:
356 # Deviation: Reject unsupported platform
357 break
358 # Audit the global Python version number
360 if rc != 0:
361 # Deviation: Reject unsupported Python version
362 break
363 # Initialize the colored logging to console
365 # Audit the "pyenv" version number
366 rc = hlp.auditPyEnv('3')
367 if rc != 0:
368 # Deviation: Reject unsupported "pyenv" version
369 break
370 # Parse arguments
371 # NOTE: Sorry, this is a complicated workaround to
372 # bypass the lack of applicability of Python "ArgumentParser"
373 # class. This simple use case to set required = False for
374 # positional argument is not included and must be patched
375 # away here.
376 log.verbose('Parsing arguments ...')
377 args = None
378 args_list = sys.argv.copy() # Clone
379 # Strip program executable path
380 args_list.pop(0)
381 # Detect and remove all optional arguments,
382 # which are not related to the positional arguments
383 help_requested = False
384 version_requested = False
385 docs_requested = False
386 for i in reversed(range(len(args_list))):
387 arg = args_list[i]
388 if arg in ['-h', '--help']:
389 help_requested = True
390 args_list.pop(i)
391 elif arg in ['-v', '--version']:
392 version_requested = True
393 args_list.pop(i)
394 elif arg in ['-d', '--docs']:
395 docs_requested = True
396 args_list.pop(i)
397 # Calculate the count of positional arguments
398 positional_count = len(args_list)
399 if docs_requested:
401 break
402 elif version_requested:
404 break
405 elif (
406 help_requested
407 or
408 (positional_count not in [1, 2])
409 ):
410 text = ("""
411Usage: pyenv virtualenv [-h] [-v] [-p | --props | --no-props ] [version] [name]
412
413Create a version-assigned and named Python virtual environment in "pyenv".
414
415Positional arguments (which can be omitted):
416 [version] Python version, which must be already installed in
417 "pyenv". Default: The global Python version.
418 [name] Short name of the new Python virtual environment.
419
420Options:
421 -h, --help Show this help message and exit.
422 -p, --props, --no-props
423 Add the files `.python-version` and `.python-env`
424 as project properties to CWD. Default: --no_props.
425 -v, --version Display the version number of this "pyenv-virtualenv"
426 release and ignore all other arguments.
427 """).strip()
428 print(text)
429 elif positional_count == 1:
430 # Case 1: single positional argument must be "Name"
431 args, rc = parseCliArguments1()
432 elif positional_count == 2:
433 # Case 2: duo positional arguments must be "Version" and "Name"
434 args, rc = parseCliArguments2()
435 if rc != 0:
436 break
437 if args is None: # -h, --help
438 break
439 # Run this application
440 log.verbose('Running application ...')
441 rc = run(args)
442 if rc != 0:
443 break
444 # Go on
445 break
446 # End while
447 except Exception as exc:
449 log.error(sys.exc_info())
450 else:
451 print(
452 '\x1b[91mERROR: Unexpected error "%s".\x1b[0m'
453 %
454 str(exc)
455 )
456 rc = 1
457 return rc
458
459
460if __name__ == "__main__":
461 sys.exit(main())
462
463
464# --- END OF CODE ------------------------------------------------------
465
tuple[(str, None), int] selectVersionDir(str ver, str realm, venv_capable=False)
Find the best version, matching the requirements.
Definition hlp.py:435
int auditPyEnv(str min_ver)
Check if "pyenv" version is greater or equal the given minimal version.
Definition hlp.py:213
int auditGlobalPythonVersion(str min_ver)
Check if Python version is greater or equal the given minimal version.
Definition hlp.py:148
str fName(str natural_name)
Convert a natural name into stripped functional name, without spaces, which is lowercase,...
Definition hlp.py:71
int auditPlatform(str name)
Check if the program in running on the required platform.
Definition hlp.py:85
(str, str) getPythonVersion()
Get selected global/local Python version in "pyenv".
Definition hlp.py:331
bool isJunction(str path)
Check if path is a junction, which has been created e.g.
Definition hlp.py:550
int setProjectProperties(str ver, str env)
Set/override project property files.
Definition hlp.py:611
notice((str, tuple) msg)
Log notice message colored to console only.
Definition log.py:197
verbose((str, tuple) msg)
Log verbose message colored to console only.
Definition log.py:209
initLogging()
Initialize the logging.
Definition log.py:71
success((str, tuple) msg)
Log success message colored to console only.
Definition log.py:185
bool isInitialized()
Definition log.py:90
info((str, tuple) msg)
Log info message colored to console only.
Definition log.py:203
error((str, tuple) msg)
Log error message colored to console only.
Definition log.py:179
tuple[(argparse.Namespace, None), int] parseCliArguments2()
Parse CLI arguments for this application.
tuple[(argparse.Namespace, None), int] parseCliArguments1()
Parse CLI arguments for this application.
int displayVersionNumber()
Display the version number and generating procedure for "pyenv-virtualenv" for Windows.
int displayDocumentation()
Display the version number and generating procedure for "pyenv-virtualenv" for Windows.
int main()
Main routine of the application.
int run(argparse.Namespace args)
Sub routine to run the application.