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', 'get_module_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]'] = {}
 32CREATE_NEW_PROCESS_GROUP = 0x00000200
 33
 34
 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 = venv_target_path(venv, debug=debug).as_posix()
 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
103
104
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 = venv_target_path(venv, allow_nonexistent=force, debug=debug).as_posix()
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
185
186
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
212
213
214verified_venvs = set()
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)
330
331
332tried_virtualenv = False
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 io
366    from contextlib import redirect_stdout, redirect_stderr
367    import sys, platform, os, pathlib, shutil
368    from meerschaum.config.static import STATIC_CONFIG
369    from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
370    from meerschaum.utils.packages import is_uv_enabled
371    venv_path = VIRTENV_RESOURCES_PATH / venv
372    docker_home_venv_path = pathlib.Path('/home/meerschaum/venvs/mrsm')
373
374    runtime_env_var = STATIC_CONFIG['environment']['runtime']
375    work_dir_env_var = STATIC_CONFIG['environment']['work_dir']
376    if (
377        not force
378        and venv == 'mrsm'
379        and os.environ.get(work_dir_env_var, None) is not None
380        and docker_home_venv_path.exists()
381    ):
382        shutil.move(docker_home_venv_path, venv_path)
383        if verify:
384            verify_venv(venv, debug=debug)
385            verified_venvs.add(venv)
386        return True
387
388    from meerschaum.utils.packages import run_python_package, attempt_import, _get_pip_os_env
389    global tried_virtualenv
390    try:
391        import venv as _venv
392        uv = attempt_import('uv', venv=None, debug=debug) if is_uv_enabled() else None
393        virtualenv = None
394    except ImportError:
395        _venv = None
396        uv = None
397        virtualenv = None
398
399    _venv_success = False
400
401    if uv is not None:
402        _venv_success = run_python_package(
403            'uv',
404            ['venv', venv_path.as_posix(), '-q'],
405            venv=None,
406            env=_get_pip_os_env(),
407            debug=debug,
408        ) == 0
409
410    if _venv is not None and not _venv_success:
411        f = io.StringIO()
412        with redirect_stdout(f):
413            _venv_success = run_python_package(
414                'venv',
415                [venv_path.as_posix()] + (
416                    ['--symlinks'] if platform.system() != 'Windows' else []
417                ),
418                venv=None, debug=debug
419            ) == 0
420        if not _venv_success:
421            print(f"Please install python3-venv.\n{f.getvalue()}\nFalling back to virtualenv...")
422        if not venv_exists(venv, debug=debug):
423            _venv = None
424    if not _venv_success:
425        virtualenv = attempt_import(
426            'virtualenv', venv=None, lazy=False, install=(not tried_virtualenv), warn=False,
427            check_update=False, color=False, debug=debug,
428        )
429        if virtualenv is None:
430            print(
431                "Failed to import `venv` or `virtualenv`! "
432                + "Please install `virtualenv` via pip then restart Meerschaum."
433            )
434            return False
435
436        tried_virtualenv = True
437        try:
438            python_folder = (
439                'python' + str(sys.version_info.major) + '.' + str(sys.version_info.minor)
440            )
441            dist_packages_path = (
442                VIRTENV_RESOURCES_PATH /
443                venv / 'local' / 'lib' / python_folder / 'dist-packages'
444            )
445            local_bin_path = VIRTENV_RESOURCES_PATH / venv / 'local' / 'bin'
446            bin_path = VIRTENV_RESOURCES_PATH / venv / 'bin'
447            vtp = venv_target_path(venv=venv, allow_nonexistent=True, debug=debug)
448            if bin_path.exists():
449                try:
450                    shutil.rmtree(bin_path)
451                except Exception as e:
452                    import traceback
453                    traceback.print_exc()
454            virtualenv.cli_run([venv_path.as_posix()])
455            if dist_packages_path.exists():
456                vtp.mkdir(exist_ok=True, parents=True)
457                for file_path in dist_packages_path.glob('*'):
458                    shutil.move(file_path, vtp)
459                shutil.rmtree(dist_packages_path)
460                #  shutil.move(dist_packages_path, vtp)
461                bin_path.mkdir(exist_ok=True, parents=True)
462                for file_path in local_bin_path.glob('*'):
463                    shutil.move(file_path, bin_path)
464                #  shutil.move(local_bin_path, bin_path)
465                shutil.rmtree(local_bin_path)
466
467        except Exception as e:
468            import traceback
469            traceback.print_exc()
470            return False
471    if verify:
472        verify_venv(venv, debug=debug)
473        verified_venvs.add(venv)
474    return True
475
476
477def venv_executable(venv: Optional[str] = 'mrsm') -> str:
478    """
479    The Python interpreter executable for a given virtual environment.
480    """
481    from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
482    import sys, platform, os
483    return (
484        sys.executable if venv is None
485        else str(
486            VIRTENV_RESOURCES_PATH
487            / venv
488            / (
489                'bin' if platform.system() != 'Windows'
490                else 'Scripts'
491            ) / (
492                'python'
493                + str(sys.version_info.major)
494                + '.'
495                + str(sys.version_info.minor)
496            )
497        )
498    )
499
500
501def venv_exec(
502    code: str,
503    venv: Optional[str] = 'mrsm',
504    env: Optional[Dict[str, str]] = None,
505    with_extras: bool = False,
506    as_proc: bool = False,
507    capture_output: bool = True,
508    debug: bool = False,
509) -> Union[bool, Tuple[int, bytes, bytes]]:
510    """
511    Execute Python code in a subprocess via a virtual environment's interpeter.
512    Return `True` if the code successfully executes, `False` on failure.
513
514    Parameters
515    ----------
516    code: str
517        The Python code to excecute.
518
519    venv: str, default 'mrsm'
520        The virtual environment to use to get the path for the Python executable.
521        If `venv` is `None`, use the default `sys.executable` path.
522
523    env: Optional[Dict[str, str]], default None
524        Optionally specify the environment variables for the subprocess.
525        Defaults to `os.environ`.
526
527    with_extras: bool, default False
528        If `True`, return a tuple of the exit code, stdout bytes, and stderr bytes.
529
530    as_proc: bool, default False
531        If `True`, return the `subprocess.Popen` object instead of executing.
532
533    Returns
534    -------
535    By default, return a bool indicating success.
536    If `as_proc` is `True`, return a `subprocess.Popen` object.
537    If `with_extras` is `True`, return a tuple of the exit code, stdout bytes, and stderr bytes.
538
539    """
540    import os
541    import subprocess
542    import platform
543    from meerschaum.utils.debug import dprint
544    executable = venv_executable(venv=venv)
545    cmd_list = [executable, '-c', code]
546    if env is None:
547        env = os.environ
548    if debug:
549        dprint(str(cmd_list))
550    if not with_extras and not as_proc:
551        return subprocess.call(cmd_list, env=env) == 0
552
553    stdout, stderr = (None, None) if not capture_output else (subprocess.PIPE, subprocess.PIPE)
554    group_kwargs = (
555        {
556            'preexec_fn': os.setsid,
557        } if platform.system() != 'Windows'
558        else {
559            'creationflags': CREATE_NEW_PROCESS_GROUP,
560        }
561    )
562    process = subprocess.Popen(
563        cmd_list,
564        stdout=stdout,
565        stderr=stderr,
566        env=env,
567        **group_kwargs
568    )
569    if as_proc:
570        return process
571    stdout, stderr = process.communicate()
572    exit_code = process.returncode
573    return exit_code, stdout, stderr
574
575
576def venv_exists(venv: Union[str, None], debug: bool = False) -> bool:
577    """
578    Determine whether a virtual environment has been created.
579    """
580    target_path = venv_target_path(venv, allow_nonexistent=True, debug=debug)
581    return target_path.exists()
582
583
584def venv_target_path(
585        venv: Union[str, None],
586        allow_nonexistent: bool = False,
587        debug: bool = False,
588    ) -> 'pathlib.Path':
589    """
590    Return a virtual environment's site-package path.
591
592    Parameters
593    ----------
594    venv: Union[str, None]
595        The virtual environment for which a path should be returned.
596
597    allow_nonexistent: bool, default False
598        If `True`, return a path even if it does not exist.
599
600    Returns
601    -------
602    The `pathlib.Path` object for the virtual environment's path.
603
604    """
605    import os, sys, platform, pathlib, site
606    from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
607    from meerschaum.config.static import STATIC_CONFIG
608
609    ### Check sys.path for a user-writable site-packages directory.
610    if venv is None:
611
612        ### Return the known value for the portable environment.
613        environment_runtime = STATIC_CONFIG['environment']['runtime']
614        if os.environ.get(environment_runtime, None) == 'portable':
615            python_version_folder = (
616                'python' + str(sys.version_info.major) + '.' + str(sys.version_info.minor)
617            )
618            executable_path = pathlib.Path(sys.executable)
619            site_packages_path = (
620                (
621                    executable_path.parent.parent / 'lib' / python_version_folder / 'site-packages'
622                ) if platform.system() != 'Windows' else (
623                    executable_path.parent / 'Lib' / 'site-packages'
624                )
625            )
626            if not site_packages_path.exists():
627                raise EnvironmentError(f"Could not find '{site_packages_path}'. Does it exist?")
628            return site_packages_path
629
630        if not inside_venv():
631            user_site_packages = site.getusersitepackages()
632            if user_site_packages is None:
633                raise EnvironmentError("Could not determine user site packages.")
634
635            site_path = pathlib.Path(user_site_packages)
636            if not site_path.exists():
637
638                ### Windows does not have `os.geteuid()`.
639                if platform.system() == 'Windows' or os.geteuid() != 0:
640                    site_path.mkdir(parents=True, exist_ok=True)
641                    return site_path
642
643                ### Allow for dist-level paths (running as root).
644                for possible_dist in site.getsitepackages():
645                    dist_path = pathlib.Path(possible_dist)
646                    if not dist_path.exists():
647                        continue
648                    return dist_path
649                
650                raise EnvironmentError("Could not determine the dist-packages directory.")
651
652            return site_path
653
654    venv_root_path = (
655        (VIRTENV_RESOURCES_PATH / venv)
656        if venv is not None else pathlib.Path(sys.prefix)
657    )
658    target_path = venv_root_path
659
660    ### Ensure 'lib' or 'Lib' exists.
661    lib = 'lib' if platform.system() != 'Windows' else 'Lib'
662    if not allow_nonexistent:
663        if not venv_root_path.exists() or lib not in os.listdir(venv_root_path):
664            print(f"Failed to find lib directory for virtual environment '{venv}'.")
665            import traceback
666            traceback.print_stack()
667            sys.exit(1)
668    target_path = target_path / lib
669
670    ### Check if a 'python3.x' folder exists.
671    python_folder = 'python' + str(sys.version_info.major) + '.' + str(sys.version_info.minor)
672    if target_path.exists():
673        target_path = (
674            (target_path / python_folder) if python_folder in os.listdir(target_path)
675            else target_path
676        )
677    else:
678        target_path = (
679            (target_path / python_folder) if platform.system() != 'Windows'
680            else target_path
681        )
682
683    ### Ensure 'site-packages' exists.
684    if allow_nonexistent or 'site-packages' in os.listdir(target_path): ### Windows
685        target_path = target_path / 'site-packages'
686    else:
687        import traceback
688        traceback.print_stack()
689        print(f"Failed to find site-packages directory for virtual environment '{venv}'.")
690        print("This may be because you are using a different Python version.")
691        print("Try deleting the following directory and restarting Meerschaum:")
692        print(VIRTENV_RESOURCES_PATH)
693        sys.exit(1)
694
695    return target_path
696
697
698def inside_venv() -> bool:
699    """
700    Determine whether current Python interpreter is running inside a virtual environment.
701    """
702    import sys
703    return (
704        hasattr(sys, 'real_prefix') or (
705            hasattr(sys, 'base_prefix')
706                and sys.base_prefix != sys.prefix
707        )
708    )
709
710
711def get_venvs() -> List[str]:
712    """
713    Return a list of all the virtual environments.
714    """
715    import os
716    from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
717    venvs = []
718    for filename in os.listdir(VIRTENV_RESOURCES_PATH):
719        path = VIRTENV_RESOURCES_PATH / filename
720        if not path.is_dir():
721            continue
722        if not venv_exists(filename):
723            continue
724        venvs.append(filename)
725    return venvs
726
727
728def get_module_venv(module) -> Union[str, None]:
729    """
730    Return the virtual environment where an imported module is installed.
731
732    Parameters
733    ----------
734    module: ModuleType
735        The imported module to inspect.
736
737    Returns
738    -------
739    The name of a venv or `None`.
740    """
741    import pathlib
742    from meerschaum.config.paths import VIRTENV_RESOURCES_PATH
743    module_path = pathlib.Path(module.__file__).resolve()
744    try:
745        rel_path = module_path.relative_to(VIRTENV_RESOURCES_PATH)
746    except ValueError:
747        return None
748
749    return rel_path.as_posix().split('/', maxsplit=1)[0]
750
751
752from 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.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
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)

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

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:
 36def activate_venv(
 37    venv: Optional[str] = 'mrsm',
 38    color: bool = True,
 39    force: bool = False,
 40    debug: bool = False,
 41    **kw
 42) -> bool:
 43    """
 44    Create a virtual environment (if it doesn't exist) and add it to `sys.path` if necessary.
 45
 46    Parameters
 47    ----------
 48    venv: Optional[str], default 'mrsm'
 49        The virtual environment to activate.
 50
 51    color: bool, default True
 52        If `True`, include color in debug text.
 53
 54    force: bool, default False
 55        If `True`, do not exit early even if the venv is currently active.
 56
 57    debug: bool, default False
 58        Verbosity toggle.
 59
 60    Returns
 61    -------
 62    A bool indicating whether the virtual environment was successfully activated.
 63
 64    """
 65    thread_id = get_ident()
 66    if active_venvs_order and active_venvs_order[0] == venv:
 67        if not force:
 68            return True
 69    import sys, platform, os
 70    from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
 71    if debug:
 72        from meerschaum.utils.debug import dprint
 73    if venv is not None:
 74        init_venv(venv=venv, debug=debug)
 75    with LOCKS['active_venvs']:
 76        if thread_id not in threads_active_venvs:
 77            threads_active_venvs[thread_id] = {}
 78        active_venvs.add(venv)
 79        if venv not in threads_active_venvs[thread_id]:
 80            threads_active_venvs[thread_id][venv] = 1
 81        else:
 82            threads_active_venvs[thread_id][venv] += 1
 83
 84        target = venv_target_path(venv, debug=debug).as_posix()
 85        if venv in active_venvs_order:
 86            sys.path.remove(target)
 87            try:
 88                active_venvs_order.remove(venv)
 89            except Exception as e:
 90                pass
 91        if venv is not None:
 92            sys.path.insert(0, target)
 93        else:
 94            if sys.path and sys.path[0] in (os.getcwd(), ''):
 95                sys.path.insert(1, target)
 96            else:
 97                sys.path.insert(0, target)
 98        try:
 99            active_venvs_order.insert(0, venv)
100        except Exception as e:
101            pass
102
103    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:
106def deactivate_venv(
107    venv: str = 'mrsm',
108    color: bool = True,
109    debug: bool = False,
110    previously_active_venvs: Union['set[str]', List[str], None] = None,
111    force: bool = False,
112    **kw
113) -> bool:
114    """
115    Remove a virtual environment from `sys.path` (if it's been activated).
116
117    Parameters
118    ----------
119    venv: str, default 'mrsm'
120        The virtual environment to deactivate.
121
122    color: bool, default True
123        If `True`, include color in debug text.
124
125    debug: bool, default False
126        Verbosity toggle.
127
128    previously_active_venvs: Union[Set[str], List[str], None]
129        If provided, skip deactivating if a virtual environment is in this iterable.
130
131    force: bool, default False
132        If `True`, forcibly deactivate the virtual environment.
133        This may cause issues with other threads, so be careful!
134
135    Returns
136    -------
137    Return a bool indicating whether the virtual environment was successfully deactivated.
138
139    """
140    import sys
141    thread_id = get_ident()
142    if venv is None:
143        if venv in active_venvs:
144            active_venvs.remove(venv)
145        return True
146
147    if previously_active_venvs and venv in previously_active_venvs and not force:
148        return True
149
150    with LOCKS['active_venvs']:
151        if venv in threads_active_venvs.get(thread_id, {}):
152            new_count = threads_active_venvs[thread_id][venv] - 1
153            if new_count > 0 and not force:
154                threads_active_venvs[thread_id][venv] = new_count
155                return True
156            else:
157                del threads_active_venvs[thread_id][venv]
158
159        if not force:
160            for other_thread_id, other_venvs in threads_active_venvs.items():
161                if other_thread_id == thread_id:
162                    continue
163                if venv in other_venvs:
164                    return True
165        else:
166            to_delete = [other_thread_id for other_thread_id in threads_active_venvs]
167            for other_thread_id in to_delete:
168                del threads_active_venvs[other_thread_id]
169
170        if venv in active_venvs:
171            active_venvs.remove(venv)
172
173    if sys.path is None:
174        return False
175
176    target = venv_target_path(venv, allow_nonexistent=force, debug=debug).as_posix()
177    with LOCKS['sys.path']:
178        if target in sys.path:
179            sys.path.remove(target)
180            try:
181                active_venvs_order.remove(venv)
182            except Exception as e:
183                pass
184
185    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_module_venv(module) -> Optional[str]:
729def get_module_venv(module) -> Union[str, None]:
730    """
731    Return the virtual environment where an imported module is installed.
732
733    Parameters
734    ----------
735    module: ModuleType
736        The imported module to inspect.
737
738    Returns
739    -------
740    The name of a venv or `None`.
741    """
742    import pathlib
743    from meerschaum.config.paths import VIRTENV_RESOURCES_PATH
744    module_path = pathlib.Path(module.__file__).resolve()
745    try:
746        rel_path = module_path.relative_to(VIRTENV_RESOURCES_PATH)
747    except ValueError:
748        return None
749
750    return rel_path.as_posix().split('/', maxsplit=1)[0]

Return the virtual environment where an imported module is installed.

Parameters
  • module (ModuleType): The imported module to inspect.
Returns
  • The name of a venv or None.
def get_venvs() -> List[str]:
712def get_venvs() -> List[str]:
713    """
714    Return a list of all the virtual environments.
715    """
716    import os
717    from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
718    venvs = []
719    for filename in os.listdir(VIRTENV_RESOURCES_PATH):
720        path = VIRTENV_RESOURCES_PATH / filename
721        if not path.is_dir():
722            continue
723        if not venv_exists(filename):
724            continue
725        venvs.append(filename)
726    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:
334def init_venv(
335    venv: str = 'mrsm',
336    verify: bool = True,
337    force: bool = False,
338    debug: bool = False,
339) -> bool:
340    """
341    Initialize the virtual environment.
342
343    Parameters
344    ----------
345    venv: str, default 'mrsm'
346        The name of the virtual environment to create.
347
348    verify: bool, default True
349        If `True`, verify that the virtual environment is in the expected state.
350
351    force: bool, default False
352        If `True`, recreate the virtual environment, even if already initalized.
353
354    Returns
355    -------
356    A `bool` indicating success.
357    """
358    if not force and venv in verified_venvs:
359        return True
360    if not force and venv_exists(venv, debug=debug):
361        if verify:
362            verify_venv(venv, debug=debug)
363            verified_venvs.add(venv)
364        return True
365
366    import io
367    from contextlib import redirect_stdout, redirect_stderr
368    import sys, platform, os, pathlib, shutil
369    from meerschaum.config.static import STATIC_CONFIG
370    from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
371    from meerschaum.utils.packages import is_uv_enabled
372    venv_path = VIRTENV_RESOURCES_PATH / venv
373    docker_home_venv_path = pathlib.Path('/home/meerschaum/venvs/mrsm')
374
375    runtime_env_var = STATIC_CONFIG['environment']['runtime']
376    work_dir_env_var = STATIC_CONFIG['environment']['work_dir']
377    if (
378        not force
379        and venv == 'mrsm'
380        and os.environ.get(work_dir_env_var, None) is not None
381        and docker_home_venv_path.exists()
382    ):
383        shutil.move(docker_home_venv_path, venv_path)
384        if verify:
385            verify_venv(venv, debug=debug)
386            verified_venvs.add(venv)
387        return True
388
389    from meerschaum.utils.packages import run_python_package, attempt_import, _get_pip_os_env
390    global tried_virtualenv
391    try:
392        import venv as _venv
393        uv = attempt_import('uv', venv=None, debug=debug) if is_uv_enabled() else None
394        virtualenv = None
395    except ImportError:
396        _venv = None
397        uv = None
398        virtualenv = None
399
400    _venv_success = False
401
402    if uv is not None:
403        _venv_success = run_python_package(
404            'uv',
405            ['venv', venv_path.as_posix(), '-q'],
406            venv=None,
407            env=_get_pip_os_env(),
408            debug=debug,
409        ) == 0
410
411    if _venv is not None and not _venv_success:
412        f = io.StringIO()
413        with redirect_stdout(f):
414            _venv_success = run_python_package(
415                'venv',
416                [venv_path.as_posix()] + (
417                    ['--symlinks'] if platform.system() != 'Windows' else []
418                ),
419                venv=None, debug=debug
420            ) == 0
421        if not _venv_success:
422            print(f"Please install python3-venv.\n{f.getvalue()}\nFalling back to virtualenv...")
423        if not venv_exists(venv, debug=debug):
424            _venv = None
425    if not _venv_success:
426        virtualenv = attempt_import(
427            'virtualenv', venv=None, lazy=False, install=(not tried_virtualenv), warn=False,
428            check_update=False, color=False, debug=debug,
429        )
430        if virtualenv is None:
431            print(
432                "Failed to import `venv` or `virtualenv`! "
433                + "Please install `virtualenv` via pip then restart Meerschaum."
434            )
435            return False
436
437        tried_virtualenv = True
438        try:
439            python_folder = (
440                'python' + str(sys.version_info.major) + '.' + str(sys.version_info.minor)
441            )
442            dist_packages_path = (
443                VIRTENV_RESOURCES_PATH /
444                venv / 'local' / 'lib' / python_folder / 'dist-packages'
445            )
446            local_bin_path = VIRTENV_RESOURCES_PATH / venv / 'local' / 'bin'
447            bin_path = VIRTENV_RESOURCES_PATH / venv / 'bin'
448            vtp = venv_target_path(venv=venv, allow_nonexistent=True, debug=debug)
449            if bin_path.exists():
450                try:
451                    shutil.rmtree(bin_path)
452                except Exception as e:
453                    import traceback
454                    traceback.print_exc()
455            virtualenv.cli_run([venv_path.as_posix()])
456            if dist_packages_path.exists():
457                vtp.mkdir(exist_ok=True, parents=True)
458                for file_path in dist_packages_path.glob('*'):
459                    shutil.move(file_path, vtp)
460                shutil.rmtree(dist_packages_path)
461                #  shutil.move(dist_packages_path, vtp)
462                bin_path.mkdir(exist_ok=True, parents=True)
463                for file_path in local_bin_path.glob('*'):
464                    shutil.move(file_path, bin_path)
465                #  shutil.move(local_bin_path, bin_path)
466                shutil.rmtree(local_bin_path)
467
468        except Exception as e:
469            import traceback
470            traceback.print_exc()
471            return False
472    if verify:
473        verify_venv(venv, debug=debug)
474        verified_venvs.add(venv)
475    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:
699def inside_venv() -> bool:
700    """
701    Determine whether current Python interpreter is running inside a virtual environment.
702    """
703    import sys
704    return (
705        hasattr(sys, 'real_prefix') or (
706            hasattr(sys, 'base_prefix')
707                and sys.base_prefix != sys.prefix
708        )
709    )

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:
188def is_venv_active(
189        venv: str = 'mrsm',
190        color : bool = True,
191        debug: bool = False
192    ) -> bool:
193    """
194    Check if a virtual environment is active.
195
196    Parameters
197    ----------
198    venv: str, default 'mrsm'
199        The virtual environment to check.
200
201    color: bool, default True
202        If `True`, include color in debug text.
203
204    debug: bool, default False
205        Verbosity toggle.
206
207    Returns
208    -------
209    A bool indicating whether the virtual environment `venv` is active.
210
211    """
212    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]]:
502def venv_exec(
503    code: str,
504    venv: Optional[str] = 'mrsm',
505    env: Optional[Dict[str, str]] = None,
506    with_extras: bool = False,
507    as_proc: bool = False,
508    capture_output: bool = True,
509    debug: bool = False,
510) -> Union[bool, Tuple[int, bytes, bytes]]:
511    """
512    Execute Python code in a subprocess via a virtual environment's interpeter.
513    Return `True` if the code successfully executes, `False` on failure.
514
515    Parameters
516    ----------
517    code: str
518        The Python code to excecute.
519
520    venv: str, default 'mrsm'
521        The virtual environment to use to get the path for the Python executable.
522        If `venv` is `None`, use the default `sys.executable` path.
523
524    env: Optional[Dict[str, str]], default None
525        Optionally specify the environment variables for the subprocess.
526        Defaults to `os.environ`.
527
528    with_extras: bool, default False
529        If `True`, return a tuple of the exit code, stdout bytes, and stderr bytes.
530
531    as_proc: bool, default False
532        If `True`, return the `subprocess.Popen` object instead of executing.
533
534    Returns
535    -------
536    By default, return a bool indicating success.
537    If `as_proc` is `True`, return a `subprocess.Popen` object.
538    If `with_extras` is `True`, return a tuple of the exit code, stdout bytes, and stderr bytes.
539
540    """
541    import os
542    import subprocess
543    import platform
544    from meerschaum.utils.debug import dprint
545    executable = venv_executable(venv=venv)
546    cmd_list = [executable, '-c', code]
547    if env is None:
548        env = os.environ
549    if debug:
550        dprint(str(cmd_list))
551    if not with_extras and not as_proc:
552        return subprocess.call(cmd_list, env=env) == 0
553
554    stdout, stderr = (None, None) if not capture_output else (subprocess.PIPE, subprocess.PIPE)
555    group_kwargs = (
556        {
557            'preexec_fn': os.setsid,
558        } if platform.system() != 'Windows'
559        else {
560            'creationflags': CREATE_NEW_PROCESS_GROUP,
561        }
562    )
563    process = subprocess.Popen(
564        cmd_list,
565        stdout=stdout,
566        stderr=stderr,
567        env=env,
568        **group_kwargs
569    )
570    if as_proc:
571        return process
572    stdout, stderr = process.communicate()
573    exit_code = process.returncode
574    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:
478def venv_executable(venv: Optional[str] = 'mrsm') -> str:
479    """
480    The Python interpreter executable for a given virtual environment.
481    """
482    from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
483    import sys, platform, os
484    return (
485        sys.executable if venv is None
486        else str(
487            VIRTENV_RESOURCES_PATH
488            / venv
489            / (
490                'bin' if platform.system() != 'Windows'
491                else 'Scripts'
492            ) / (
493                'python'
494                + str(sys.version_info.major)
495                + '.'
496                + str(sys.version_info.minor)
497            )
498        )
499    )

The Python interpreter executable for a given virtual environment.

def venv_exists(venv: Optional[str], debug: bool = False) -> bool:
577def venv_exists(venv: Union[str, None], debug: bool = False) -> bool:
578    """
579    Determine whether a virtual environment has been created.
580    """
581    target_path = venv_target_path(venv, allow_nonexistent=True, debug=debug)
582    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:
585def venv_target_path(
586        venv: Union[str, None],
587        allow_nonexistent: bool = False,
588        debug: bool = False,
589    ) -> 'pathlib.Path':
590    """
591    Return a virtual environment's site-package path.
592
593    Parameters
594    ----------
595    venv: Union[str, None]
596        The virtual environment for which a path should be returned.
597
598    allow_nonexistent: bool, default False
599        If `True`, return a path even if it does not exist.
600
601    Returns
602    -------
603    The `pathlib.Path` object for the virtual environment's path.
604
605    """
606    import os, sys, platform, pathlib, site
607    from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
608    from meerschaum.config.static import STATIC_CONFIG
609
610    ### Check sys.path for a user-writable site-packages directory.
611    if venv is None:
612
613        ### Return the known value for the portable environment.
614        environment_runtime = STATIC_CONFIG['environment']['runtime']
615        if os.environ.get(environment_runtime, None) == 'portable':
616            python_version_folder = (
617                'python' + str(sys.version_info.major) + '.' + str(sys.version_info.minor)
618            )
619            executable_path = pathlib.Path(sys.executable)
620            site_packages_path = (
621                (
622                    executable_path.parent.parent / 'lib' / python_version_folder / 'site-packages'
623                ) if platform.system() != 'Windows' else (
624                    executable_path.parent / 'Lib' / 'site-packages'
625                )
626            )
627            if not site_packages_path.exists():
628                raise EnvironmentError(f"Could not find '{site_packages_path}'. Does it exist?")
629            return site_packages_path
630
631        if not inside_venv():
632            user_site_packages = site.getusersitepackages()
633            if user_site_packages is None:
634                raise EnvironmentError("Could not determine user site packages.")
635
636            site_path = pathlib.Path(user_site_packages)
637            if not site_path.exists():
638
639                ### Windows does not have `os.geteuid()`.
640                if platform.system() == 'Windows' or os.geteuid() != 0:
641                    site_path.mkdir(parents=True, exist_ok=True)
642                    return site_path
643
644                ### Allow for dist-level paths (running as root).
645                for possible_dist in site.getsitepackages():
646                    dist_path = pathlib.Path(possible_dist)
647                    if not dist_path.exists():
648                        continue
649                    return dist_path
650                
651                raise EnvironmentError("Could not determine the dist-packages directory.")
652
653            return site_path
654
655    venv_root_path = (
656        (VIRTENV_RESOURCES_PATH / venv)
657        if venv is not None else pathlib.Path(sys.prefix)
658    )
659    target_path = venv_root_path
660
661    ### Ensure 'lib' or 'Lib' exists.
662    lib = 'lib' if platform.system() != 'Windows' else 'Lib'
663    if not allow_nonexistent:
664        if not venv_root_path.exists() or lib not in os.listdir(venv_root_path):
665            print(f"Failed to find lib directory for virtual environment '{venv}'.")
666            import traceback
667            traceback.print_stack()
668            sys.exit(1)
669    target_path = target_path / lib
670
671    ### Check if a 'python3.x' folder exists.
672    python_folder = 'python' + str(sys.version_info.major) + '.' + str(sys.version_info.minor)
673    if target_path.exists():
674        target_path = (
675            (target_path / python_folder) if python_folder in os.listdir(target_path)
676            else target_path
677        )
678    else:
679        target_path = (
680            (target_path / python_folder) if platform.system() != 'Windows'
681            else target_path
682        )
683
684    ### Ensure 'site-packages' exists.
685    if allow_nonexistent or 'site-packages' in os.listdir(target_path): ### Windows
686        target_path = target_path / 'site-packages'
687    else:
688        import traceback
689        traceback.print_stack()
690        print(f"Failed to find site-packages directory for virtual environment '{venv}'.")
691        print("This may be because you are using a different Python version.")
692        print("Try deleting the following directory and restarting Meerschaum:")
693        print(VIRTENV_RESOURCES_PATH)
694        sys.exit(1)
695
696    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:
216def verify_venv(
217        venv: str,
218        debug: bool = False,
219    ) -> None:
220    """
221    Verify that the virtual environment matches the expected state.
222    """
223    import pathlib, platform, os, shutil, subprocess, sys
224    from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
225    from meerschaum.utils.process import run_process
226    from meerschaum.utils.misc import make_symlink, is_symlink
227    from meerschaum.utils.warnings import warn
228    venv_path = VIRTENV_RESOURCES_PATH / venv
229    bin_path = venv_path / (
230        'bin' if platform.system() != 'Windows' else "Scripts"
231    )
232    current_python_versioned_name = (
233        'python' + str(sys.version_info.major) + '.' + str(sys.version_info.minor)
234        + ('' if platform.system() != 'Windows' else '.exe')
235    )
236
237    if not (bin_path / current_python_versioned_name).exists():
238        init_venv(venv, verify=False, force=True, debug=debug)
239        current_python_in_venv_path = pathlib.Path(venv_executable(venv=venv))
240        current_python_in_sys_path = pathlib.Path(venv_executable(venv=None))
241        if not current_python_in_venv_path.exists():
242            if is_symlink(current_python_in_venv_path):
243                try:
244                    current_python_in_venv_path.unlink()
245                except Exception as e:
246                    print(f"Unable to remove symlink {current_python_in_venv_path}:\n{e}")
247            try:
248                make_symlink(current_python_in_sys_path, current_python_in_venv_path)
249            except Exception as e:
250                print(
251                    f"Unable to create symlink {current_python_in_venv_path} "
252                    + f"to {current_python_in_sys_path}."
253                )
254        files_to_inspect = sorted(os.listdir(bin_path), reverse=True)
255    else:
256        files_to_inspect = [current_python_versioned_name]
257
258    def get_python_version(python_path: pathlib.Path) -> Union[str, None]:
259        """
260        Return the version for the python binary at the given path.
261        """
262        try:
263            ### It might be a broken symlink, so skip on errors.
264            if debug:
265                print(f"Getting python version for {python_path}")
266            proc = run_process(
267                [str(python_path), '-V'],
268                as_proc=True,
269                capture_output=True,
270            )
271            stdout, stderr = proc.communicate(timeout=1.0)
272        except Exception as e:
273            ### E.g. the symlink may be broken.
274            if is_symlink(python_path):
275                try:
276                    python_path.unlink()
277                except Exception as _e:
278                    print(f"Unable to remove broken symlink {python_path}:\n{e}\n{_e}")
279            return None
280        return stdout.decode('utf-8').strip().replace('Python ', '')
281
282    ### Ensure the versions are symlinked correctly.
283    for filename in files_to_inspect:
284        if not filename.startswith('python'):
285            continue
286        python_path = bin_path / filename
287        version = get_python_version(python_path)
288        if version is None:
289            continue
290        try:
291            major_version = version.split('.', maxsplit=1)[0]
292            minor_version = version.split('.', maxsplit=2)[1]
293        except IndexError:
294            return
295        python_versioned_name = (
296            'python' + major_version + '.' + minor_version
297            + ('' if platform.system() != 'Windows' else '.exe')
298        )
299
300        ### E.g. python3.10 actually links to Python 3.10.
301        if filename == python_versioned_name:
302            real_path = pathlib.Path(os.path.realpath(python_path))
303            if not real_path.exists():
304                try:
305                    python_path.unlink()
306                except Exception as e:
307                    pass
308                init_venv(venv, verify=False, force=True, debug=debug)
309                if not python_path.exists():
310                    raise FileNotFoundError(f"Unable to verify Python symlink:\n{python_path}")
311
312            if python_path == real_path:
313                continue
314
315            try:
316                python_path.unlink()
317            except Exception as e:
318                pass
319            success, msg = make_symlink(real_path, python_path)
320            if not success:
321                warn(msg, color=False)
322            continue
323
324        python_versioned_path = bin_path / python_versioned_name
325        if python_versioned_path.exists():
326            ### Avoid circular symlinks.
327            if get_python_version(python_versioned_path) == version:
328                continue
329            python_versioned_path.unlink()
330        shutil.move(python_path, python_versioned_path)

Verify that the virtual environment matches the expected state.