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 f'\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 f'\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(f'1) Auditing pyenv and path environment variables are correctly set ...')
219 key = 'PYENV_ROOT'
220 if not (key in os.environ):
221 log.error(f'Cannot find environment variable "{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(f'Cannot find environment variable "{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(f'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(f'2) Auditing "pyenv" version is {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(f'"pyenv {current_version}" is not supported.')
255 log.notice(f'Install/configure "pyenv {min_ver}+". Then try again.')
256 rc = 1
257 break
258 log.verbose('No deviation detected at observation 2.')
259 log.verbose(f'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 f'Cannot find "pyenv" root directory "{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 f'Cancelling program.'
303 )
304 log.info(
305 f'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(f'Version number "{ver}" is not matching itself (match = "{ver1}".')
448 log.info(f'Check/correct the version sourced from "({realm})". Then try again.')
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(f'Filtered version: {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(f'Selected version: {dir2}')
467 return dir2, 0
468 # End for
469 # End if
470 log.error(f'Cannot find a Python version, which matches "{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 = f'{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 = f'{os.sep}{os.sep}?{os.sep}'
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(f'Automatically corrected version from "{ver}" to "{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 f'"Python "{ver1}" is not installed in "pyenv".'
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(f'Automatically corrected name from "{env}" to "{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(f'"Python {ver1}" has no virtual environment "{env1}" in "pyenv".')
689 log.info(f'Select version from {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(f'Cannot find "{ver_path}".')
850 if os.path.isfile(env_path):
851 os.remove(env_path)
852 else:
853 log.warning(f'Cannot find "{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# @return RC = 0 or other values in case of error.
867 rc: int = 0
868 # noinspection PyBroadException
869 try:
870 # List project properties in folder tree
872 'Listing project properties ...'
873 )
874 up = os.environ['USERPROFILE']
875 # Pass 1: Generate list of project properties.
876 # 1. Version property
877 fnv = '.python-version'
879 log.debug(f'Version file path: "{fpv}".')
880 dtv = ''
881 if os.path.isfile(fpv):
882 fnv = fpv
883 if fnv.startswith(up):
884 fnv = '%USERPROFILE%' + fnv[len(up):]
885 with open(fpv, 'r') as f:
886 dtv = f.read().strip()
887 log.debug(f'Version: "{dtv}".')
888 else:
889 fnv = ''
890 prv = f'({dtv})'
891 acv = ' '
892 if (
893 ('PROMPT' in os.environ)
894 and
895 (prv in os.environ['PROMPT'])
896 ):
897 acv = '*'
898 # 2. Name property
899 fnn = '.python-env'
901 log.debug(f'Name file path: "{fpn}".')
902 dtn = ''
903 if os.path.isfile(fpn):
904 fnn = fpn
905 if fnn.startswith(up):
906 fnn = '%USERPROFILE%' + fnn[len(up):]
907 with open(fpn, 'r') as f:
908 dtn = f.read().strip()
909 log.debug(f'Name: "{dtn}".')
910 else:
911 fnn = ''
912 prn = f'({dtn})'
913 acn = ' '
914 if (
915 ('PROMPT' in os.environ)
916 and
917 (prn in os.environ['PROMPT'])
918 ):
919 acn = '*'
920 # 3. TreeFolders to exclude property
921 fne = '.tree-excludes'
923 log.debug(f'Excludes file path: "{fpe}".')
924 dte = ''
925 if os.path.isfile(fpe):
926 fne = fpe
927 if fne.startswith(up):
928 fne = '%USERPROFILE%' + fne[len(up):]
929 with open(fpe, 'r') as f:
930 dte = f.read().strip()
931 log.debug(f'Excludes: {dte}.')
932 ace = '*'
933 else:
934 fne = ''
935 ace = ' '
936 # Generate data
937 # noinspection SpellCheckingInspection
938 data = [
939 [tbl.HEADER, 'A', 'Property Path', 'ID', '"pyenv" Location/Content'],
940 [tbl.SEPARATOR]
941 ]
942 dtv_dir = os.path.join(
943 os.environ['PYENV_ROOT'],
944 'versions',
945 dtv
946 )
947 if os.path.isdir(dtv_dir):
948 data.append([
949 tbl.DATA,
950 acv,
951 fnv,
952 f'\x1b[96m{dtv}\x1b[0m',
953 dtv_dir
954 ])
955 dtn_dir = os.path.join(
956 os.environ['PYENV_ROOT'],
957 'versions',
958 dtv,
959 'envs',
960 dtn
961 )
962 if os.path.isdir(dtn_dir):
963 data.append([
964 tbl.DATA,
965 acn,
966 fnn,
967 f'\x1b[93m{dtn}\x1b[0m',
968 dtn_dir
969 ])
970 if len(dte) > 0:
971 data.append([
972 tbl.DATA,
973 ace,
974 fne,
975 '\x1b[95m:tuple\x1b[0m',
976 dte
977 ])
978 data.append([tbl.SEPARATOR])
979 # Pass 2: Output list of project properties.
980 table = tbl.SimpleTable(
981 data,
982 headline='LOCAL PROJECT PROPERTIES (A = active):'
983 )
984 table.run()
985 # Pass 3: Output CWD files and folder tree
987 'Listing project properties in files and folders tree ...'
988 )
989 print('\nLOCAL FOLDER TREE:\n')
990 tre.tree(
991 os.getcwd(),
992 exclude=(getTreeFoldersToExclude())
993 )
994 except:
995 log.error(sys.exc_info())
996 rc = 1
997 return rc
998
999## Get the directory tree folder names to exclude from project property file ".tree-excludes".
1000#
1001# @return Tuple of folder names to exclude or empty tuple in case of not found or error.
1002def getTreeFoldersToExclude() -> (tuple, tuple[str]):
1003 result: tuple = ()
1004 # noinspection PyBroadException
1005 try:
1006 file_path = scanCwdAndAncestorsForFile('.tree-excludes')
1007 if file_path == '':
1008 return result
1009 with open(file_path, 'r') as f:
1010 result_str = f.read().strip()
1011 # noinspection PyBroadException
1012 try:
1013 result: (tuple, tuple[str]) = eval(result_str)
1014 if not isinstance(result, (tuple, tuple[str])):
1015 raise TypeError()
1016 except:
1017 log.error(f'Content of file "{file_path}", "{result_str}" cannot be interpreted as "empty tuple" or "tuple of str".')
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 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:1002
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
int listProjectProperties()
Display the table, which shows a list about project properties.
Definition hlp.py:866
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