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 ctypes
19import os
20import shutil
21import subprocess
22import sys
23
24# Avoid colored output problems
25os.system('')
26
27# Community
28try:
29 import virtualenv
30except ImportError():
31 print(
32 '\x1b[101mCRITICAL %s\x1b[0m'
33 %
34 'Cannot find package "%s".'
35 %
36 'virtualenv'
37 )
38 print(
39 '\x1b[37mINFO %s\x1b[0m'
40 %
41 'Install it using "pip". Then try again.')
42 import virtualenv
43
44# My
45import lib.log as log
46import lib.hlp as hlp
47
48# --- RUN ---------------------------------------------------------------
49
50## Sub routine to run the application.
51#
52# @param args Parsed command line arguments of this application.
53# @return RC = 0 or other values in case of error.
54def run(args: argparse.Namespace) -> int:
55 # noinspection PyUnusedLocal
56 rc: int = 0
57 cwd = os.getcwd()
58 # noinspection PyBroadException
59 try:
60 while True:
61 # Force to select the "Version" argument,
62 # even if it is not given.
63 try:
64 # Use the given "version" argument
65 version: str = args.version
66 realm = 'argument'
67 except AttributeError:
68 # Use the "pyenv" python version
69 version, realm = hlp.getPythonVersion()
70 if version == '':
71 log.error('Cannot determine the version number of the "pyenv" Python version.')
72 log.info('Trying next, set the version number explicitly. See --help.')
73 # noinspection PyUnusedLocal
74 rc = 1
75 break
76 # Optimize arguments
77 version = version.strip()
78 name: str = hlp.fName(args.name)
79 props: bool = args.props
80 # Output actual arguments
81 log.info('Creating Python virtual environment in "pyenv":')
82 log.info(' * Version: {} ({})'.format(version, realm))
83 log.info(' * Name: {}'.format(name))
84 log.info(' * Set project properties: {}'.format(props))
85 # Filter/select best virtual environment capable version
86 selected_version_dir, rc = hlp.selectVersionDir(
87 version,
88 realm,
89 venv_capable=True
90 )
91 if rc != 0:
92 break
93 selected_version = os.path.basename(selected_version_dir)
94 # Check and make directories
95 envs_dir = os.path.join(selected_version_dir, 'envs')
96 if not os.path.isdir(envs_dir):
97 log.verbose('Creating "{}" folder ...'.format(envs_dir))
98 os.mkdir(envs_dir)
99 # Create Python virtual environment
100 venv_dir = os.path.join(envs_dir, name)
101 if os.path.isdir(venv_dir):
102 # Remove existing directory
103 log.verbose('Overriding folder "{}" ...'.format(venv_dir))
104 shutil.rmtree(venv_dir)
105 else:
106 log.verbose('Creating folder "{}" ...'.format(venv_dir))
107 # Get executable path for the selected Python version
108 python_exe_path = os.path.join(
109 selected_version_dir,
110 'python.exe'
111 )
112 # Generate Python virtual environment
113 cmd = [
114 python_exe_path,
115 '-m',
116 'venv',
117 venv_dir
118 ]
119 log.verbose('Execute: {}'.format(python_exe_path))
120 log.info('This will take some seconds ...')
121 # noinspection SpellCheckingInspection
122 cp = subprocess.run(
123 cmd,
124 stdout=subprocess.DEVNULL,
125 stderr=subprocess.DEVNULL,
126 shell=True
127 )
128 rc = cp.returncode
129 if rc != 0:
130 log.error(
131 'Unexpectedly cannot create ' +
132 'Python virtual environment.'
133 )
134 log.info('Check/repair Python and "pyenv". Then try again.')
135 # noinspection PyUnusedLocal
136 rc = 1
137 break
138 # Create junction to virtual environment directory
139 # within the Python versions directory.
140 venv_version_dir = os.path.join(
141 os.environ['PYENV_ROOT'],
142 'versions',
143 '{}-{}'.format(name, selected_version)
144 )
145 if hlp.isJunction(venv_version_dir):
146 # Remove existing junction
148 'Overriding junction "{}" → "{}" ...'.format(
149 venv_version_dir,
150 venv_dir
151 )
152 )
153 os.remove(venv_version_dir)
154 else:
155 log.info(
156 'Creating junction "{}" → "{}" ...'.format(
157 venv_version_dir,
158 venv_dir
159 )
160 )
161 # Check privileges
162 if ctypes.windll.shell32.IsUserAnAdmin() == 0:
163 # Running with 'User' privileges.
164 # Bypassing Windows "'mklink' only as 'Administrator'" limitation.
165 s = input("\x1b[94mDo you permit needed 'Administrator' privileges?\x1b[0m [\x1b[92mY\x1b[0m|\x1b[91mn\x1b[0m]: ").strip().upper()
166 if len(s) == 0:
167 s = 'Y'
168 else:
169 s = s[:1]
170 if s == 'N':
171 # noinspection PyUnusedLocal
172 rc = 130
173 break
174 cmd = 'powershell -command "Start-Process -FilePath \'{}\' -ArgumentList \'{}\', \'{}\' -Verb runAs -Wait"'.format(
175 os.path.join(
176 os.path.dirname(os.path.abspath(__file__)),
177 'create_junction.bat'
178 ),
179 venv_version_dir,
180 venv_dir
181 )
182 log.verbose('Execute: {}'.format(cmd))
183 cp = subprocess.run(
184 cmd,
185 shell=True
186 )
187 else:
188 # Running with 'Administrator' privileges.
189 # noinspection SpellCheckingInspection
190 cmd = 'powershell -file "{}" "{}" "{}"'.format(
191 os.path.join(
192 os.path.dirname(os.path.abspath(__file__)),
193 'create_junction.ps1'
194 ),
195 venv_version_dir,
196 venv_dir
197 )
198 log.verbose('Execute: {}'.format(cmd))
199 cp = subprocess.run(
200 cmd,
201 shell=True
202 )
203 rc = cp.returncode
204 if rc == 0:
205 log.success('Junction "{}" → "{}" has been created.'.format(
206 venv_version_dir,
207 venv_dir
208 ))
209 else:
210 log.error(
211 'Unexpectedly cannot create junction to Python virtual environment directory.'
212 )
213 log.info(
214 'Analyze/configure your file access/permissions of the junction or decide to call this script as "Administrator". Then try again.'
215 )
216 # noinspection PyUnusedLocal
217 rc = 1
218 break
219 if props:
220 log.verbose('Setting project properties ...')
221 rc = hlp.setProjectProperties(selected_version, name)
222 if rc == 0:
223 log.verbose('Project properties successfully set.')
224 else:
225 log.error('Cannot set project properties.')
226 # Log success message
227 print('')
229 'Virtual environment "{}" is installed '.format(name) +
230 'in "pyenv", depending on "Python {}".'.format(selected_version)
231 )
232 # Go on
233 break
234 # End while
235 except:
236 log.error(sys.exc_info())
237 rc = 1
238 finally:
239 os.chdir(cwd)
240 return rc
241
242
243# --- MAIN --------------------------------------------------------------
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 parseCliArguments1() -> 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 'name',
275 help='Short name of the new Python virtual environment.'
276 )
277# --- END CHANGE -------------------------------------------------------
278 return parser.parse_args(), rc
279 except SystemExit:
280 return None, 0
281 except:
282 log.error(sys.exc_info())
283 return None, 1
284
285## Parse CLI arguments for this application.<br>
286# <br>
287# Implement this as required, but don't touch the interface definition
288# for input and output.
289#
290# @return A tuple of:
291# * Namespace to read arguments in "dot" notation or None
292# in case of help or error.
293# * RC = 0 or another value in case of error.
294def parseCliArguments2() -> tuple[(argparse.Namespace, None), int]:
295 rc: int = 0
296 # noinspection PyBroadException
297 try:
298 parser = argparse.ArgumentParser(
299# --- BEGIN CHANGE -----------------------------------------------------
300 prog='pyenv virtualenv',
301 description='Create a version-assigned and named ' +
302 'Python virtual environment in "pyenv".'
303 )
304 # Add flag
305 parser.add_argument(
306 '-p', '--props',
307 dest='props',
308 action=argparse.BooleanOptionalAction,
309 help='As project properties, add the files ' +
310 '`.python-version` and `.python-env` to CWD. Default: --no_props.'
311 )
312 # Add positional str argument
313 parser.add_argument(
314 'version',
315 help='Python version, which must be already installed ' +
316 'in "pyenv".'
317 )
318 # Add positional str argument
319 parser.add_argument(
320 'name',
321 help='Short name of the new Python virtual environment.'
322 )
323# --- END CHANGE -------------------------------------------------------
324 return parser.parse_args(), rc
325 except SystemExit:
326 return None, 0
327 except:
328 log.error(sys.exc_info())
329 return None, 1
330
331## Display the version number and generating procedure for "pyenv-virtualenv" for Windows.
332#
333# @return RC = 0 or another value in case of error
335 # noinspection PyBroadException
336 try:
337 cmd = os.path.join(
338 os.environ['PYENV_ROOT'],
339 'plugins',
340 'pyenv-virtualenv',
341 'docs',
342 'open_doxygen_docs.bat'
343 )
344 log.verbose('Execute: {}'.format(cmd))
345 cp = subprocess.run(
346 cmd,
347 shell=True
348 )
349 rc = cp.returncode
350 if rc != 0:
351 log.error('Displaying Doxygen documentation failed (RC = {}).'.format(rc))
352 log.info('Check/reinstall "pyenv-virtualenv". Then try again. ')
353 log.notice('CANCELED.')
354 rc = 130
355 except:
356 log.error(sys.exc_info())
357 rc = 1
358 return rc
359
360## Display the version number and generating procedure for "pyenv-virtualenv" for Windows.
361#
362# @return RC = 0 or another value in case of error
364 # noinspection PyBroadException
365 try:
366 file_path = os.path.join(
367 os.environ['PYENV_ROOT'],
368 'plugins',
369 'pyenv-virtualenv',
370 '.version'
371 )
372 if not os.path.isfile(file_path):
373 log.error('Cannot display version number.')
374 log.info('Reinstall the missing file "{}".'.format(file_path))
375 log.notice('CANCELED.')
376 return 130
377 with open(file_path, 'r') as f:
378 version = f.read().strip()
379 # noinspection SpellCheckingInspection
380 print('"\x1b[92mpyenv-virtualenv\x1b[0m" for Windows \x1b[96m{}\x1b[0m (\x1b[93mpython -m venv\x1b[0m).'.format(
381 version
382 ))
383 return 0
384 except:
385 log.error(sys.exc_info())
386 return 1
387
388## Main routine of the application.
389#
390# @return RC = 0 or other values in case of error.
391def main() -> int:
392 # noinspection PyBroadException
393 try:
394 while True:
395 # Audit the operating system platform
396 rc = hlp.auditPlatform('Windows')
397 if rc != 0:
398 # Deviation: Reject unsupported platform
399 break
400 # Audit the global Python version number
402 if rc != 0:
403 # Deviation: Reject unsupported Python version
404 break
405 # Initialize the colored logging to console
407 # Audit the "pyenv" version number
408 rc = hlp.auditPyEnv('3')
409 if rc != 0:
410 # Deviation: Reject unsupported "pyenv" version
411 break
412 # Parse arguments
413 # NOTE: Sorry, this is a complicated workaround to
414 # bypass the lack of applicability of Python "ArgumentParser"
415 # class. This simple use case to set required = False for
416 # positional argument is not included and must be patched
417 # away here.
418 log.verbose('Parsing arguments ...')
419 args = None
420 args_list = sys.argv.copy() # Clone
421 # Strip program executable path
422 args_list.pop(0)
423 # Detect and remove all optional arguments,
424 # which are not related to the positional arguments
425 help_requested = False
426 version_requested = False
427 docs_requested = False
428 # noinspection PyUnusedLocal
429 props_requested = False
430 for i in reversed(range(len(args_list))):
431 arg = args_list[i]
432 if arg in ['-h', '--help']:
433 help_requested = True
434 args_list.pop(i)
435 elif arg in ['-v', '--version']:
436 version_requested = True
437 args_list.pop(i)
438 elif arg in ['-d', '--docs']:
439 docs_requested = True
440 args_list.pop(i)
441 elif arg in ['-p', '--props']:
442 # noinspection PyUnusedLocal
443 props_requested = True
444 args_list.pop(i)
445 # Calculate the count of positional arguments
446 positional_count = len(args_list)
447 if docs_requested:
449 break
450 elif version_requested:
452 break
453 elif (
454 help_requested
455 or
456 (positional_count not in [1, 2])
457 ):
458 text = ("""
459Usage: pyenv virtualenv [-h] [-v] [-p | --props | --no-props ] [version] [name]
460
461Create a version-assigned and named Python virtual environment in "pyenv".
462
463Positional arguments (which can be omitted):
464 [version] Python version, which must be already installed in
465 "pyenv". Default: The global Python version.
466 [name] Short name of the new Python virtual environment.
467
468Options:
469 -h, --help Show this help message and exit.
470 -p, --props, --no-props
471 Add the files `.python-version` and `.python-env`
472 as project properties to CWD. Default: --no_props.
473 -v, --version Display the version number of this "pyenv-virtualenv"
474 release and ignore all other arguments.
475 """).strip()
476 print(text)
477 elif positional_count == 1:
478 # Case 1: single positional argument must be "Name"
479 args, rc = parseCliArguments1()
480 elif positional_count == 2:
481 # Case 2: duo positional arguments must be "Version" and "Name"
482 args, rc = parseCliArguments2()
483 if rc != 0:
484 break
485 if args is None: # -h, --help
486 break
487 # Run this application
488 log.verbose('Running application ...')
489 rc = run(args)
490 if rc != 0:
491 break
492 # Go on
493 break
494 # End while
495 except Exception as exc:
497 log.error(sys.exc_info())
498 else:
499 print(
500 '\x1b[91mERROR: Unexpected error "%s".\x1b[0m'
501 %
502 str(exc)
503 )
504 rc = 1
505 return rc
506
507
508if __name__ == "__main__":
509 sys.exit(main())
510
511
512# --- END OF CODE ------------------------------------------------------
513
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.