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
class 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
>>>
Venv( venv: Union[str, meerschaum.plugins._Plugin.Plugin, NoneType] = 'mrsm', debug: bool = False)
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)
def activate(self, debug: bool = False) -> bool:
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.

def deactivate(self, debug: bool = False) -> bool:
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.

target_path: pathlib.Path

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).

root_path: pathlib.Path

Return the top-level path for this virtual environment.

def activate_venv( venv: Optional[str] = 'mrsm', color: bool = True, force: bool = False, debug: bool = False, **kw) -> bool:
 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.
def deactivate_venv( venv: str = 'mrsm', color: bool = True, debug: bool = False, previously_active_venvs: Union[set[str], List[str], NoneType] = None, force: bool = False, **kw) -> bool:
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.
def get_venvs() -> List[str]:
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.

def init_venv( venv: str = 'mrsm', verify: bool = True, force: bool = False, debug: bool = False) -> bool:
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.
def inside_venv() -> bool:
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.

def is_venv_active(venv: str = 'mrsm', color: bool = True, debug: bool = False) -> bool:
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.
def venv_exec( code: str, venv: Optional[str] = 'mrsm', env: Optional[Dict[str, str]] = None, with_extras: bool = False, as_proc: bool = False, capture_output: bool = True, debug: bool = False) -> Union[bool, Tuple[int, bytes, bytes]]:
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 is None, use the default sys.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 the subprocess.Popen object instead of executing.
Returns
  • By default, return a bool indicating success.
  • If as_proc is True, return a subprocess.Popen object.
  • If with_extras is True, return a tuple of the exit code, stdout bytes, and stderr bytes.
def venv_executable(venv: Optional[str] = 'mrsm') -> str:
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.

def venv_exists(venv: Optional[str], debug: bool = False) -> bool:
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.

def venv_target_path( venv: Optional[str], allow_nonexistent: bool = False, debug: bool = False) -> pathlib.Path:
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.
def verify_venv(venv: str, debug: bool = False) -> None:
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.