meerschaum.utils.venv
Manage virtual environments.
1#! /usr/bin/env python3 2# -*- coding: utf-8 -*- 3# vim:fenc=utf-8 4 5""" 6Manage virtual environments. 7""" 8 9from __future__ import annotations 10 11from meerschaum.utils.typing import Optional, Union, Dict, List, Tuple 12from meerschaum.utils.threading import RLock, get_ident 13 14__all__ = sorted([ 15 'activate_venv', 'deactivate_venv', 'init_venv', 16 'inside_venv', 'is_venv_active', 'venv_exec', 17 'venv_executable', 'venv_exists', 'venv_target_path', 18 'Venv', 'get_venvs', 'verify_venv', 19]) 20__pdoc__ = {'Venv': True} 21 22LOCKS = { 23 'sys.path': RLock(), 24 'active_venvs': RLock(), 25 'venvs_active_counts': RLock(), 26} 27 28active_venvs = set() 29active_venvs_counts: Dict[str, int] = {} 30active_venvs_order: List[Optional[str]] = [] 31threads_active_venvs: Dict[int, 'set[str]'] = {} 32 33 34def activate_venv( 35 venv: Optional[str] = 'mrsm', 36 color: bool = True, 37 force: bool = False, 38 debug: bool = False, 39 **kw 40 ) -> bool: 41 """ 42 Create a virtual environment (if it doesn't exist) and add it to `sys.path` if necessary. 43 44 Parameters 45 ---------- 46 venv: Optional[str], default 'mrsm' 47 The virtual environment to activate. 48 49 color: bool, default True 50 If `True`, include color in debug text. 51 52 force: bool, default False 53 If `True`, do not exit early even if the venv is currently active. 54 55 debug: bool, default False 56 Verbosity toggle. 57 58 Returns 59 ------- 60 A bool indicating whether the virtual environment was successfully activated. 61 62 """ 63 thread_id = get_ident() 64 if active_venvs_order and active_venvs_order[0] == venv: 65 if not force: 66 return True 67 import sys, platform, os 68 from meerschaum.config._paths import VIRTENV_RESOURCES_PATH 69 if debug: 70 from meerschaum.utils.debug import dprint 71 if venv is not None: 72 init_venv(venv=venv, debug=debug) 73 with LOCKS['active_venvs']: 74 if thread_id not in threads_active_venvs: 75 threads_active_venvs[thread_id] = {} 76 active_venvs.add(venv) 77 if venv not in threads_active_venvs[thread_id]: 78 threads_active_venvs[thread_id][venv] = 1 79 else: 80 threads_active_venvs[thread_id][venv] += 1 81 82 target = str(venv_target_path(venv, debug=debug)) 83 if venv in active_venvs_order: 84 sys.path.remove(target) 85 try: 86 active_venvs_order.remove(venv) 87 except Exception as e: 88 pass 89 if venv is not None: 90 sys.path.insert(0, target) 91 else: 92 if sys.path and sys.path[0] in (os.getcwd(), ''): 93 sys.path.insert(1, target) 94 else: 95 sys.path.insert(0, target) 96 try: 97 active_venvs_order.insert(0, venv) 98 except Exception as e: 99 pass 100 101 return True 102 103 104def deactivate_venv( 105 venv: str = 'mrsm', 106 color: bool = True, 107 debug: bool = False, 108 previously_active_venvs: Union['set[str]', List[str], None] = None, 109 force: bool = False, 110 **kw 111 ) -> bool: 112 """ 113 Remove a virtual environment from `sys.path` (if it's been activated). 114 115 Parameters 116 ---------- 117 venv: str, default 'mrsm' 118 The virtual environment to deactivate. 119 120 color: bool, default True 121 If `True`, include color in debug text. 122 123 debug: bool, default False 124 Verbosity toggle. 125 126 previously_active_venvs: Union[Set[str], List[str], None] 127 If provided, skip deactivating if a virtual environment is in this iterable. 128 129 force: bool, default False 130 If `True`, forcibly deactivate the virtual environment. 131 This may cause issues with other threads, so be careful! 132 133 Returns 134 ------- 135 Return a bool indicating whether the virtual environment was successfully deactivated. 136 137 """ 138 import sys 139 thread_id = get_ident() 140 if venv is None: 141 if venv in active_venvs: 142 active_venvs.remove(venv) 143 return True 144 145 if previously_active_venvs and venv in previously_active_venvs and not force: 146 return True 147 148 with LOCKS['active_venvs']: 149 if venv in threads_active_venvs.get(thread_id, {}): 150 new_count = threads_active_venvs[thread_id][venv] - 1 151 if new_count > 0 and not force: 152 threads_active_venvs[thread_id][venv] = new_count 153 return True 154 else: 155 del threads_active_venvs[thread_id][venv] 156 157 if not force: 158 for other_thread_id, other_venvs in threads_active_venvs.items(): 159 if other_thread_id == thread_id: 160 continue 161 if venv in other_venvs: 162 return True 163 else: 164 to_delete = [other_thread_id for other_thread_id in threads_active_venvs] 165 for other_thread_id in to_delete: 166 del threads_active_venvs[other_thread_id] 167 168 if venv in active_venvs: 169 active_venvs.remove(venv) 170 171 if sys.path is None: 172 return False 173 174 target = str(venv_target_path(venv, allow_nonexistent=force, debug=debug)) 175 with LOCKS['sys.path']: 176 if target in sys.path: 177 sys.path.remove(target) 178 try: 179 active_venvs_order.remove(venv) 180 except Exception as e: 181 pass 182 183 return True 184 185 186def is_venv_active( 187 venv: str = 'mrsm', 188 color : bool = True, 189 debug: bool = False 190 ) -> bool: 191 """ 192 Check if a virtual environment is active. 193 194 Parameters 195 ---------- 196 venv: str, default 'mrsm' 197 The virtual environment to check. 198 199 color: bool, default True 200 If `True`, include color in debug text. 201 202 debug: bool, default False 203 Verbosity toggle. 204 205 Returns 206 ------- 207 A bool indicating whether the virtual environment `venv` is active. 208 209 """ 210 return venv in active_venvs 211 212 213verified_venvs = set() 214def verify_venv( 215 venv: str, 216 debug: bool = False, 217 ) -> None: 218 """ 219 Verify that the virtual environment matches the expected state. 220 """ 221 import pathlib, platform, os, shutil, subprocess, sys 222 from meerschaum.config._paths import VIRTENV_RESOURCES_PATH 223 from meerschaum.utils.process import run_process 224 from meerschaum.utils.misc import make_symlink, is_symlink 225 from meerschaum.utils.warnings import warn 226 venv_path = VIRTENV_RESOURCES_PATH / venv 227 bin_path = venv_path / ( 228 'bin' if platform.system() != 'Windows' else "Scripts" 229 ) 230 current_python_versioned_name = ( 231 'python' + str(sys.version_info.major) + '.' + str(sys.version_info.minor) 232 + ('' if platform.system() != 'Windows' else '.exe') 233 ) 234 235 if not (bin_path / current_python_versioned_name).exists(): 236 init_venv(venv, verify=False, force=True, debug=debug) 237 current_python_in_venv_path = pathlib.Path(venv_executable(venv=venv)) 238 current_python_in_sys_path = pathlib.Path(venv_executable(venv=None)) 239 if not current_python_in_venv_path.exists(): 240 if is_symlink(current_python_in_venv_path): 241 try: 242 current_python_in_venv_path.unlink() 243 except Exception as e: 244 print(f"Unable to remove symlink {current_python_in_venv_path}:\n{e}") 245 try: 246 make_symlink(current_python_in_sys_path, current_python_in_venv_path) 247 except Exception as e: 248 print( 249 f"Unable to create symlink {current_python_in_venv_path} " 250 + f"to {current_python_in_sys_path}." 251 ) 252 files_to_inspect = sorted(os.listdir(bin_path), reverse=True) 253 else: 254 files_to_inspect = [current_python_versioned_name] 255 256 def get_python_version(python_path: pathlib.Path) -> Union[str, None]: 257 """ 258 Return the version for the python binary at the given path. 259 """ 260 try: 261 ### It might be a broken symlink, so skip on errors. 262 if debug: 263 print(f"Getting python version for {python_path}") 264 proc = run_process( 265 [str(python_path), '-V'], 266 as_proc = True, 267 capture_output = True, 268 ) 269 stdout, stderr = proc.communicate(timeout=1.0) 270 except Exception as e: 271 ### E.g. the symlink may be broken. 272 if is_symlink(python_path): 273 try: 274 python_path.unlink() 275 except Exception as _e: 276 print(f"Unable to remove broken symlink {python_path}:\n{e}\n{_e}") 277 return None 278 return stdout.decode('utf-8').strip().replace('Python ', '') 279 280 ### Ensure the versions are symlinked correctly. 281 for filename in files_to_inspect: 282 if not filename.startswith('python'): 283 continue 284 python_path = bin_path / filename 285 version = get_python_version(python_path) 286 if version is None: 287 continue 288 try: 289 major_version = version.split('.', maxsplit=1)[0] 290 minor_version = version.split('.', maxsplit=2)[1] 291 except IndexError: 292 return 293 python_versioned_name = ( 294 'python' + major_version + '.' + minor_version 295 + ('' if platform.system() != 'Windows' else '.exe') 296 ) 297 298 ### E.g. python3.10 actually links to Python 3.10. 299 if filename == python_versioned_name: 300 real_path = pathlib.Path(os.path.realpath(python_path)) 301 if not real_path.exists(): 302 try: 303 python_path.unlink() 304 except Exception as e: 305 pass 306 init_venv(venv, verify=False, force=True, debug=debug) 307 if not python_path.exists(): 308 raise FileNotFoundError(f"Unable to verify Python symlink:\n{python_path}") 309 310 if python_path == real_path: 311 continue 312 313 try: 314 python_path.unlink() 315 except Exception as e: 316 pass 317 success, msg = make_symlink(real_path, python_path) 318 if not success: 319 warn(msg, color=False) 320 continue 321 322 python_versioned_path = bin_path / python_versioned_name 323 if python_versioned_path.exists(): 324 ### Avoid circular symlinks. 325 if get_python_version(python_versioned_path) == version: 326 continue 327 python_versioned_path.unlink() 328 shutil.move(python_path, python_versioned_path) 329 330 331tried_virtualenv = False 332def init_venv( 333 venv: str = 'mrsm', 334 verify: bool = True, 335 force: bool = False, 336 debug: bool = False, 337 ) -> bool: 338 """ 339 Initialize the virtual environment. 340 341 Parameters 342 ---------- 343 venv: str, default 'mrsm' 344 The name of the virtual environment to create. 345 346 verify: bool, default True 347 If `True`, verify that the virtual environment is in the expected state. 348 349 force: bool, default False 350 If `True`, recreate the virtual environment, even if already initalized. 351 352 Returns 353 ------- 354 A `bool` indicating success. 355 """ 356 if not force and venv in verified_venvs: 357 return True 358 if not force and venv_exists(venv, debug=debug): 359 if verify: 360 verify_venv(venv, debug=debug) 361 verified_venvs.add(venv) 362 return True 363 364 import sys, platform, os, pathlib, shutil 365 from meerschaum.config.static import STATIC_CONFIG 366 from meerschaum.config._paths import VIRTENV_RESOURCES_PATH 367 venv_path = VIRTENV_RESOURCES_PATH / venv 368 docker_home_venv_path = pathlib.Path('/home/meerschaum/venvs/mrsm') 369 370 runtime_env_var = STATIC_CONFIG['environment']['runtime'] 371 work_dir_env_var = STATIC_CONFIG['environment']['work_dir'] 372 if ( 373 not force 374 and venv == 'mrsm' 375 and os.environ.get(work_dir_env_var, None) is not None 376 and docker_home_venv_path.exists() 377 ): 378 shutil.move(docker_home_venv_path, venv_path) 379 if verify: 380 verify_venv(venv, debug=debug) 381 verified_venvs.add(venv) 382 return True 383 384 from meerschaum.utils.packages import run_python_package, attempt_import 385 global tried_virtualenv 386 try: 387 import venv as _venv 388 virtualenv = None 389 except ImportError: 390 _venv = None 391 virtualenv = None 392 393 394 _venv_success = False 395 if _venv is not None: 396 import io 397 from contextlib import redirect_stdout 398 f = io.StringIO() 399 with redirect_stdout(f): 400 _venv_success = run_python_package( 401 'venv', 402 [str(venv_path)] + ( 403 ['--symlinks'] if platform.system() != 'Windows' else [] 404 ), 405 venv=None, debug=debug 406 ) == 0 407 if not _venv_success: 408 print(f"Please install python3-venv.\n{f.getvalue()}\nFalling back to virtualenv...") 409 if not venv_exists(venv, debug=debug): 410 _venv = None 411 if not _venv_success: 412 virtualenv = attempt_import( 413 'virtualenv', venv=None, lazy=False, install=(not tried_virtualenv), warn=False, 414 check_update=False, color=False, debug=debug, 415 ) 416 if virtualenv is None: 417 print( 418 "Failed to import `venv` or `virtualenv`! " 419 + "Please install `virtualenv` via pip then restart Meerschaum." 420 ) 421 return False 422 423 tried_virtualenv = True 424 try: 425 python_folder = ( 426 'python' + str(sys.version_info.major) + '.' + str(sys.version_info.minor) 427 ) 428 dist_packages_path = ( 429 VIRTENV_RESOURCES_PATH / 430 venv / 'local' / 'lib' / python_folder / 'dist-packages' 431 ) 432 local_bin_path = VIRTENV_RESOURCES_PATH / venv / 'local' / 'bin' 433 bin_path = VIRTENV_RESOURCES_PATH / venv / 'bin' 434 vtp = venv_target_path(venv=venv, allow_nonexistent=True, debug=debug) 435 if bin_path.exists(): 436 try: 437 shutil.rmtree(bin_path) 438 except Exception as e: 439 import traceback 440 traceback.print_exc() 441 virtualenv.cli_run([str(venv_path)]) 442 if dist_packages_path.exists(): 443 vtp.mkdir(exist_ok=True, parents=True) 444 for file_path in dist_packages_path.glob('*'): 445 shutil.move(file_path, vtp) 446 shutil.rmtree(dist_packages_path) 447 # shutil.move(dist_packages_path, vtp) 448 bin_path.mkdir(exist_ok=True, parents=True) 449 for file_path in local_bin_path.glob('*'): 450 shutil.move(file_path, bin_path) 451 # shutil.move(local_bin_path, bin_path) 452 shutil.rmtree(local_bin_path) 453 454 except Exception as e: 455 import traceback 456 traceback.print_exc() 457 return False 458 if verify: 459 verify_venv(venv, debug=debug) 460 verified_venvs.add(venv) 461 return True 462 463 464def venv_executable(venv: Optional[str] = 'mrsm') -> str: 465 """ 466 The Python interpreter executable for a given virtual environment. 467 """ 468 from meerschaum.config._paths import VIRTENV_RESOURCES_PATH 469 import sys, platform, os 470 return ( 471 sys.executable if venv is None 472 else str( 473 VIRTENV_RESOURCES_PATH 474 / venv 475 / ( 476 'bin' if platform.system() != 'Windows' 477 else 'Scripts' 478 ) / ( 479 'python' 480 + str(sys.version_info.major) 481 + '.' 482 + str(sys.version_info.minor) 483 ) 484 ) 485 ) 486 487 488def venv_exec( 489 code: str, 490 venv: Optional[str] = 'mrsm', 491 env: Optional[Dict[str, str]] = None, 492 with_extras: bool = False, 493 as_proc: bool = False, 494 capture_output: bool = True, 495 debug: bool = False, 496 ) -> Union[bool, Tuple[int, bytes, bytes]]: 497 """ 498 Execute Python code in a subprocess via a virtual environment's interpeter. 499 Return `True` if the code successfully executes, `False` on failure. 500 501 Parameters 502 ---------- 503 code: str 504 The Python code to excecute. 505 506 venv: str, default 'mrsm' 507 The virtual environment to use to get the path for the Python executable. 508 If `venv` is `None`, use the default `sys.executable` path. 509 510 env: Optional[Dict[str, str]], default None 511 Optionally specify the environment variables for the subprocess. 512 Defaults to `os.environ`. 513 514 with_extras: bool, default False 515 If `True`, return a tuple of the exit code, stdout bytes, and stderr bytes. 516 517 as_proc: bool, default False 518 If `True`, return the `subprocess.Popen` object instead of executing. 519 520 Returns 521 ------- 522 By default, return a bool indicating success. 523 If `as_proc` is `True`, return a `subprocess.Popen` object. 524 If `with_extras` is `True`, return a tuple of the exit code, stdout bytes, and stderr bytes. 525 526 """ 527 import os 528 import subprocess 529 from meerschaum.utils.debug import dprint 530 executable = venv_executable(venv=venv) 531 cmd_list = [executable, '-c', code] 532 if env is None: 533 env = os.environ 534 if debug: 535 dprint(str(cmd_list)) 536 if not with_extras and not as_proc: 537 return subprocess.call(cmd_list, env=env) == 0 538 539 stdout, stderr = (None, None) if not capture_output else (subprocess.PIPE, subprocess.PIPE) 540 process = subprocess.Popen( 541 cmd_list, 542 stdout = stdout, 543 stderr = stderr, 544 env = env, 545 ) 546 if as_proc: 547 return process 548 stdout, stderr = process.communicate() 549 exit_code = process.returncode 550 return exit_code, stdout, stderr 551 552 553def venv_exists(venv: Union[str, None], debug: bool = False) -> bool: 554 """ 555 Determine whether a virtual environment has been created. 556 """ 557 target_path = venv_target_path(venv, allow_nonexistent=True, debug=debug) 558 return target_path.exists() 559 560 561def venv_target_path( 562 venv: Union[str, None], 563 allow_nonexistent: bool = False, 564 debug: bool = False, 565 ) -> 'pathlib.Path': 566 """ 567 Return a virtual environment's site-package path. 568 569 Parameters 570 ---------- 571 venv: Union[str, None] 572 The virtual environment for which a path should be returned. 573 574 allow_nonexistent: bool, default False 575 If `True`, return a path even if it does not exist. 576 577 Returns 578 ------- 579 The `pathlib.Path` object for the virtual environment's path. 580 581 """ 582 import os, sys, platform, pathlib, site 583 from meerschaum.config._paths import VIRTENV_RESOURCES_PATH 584 from meerschaum.config.static import STATIC_CONFIG 585 586 ### Check sys.path for a user-writable site-packages directory. 587 if venv is None: 588 589 ### Return the known value for the portable environment. 590 environment_runtime = STATIC_CONFIG['environment']['runtime'] 591 if os.environ.get(environment_runtime, None) == 'portable': 592 python_version_folder = ( 593 'python' + str(sys.version_info.major) + '.' + str(sys.version_info.minor) 594 ) 595 executable_path = pathlib.Path(sys.executable) 596 site_packages_path = ( 597 ( 598 executable_path.parent.parent / 'lib' / python_version_folder / 'site-packages' 599 ) if platform.system() != 'Windows' else ( 600 executable_path.parent / 'Lib' / 'site-packages' 601 ) 602 ) 603 if not site_packages_path.exists(): 604 raise EnvironmentError(f"Could not find '{site_packages_path}'. Does it exist?") 605 return site_packages_path 606 607 if not inside_venv(): 608 site_path = pathlib.Path(site.getusersitepackages()) 609 if not site_path.exists(): 610 611 ### Windows does not have `os.geteuid()`. 612 if platform.system() == 'Windows' or os.geteuid() != 0: 613 site_path.mkdir(parents=True, exist_ok=True) 614 return site_path 615 616 ### Allow for dist-level paths (running as root). 617 for possible_dist in reversed(site.getsitepackages()): 618 dist_path = pathlib.Path(possible_dist) 619 if not dist_path.exists(): 620 continue 621 return dist_path 622 623 raise EnvironmentError("Could not determine the dist-packages directory.") 624 625 return site_path 626 627 venv_root_path = ( 628 (VIRTENV_RESOURCES_PATH / venv) 629 if venv is not None else pathlib.Path(sys.prefix) 630 ) 631 target_path = venv_root_path 632 633 ### Ensure 'lib' or 'Lib' exists. 634 lib = 'lib' if platform.system() != 'Windows' else 'Lib' 635 if not allow_nonexistent: 636 if not venv_root_path.exists() or lib not in os.listdir(venv_root_path): 637 print(f"Failed to find lib directory for virtual environment '{venv}'.") 638 import traceback 639 traceback.print_stack() 640 sys.exit(1) 641 target_path = target_path / lib 642 643 ### Check if a 'python3.x' folder exists. 644 python_folder = 'python' + str(sys.version_info.major) + '.' + str(sys.version_info.minor) 645 if target_path.exists(): 646 target_path = ( 647 (target_path / python_folder) if python_folder in os.listdir(target_path) 648 else target_path 649 ) 650 else: 651 target_path = ( 652 (target_path / python_folder) if platform.system() != 'Windows' 653 else target_path 654 ) 655 656 ### Ensure 'site-packages' exists. 657 if allow_nonexistent or 'site-packages' in os.listdir(target_path): ### Windows 658 target_path = target_path / 'site-packages' 659 else: 660 import traceback 661 traceback.print_stack() 662 print(f"Failed to find site-packages directory for virtual environment '{venv}'.") 663 print("This may be because you are using a different Python version.") 664 print("Try deleting the following directory and restarting Meerschaum:") 665 print(VIRTENV_RESOURCES_PATH) 666 sys.exit(1) 667 668 return target_path 669 670 671def inside_venv() -> bool: 672 """ 673 Determine whether current Python interpreter is running inside a virtual environment. 674 """ 675 import sys 676 return ( 677 hasattr(sys, 'real_prefix') or ( 678 hasattr(sys, 'base_prefix') 679 and sys.base_prefix != sys.prefix 680 ) 681 ) 682 683 684def get_venvs() -> List[str]: 685 """ 686 Return a list of all the virtual environments. 687 """ 688 import os 689 from meerschaum.config._paths import VIRTENV_RESOURCES_PATH 690 venvs = [] 691 for filename in os.listdir(VIRTENV_RESOURCES_PATH): 692 path = VIRTENV_RESOURCES_PATH / filename 693 if not path.is_dir(): 694 continue 695 if not venv_exists(filename): 696 continue 697 venvs.append(filename) 698 return venvs 699 700 701from meerschaum.utils.venv._Venv import Venv
18class Venv: 19 """ 20 Manage a virtual enviroment's activation status. 21 22 Examples 23 -------- 24 >>> from meerschaum.plugins import Plugin 25 >>> with Venv('mrsm') as venv: 26 ... import pandas 27 >>> with Venv(Plugin('noaa')) as venv: 28 ... import requests 29 >>> venv = Venv('mrsm') 30 >>> venv.activate() 31 True 32 >>> venv.deactivate() 33 True 34 >>> 35 """ 36 37 def __init__( 38 self, 39 venv: Union[str, 'meerschaum.plugins.Plugin', None] = 'mrsm', 40 debug: bool = False, 41 ) -> None: 42 from meerschaum.utils.venv import activate_venv, deactivate_venv, active_venvs 43 ### For some weird threading issue, 44 ### we can't use `isinstance` here. 45 if 'meerschaum.plugins._Plugin' in str(type(venv)): 46 self._venv = venv.name 47 self._activate = venv.activate_venv 48 self._deactivate = venv.deactivate_venv 49 self._kwargs = {} 50 else: 51 self._venv = venv 52 self._activate = activate_venv 53 self._deactivate = deactivate_venv 54 self._kwargs = {'venv': venv} 55 self._debug = debug 56 ### In case someone calls `deactivate()` before `activate()`. 57 self._kwargs['previously_active_venvs'] = copy.deepcopy(active_venvs) 58 59 60 def activate(self, debug: bool = False) -> bool: 61 """ 62 Activate this virtual environment. 63 If a `meerschaum.plugins.Plugin` was provided, its dependent virtual environments 64 will also be activated. 65 """ 66 from meerschaum.utils.venv import active_venvs 67 self._kwargs['previously_active_venvs'] = copy.deepcopy(active_venvs) 68 return self._activate(debug=(debug or self._debug), **self._kwargs) 69 70 71 def deactivate(self, debug: bool = False) -> bool: 72 """ 73 Deactivate this virtual environment. 74 If a `meerschaum.plugins.Plugin` was provided, its dependent virtual environments 75 will also be deactivated. 76 """ 77 return self._deactivate(debug=(debug or self._debug), **self._kwargs) 78 79 80 @property 81 def target_path(self) -> pathlib.Path: 82 """ 83 Return the target site-packages path for this virtual environment. 84 A `meerschaum.utils.venv.Venv` may have one virtual environment per minor Python version 85 (e.g. Python 3.10 and Python 3.7). 86 """ 87 from meerschaum.utils.venv import venv_target_path 88 return venv_target_path(venv=self._venv, allow_nonexistent=True, debug=self._debug) 89 90 91 @property 92 def root_path(self) -> pathlib.Path: 93 """ 94 Return the top-level path for this virtual environment. 95 """ 96 from meerschaum.config._paths import VIRTENV_RESOURCES_PATH 97 if self._venv is None: 98 return self.target_path.parent 99 return VIRTENV_RESOURCES_PATH / self._venv 100 101 102 def __enter__(self) -> None: 103 self.activate(debug=self._debug) 104 105 106 def __exit__(self, exc_type, exc_value, exc_traceback) -> None: 107 self.deactivate(debug=self._debug) 108 109 110 def __str__(self) -> str: 111 quote = "'" if self._venv is not None else "" 112 return "Venv(" + quote + str(self._venv) + quote + ")" 113 114 115 def __repr__(self) -> str: 116 return self.__str__()
Manage a virtual enviroment's activation status.
Examples
>>> from meerschaum.plugins import Plugin
>>> with Venv('mrsm') as venv:
... import pandas
>>> with Venv(Plugin('noaa')) as venv:
... import requests
>>> venv = Venv('mrsm')
>>> venv.activate()
True
>>> venv.deactivate()
True
>>>
37 def __init__( 38 self, 39 venv: Union[str, 'meerschaum.plugins.Plugin', None] = 'mrsm', 40 debug: bool = False, 41 ) -> None: 42 from meerschaum.utils.venv import activate_venv, deactivate_venv, active_venvs 43 ### For some weird threading issue, 44 ### we can't use `isinstance` here. 45 if 'meerschaum.plugins._Plugin' in str(type(venv)): 46 self._venv = venv.name 47 self._activate = venv.activate_venv 48 self._deactivate = venv.deactivate_venv 49 self._kwargs = {} 50 else: 51 self._venv = venv 52 self._activate = activate_venv 53 self._deactivate = deactivate_venv 54 self._kwargs = {'venv': venv} 55 self._debug = debug 56 ### In case someone calls `deactivate()` before `activate()`. 57 self._kwargs['previously_active_venvs'] = copy.deepcopy(active_venvs)
60 def activate(self, debug: bool = False) -> bool: 61 """ 62 Activate this virtual environment. 63 If a `meerschaum.plugins.Plugin` was provided, its dependent virtual environments 64 will also be activated. 65 """ 66 from meerschaum.utils.venv import active_venvs 67 self._kwargs['previously_active_venvs'] = copy.deepcopy(active_venvs) 68 return self._activate(debug=(debug or self._debug), **self._kwargs)
Activate this virtual environment.
If a meerschaum.plugins.Plugin
was provided, its dependent virtual environments
will also be activated.
71 def deactivate(self, debug: bool = False) -> bool: 72 """ 73 Deactivate this virtual environment. 74 If a `meerschaum.plugins.Plugin` was provided, its dependent virtual environments 75 will also be deactivated. 76 """ 77 return self._deactivate(debug=(debug or self._debug), **self._kwargs)
Deactivate this virtual environment.
If a meerschaum.plugins.Plugin
was provided, its dependent virtual environments
will also be deactivated.
Return the target site-packages path for this virtual environment.
A meerschaum.utils.venv.Venv
may have one virtual environment per minor Python version
(e.g. Python 3.10 and Python 3.7).
35def activate_venv( 36 venv: Optional[str] = 'mrsm', 37 color: bool = True, 38 force: bool = False, 39 debug: bool = False, 40 **kw 41 ) -> bool: 42 """ 43 Create a virtual environment (if it doesn't exist) and add it to `sys.path` if necessary. 44 45 Parameters 46 ---------- 47 venv: Optional[str], default 'mrsm' 48 The virtual environment to activate. 49 50 color: bool, default True 51 If `True`, include color in debug text. 52 53 force: bool, default False 54 If `True`, do not exit early even if the venv is currently active. 55 56 debug: bool, default False 57 Verbosity toggle. 58 59 Returns 60 ------- 61 A bool indicating whether the virtual environment was successfully activated. 62 63 """ 64 thread_id = get_ident() 65 if active_venvs_order and active_venvs_order[0] == venv: 66 if not force: 67 return True 68 import sys, platform, os 69 from meerschaum.config._paths import VIRTENV_RESOURCES_PATH 70 if debug: 71 from meerschaum.utils.debug import dprint 72 if venv is not None: 73 init_venv(venv=venv, debug=debug) 74 with LOCKS['active_venvs']: 75 if thread_id not in threads_active_venvs: 76 threads_active_venvs[thread_id] = {} 77 active_venvs.add(venv) 78 if venv not in threads_active_venvs[thread_id]: 79 threads_active_venvs[thread_id][venv] = 1 80 else: 81 threads_active_venvs[thread_id][venv] += 1 82 83 target = str(venv_target_path(venv, debug=debug)) 84 if venv in active_venvs_order: 85 sys.path.remove(target) 86 try: 87 active_venvs_order.remove(venv) 88 except Exception as e: 89 pass 90 if venv is not None: 91 sys.path.insert(0, target) 92 else: 93 if sys.path and sys.path[0] in (os.getcwd(), ''): 94 sys.path.insert(1, target) 95 else: 96 sys.path.insert(0, target) 97 try: 98 active_venvs_order.insert(0, venv) 99 except Exception as e: 100 pass 101 102 return True
Create a virtual environment (if it doesn't exist) and add it to sys.path
if necessary.
Parameters
- venv (Optional[str], default 'mrsm'): The virtual environment to activate.
- color (bool, default True):
If
True
, include color in debug text. - force (bool, default False):
If
True
, do not exit early even if the venv is currently active. - debug (bool, default False): Verbosity toggle.
Returns
- A bool indicating whether the virtual environment was successfully activated.
105def deactivate_venv( 106 venv: str = 'mrsm', 107 color: bool = True, 108 debug: bool = False, 109 previously_active_venvs: Union['set[str]', List[str], None] = None, 110 force: bool = False, 111 **kw 112 ) -> bool: 113 """ 114 Remove a virtual environment from `sys.path` (if it's been activated). 115 116 Parameters 117 ---------- 118 venv: str, default 'mrsm' 119 The virtual environment to deactivate. 120 121 color: bool, default True 122 If `True`, include color in debug text. 123 124 debug: bool, default False 125 Verbosity toggle. 126 127 previously_active_venvs: Union[Set[str], List[str], None] 128 If provided, skip deactivating if a virtual environment is in this iterable. 129 130 force: bool, default False 131 If `True`, forcibly deactivate the virtual environment. 132 This may cause issues with other threads, so be careful! 133 134 Returns 135 ------- 136 Return a bool indicating whether the virtual environment was successfully deactivated. 137 138 """ 139 import sys 140 thread_id = get_ident() 141 if venv is None: 142 if venv in active_venvs: 143 active_venvs.remove(venv) 144 return True 145 146 if previously_active_venvs and venv in previously_active_venvs and not force: 147 return True 148 149 with LOCKS['active_venvs']: 150 if venv in threads_active_venvs.get(thread_id, {}): 151 new_count = threads_active_venvs[thread_id][venv] - 1 152 if new_count > 0 and not force: 153 threads_active_venvs[thread_id][venv] = new_count 154 return True 155 else: 156 del threads_active_venvs[thread_id][venv] 157 158 if not force: 159 for other_thread_id, other_venvs in threads_active_venvs.items(): 160 if other_thread_id == thread_id: 161 continue 162 if venv in other_venvs: 163 return True 164 else: 165 to_delete = [other_thread_id for other_thread_id in threads_active_venvs] 166 for other_thread_id in to_delete: 167 del threads_active_venvs[other_thread_id] 168 169 if venv in active_venvs: 170 active_venvs.remove(venv) 171 172 if sys.path is None: 173 return False 174 175 target = str(venv_target_path(venv, allow_nonexistent=force, debug=debug)) 176 with LOCKS['sys.path']: 177 if target in sys.path: 178 sys.path.remove(target) 179 try: 180 active_venvs_order.remove(venv) 181 except Exception as e: 182 pass 183 184 return True
Remove a virtual environment from sys.path
(if it's been activated).
Parameters
- venv (str, default 'mrsm'): The virtual environment to deactivate.
- color (bool, default True):
If
True
, include color in debug text. - debug (bool, default False): Verbosity toggle.
- previously_active_venvs (Union[Set[str], List[str], None]): If provided, skip deactivating if a virtual environment is in this iterable.
- force (bool, default False):
If
True
, forcibly deactivate the virtual environment. This may cause issues with other threads, so be careful!
Returns
- Return a bool indicating whether the virtual environment was successfully deactivated.
685def get_venvs() -> List[str]: 686 """ 687 Return a list of all the virtual environments. 688 """ 689 import os 690 from meerschaum.config._paths import VIRTENV_RESOURCES_PATH 691 venvs = [] 692 for filename in os.listdir(VIRTENV_RESOURCES_PATH): 693 path = VIRTENV_RESOURCES_PATH / filename 694 if not path.is_dir(): 695 continue 696 if not venv_exists(filename): 697 continue 698 venvs.append(filename) 699 return venvs
Return a list of all the virtual environments.
333def init_venv( 334 venv: str = 'mrsm', 335 verify: bool = True, 336 force: bool = False, 337 debug: bool = False, 338 ) -> bool: 339 """ 340 Initialize the virtual environment. 341 342 Parameters 343 ---------- 344 venv: str, default 'mrsm' 345 The name of the virtual environment to create. 346 347 verify: bool, default True 348 If `True`, verify that the virtual environment is in the expected state. 349 350 force: bool, default False 351 If `True`, recreate the virtual environment, even if already initalized. 352 353 Returns 354 ------- 355 A `bool` indicating success. 356 """ 357 if not force and venv in verified_venvs: 358 return True 359 if not force and venv_exists(venv, debug=debug): 360 if verify: 361 verify_venv(venv, debug=debug) 362 verified_venvs.add(venv) 363 return True 364 365 import sys, platform, os, pathlib, shutil 366 from meerschaum.config.static import STATIC_CONFIG 367 from meerschaum.config._paths import VIRTENV_RESOURCES_PATH 368 venv_path = VIRTENV_RESOURCES_PATH / venv 369 docker_home_venv_path = pathlib.Path('/home/meerschaum/venvs/mrsm') 370 371 runtime_env_var = STATIC_CONFIG['environment']['runtime'] 372 work_dir_env_var = STATIC_CONFIG['environment']['work_dir'] 373 if ( 374 not force 375 and venv == 'mrsm' 376 and os.environ.get(work_dir_env_var, None) is not None 377 and docker_home_venv_path.exists() 378 ): 379 shutil.move(docker_home_venv_path, venv_path) 380 if verify: 381 verify_venv(venv, debug=debug) 382 verified_venvs.add(venv) 383 return True 384 385 from meerschaum.utils.packages import run_python_package, attempt_import 386 global tried_virtualenv 387 try: 388 import venv as _venv 389 virtualenv = None 390 except ImportError: 391 _venv = None 392 virtualenv = None 393 394 395 _venv_success = False 396 if _venv is not None: 397 import io 398 from contextlib import redirect_stdout 399 f = io.StringIO() 400 with redirect_stdout(f): 401 _venv_success = run_python_package( 402 'venv', 403 [str(venv_path)] + ( 404 ['--symlinks'] if platform.system() != 'Windows' else [] 405 ), 406 venv=None, debug=debug 407 ) == 0 408 if not _venv_success: 409 print(f"Please install python3-venv.\n{f.getvalue()}\nFalling back to virtualenv...") 410 if not venv_exists(venv, debug=debug): 411 _venv = None 412 if not _venv_success: 413 virtualenv = attempt_import( 414 'virtualenv', venv=None, lazy=False, install=(not tried_virtualenv), warn=False, 415 check_update=False, color=False, debug=debug, 416 ) 417 if virtualenv is None: 418 print( 419 "Failed to import `venv` or `virtualenv`! " 420 + "Please install `virtualenv` via pip then restart Meerschaum." 421 ) 422 return False 423 424 tried_virtualenv = True 425 try: 426 python_folder = ( 427 'python' + str(sys.version_info.major) + '.' + str(sys.version_info.minor) 428 ) 429 dist_packages_path = ( 430 VIRTENV_RESOURCES_PATH / 431 venv / 'local' / 'lib' / python_folder / 'dist-packages' 432 ) 433 local_bin_path = VIRTENV_RESOURCES_PATH / venv / 'local' / 'bin' 434 bin_path = VIRTENV_RESOURCES_PATH / venv / 'bin' 435 vtp = venv_target_path(venv=venv, allow_nonexistent=True, debug=debug) 436 if bin_path.exists(): 437 try: 438 shutil.rmtree(bin_path) 439 except Exception as e: 440 import traceback 441 traceback.print_exc() 442 virtualenv.cli_run([str(venv_path)]) 443 if dist_packages_path.exists(): 444 vtp.mkdir(exist_ok=True, parents=True) 445 for file_path in dist_packages_path.glob('*'): 446 shutil.move(file_path, vtp) 447 shutil.rmtree(dist_packages_path) 448 # shutil.move(dist_packages_path, vtp) 449 bin_path.mkdir(exist_ok=True, parents=True) 450 for file_path in local_bin_path.glob('*'): 451 shutil.move(file_path, bin_path) 452 # shutil.move(local_bin_path, bin_path) 453 shutil.rmtree(local_bin_path) 454 455 except Exception as e: 456 import traceback 457 traceback.print_exc() 458 return False 459 if verify: 460 verify_venv(venv, debug=debug) 461 verified_venvs.add(venv) 462 return True
Initialize the virtual environment.
Parameters
- venv (str, default 'mrsm'): The name of the virtual environment to create.
- verify (bool, default True):
If
True
, verify that the virtual environment is in the expected state. - force (bool, default False):
If
True
, recreate the virtual environment, even if already initalized.
Returns
- A
bool
indicating success.
672def inside_venv() -> bool: 673 """ 674 Determine whether current Python interpreter is running inside a virtual environment. 675 """ 676 import sys 677 return ( 678 hasattr(sys, 'real_prefix') or ( 679 hasattr(sys, 'base_prefix') 680 and sys.base_prefix != sys.prefix 681 ) 682 )
Determine whether current Python interpreter is running inside a virtual environment.
187def is_venv_active( 188 venv: str = 'mrsm', 189 color : bool = True, 190 debug: bool = False 191 ) -> bool: 192 """ 193 Check if a virtual environment is active. 194 195 Parameters 196 ---------- 197 venv: str, default 'mrsm' 198 The virtual environment to check. 199 200 color: bool, default True 201 If `True`, include color in debug text. 202 203 debug: bool, default False 204 Verbosity toggle. 205 206 Returns 207 ------- 208 A bool indicating whether the virtual environment `venv` is active. 209 210 """ 211 return venv in active_venvs
Check if a virtual environment is active.
Parameters
- venv (str, default 'mrsm'): The virtual environment to check.
- color (bool, default True):
If
True
, include color in debug text. - debug (bool, default False): Verbosity toggle.
Returns
- A bool indicating whether the virtual environment
venv
is active.
489def venv_exec( 490 code: str, 491 venv: Optional[str] = 'mrsm', 492 env: Optional[Dict[str, str]] = None, 493 with_extras: bool = False, 494 as_proc: bool = False, 495 capture_output: bool = True, 496 debug: bool = False, 497 ) -> Union[bool, Tuple[int, bytes, bytes]]: 498 """ 499 Execute Python code in a subprocess via a virtual environment's interpeter. 500 Return `True` if the code successfully executes, `False` on failure. 501 502 Parameters 503 ---------- 504 code: str 505 The Python code to excecute. 506 507 venv: str, default 'mrsm' 508 The virtual environment to use to get the path for the Python executable. 509 If `venv` is `None`, use the default `sys.executable` path. 510 511 env: Optional[Dict[str, str]], default None 512 Optionally specify the environment variables for the subprocess. 513 Defaults to `os.environ`. 514 515 with_extras: bool, default False 516 If `True`, return a tuple of the exit code, stdout bytes, and stderr bytes. 517 518 as_proc: bool, default False 519 If `True`, return the `subprocess.Popen` object instead of executing. 520 521 Returns 522 ------- 523 By default, return a bool indicating success. 524 If `as_proc` is `True`, return a `subprocess.Popen` object. 525 If `with_extras` is `True`, return a tuple of the exit code, stdout bytes, and stderr bytes. 526 527 """ 528 import os 529 import subprocess 530 from meerschaum.utils.debug import dprint 531 executable = venv_executable(venv=venv) 532 cmd_list = [executable, '-c', code] 533 if env is None: 534 env = os.environ 535 if debug: 536 dprint(str(cmd_list)) 537 if not with_extras and not as_proc: 538 return subprocess.call(cmd_list, env=env) == 0 539 540 stdout, stderr = (None, None) if not capture_output else (subprocess.PIPE, subprocess.PIPE) 541 process = subprocess.Popen( 542 cmd_list, 543 stdout = stdout, 544 stderr = stderr, 545 env = env, 546 ) 547 if as_proc: 548 return process 549 stdout, stderr = process.communicate() 550 exit_code = process.returncode 551 return exit_code, stdout, stderr
Execute Python code in a subprocess via a virtual environment's interpeter.
Return True
if the code successfully executes, False
on failure.
Parameters
- code (str): The Python code to excecute.
- venv (str, default 'mrsm'):
The virtual environment to use to get the path for the Python executable.
If
venv
isNone
, use the defaultsys.executable
path. - env (Optional[Dict[str, str]], default None):
Optionally specify the environment variables for the subprocess.
Defaults to
os.environ
. - with_extras (bool, default False):
If
True
, return a tuple of the exit code, stdout bytes, and stderr bytes. - as_proc (bool, default False):
If
True
, return thesubprocess.Popen
object instead of executing.
Returns
- By default, return a bool indicating success.
- If
as_proc
isTrue
, return asubprocess.Popen
object. - If
with_extras
isTrue
, return a tuple of the exit code, stdout bytes, and stderr bytes.
465def venv_executable(venv: Optional[str] = 'mrsm') -> str: 466 """ 467 The Python interpreter executable for a given virtual environment. 468 """ 469 from meerschaum.config._paths import VIRTENV_RESOURCES_PATH 470 import sys, platform, os 471 return ( 472 sys.executable if venv is None 473 else str( 474 VIRTENV_RESOURCES_PATH 475 / venv 476 / ( 477 'bin' if platform.system() != 'Windows' 478 else 'Scripts' 479 ) / ( 480 'python' 481 + str(sys.version_info.major) 482 + '.' 483 + str(sys.version_info.minor) 484 ) 485 ) 486 )
The Python interpreter executable for a given virtual environment.
554def venv_exists(venv: Union[str, None], debug: bool = False) -> bool: 555 """ 556 Determine whether a virtual environment has been created. 557 """ 558 target_path = venv_target_path(venv, allow_nonexistent=True, debug=debug) 559 return target_path.exists()
Determine whether a virtual environment has been created.
562def venv_target_path( 563 venv: Union[str, None], 564 allow_nonexistent: bool = False, 565 debug: bool = False, 566 ) -> 'pathlib.Path': 567 """ 568 Return a virtual environment's site-package path. 569 570 Parameters 571 ---------- 572 venv: Union[str, None] 573 The virtual environment for which a path should be returned. 574 575 allow_nonexistent: bool, default False 576 If `True`, return a path even if it does not exist. 577 578 Returns 579 ------- 580 The `pathlib.Path` object for the virtual environment's path. 581 582 """ 583 import os, sys, platform, pathlib, site 584 from meerschaum.config._paths import VIRTENV_RESOURCES_PATH 585 from meerschaum.config.static import STATIC_CONFIG 586 587 ### Check sys.path for a user-writable site-packages directory. 588 if venv is None: 589 590 ### Return the known value for the portable environment. 591 environment_runtime = STATIC_CONFIG['environment']['runtime'] 592 if os.environ.get(environment_runtime, None) == 'portable': 593 python_version_folder = ( 594 'python' + str(sys.version_info.major) + '.' + str(sys.version_info.minor) 595 ) 596 executable_path = pathlib.Path(sys.executable) 597 site_packages_path = ( 598 ( 599 executable_path.parent.parent / 'lib' / python_version_folder / 'site-packages' 600 ) if platform.system() != 'Windows' else ( 601 executable_path.parent / 'Lib' / 'site-packages' 602 ) 603 ) 604 if not site_packages_path.exists(): 605 raise EnvironmentError(f"Could not find '{site_packages_path}'. Does it exist?") 606 return site_packages_path 607 608 if not inside_venv(): 609 site_path = pathlib.Path(site.getusersitepackages()) 610 if not site_path.exists(): 611 612 ### Windows does not have `os.geteuid()`. 613 if platform.system() == 'Windows' or os.geteuid() != 0: 614 site_path.mkdir(parents=True, exist_ok=True) 615 return site_path 616 617 ### Allow for dist-level paths (running as root). 618 for possible_dist in reversed(site.getsitepackages()): 619 dist_path = pathlib.Path(possible_dist) 620 if not dist_path.exists(): 621 continue 622 return dist_path 623 624 raise EnvironmentError("Could not determine the dist-packages directory.") 625 626 return site_path 627 628 venv_root_path = ( 629 (VIRTENV_RESOURCES_PATH / venv) 630 if venv is not None else pathlib.Path(sys.prefix) 631 ) 632 target_path = venv_root_path 633 634 ### Ensure 'lib' or 'Lib' exists. 635 lib = 'lib' if platform.system() != 'Windows' else 'Lib' 636 if not allow_nonexistent: 637 if not venv_root_path.exists() or lib not in os.listdir(venv_root_path): 638 print(f"Failed to find lib directory for virtual environment '{venv}'.") 639 import traceback 640 traceback.print_stack() 641 sys.exit(1) 642 target_path = target_path / lib 643 644 ### Check if a 'python3.x' folder exists. 645 python_folder = 'python' + str(sys.version_info.major) + '.' + str(sys.version_info.minor) 646 if target_path.exists(): 647 target_path = ( 648 (target_path / python_folder) if python_folder in os.listdir(target_path) 649 else target_path 650 ) 651 else: 652 target_path = ( 653 (target_path / python_folder) if platform.system() != 'Windows' 654 else target_path 655 ) 656 657 ### Ensure 'site-packages' exists. 658 if allow_nonexistent or 'site-packages' in os.listdir(target_path): ### Windows 659 target_path = target_path / 'site-packages' 660 else: 661 import traceback 662 traceback.print_stack() 663 print(f"Failed to find site-packages directory for virtual environment '{venv}'.") 664 print("This may be because you are using a different Python version.") 665 print("Try deleting the following directory and restarting Meerschaum:") 666 print(VIRTENV_RESOURCES_PATH) 667 sys.exit(1) 668 669 return target_path
Return a virtual environment's site-package path.
Parameters
- venv (Union[str, None]): The virtual environment for which a path should be returned.
- allow_nonexistent (bool, default False):
If
True
, return a path even if it does not exist.
Returns
- The
pathlib.Path
object for the virtual environment's path.
215def verify_venv( 216 venv: str, 217 debug: bool = False, 218 ) -> None: 219 """ 220 Verify that the virtual environment matches the expected state. 221 """ 222 import pathlib, platform, os, shutil, subprocess, sys 223 from meerschaum.config._paths import VIRTENV_RESOURCES_PATH 224 from meerschaum.utils.process import run_process 225 from meerschaum.utils.misc import make_symlink, is_symlink 226 from meerschaum.utils.warnings import warn 227 venv_path = VIRTENV_RESOURCES_PATH / venv 228 bin_path = venv_path / ( 229 'bin' if platform.system() != 'Windows' else "Scripts" 230 ) 231 current_python_versioned_name = ( 232 'python' + str(sys.version_info.major) + '.' + str(sys.version_info.minor) 233 + ('' if platform.system() != 'Windows' else '.exe') 234 ) 235 236 if not (bin_path / current_python_versioned_name).exists(): 237 init_venv(venv, verify=False, force=True, debug=debug) 238 current_python_in_venv_path = pathlib.Path(venv_executable(venv=venv)) 239 current_python_in_sys_path = pathlib.Path(venv_executable(venv=None)) 240 if not current_python_in_venv_path.exists(): 241 if is_symlink(current_python_in_venv_path): 242 try: 243 current_python_in_venv_path.unlink() 244 except Exception as e: 245 print(f"Unable to remove symlink {current_python_in_venv_path}:\n{e}") 246 try: 247 make_symlink(current_python_in_sys_path, current_python_in_venv_path) 248 except Exception as e: 249 print( 250 f"Unable to create symlink {current_python_in_venv_path} " 251 + f"to {current_python_in_sys_path}." 252 ) 253 files_to_inspect = sorted(os.listdir(bin_path), reverse=True) 254 else: 255 files_to_inspect = [current_python_versioned_name] 256 257 def get_python_version(python_path: pathlib.Path) -> Union[str, None]: 258 """ 259 Return the version for the python binary at the given path. 260 """ 261 try: 262 ### It might be a broken symlink, so skip on errors. 263 if debug: 264 print(f"Getting python version for {python_path}") 265 proc = run_process( 266 [str(python_path), '-V'], 267 as_proc = True, 268 capture_output = True, 269 ) 270 stdout, stderr = proc.communicate(timeout=1.0) 271 except Exception as e: 272 ### E.g. the symlink may be broken. 273 if is_symlink(python_path): 274 try: 275 python_path.unlink() 276 except Exception as _e: 277 print(f"Unable to remove broken symlink {python_path}:\n{e}\n{_e}") 278 return None 279 return stdout.decode('utf-8').strip().replace('Python ', '') 280 281 ### Ensure the versions are symlinked correctly. 282 for filename in files_to_inspect: 283 if not filename.startswith('python'): 284 continue 285 python_path = bin_path / filename 286 version = get_python_version(python_path) 287 if version is None: 288 continue 289 try: 290 major_version = version.split('.', maxsplit=1)[0] 291 minor_version = version.split('.', maxsplit=2)[1] 292 except IndexError: 293 return 294 python_versioned_name = ( 295 'python' + major_version + '.' + minor_version 296 + ('' if platform.system() != 'Windows' else '.exe') 297 ) 298 299 ### E.g. python3.10 actually links to Python 3.10. 300 if filename == python_versioned_name: 301 real_path = pathlib.Path(os.path.realpath(python_path)) 302 if not real_path.exists(): 303 try: 304 python_path.unlink() 305 except Exception as e: 306 pass 307 init_venv(venv, verify=False, force=True, debug=debug) 308 if not python_path.exists(): 309 raise FileNotFoundError(f"Unable to verify Python symlink:\n{python_path}") 310 311 if python_path == real_path: 312 continue 313 314 try: 315 python_path.unlink() 316 except Exception as e: 317 pass 318 success, msg = make_symlink(real_path, python_path) 319 if not success: 320 warn(msg, color=False) 321 continue 322 323 python_versioned_path = bin_path / python_versioned_name 324 if python_versioned_path.exists(): 325 ### Avoid circular symlinks. 326 if get_python_version(python_versioned_path) == version: 327 continue 328 python_versioned_path.unlink() 329 shutil.move(python_path, python_versioned_path)
Verify that the virtual environment matches the expected state.