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
hlp.py
Go to the documentation of this file.
1##
2# @package hlp
3# @file hlp.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# Application-specific Helper Library to deliver complex program
11# features as single function calls for all "pyenv-virtualenv"
12# utilities.
13#
14
15# --- IMPORTS ----------------------------------------------------------
16
17# Python
18import glob
19import os
20import platform
21import re
22import sys
23
24# Community
25# (None)
26
27# My
28try:
29 # noinspection PyUnresolvedReferences
30 import lib.log as log
31except ImportError():
32 try:
33 # noinspection PyUnresolvedReferences
34 import log
35 except ImportError():
36 # noinspection PyUnresolvedReferences
37 import log
38try:
39 # noinspection PyUnresolvedReferences
40 import lib.tbl as tbl
41except ImportError():
42 try:
43 # noinspection PyUnresolvedReferences
44 import tbl
45 except ImportError():
46 # noinspection PyUnresolvedReferences
47 import tbl
48try:
49 # noinspection PyUnresolvedReferences
50 import lib.tre as tre
51except ImportError():
52 try:
53 # noinspection PyUnresolvedReferences
54 import tre
55 except ImportError():
56 # noinspection PyUnresolvedReferences
57 import tre
58
59
60# --- HELPER -----------------------------------------------------------
61
62## Convert a natural name into stripped functional name, without spaces,
63# which is lowercase, is file name safe and is technical safe.
64# NOTE: The resulting name is file name safe and also excludes all kind of
65# technical used characters, including those to insert variables or HTML tags.
66# By this pattern excluded characters are replaced by underscore "_".
67#
68# @param natural_name Natural name to convert.
69# @return Safe functional name.
70# @see https://www.regextester.com/
71def fName(natural_name: str) -> str:
72 return re.sub(
73 r"[/\\?$%#+\-*:|\"'<>()\[\]{}\x7F\x00-\x20]",
74 "_", natural_name.strip()
75 ).casefold().strip('_')
76
77## Check if the program in running on the required platform.
78# NOTE: Due the program is possibly started
79# in an outdated Python 3 version, it is not permitted
80# to use the library "log" inside this function.
81# So, the "print" output must be manually leveled and colorized.
82#
83# @param name Required platform name.
84# @return RC = 0 or other values in case of error.
85def auditPlatform(name: str) -> int:
86 rc: int = 0
87 # noinspection PyBroadException
88 try:
89 while True:
90 verbose = (
91 ('LOG_LEVEL' in os.environ)
92 and
93 (int(os.environ['LOG_LEVEL']) <= 15)
94 )
95 if verbose: print(
96 '\x1b[94mVERBOSE 1) Auditing platform is "%s" ...\x1b[0m' % name,
97 flush=True
98 )
99 if not platform.system() == name:
100 print(
101 '\x1b[91mERROR Platform "%s" is not supported to execute this program.\x1b[0m' % platform.system(),
102 flush=True
103 )
104 print(
105 '\x1b[95mNOTICE Run this program on "%s" only\x1b[0m' % name,
106 flush=True
107 )
108 rc = 1
109 break
110 if verbose: print(
111 '\x1b[94mVERBOSE No deviation detected at observation 1.\x1b[0m',
112 flush=True
113 )
114 if verbose: print(
115 '\x1b[94mVERBOSE Audit result: Zero deviations.\x1b[0m',
116 flush=True
117 )
118 # Go on
119 break
120 # End while
121 if rc != 0:
122 print(
123 '\x1b[91mERROR Cancelling program.\x1b[0m',
124 flush=True
125 )
126 print(
127 '\x1b[37mINFO See detected deviation and remediation proposals/instructions above.\x1b[0m',
128 flush=True
129 )
130 # End if
131 except Exception as exc:
132 print(
133 '\x1b[91mERROR Unexpected error "%s".\x1b[0m' % str(exc),
134 flush=True
135 )
136 rc = 1
137 return rc
138
139## Check if Python version is greater or equal
140# the given minimal version.
141# NOTE: Due the program is possibly started
142# in an outdated Python 3 version, it is not permitted
143# to use the library "log" inside this function.
144# So, the "print" output must be manually leveled and colorized.
145#
146# @param min_ver Minimal permitted version in max. 3 numbers separated by dot (e.g. "3", "3.6", "3.6.5", etc.).
147# @return RC = 0 or other values in case of error.
148def auditGlobalPythonVersion(min_ver: str) -> int:
149 rc: int = 0
150 # noinspection PyBroadException
151 try:
152 while True:
153 verbose = (
154 ('LOG_LEVEL' in os.environ)
155 and
156 (int(os.environ['LOG_LEVEL']) <= 15)
157 )
158 if verbose: print(
159 '\x1b[94mVERBOSE 1) Auditing global Python version is %s+ ...\x1b[0m' % min_ver,
160 flush=True
161 )
162 tup_cur = tuple(map(int, platform.python_version_tuple()))
163 tup_min = tuple(map(int, tuple(min_ver.split('.'))))
164 if tup_cur < tup_min:
165 print(
166 '\x1b[91mERROR Using outdated "Python %s".\x1b[0m' % (
167 platform.python_version()
168 ),
169 flush=True
170 )
171 print(
172 '\x1b[95mNOTICE Install "Python %s+" into "pyenv". Then try again.\x1b[0m' % (
173 platform.python_version()
174 ),
175 flush=True
176 )
177 rc = 1
178 break
179 if verbose: print(
180 '\x1b[94mVERBOSE No deviation detected at observation 1.\x1b[0m',
181 flush=True
182 )
183 if verbose: print(
184 '\x1b[94mVERBOSE Audit result: Zero deviations.\x1b[0m',
185 flush=True
186 )
187 # Go on
188 break
189 # End while
190 if rc != 0:
191 print(
192 '\x1b[91mERROR Cancelling program.\x1b[0m',
193 flush=True
194 )
195 print(
196 '\x1b[95mNOTICE See detected deviation and remediation proposals/instructions above.\x1b[0m',
197 flush=True
198 )
199 # End if
200 except Exception as exc:
201 print(
202 '\x1b[91mERROR Unexpected error "%s".\x1b[0m' % str(exc),
203 flush=True
204 )
205 rc = 1
206 return rc
207
208## Check if "pyenv" version is greater or equal
209# the given minimal version.
210#
211# @param min_ver Minimal permitted version in max. 3 numbers separated by dot (e.g. "3", "3.6", "3.6.5", etc.).
212# @return RC = 0 or other values in case of error.
213def auditPyEnv(min_ver: str) -> int:
214 rc: int = 0
215 # noinspection PyBroadException
216 try:
217 while True:
218 log.verbose('1) Auditing pyenv and path environment variables are correctly set ...')
219 key = 'PYENV_ROOT'
220 if not (key in os.environ):
221 log.error('Cannot find environment variable "{}".'.format(key))
223 'Check/install/repair "pyenv". See: "%USERPROFILE%.pyenv". Then try again.'
224 )
225 rc = 2
226 break
227 key = 'PATH'
228 if not (key in os.environ):
229 log.error('Cannot find environment variable "{}".'.format(key))
231 'Check/restart the terminal shell. Then try again. If this doses not help further on, reboot your computer.'
232 )
233 rc = 2
234 break
235 path = os.environ['PATH'].strip()
236 if len(path) == 0:
237 log.error('The PATH environment variable is empty.')
239 'Check/restart the terminal shell. Then try again. If this doses not help further on, reboot your computer.'
240 )
241 rc = 2
242 break
243 log.verbose('No deviation detected at observation 1.')
244 log.verbose('2) Auditing "pyenv" version is {}+ ...'.format(min_ver))
245 current_version = getPyEnvVersion()
246 if len(current_version) == 0:
247 log.error('Cannot determine the "pyenv" version.')
248 log.notice('Install/configure "pyenv". Then try again.')
249 rc = 1
250 break
251 tup_cur = tuple(map(int, tuple(current_version.split('.'))))
252 tup_min = tuple(map(int, tuple(min_ver.split('.'))))
253 if tup_cur < tup_min:
254 log.error('"pyenv {}" is not supported.'.format(current_version))
255 log.notice('Install/configure "pyenv {}+". Then try again.'.format(min_ver))
256 rc = 1
257 break
258 log.verbose('No deviation detected at observation 2.')
259 log.verbose('3) Auditing "pyenv" Python versions are virtual environment capable ...')
260 pyenv_root_dir = os.environ['PYENV_ROOT']
261 if not os.path.isdir(pyenv_root_dir):
262 log.error(
263 'Cannot find "pyenv" root directory "{}".'.format(pyenv_root_dir)
264 )
265 log.info(
266 'Check/install/repair "pyenv". See: "%USERPROFILE%.pyenv".'
267 )
268 rc = 1
269 break
270 log.verbose('No deviation detected at observation 3.')
271 log.verbose('4) Auditing "pyenv" has virtual environment-capable Python versions ...')
272 # Determine Python version, which is capable
273 # to install Python virtual environment.
274 versions_dir = os.path.join(
275 os.environ['PYENV_ROOT'],
276 'versions'
277 )
278 if not os.path.isdir(versions_dir):
279 log.error('Cannot find any Python version in "pyenv".')
280 log.notice('Install a Python version 3.3+ into "pyenv".')
281 rc = 1
282 break
283 vers = getPythonVersions(
284 version='*',
285 venv_capable=True,
286 as_paths = True
287 )
288 if len(vers) == 0:
289 log.error(
290 'Cannot find a virtual environment capable version in "pyenv".'
291 )
292 log.notice('Install a Python version 3.3+ into "pyenv".')
293 rc = 1
294 break
295 log.verbose('No deviation detected at observation 4.')
296 log.verbose('Audit result: Zero deviations.')
297 # Go on
298 break
299 # End while
300 if rc != 0:
301 log.error(
302 'Cancelling program.'
303 )
304 log.info(
305 'See deviating error messages and remediation proposals/instructions above.'
306 )
307 except:
308 log.error(sys.exc_info())
309 rc = 1
310 return rc
311
312## Get "pyenv" version.
313#
314# @return Version number or empty string in case of error.
315def getPyEnvVersion() -> str:
316 file_path = os.path.abspath(
317 os.path.join(os.environ['PYENV_ROOT'], '..', '.version')
318 )
319 if not os.path.isfile(file_path):
320 return ''
321 ver = ''
322 with open(file_path, 'r') as f:
323 ver = f.read().strip()
324 return ver
325
326## Get selected global/local Python version in "pyenv".
327#
328# @return Tuple of:
329# * Version number or empty string in case of non-selected or error.
330# * Realm name, which delivers the Python version as element of {"local", "global"}.
331def getPythonVersion() -> (str, str):
332 # Primary: Find local Python version in local project properties
333 realm = "local"
334 ver_path = scanCwdAndAncestorsForFile('.python-version')
335 if len(ver_path) > 0:
336 with open(ver_path, 'r') as f:
337 ver = f.read().strip()
338 return ver, realm
339 # Secondary: Find global Python version in "pyenv"
340 realm = "global"
341 ver_path = os.path.join(os.environ['PYENV_ROOT'], 'version')
342 if not os.path.isfile(ver_path):
343 return '', ''
344 ver = ''
345 with open(ver_path, 'r') as f:
346 ver = f.read().strip()
347 return ver, realm
348
349## Scan the CWD and its path ancestors for a specific file.
350#
351# @param file_name Name of the file to find.
352# @return Path to found file or empty string in case of not found.
353def scanCwdAndAncestorsForFile(file_name: str) -> str:
354 dir_path: str = os.getcwd()
355 while True:
356 # Check if file is there
357 file_path = os.path.join(dir_path, file_name)
358 if os.path.isfile(file_path):
359 # Found path
360 return file_path
361 # Check if finished
362 if len(dir_path) <= 3:
363 # Not found path
364 break
365 # Next path ancestor
366 dir_path = os.path.dirname(dir_path)
367 # End while
368 return ''
369
370## Get the "*" marker for this version,
371# if it is the globally selected version in "pyenv".
372#
373# @param ver Version to check as directory path or version string.
374# @return A star ("*") or empty string in any other case.
375def getGlobalStar(ver: str) -> str:
376 ver1 = ver.strip()
377 if os.path.isdir(ver1):
378 ver2 = os.path.basename(ver1)
379 else:
380 ver2 = ver1
381 ver, realm = getPythonVersion()
382 if ver2 == ver:
383 return '*'
384 else:
385 return ''
386
387## Check if the Python version is capable to run virtual environment.
388#
389# @param ver Python version string or path to Python version folder to check.
390# @return Flag to state if the version is capable
391# to run virtual environment.
392# It is False in case of error.
393def isPythonVenvVersion(ver: str) -> bool:
394 # noinspection PyBroadException
395 try:
396 if os.path.isdir(ver):
397 ver1 = os.path.basename(ver)
398 else:
399 ver1 = ver
400 ver2 = re.search(r'\s*([\d.]+)', ver1).group(1)
401 if ver2 != ver1:
402 return False
403 lst = ver2.split('.')
404 while len(lst) < 2:
405 lst.append('0')
406 maj, mno, _ = tuple(lst)
407 maj = int(maj)
408 mno = int(mno)
409 return (
410 (maj > 3)
411 or
412 (
413 (maj == 3)
414 and
415 (mno >= 3)
416 )
417 )
418 except:
419 log.error(sys.exc_info())
420 return False
421
422## Find the best version, matching the requirements.
423# Select best Python version directory.
424#
425# @param ver Number of required version.
426# @param realm Name of the realm, which sources the version number.
427# @param venv_capable Flag to filter for venv-capable versions only. Default: False = no restrictions.
428# @return Tuple of:
429# * Matched Python version directory or None in case of error.
430# * RC = 0 or other values in case of error.
432 ver: str,
433 realm: str,
434 venv_capable=False
435) -> tuple[(str, None), int]:
436 # noinspection PyBroadException
437 try:
438 # Load all versions
439 dirs: list[str] = getPythonVersions(
440 '*',
441 venv_capable=venv_capable,
442 as_paths=True
443 )
444 # Parse required version
445 ver1 = re.search(r'\s*([\d.]+)', ver).group(1)
446 if ver1 != ver:
447 log.error('Version number "{}" is not matching itself (match = "{}".'.format(ver, ver1))
448 log.info('Check/correct the version sourced from "({})". Then try again.'.format(realm))
449 return None, 1
450 tup1 = tuple(ver1.split('.'))
451 # Pass 1: Filter all versions starting with that version
452 lst = []
453 log.verbose('Filtering Python versions ...')
454 for dir1 in dirs:
455 log.debug('Filtered version: {}'.format(dir1))
456 ver2 = os.path.basename(dir1)
457 tup2 = tuple(ver2.split('.'))
458 if tup2[:len(tup1)] == tup1: # Starting with short version
459 lst.append(tup2)
460 # Pass 2: From result of pass 1 filter maximum version
461 if len(lst) > 0:
462 max_tup = max(lst)
463 max_nam = '.'.join(list(max_tup))
464 for dir2 in dirs:
465 if os.path.basename(dir2) == max_nam:
466 log.verbose('Selected version: {}'.format(dir2))
467 return dir2, 0
468 # End for
469 # End if
470 log.error('Cannot find a Python version, which matches "{}".'.format(ver1))
471 log.info('Calling "pyenv virtualenvs", ensure that the wanted version is installed. Then try again.')
472 return None, 2
473 except:
474 log.error(sys.exc_info())
475 return None, 1
476
477## Get the colored virtual environment capability str for the specific version number or path.
478#
479# @param ver Python version number or path to observe.
480
481def getColoredVenvCapability(ver: str) -> str:
482 if isPythonVenvVersion(ver):
483 return '\x1b[92mTrue\x1b[0m'
484 else:
485 return'\x1b[93mFalse\x1b[0m'
486
487## Get list of installed Python version directories in "pyenv".
488#
489# @param version Exact or wildcard version name.
490# Default: '*' = all.
491# @param venv_capable Flag to filter for venv-capable versions only.
492# Default: False = no restrictions.
493# @param as_paths Flag to permit output as paths.
494# Default: False = output as names.
495# @return List of Python version directory paths.
497 version: str='*',
498 venv_capable: bool=False,
499 as_paths: bool=False
500) -> list[str]:
501 result = []
502 vers_path = os.path.join(
503 os.environ['PYENV_ROOT'],
504 'versions',
505 )
506 vers = glob.glob(os.path.join(vers_path, version))
507 if len(vers) > 0:
508 for ver in vers:
509 if (not venv_capable) or (isPythonVenvVersion(ver)):
510 exe_path = os.path.join(ver, 'python.exe')
511 cfg_path = os.path.join(ver, 'pyvenv.cfg')
512 if (
513 (os.path.isfile(exe_path))
514 and
515 (not os.path.isfile(cfg_path))
516 ):
517 if as_paths:
518 item = ver
519 else:
520 item = os.path.basename(ver)
521 result.append(item)
522 if len(result) >= 2:
523 # Sort resulting versions paths in descending order.
524 # NOTE: For a small data amount, the simple 'loop sort' algorythm is sufficient and easy to code.
525 for i1 in range(len(result)):
526 p1: str = result[i1]
527 v1: list = os.path.basename(p1).split('.')
528 t1: tuple = tuple(map(int, v1))
529 for i2 in range(i1 + 1, len(result)):
530 p2: str = result[i2]
531 v2: list = os.path.basename(p2).split('.')
532 t2: tuple = tuple(map(int, v2))
533 if t2 > t1: # Descending order
534 # Exchange version paths
535 m: str = '{}'.format(result[i1]) # Clone
536 result[i1] = result[i2]
537 result[i2] = m
538 # End if
539 # End for
540 # End for
541 # End if
542 return result
543
544## Check if path is a junction, which has been created
545# e.g. using the "mklink /J" command in Windows.
546#
547# @param path The path to the possible junction.
548# @return Flag, which states that the path is a junction.
549# @see https://stackoverflow.com/questions/47469836/how-to-tell-if-a-directory-is-a-windows-junction-in-python
550def isJunction(path: str) -> bool:
551 try:
552 return bool(os.readlink(path))
553 except OSError:
554 return False
555
556## Scan the Pythons versions in "pyenv" for junctions,
557# which points to a specific virtual environment directory path.
558#
559# @param path Specific virtual environment directory path.
560# @return List of paths to junctions.
561def getEnvJunctions(path: str) -> list[str]:
562 # noinspection PyBroadException
563 try:
564 result = []
565 version_dir = os.path.join(
566 os.environ['PYENV_ROOT'],
567 'versions'
568 )
569 vers = glob.glob(os.path.join(
570 version_dir,
571 '*'
572 ))
573 for ver in vers:
574 if isJunction(ver):
575 # Get link value
576 link = os.readlink(ver)
577 # Strip Windows-specific link prefix if exists
578 pref = '\\\\?\\'
579 if link.startswith(pref):
580 link = link.removeprefix(pref)
581 # Compare
582 if link == path:
583 # Append to result
584 result.append(ver)
585 return result
586 except:
587 log.error(sys.exc_info())
588 return []
589
590## Get the content of project property file.
591#
592# @param file_path Path to project property file.
593# @return Text, which the file is containing or empty string in case of error.
594def getProjectPropertyFileStr(file_path: str) -> str:
595 result = ''
596 # noinspection PyBroadException
597 try:
598 with open(file_path, 'r') as f:
599 result = f.read().strip()
600 return result
601 except:
602 log.error(sys.exc_info())
603 return ''
604
605## Set/override project property files.
606# NOTE: The files are written into CWD.
607#
608# @param ver Python version number.
609# @param env Name of Python virtual environment under that version in "pyenv".
610# @return RC = 0 or other values in case of error.
611def setProjectProperties(ver: str, env: str) -> int:
612 rc: int = 0
613 # noinspection PyBroadException
614 try:
615 while True:
617 'Configuring project properties ...'
618 )
619 # Determine the project property file paths in CWD
620 ver_prop_path = os.path.join(
621 os.getcwd(),
622 '.python-version'
623 )
624 env_prop_path = os.path.join(
625 os.getcwd(),
626 '.python-env'
627 )
628 # --- VERSION ----------------------------------------------
629 # Check/correct version string
630 ver1 = re.search(r'\s*([\d.]+)', ver.strip()).group(1)
631 if ver1 != ver:
632 log.notice('Automatically corrected version from "{}" to "{}".'.format(ver, ver1))
633 # Check if Python version is installed
634 ver_path = os.path.join(
635 os.environ['PYENV_ROOT'],
636 'versions',
637 ver1
638 )
639 exe_path = os.path.join(
640 ver_path,
641 'python.exe'
642 )
643 if not (
644 os.path.isdir(ver_path)
645 and
646 os.path.isfile(exe_path)
647 ):
648 log.error(
649 '"Python "{}" is not installed in "pyenv".'.format(ver1)
650 )
651 log.info(
652 'Select version from {}.'.format(
654 version='*',
655 venv_capable=True,
656 as_paths=False
657 )
658 )
659 )
660 rc = 2
661 break
662 # Write Python version property file
663 with open(ver_prop_path, 'w') as ver_f:
664 ver_f.write(ver1)
665 # --- VIRTUAL ENVIRONMENT ----------------------------------
666 # Check/correct "env" string
667 env1 = fName(env)
668 if env1 != env:
669 log.notice('Automatically corrected name from "{}" to "{}".'.format(env, env1))
670 # Check if virtual environment exists under that version
671 env_path = os.path.join(
672 os.environ['PYENV_ROOT'],
673 'versions',
674 ver1,
675 'envs',
676 env1
677 )
678 exe_path = os.path.join(
679 env_path,
680 'Scripts',
681 'python.exe'
682 )
683 if not (
684 os.path.isdir(env_path)
685 and
686 os.path.isfile(exe_path)
687 ):
688 log.error('"Python {}" has no virtual environment "{}" in "pyenv".'.format(ver1, env1))
689 log.info('Select version from {}'.format(getEnvs(ver1)))
690 rc = 2
691 break
692 # Write virtual environment property file
693 with open(env_prop_path, 'w') as ver_f:
694 ver_f.write(env1)
695 # Go on
696 break
697 # End while
698 except:
699 log.error(sys.exc_info())
700 rc = 1
701 return rc
702
703## Get list of installed virtual environments
704# for a specific Python version in "pyenv".
705# Output as names or paths.
706#
707# @param ver Number of required Python version as str
708# or path to Python version.
709# @param name Virtual environment name as str or wildcard str.
710# Default: '*' = all.
711# @param as_paths Flag to permit output as paths.
712# Default: False = output as names.
713# @return List of virtual environment names or paths.
715 ver: str,
716 name: str = '*',
717 as_paths: bool=False
718) -> list[str]:
719 result: list[str] = []
720 if os.path.isdir(ver):
721 envs_path = os.path.join(
722 ver,
723 'envs'
724 )
725 else:
726 envs_path = os.path.join(
727 os.environ['PYENV_ROOT'],
728 'versions',
729 ver,
730 'envs'
731 )
732 envs = glob.glob(os.path.join(envs_path, name))
733 if len(envs) > 0:
734 for env in envs:
735 cfg_path = os.path.join(
736 env,
737 'pyvenv.cfg'
738 )
739 exe_path = os.path.join(
740 env,
741 'Scripts',
742 'python.exe'
743 )
744 if (
745 (os.path.isfile(cfg_path))
746 and
747 (os.path.isfile(exe_path))
748 ):
749 if as_paths:
750 item = env
751 else:
752 item = os.path.basename(env)
753 result.append(item)
754 return result
755
756## Get list of installed virtual environments
757# for a specific Python version in "pyenv".
758# Output as names or paths.
759#
760# @param name Virtual environment name as str or wildcard str.
761# Default: '*' = all.
762# @param as_paths Flag to permit output as paths.
763# Default: False = output as names.
764# @return List of virtual environment names or paths.
766 name: str = '*',
767 as_paths: bool=False
768) -> list[str]:
769 result: list[str] = []
770 vers = getPythonVersions(
771 venv_capable=True,
772 as_paths=True
773 )
774 for ver in vers:
775 if os.path.isdir(ver):
776 envs_path = os.path.join(
777 ver,
778 'envs'
779 )
780 else:
781 envs_path = os.path.join(
782 os.environ['PYENV_ROOT'],
783 'versions',
784 ver,
785 'envs'
786 )
787 envs = glob.glob(os.path.join(envs_path, name))
788 if len(envs) > 0:
789 for env in envs:
790 cfg_path = os.path.join(
791 env,
792 'pyvenv.cfg'
793 )
794 exe_path = os.path.join(
795 env,
796 'Scripts',
797 'python.exe'
798 )
799 if (
800 (os.path.isfile(cfg_path))
801 and
802 (os.path.isfile(exe_path))
803 ):
804 if as_paths:
805 item = env
806 else:
807 item = os.path.basename(env)
808 result.append(item)
809 return result
810
811## Parse virtual environment directory path.
812#
813# @param env_dir Path to version-based virtual environment directory
814# in "pyenv".
815# @return Tuple of:
816# * Python version string
817# * Virtual environment name.
818# * Path to virtual environment directory beginning with "%USERPROFILE%".
819def parseEnvDir(env_dir: str) -> tuple[str, str, str]:
820 items: list[str] = os.path.abspath(env_dir).split(os.sep)
821 version = items[6]
822 name = items[8]
823 items1 = ['%USERPROFILE%'] + items[3:]
824 path = os.sep.join(items1)
825 return version, name, path
826
827## Set project property files.
828# NOTE: The files will be removed from CWD.
829#
830# @return RC = 0 or other values in case of error.
832 rc: int = 0
833 # noinspection PyBroadException
834 try:
836 'Configuring project properties ...'
837 )
838 ver_path = os.path.join(
839 os.getcwd(),
840 '.python-version'
841 )
842 env_path = os.path.join(
843 os.getcwd(),
844 '.python-env'
845 )
846 if os.path.isfile(ver_path):
847 os.remove(ver_path)
848 else:
849 log.warning('Cannot find "{}".'.format(ver_path))
850 if os.path.isfile(env_path):
851 os.remove(env_path)
852 else:
853 log.warning('Cannot find "{}".'.format(env_path))
854 except:
855 log.error(sys.exc_info())
856 rc = 1
857 return rc
858
859## Display the table, which shows a list about project properties.
860# NOTE: The project property files are located in the project folder
861# with the application executable/script. Change directory to that
862# location before you use a feature, which outputs the project
863# properties.
864#
865# @param show_tree Enable tree output.
866# Default: False.
867# @return RC = 0 or other values in case of error.
868def listProjectProperties(show_tree: bool = False) -> int:
869 rc: int = 0
870 # noinspection PyBroadException
871 try:
872 # List project properties in folder tree
874 'Listing project properties ...'
875 )
876 up = os.environ['USERPROFILE']
877 # Pass 1: Generate list of project properties.
878 # 1. Version property
879 fnv = '.python-version'
881 log.debug('Version file path: "{}".'.format(fpv))
882 dtv = ''
883 acv = ' '
884 if os.path.isfile(fpv):
885 fnv = fpv
886 if fnv.startswith(up):
887 fnv = '%USERPROFILE%' + fnv[len(up):]
888 with open(fpv, 'r') as f:
889 dtv = f.read().strip()
890 log.debug('Version: "{}".'.format(dtv))
891 acv = '*'
892 else:
893 fnv = ''
894 # 2. Name property
895 fnn = '.python-env'
897 log.debug('Name file path: "{}".'.format(fpn))
898 dtn = ''
899 if os.path.isfile(fpn):
900 fnn = fpn
901 if fnn.startswith(up):
902 fnn = '%USERPROFILE%' + fnn[len(up):]
903 with open(fpn, 'r') as f:
904 dtn = f.read().strip()
905 log.debug('Name: "{}".'.format(dtn))
906 else:
907 fnn = ''
908 prn = '({})'.format(dtn)
909 acn = ' '
910 if (
911 ('PROMPT' in os.environ)
912 and
913 (prn in os.environ['PROMPT'])
914 ):
915 acn = '*'
916 # 3. TreeFolders to exclude property
917 fne = '.tree-excludes'
919 log.debug('Excludes file path: "{}".'.format(fpe))
920 dte = ''
921 if os.path.isfile(fpe):
922 fne = fpe
923 if fne.startswith(up):
924 fne = '%USERPROFILE%' + fne[len(up):]
925 with open(fpe, 'r') as f:
926 dte = f.read().strip()
927 log.debug('Excludes: {}.'.format(dte))
928 ace = '*'
929 else:
930 fne = ''
931 ace = ' '
932 # Generate data
933 # noinspection SpellCheckingInspection
934 data = [
935 [tbl.HEADER, 'A', 'Property Path', 'ID', '"pyenv" Location/Content'],
936 [tbl.SEPARATOR]
937 ]
938 dtv_dir = os.path.join(
939 os.environ['PYENV_ROOT'],
940 'versions',
941 dtv
942 )
943 if (len(dtv) > 0) and os.path.isdir(dtv_dir):
944 data.append([
945 tbl.DATA,
946 acv,
947 fnv,
948 '\x1b[96m{}\x1b[0m'.format(dtv),
949 dtv_dir
950 ])
951 dtn_dir = os.path.join(
952 os.environ['PYENV_ROOT'],
953 'versions',
954 dtv,
955 'envs',
956 dtn
957 )
958 if os.path.isdir(dtn_dir):
959 data.append([
960 tbl.DATA,
961 acn,
962 fnn,
963 '\x1b[93m{}\x1b[0m'.format(dtn),
964 dtn_dir
965 ])
966 if len(dte) > 0:
967 data.append([
968 tbl.DATA,
969 ace,
970 fne,
971 '\x1b[95m:tuple\x1b[0m',
972 dte
973 ])
974 data.append([tbl.SEPARATOR])
975 # Pass 2: Output list of project properties.
976 table = tbl.SimpleTable(
977 data,
978 headline='LOCAL PROJECT PROPERTIES (A = active):'
979 )
980 table.run()
981 # Pass 3: Output CWD files and folder tree
983 'Listing project properties in files and folders tree ...'
984 )
985 if show_tree:
986 print('\nLOCAL FOLDER TREE:\n')
987 tre.tree(
988 os.getcwd(),
989 exclude=(getTreeFoldersToExclude())
990 )
991 except:
992 log.error(sys.exc_info())
993 rc = 1
994 return rc
995
996## Get the directory tree folder names to exclude from project property file ".tree-excludes".
997#
998# @return Tuple of folder names to exclude or empty tuple in case of not found or error.
999def getTreeFoldersToExclude() -> (tuple, tuple[str]):
1000 result: tuple = ()
1001 # noinspection PyBroadException
1002 try:
1003 file_path = scanCwdAndAncestorsForFile('.tree-excludes')
1004 if file_path == '':
1005 return result
1006 with open(file_path, 'r') as f:
1007 result_str = f.read().strip()
1008 # noinspection PyBroadException
1009 try:
1010 result: (tuple, tuple[str]) = eval(result_str)
1011 if not isinstance(result, (tuple, tuple[str])):
1012 raise TypeError()
1013 except:
1014 log.error('Content of file "{}", "{}" cannot be interpreted as "empty tuple" or "tuple of str".'.format(
1015 file_path,
1016 result_str
1017 ))
1018 log.info('Check/repair this file. Content example: "(\'docs\', \'.idea\', \'__pycache__\')".')
1019 except:
1020 log.error(sys.exc_info())
1021 return result
1022
1023
1024# --- END OF CODE ------------------------------------------------------
1025
tuple[(str, None), int] selectVersionDir(str ver, str realm, venv_capable=False)
Find the best version, matching the requirements.
Definition hlp.py:435
int listProjectProperties(bool show_tree=False)
Display the table, which shows a list about project properties.
Definition hlp.py:868
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
(tuple, tuple[str]) getTreeFoldersToExclude()
Get the directory tree folder names to exclude from project property file ".tree-excludes".
Definition hlp.py:999
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
str getGlobalStar(str ver)
Get the "*" marker for this version, if it is the globally selected version in "pyenv".
Definition hlp.py:375
str getProjectPropertyFileStr(str file_path)
Get the content of project property file.
Definition hlp.py:594
tuple[str, str, str] parseEnvDir(str env_dir)
Parse virtual environment directory path.
Definition hlp.py:819
list[str] getAllEnvs(str name=' *', bool as_paths=False)
Get list of installed virtual environments for a specific Python version in "pyenv".
Definition hlp.py:768
list[str] getEnvs(str ver, str name=' *', bool as_paths=False)
Get list of installed virtual environments for a specific Python version in "pyenv".
Definition hlp.py:718
int unsetProjectProperties()
Set project property files.
Definition hlp.py:831
str getPyEnvVersion()
Get "pyenv" version.
Definition hlp.py:315
int setProjectProperties(str ver, str env)
Set/override project property files.
Definition hlp.py:611
str scanCwdAndAncestorsForFile(str file_name)
Scan the CWD and its path ancestors for a specific file.
Definition hlp.py:353
list[str] getEnvJunctions(str path)
Scan the Pythons versions in "pyenv" for junctions, which points to a specific virtual environment di...
Definition hlp.py:561
bool isPythonVenvVersion(str ver)
Check if the Python version is capable to run virtual environment.
Definition hlp.py:393
list[str] getPythonVersions(str version=' *', bool venv_capable=False, bool as_paths=False)
Get list of installed Python version directories in "pyenv".
Definition hlp.py:500
str getColoredVenvCapability(str ver)
Get the colored virtual environment capability str for the specific version number or path.
Definition hlp.py:481
debug((str, tuple) msg)
Log debug message colored to console only.
Definition log.py:215
warning((str, tuple) msg)
Log warning message colored to console only.
Definition log.py:191
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
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
tree((Path, str) root_path, int recursion_level=-1, bool limit_to_directories=False, int length_limit=1000, tuple exclude=())
Given a directory path, print a visual tree structure.
Definition tre.py:84