meerschaum.utils.packages

Functions for managing packages and virtual environments reside here.

   1#! /usr/bin/env python
   2# -*- coding: utf-8 -*-
   3# vim:fenc=utf-8
   4
   5"""
   6Functions for managing packages and virtual environments reside here.
   7"""
   8
   9from __future__ import annotations
  10
  11import importlib.util, os, pathlib, re
  12from meerschaum.utils.typing import Any, List, SuccessTuple, Optional, Union, Tuple, Dict, Iterable
  13from meerschaum.utils.threading import Lock, RLock
  14from meerschaum.utils.packages._packages import (
  15    packages,
  16    all_packages,
  17    get_install_names,
  18    _MRSM_PACKAGE_ARCHIVES_PREFIX,
  19)
  20from meerschaum.utils.venv import (
  21    activate_venv,
  22    deactivate_venv,
  23    venv_executable,
  24    venv_exec,
  25    venv_exists,
  26    venv_target_path,
  27    inside_venv,
  28    Venv,
  29    init_venv,
  30)
  31
  32_import_module = importlib.import_module
  33_import_hook_venv = None
  34_locks = {
  35    '_pkg_resources_get_distribution': RLock(),
  36    'import_versions': RLock(),
  37    '_checked_for_updates': RLock(),
  38    '_is_installed_first_check': RLock(),
  39    'emitted_pandas_warning': RLock(),
  40}
  41_checked_for_updates = set()
  42_is_installed_first_check: Dict[str, bool] = {}
  43
  44
  45def get_module_path(
  46    import_name: str,
  47    venv: Optional[str] = 'mrsm',
  48    debug: bool = False,
  49    _try_install_name_on_fail: bool = True,
  50) -> Union[pathlib.Path, None]:
  51    """
  52    Get a module's path without importing.
  53    """
  54    import site
  55    if debug:
  56        from meerschaum.utils.debug import dprint
  57    if not _try_install_name_on_fail:
  58        install_name = _import_to_install_name(import_name, with_version=False)
  59        install_name_lower = install_name.lower().replace('-', '_')
  60        import_name_lower = install_name_lower
  61    else:
  62        import_name_lower = import_name.lower().replace('-', '_')
  63
  64    vtp = venv_target_path(venv, allow_nonexistent=True, debug=debug)
  65    if not vtp.exists():
  66        if debug:
  67            dprint(
  68                (
  69                    "Venv '{venv}' does not exist, cannot import "
  70                    + f"'{import_name}'."
  71                ),
  72                color = False,
  73            )
  74        return None
  75
  76    venv_target_candidate_paths = [vtp]
  77    if venv is None:
  78        site_user_packages_dirs = [
  79            pathlib.Path(site.getusersitepackages())
  80        ] if not inside_venv() else []
  81        site_packages_dirs = [pathlib.Path(path) for path in site.getsitepackages()]
  82
  83        paths_to_add = [
  84            path
  85            for path in site_user_packages_dirs + site_packages_dirs
  86            if path not in venv_target_candidate_paths
  87        ]
  88        venv_target_candidate_paths += paths_to_add
  89
  90    candidates = []
  91    for venv_target_candidate in venv_target_candidate_paths:
  92        try:
  93            file_names = os.listdir(venv_target_candidate)
  94        except FileNotFoundError:
  95            continue
  96        for file_name in file_names:
  97            file_name_lower = file_name.lower().replace('-', '_')
  98            if not file_name_lower.startswith(import_name_lower):
  99                continue
 100            if file_name.endswith('dist_info'):
 101                continue
 102            file_path = venv_target_candidate / file_name
 103
 104            ### Most likely: Is a directory with __init__.py
 105            if file_name_lower == import_name_lower and file_path.is_dir():
 106                init_path = file_path / '__init__.py'
 107                if init_path.exists():
 108                    candidates.append(init_path)
 109
 110            ### May be a standalone .py file.
 111            elif file_name_lower == import_name_lower + '.py':
 112                candidates.append(file_path)
 113
 114            ### Compiled wheels (e.g. pyodbc)
 115            elif file_name_lower.startswith(import_name_lower + '.'):
 116                candidates.append(file_path)
 117
 118    if len(candidates) == 1:
 119        return candidates[0]
 120
 121    if not candidates:
 122        if _try_install_name_on_fail:
 123            return get_module_path(
 124                import_name, venv=venv, debug=debug,
 125                _try_install_name_on_fail=False
 126            )
 127        return None
 128
 129    specs_paths = []
 130    for candidate_path in candidates:
 131        spec = importlib.util.spec_from_file_location(import_name, str(candidate_path))
 132        if spec is not None:
 133            return candidate_path
 134    
 135    return None
 136
 137
 138def manually_import_module(
 139    import_name: str,
 140    venv: Optional[str] = 'mrsm',
 141    check_update: bool = True,
 142    check_pypi: bool = False,
 143    install: bool = True,
 144    split: bool = True,
 145    warn: bool = True,
 146    color: bool = True,
 147    debug: bool = False,
 148    use_sys_modules: bool = True,
 149) -> Union['ModuleType', None]:
 150    """
 151    Manually import a module from a virtual environment (or the base environment).
 152
 153    Parameters
 154    ----------
 155    import_name: str
 156        The name of the module.
 157        
 158    venv: Optional[str], default 'mrsm'
 159        The virtual environment to read from.
 160
 161    check_update: bool, default True
 162        If `True`, examine whether the available version of the package meets the required version.
 163
 164    check_pypi: bool, default False
 165        If `True`, check PyPI for updates before importing.
 166
 167    install: bool, default True
 168        If `True`, install the package if it's not installed or needs an update.
 169
 170    split: bool, default True
 171        If `True`, split `import_name` on periods to get the package name.
 172
 173    warn: bool, default True
 174        If `True`, raise a warning if the package cannot be imported.
 175
 176    color: bool, default True
 177        If `True`, use color output for debug and warning text.
 178
 179    debug: bool, default False
 180        Verbosity toggle.
 181
 182    use_sys_modules: bool, default True
 183        If `True`, return the module in `sys.modules` if it exists.
 184        Otherwise continue with manually importing.
 185
 186    Returns
 187    -------
 188    The specified module or `None` if it can't be imported.
 189
 190    """
 191    import sys
 192    _previously_imported = import_name in sys.modules
 193    if _previously_imported and use_sys_modules:
 194        return sys.modules[import_name]
 195
 196    from meerschaum.utils.warnings import warn as warn_function
 197    import warnings
 198    root_name = import_name.split('.')[0] if split else import_name
 199    install_name = _import_to_install_name(root_name)
 200
 201    root_path = get_module_path(root_name, venv=venv)
 202    if root_path is None:
 203        return None
 204
 205    mod_path = root_path
 206    if mod_path.is_dir():
 207        for _dir in import_name.split('.')[:-1]:
 208            mod_path = mod_path / _dir
 209            possible_end_module_filename = import_name.split('.')[-1] + '.py'
 210            try:
 211                mod_path = (
 212                    (mod_path / possible_end_module_filename)
 213                    if possible_end_module_filename in os.listdir(mod_path)
 214                    else (
 215                        mod_path / import_name.split('.')[-1] / '__init__.py'
 216                    )
 217                )
 218            except Exception:
 219                mod_path = None
 220
 221    spec = (
 222        importlib.util.find_spec(import_name)
 223        if mod_path is None or not mod_path.exists()
 224        else importlib.util.spec_from_file_location(import_name, str(mod_path))
 225    )
 226    root_spec = (
 227        importlib.util.find_spec(root_name)
 228        if not root_path.exists()
 229        else importlib.util.spec_from_file_location(root_name, str(root_path))
 230    )
 231
 232    ### Check for updates before importing.
 233    _version = (
 234        determine_version(
 235            pathlib.Path(root_spec.origin),
 236            import_name=root_name, venv=venv, debug=debug
 237        ) if root_spec is not None and root_spec.origin is not None else None
 238    )
 239
 240    if _version is not None:
 241        if check_update:
 242            if need_update(
 243                None,
 244                import_name=root_name,
 245                version=_version,
 246                check_pypi=check_pypi,
 247                debug=debug,
 248            ):
 249                if install:
 250                    if not pip_install(
 251                        root_name,
 252                        venv=venv,
 253                        split=False,
 254                        check_update=check_update,
 255                        color=color,
 256                        debug=debug
 257                    ) and warn:
 258                        warn_function(
 259                            f"There's an update available for '{install_name}', "
 260                            + "but it failed to install. "
 261                            + "Try installig via Meerschaum with "
 262                            + "`install packages '{install_name}'`.",
 263                            ImportWarning,
 264                            stacklevel=3,
 265                            color=False,
 266                        )
 267                elif warn:
 268                    warn_function(
 269                        f"There's an update available for '{root_name}'.",
 270                        stack=False,
 271                        color=False,
 272                    )
 273                spec = (
 274                    importlib.util.find_spec(import_name)
 275                    if mod_path is None or not mod_path.exists()
 276                    else importlib.util.spec_from_file_location(import_name, str(mod_path))
 277                )
 278
 279    if spec is None:
 280        try:
 281            mod = _import_module(import_name)
 282        except Exception:
 283            mod = None
 284        return mod
 285
 286    with Venv(venv, debug=debug):
 287        mod = importlib.util.module_from_spec(spec)
 288        old_sys_mod = sys.modules.get(import_name, None)
 289        sys.modules[import_name] = mod
 290
 291        try:
 292            with warnings.catch_warnings():
 293                warnings.filterwarnings('ignore', 'The NumPy')
 294                spec.loader.exec_module(mod)
 295        except Exception:
 296            pass
 297        mod = _import_module(import_name)
 298        if old_sys_mod is not None:
 299            sys.modules[import_name] = old_sys_mod
 300        else:
 301            del sys.modules[import_name]
 302
 303    return mod
 304
 305
 306def _import_to_install_name(import_name: str, with_version: bool = True) -> str:
 307    """
 308    Try to translate an import name to an installation name.
 309    """
 310    install_name = all_packages.get(import_name, import_name)
 311    if with_version:
 312        return install_name
 313    return get_install_no_version(install_name)
 314
 315
 316def _import_to_dir_name(import_name: str) -> str:
 317    """
 318    Translate an import name to the package name in the sites-packages directory.
 319    """
 320    import re
 321    return re.split(
 322        r'[<>=\[]', all_packages.get(import_name, import_name)
 323    )[0].replace('-', '_').lower() 
 324
 325
 326def _install_to_import_name(install_name: str) -> str:
 327    """
 328    Translate an installation name to a package's import name.
 329    """
 330    _install_no_version = get_install_no_version(install_name)
 331    return get_install_names().get(_install_no_version, _install_no_version)
 332
 333
 334def get_install_no_version(install_name: str) -> str:
 335    """
 336    Strip the version information from the install name.
 337    """
 338    import re
 339    return re.split(r'[\[=<>,! \]]', install_name)[0]
 340
 341
 342import_versions = {}
 343def determine_version(
 344    path: pathlib.Path,
 345    import_name: Optional[str] = None,
 346    venv: Optional[str] = 'mrsm',
 347    search_for_metadata: bool = True,
 348    split: bool = True,
 349    warn: bool = False,
 350    debug: bool = False,
 351) -> Union[str, None]:
 352    """
 353    Determine a module's `__version__` string from its filepath.
 354    
 355    First it searches for pip metadata, then it attempts to import the module in a subprocess.
 356
 357    Parameters
 358    ----------
 359    path: pathlib.Path
 360        The file path of the module.
 361
 362    import_name: Optional[str], default None
 363        The name of the module. If omitted, it will be determined from the file path.
 364        Defaults to `None`.
 365
 366    venv: Optional[str], default 'mrsm'
 367        The virtual environment of the Python interpreter to use if importing is necessary.
 368
 369    search_for_metadata: bool, default True
 370        If `True`, search the pip site_packages directory (assumed to be the parent)
 371        for the corresponding dist-info directory.
 372
 373    warn: bool, default True
 374        If `True`, raise a warning if the module fails to import in the subprocess.
 375
 376    split: bool, default True
 377        If `True`, split the determined import name by periods to get the room name.
 378
 379    Returns
 380    -------
 381    The package's version string if available or `None`.
 382    If multiple versions are found, it will trigger an import in a subprocess.
 383
 384    """
 385    with _locks['import_versions']:
 386        if venv not in import_versions:
 387            import_versions[venv] = {}
 388    import os
 389    old_cwd = os.getcwd()
 390    from meerschaum.utils.warnings import warn as warn_function
 391    if import_name is None:
 392        import_name = path.parent.stem if path.stem == '__init__' else path.stem
 393        import_name = import_name.split('.')[0] if split else import_name
 394    if import_name in import_versions[venv]:
 395        return import_versions[venv][import_name]
 396    _version = None
 397    module_parent_dir = (
 398        path.parent.parent if path.stem == '__init__' else path.parent
 399    ) if path is not None else venv_target_path(venv, allow_nonexistent=True, debug=debug)
 400
 401    if not module_parent_dir.exists():
 402        return None
 403
 404    installed_dir_name = _import_to_dir_name(import_name)
 405    clean_installed_dir_name = installed_dir_name.lower().replace('-', '_')
 406
 407    ### First, check if a dist-info directory exists.
 408    _found_versions = []
 409    if search_for_metadata:
 410        try:
 411            filenames = os.listdir(module_parent_dir)
 412        except FileNotFoundError:
 413            filenames = []
 414        for filename in filenames:
 415            if not filename.endswith('.dist-info'):
 416                continue
 417            filename_lower = filename.lower()
 418            if not filename_lower.startswith(clean_installed_dir_name + '-'):
 419                continue
 420            _v = filename.replace('.dist-info', '').split("-")[-1]
 421            _found_versions.append(_v)
 422
 423    if len(_found_versions) == 1:
 424        _version = _found_versions[0]
 425        with _locks['import_versions']:
 426            import_versions[venv][import_name] = _version
 427        return _found_versions[0]
 428
 429    if not _found_versions:
 430        try:
 431            import importlib.metadata as importlib_metadata
 432        except ImportError:
 433            importlib_metadata = attempt_import(
 434                'importlib_metadata',
 435                debug=debug, check_update=False, precheck=False,
 436                color=False, check_is_installed=False, lazy=False,
 437            )
 438        try:
 439            os.chdir(module_parent_dir)
 440            _version = importlib_metadata.metadata(import_name)['Version']
 441        except Exception:
 442            _version = None
 443        finally:
 444            os.chdir(old_cwd)
 445
 446        if _version is not None:
 447            with _locks['import_versions']:
 448                import_versions[venv][import_name] = _version
 449            return _version
 450
 451    if debug:
 452        print(f'Found multiple versions for {import_name}: {_found_versions}')
 453
 454    module_parent_dir_str = module_parent_dir.as_posix()
 455
 456    ### Not a pip package, so let's try importing the module directly (in a subprocess).
 457    _no_version_str = 'no-version'
 458    code = (
 459        f"import sys, importlib; sys.path.insert(0, '{module_parent_dir_str}');\n"
 460        + f"module = importlib.import_module('{import_name}');\n"
 461        + "try:\n"
 462        + "  print(module.__version__ , end='')\n"
 463        + "except:\n"
 464        + f"  print('{_no_version_str}', end='')"
 465    )
 466    exit_code, stdout_bytes, stderr_bytes = venv_exec(
 467        code, venv=venv, with_extras=True, debug=debug
 468    )
 469    stdout, stderr = stdout_bytes.decode('utf-8'), stderr_bytes.decode('utf-8')
 470    _version = stdout.split('\n')[-1] if exit_code == 0 else None
 471    _version = _version if _version != _no_version_str else None
 472
 473    if _version is None:
 474        _version = _get_package_metadata(import_name, venv).get('version', None)
 475    if _version is None and warn:
 476        warn_function(
 477            f"Failed to determine a version for '{import_name}':\n{stderr}",
 478            stack = False
 479        )
 480
 481    ### If `__version__` doesn't exist, return `None`.
 482    import_versions[venv][import_name] = _version
 483    return _version
 484
 485
 486def _get_package_metadata(import_name: str, venv: Optional[str]) -> Dict[str, str]:
 487    """
 488    Get a package's metadata from pip.
 489    This is useful for getting a version when no `__version__` is defined
 490    and multiple versions are installed.
 491
 492    Parameters
 493    ----------
 494    import_name: str
 495        The package's import or installation name.
 496
 497    venv: Optional[str]
 498        The virtual environment which contains the package.
 499
 500    Returns
 501    -------
 502    A dictionary of metadata from pip.
 503    """
 504    from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
 505    install_name = _import_to_install_name(import_name)
 506    if install_name.startswith(_MRSM_PACKAGE_ARCHIVES_PREFIX):
 507        return {}
 508    _args = ['pip', 'show', install_name]
 509    if venv is not None:
 510        cache_dir_path = VIRTENV_RESOURCES_PATH / venv / 'cache'
 511        _args += ['--cache-dir', cache_dir_path.as_posix()]
 512
 513    if use_uv():
 514        package_name = 'uv'
 515        _args = ['pip', 'show', install_name]
 516    else:
 517        package_name = 'pip'
 518        _args = ['show', install_name]
 519
 520    proc = run_python_package(
 521        package_name, _args,
 522        capture_output=True, as_proc=True, venv=venv, universal_newlines=True,
 523    )
 524    outs, errs = proc.communicate()
 525    lines = outs.split('\n')
 526    meta = {}
 527    for line in lines:
 528        vals = line.split(": ")
 529        if len(vals) != 2:
 530            continue
 531        k, v = vals[0].lower(), vals[1]
 532        if v and 'UNKNOWN' not in v:
 533            meta[k] = v
 534    return meta
 535
 536
 537def need_update(
 538    package: Optional['ModuleType'] = None,
 539    install_name: Optional[str] = None,
 540    import_name: Optional[str] = None,
 541    version: Optional[str] = None,
 542    check_pypi: bool = False,
 543    split: bool = True,
 544    color: bool = True,
 545    debug: bool = False,
 546    _run_determine_version: bool = True,
 547) -> bool:
 548    """
 549    Check if a Meerschaum dependency needs an update.
 550    Returns a bool for whether or not a package needs to be updated.
 551
 552    Parameters
 553    ----------
 554    package: 'ModuleType'
 555        The module of the package to be updated.
 556
 557    install_name: Optional[str], default None
 558        If provided, use this string to determine the required version.
 559        Otherwise use the install name defined in `meerschaum.utils.packages._packages`.
 560
 561    import_name:
 562        If provided, override the package's `__name__` string.
 563
 564    version: Optional[str], default None
 565        If specified, override the package's `__version__` string.
 566
 567    check_pypi: bool, default False
 568        If `True`, check pypi.org for updates.
 569        Defaults to `False`.
 570
 571    split: bool, default True
 572        If `True`, split the module's name on periods to detrive the root name.
 573        Defaults to `True`.
 574
 575    color: bool, default True
 576        If `True`, format debug output.
 577        Defaults to `True`.
 578
 579    debug: bool, default True
 580        Verbosity toggle.
 581
 582    Returns
 583    -------
 584    A bool indicating whether the package requires an update.
 585
 586    """
 587    if debug:
 588        from meerschaum.utils.debug import dprint
 589    from meerschaum.utils.warnings import warn as warn_function
 590    import re
 591    root_name = (
 592        package.__name__.split('.')[0] if split else package.__name__
 593    ) if import_name is None else (
 594        import_name.split('.')[0] if split else import_name
 595    )
 596    install_name = install_name or _import_to_install_name(root_name)
 597    with _locks['_checked_for_updates']:
 598        if install_name in _checked_for_updates:
 599            return False
 600        _checked_for_updates.add(install_name)
 601
 602    _install_no_version = get_install_no_version(install_name)
 603    required_version = (
 604        install_name
 605        .replace(_install_no_version, '')
 606    )
 607    if ']' in required_version:
 608        required_version = required_version.split(']')[1]
 609
 610    ### No minimum version was specified, and we're not going to check PyPI.
 611    if not required_version and not check_pypi:
 612        return False
 613
 614    ### NOTE: Sometimes (rarely), we depend on a development build of a package.
 615    if '.dev' in required_version:
 616        required_version = required_version.split('.dev')[0]
 617    if version and '.dev' in version:
 618        version = version.split('.dev')[0]
 619
 620    try:
 621        if not version:
 622            if not _run_determine_version:
 623                version = determine_version(
 624                    pathlib.Path(package.__file__),
 625                    import_name=root_name, warn=False, debug=debug
 626                )
 627        if version is None:
 628            return False
 629    except Exception as e:
 630        if debug:
 631            dprint(str(e), color=color)
 632            dprint("No version could be determined from the installed package.", color=color)
 633        return False
 634    split_version = version.split('.')
 635    last_part = split_version[-1]
 636    if len(split_version) == 2:
 637        version = '.'.join(split_version) + '.0'
 638    elif 'dev' in last_part or 'rc' in last_part:
 639        tag = 'dev' if 'dev' in last_part else 'rc'
 640        last_sep = '-'
 641        if not last_part.startswith(tag):
 642            last_part = f'-{tag}'.join(last_part.split(tag))
 643            last_sep = '.'
 644        version = '.'.join(split_version[:-1]) + last_sep + last_part
 645    elif len(split_version) > 3:
 646        version = '.'.join(split_version[:3])
 647
 648    packaging_version = attempt_import(
 649        'packaging.version', check_update=False, lazy=False, debug=debug,
 650    )
 651
 652    ### Get semver if necessary
 653    if required_version:
 654        semver_path = get_module_path('semver', debug=debug)
 655        if semver_path is None:
 656            no_venv_semver_path = get_module_path('semver', venv=None, debug=debug)
 657            if no_venv_semver_path is None:
 658                pip_install(_import_to_install_name('semver'), debug=debug)
 659        semver = attempt_import('semver', check_update=False, lazy=False, debug=debug)
 660    if check_pypi:
 661        ### Check PyPI for updates
 662        update_checker = attempt_import(
 663            'update_checker', lazy=False, check_update=False, debug=debug
 664        )
 665        checker = update_checker.UpdateChecker()
 666        result = checker.check(_install_no_version, version)
 667    else:
 668        ### Skip PyPI and assume we can't be sure.
 669        result = None
 670
 671    ### Compare PyPI's version with our own.
 672    if result is not None:
 673        ### We have a result from PyPI and a stated required version.
 674        if required_version:
 675            try:
 676                return semver.Version.parse(result.available_version).match(required_version)
 677            except AttributeError as e:
 678                pip_install(_import_to_install_name('semver'), venv='mrsm', debug=debug)
 679                semver = manually_import_module('semver', venv='mrsm')
 680                return semver.Version.parse(version).match(required_version)
 681            except Exception as e:
 682                if debug:
 683                    dprint(f"Failed to match versions with exception:\n{e}", color=color)
 684                return False
 685
 686        ### If `check_pypi` and we don't have a required version, check if PyPI's version
 687        ### is newer than the installed version.
 688        else:
 689            return (
 690                packaging_version.parse(result.available_version) > 
 691                packaging_version.parse(version)
 692            )
 693
 694    ### We might be depending on a prerelease.
 695    ### Sanity check that the required version is not greater than the installed version. 
 696    required_version = (
 697        required_version.replace(_MRSM_PACKAGE_ARCHIVES_PREFIX, '')
 698        .replace(' @ ', '').replace('wheels', '').replace('+mrsm', '').replace('/-', '')
 699        .replace('-py3-none-any.whl', '')
 700    )
 701
 702    if 'a' in required_version:
 703        required_version = required_version.replace('a', '-pre.').replace('+mrsm', '')
 704        version = version.replace('a', '-pre.').replace('+mrsm', '')
 705    try:
 706        return (
 707            (not semver.Version.parse(version).match(required_version))
 708            if required_version else False
 709        )
 710    except AttributeError:
 711        pip_install(_import_to_install_name('semver'), venv='mrsm', debug=debug)
 712        semver = manually_import_module('semver', venv='mrsm', debug=debug)
 713        return (
 714            (not semver.Version.parse(version).match(required_version))
 715            if required_version else False
 716        )
 717    except Exception as e:
 718        print(f"Unable to parse version ({version}) for package '{import_name}'.")
 719        print(e)
 720        if debug:
 721            dprint(e)
 722        return False
 723    try:
 724        return (
 725            packaging_version.parse(version) > 
 726            packaging_version.parse(required_version)
 727        )
 728    except Exception as e:
 729        if debug:
 730            dprint(e)
 731        return False
 732    return False
 733
 734
 735def get_pip(
 736    venv: Optional[str] = 'mrsm',
 737    color: bool = True,
 738    debug: bool = False,
 739) -> bool:
 740    """
 741    Download and run the get-pip.py script.
 742
 743    Parameters
 744    ----------
 745    venv: Optional[str], default 'mrsm'
 746        The virtual environment into which to install `pip`.
 747
 748    color: bool, default True
 749        If `True`, force color output.
 750
 751    debug: bool, default False
 752        Verbosity toggle.
 753
 754    Returns
 755    -------
 756    A bool indicating success.
 757
 758    """
 759    import sys
 760    import subprocess
 761    from meerschaum.utils.misc import wget
 762    from meerschaum.config._paths import CACHE_RESOURCES_PATH
 763    from meerschaum._internal.static import STATIC_CONFIG
 764    url = STATIC_CONFIG['system']['urls']['get-pip.py']
 765    dest = CACHE_RESOURCES_PATH / 'get-pip.py'
 766    try:
 767        wget(url, dest, color=False, debug=debug)
 768    except Exception:
 769        print(f"Failed to fetch pip from '{url}'. Please install pip and restart Meerschaum.") 
 770        sys.exit(1)
 771    if venv is not None:
 772        init_venv(venv=venv, debug=debug)
 773    cmd_list = [venv_executable(venv=venv), dest.as_posix()] 
 774    return subprocess.call(cmd_list, env=_get_pip_os_env(color=color)) == 0
 775
 776
 777def pip_install(
 778    *install_names: str,
 779    args: Optional[List[str]] = None,
 780    requirements_file_path: Union[pathlib.Path, str, None] = None,
 781    venv: Optional[str] = 'mrsm',
 782    split: bool = False,
 783    check_update: bool = True,
 784    check_pypi: bool = True,
 785    check_wheel: bool = True,
 786    _uninstall: bool = False,
 787    _from_completely_uninstall: bool = False,
 788    _install_uv_pip: bool = True,
 789    _use_uv_pip: bool = True,
 790    color: bool = True,
 791    silent: bool = False,
 792    debug: bool = False,
 793) -> bool:
 794    """
 795    Install packages from PyPI with `pip`.
 796
 797    Parameters
 798    ----------
 799    *install_names: str
 800        The installation names of packages to be installed.
 801        This includes version restrictions.
 802        Use `_import_to_install_name()` to get the predefined `install_name` for a package
 803        from its import name.
 804
 805    args: Optional[List[str]], default None
 806        A list of command line arguments to pass to `pip`.
 807        If not provided, default to `['--upgrade']` if `_uninstall` is `False`, else `[]`.
 808
 809    requirements_file_path: Optional[pathlib.Path, str], default None
 810        If provided, append `['-r', '/path/to/requirements.txt']` to `args`.
 811
 812    venv: str, default 'mrsm'
 813        The virtual environment to install into.
 814
 815    split: bool, default False
 816        If `True`, split on periods and only install the root package name.
 817
 818    check_update: bool, default True
 819        If `True`, check if the package requires an update.
 820
 821    check_pypi: bool, default True
 822        If `True` and `check_update` is `True`, check PyPI for the latest version.
 823
 824    check_wheel: bool, default True
 825        If `True`, check if `wheel` is available.
 826
 827    _uninstall: bool, default False
 828        If `True`, uninstall packages instead.
 829
 830    color: bool, default True
 831        If `True`, include color in debug text.
 832
 833    silent: bool, default False
 834        If `True`, skip printing messages.
 835
 836    debug: bool, default False
 837        Verbosity toggle.
 838
 839    Returns
 840    -------
 841    A bool indicating success.
 842
 843    """
 844    from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
 845    from meerschaum._internal.static import STATIC_CONFIG
 846    from meerschaum.utils.warnings import warn
 847    if args is None:
 848        args = ['--upgrade'] if not _uninstall else []
 849    ANSI = True if color else False
 850    if check_wheel:
 851        have_wheel = venv_contains_package('wheel', venv=venv, debug=debug)
 852
 853    daemon_env_var = STATIC_CONFIG['environment']['daemon_id']
 854    inside_daemon = daemon_env_var in os.environ
 855    if inside_daemon:
 856        silent = True
 857
 858    _args = list(args)
 859    have_pip = venv_contains_package('pip', venv=None, debug=debug)
 860    pip_venv = None
 861    try:
 862        import pip
 863        have_pip = True
 864    except ImportError:
 865        have_pip = False
 866    try:
 867        import uv
 868        uv_bin = uv.find_uv_bin()
 869        have_uv_pip = True
 870    except (ImportError, FileNotFoundError):
 871        uv_bin = None
 872        have_uv_pip = False
 873
 874    if have_pip and not have_uv_pip and _install_uv_pip and is_uv_enabled():
 875        if not pip_install(
 876            'uv', 'PyYAML',
 877            venv=None,
 878            debug=debug,
 879            _install_uv_pip=False,
 880            check_update=False,
 881            check_pypi=False,
 882            check_wheel=False,
 883        ) and not silent:
 884            warn(
 885                f"Failed to install `uv` for virtual environment '{venv}'.",
 886                color=False,
 887            )
 888
 889    use_uv_pip = (
 890        _use_uv_pip
 891        and venv_contains_package('uv', venv=None, debug=debug)
 892        and uv_bin is not None
 893        and venv is not None
 894        and is_uv_enabled()
 895    )
 896
 897    import sys
 898    if not have_pip and not use_uv_pip:
 899        have_mrsm_pip = venv_contains_package('pip', venv='mrsm')
 900        if not have_mrsm_pip and not get_pip(venv=venv, color=color, debug=debug):
 901            import sys
 902            minor = sys.version_info.minor
 903            print(
 904                "\nFailed to import `pip` and `ensurepip`.\n"
 905                + "If you are running Ubuntu/Debian, "
 906                + f"you might need to install `python3.{minor}-distutils`:\n\n"
 907                + f"    sudo apt install python3.{minor}-pip python3.{minor}-venv\n\n"
 908                + "Please install pip and restart Meerschaum.\n\n"
 909                + "You can find instructions on installing `pip` here:\n"
 910                + "https://pip.pypa.io/en/stable/installing/"
 911            )
 912            sys.exit(1)
 913
 914        pip = attempt_import('pip', lazy=False)
 915        pip_venv = 'mrsm'
 916
 917    with Venv(venv, debug=debug):
 918        if venv is not None:
 919            if (
 920                '--ignore-installed' not in args
 921                and '-I' not in _args
 922                and not _uninstall
 923                and not use_uv_pip
 924            ):
 925                _args += ['--ignore-installed']
 926            if '--cache-dir' not in args and not _uninstall:
 927                cache_dir_path = VIRTENV_RESOURCES_PATH / venv / 'cache'
 928                _args += ['--cache-dir', str(cache_dir_path)]
 929
 930        if 'pip' not in ' '.join(_args) and not use_uv_pip:
 931            if check_update and not _uninstall:
 932                pip = attempt_import('pip', venv=venv, install=False, debug=debug, lazy=False)
 933                if need_update(pip, check_pypi=check_pypi, debug=debug):
 934                    _args.append(all_packages['pip'])
 935
 936        _args = (['install'] if not _uninstall else ['uninstall']) + _args
 937
 938        if check_wheel and not _uninstall and not use_uv_pip:
 939            if not have_wheel:
 940                setup_packages_to_install = (
 941                    ['setuptools', 'wheel', 'PyYAML']
 942                    + (['uv'] if is_uv_enabled() else [])
 943                )
 944                if not pip_install(
 945                    *setup_packages_to_install,
 946                    venv=venv,
 947                    check_update=False,
 948                    check_pypi=False,
 949                    check_wheel=False,
 950                    debug=debug,
 951                    _install_uv_pip=False,
 952                ) and not silent:
 953                    from meerschaum.utils.misc import items_str
 954                    warn(
 955                        (
 956                            f"Failed to install {items_str(setup_packages_to_install)} for virtual "
 957                            + f"environment '{venv}'."
 958                        ),
 959                        color=False,
 960                    )
 961
 962        if requirements_file_path is not None:
 963            _args.append('-r')
 964            _args.append(pathlib.Path(requirements_file_path).resolve().as_posix())
 965
 966        if not ANSI and '--no-color' not in _args:
 967            _args.append('--no-color')
 968
 969        if '--no-input' not in _args and not use_uv_pip:
 970            _args.append('--no-input')
 971
 972        if _uninstall and '-y' not in _args and not use_uv_pip:
 973            _args.append('-y')
 974
 975        if '--no-warn-conflicts' not in _args and not _uninstall and not use_uv_pip:
 976            _args.append('--no-warn-conflicts')
 977
 978        if '--disable-pip-version-check' not in _args and not use_uv_pip:
 979            _args.append('--disable-pip-version-check')
 980
 981        if '--target' not in _args and '-t' not in _args and not (not use_uv_pip and _uninstall):
 982            if venv is not None:
 983                vtp = venv_target_path(venv, allow_nonexistent=True, debug=debug)
 984                if not vtp.exists():
 985                    if not init_venv(venv, force=True):
 986                        vtp.mkdir(parents=True, exist_ok=True)
 987                _args += ['--target', venv_target_path(venv, debug=debug).as_posix()]
 988        elif (
 989            '--target' not in _args
 990                and '-t' not in _args
 991                and not inside_venv()
 992                and not _uninstall
 993                and not use_uv_pip
 994        ):
 995            _args.append('--user')
 996
 997        if venv is None and '--break-system-packages' not in _args:
 998            _args.append('--break-system-packages')
 999
1000        if debug:
1001            if '-v' not in _args or '-vv' not in _args or '-vvv' not in _args:
1002                if use_uv_pip:
1003                    _args.append('--verbose')
1004        else:
1005            if '-q' not in _args or '-qq' not in _args or '-qqq' not in _args:
1006                pass
1007
1008        _packages = [
1009            (
1010                get_install_no_version(install_name)
1011                if _uninstall or install_name.startswith(_MRSM_PACKAGE_ARCHIVES_PREFIX)
1012                else install_name
1013            )
1014            for install_name in install_names
1015        ]
1016        msg = "Installing packages:" if not _uninstall else "Uninstalling packages:"
1017        for p in _packages:
1018            msg += f'\n  - {p}'
1019        if not silent:
1020            print(msg)
1021
1022        if _uninstall and not _from_completely_uninstall and not use_uv_pip:
1023            for install_name in _packages:
1024                _install_no_version = get_install_no_version(install_name)
1025                if _install_no_version in ('pip', 'wheel', 'uv'):
1026                    continue
1027                if not completely_uninstall_package(
1028                    _install_no_version,
1029                    venv=venv,
1030                    debug=debug,
1031                ) and not silent:
1032                    warn(
1033                        f"Failed to clean up package '{_install_no_version}'.",
1034                    )
1035
1036        ### NOTE: Only append the `--prerelease=allow` flag if we explicitly depend on a prerelease.
1037        if use_uv_pip:
1038            _args.insert(0, 'pip')
1039            if not _uninstall and get_prerelease_dependencies(_packages):
1040                _args.append('--prerelease=allow')
1041
1042        rc = run_python_package(
1043            ('pip' if not use_uv_pip else 'uv'),
1044            _args + _packages,
1045            venv=pip_venv,
1046            env=_get_pip_os_env(color=color),
1047            debug=debug,
1048        )
1049        if debug:
1050            print(f"{rc=}")
1051        success = rc == 0
1052
1053    msg = (
1054        "Successfully " + ('un' if _uninstall else '') + "installed packages." if success 
1055        else "Failed to " + ('un' if _uninstall else '') + "install packages."
1056    )
1057    if not silent:
1058        print(msg)
1059    if debug and not silent:
1060        print('pip ' + ('un' if _uninstall else '') + 'install returned:', success)
1061    return success
1062
1063
1064def get_prerelease_dependencies(_packages: Optional[List[str]] = None):
1065    """
1066    Return a list of explicitly prerelease dependencies from a list of packages.
1067    """
1068    if _packages is None:
1069        _packages = list(all_packages.keys())
1070    prelrease_strings = ['dev', 'rc', 'a']
1071    prerelease_packages = []
1072    for install_name in _packages:
1073        _install_no_version = get_install_no_version(install_name)
1074        import_name = _install_to_import_name(install_name)
1075        install_with_version = _import_to_install_name(import_name)
1076        version_only = (
1077            install_with_version.lower().replace(_install_no_version.lower(), '')
1078            .split(']')[-1]
1079        )
1080
1081        is_prerelease = False
1082        for prelrease_string in prelrease_strings:
1083            if prelrease_string in version_only:
1084                is_prerelease = True
1085
1086        if is_prerelease:
1087            prerelease_packages.append(install_name)
1088    return prerelease_packages
1089
1090
1091def completely_uninstall_package(
1092    install_name: str,
1093    venv: str = 'mrsm',
1094    debug: bool = False,
1095) -> bool:
1096    """
1097    Continue calling `pip uninstall` until a package is completely
1098    removed from a virtual environment. 
1099    This is useful for dealing with multiple installed versions of a package.
1100    """
1101    attempts = 0
1102    _install_no_version = get_install_no_version(install_name)
1103    clean_install_no_version = _install_no_version.lower().replace('-', '_')
1104    installed_versions = []
1105    vtp = venv_target_path(venv, allow_nonexistent=True, debug=debug)
1106    if not vtp.exists():
1107        return True
1108
1109    for file_name in os.listdir(vtp):
1110        if not file_name.endswith('.dist-info'):
1111            continue
1112        clean_dist_info = file_name.replace('-', '_').lower()
1113        if not clean_dist_info.startswith(clean_install_no_version):
1114            continue
1115        installed_versions.append(file_name)
1116
1117    max_attempts = len(installed_versions)
1118    while attempts < max_attempts:
1119        if not venv_contains_package(
1120            _install_to_import_name(_install_no_version),
1121            venv=venv, debug=debug,
1122        ):
1123            return True
1124        if not pip_uninstall(
1125            _install_no_version,
1126            venv = venv,
1127            silent = (not debug),
1128            _from_completely_uninstall = True,
1129            debug = debug,
1130        ):
1131            return False
1132        attempts += 1
1133    return False
1134
1135
1136def pip_uninstall(
1137    *args, **kw
1138) -> bool:
1139    """
1140    Uninstall Python packages.
1141    This function is a wrapper around `pip_install()` but with `_uninstall` enforced as `True`.
1142    """
1143    return pip_install(*args, _uninstall=True, **{k: v for k, v in kw.items() if k != '_uninstall'})
1144
1145
1146def run_python_package(
1147    package_name: str,
1148    args: Optional[List[str]] = None,
1149    venv: Optional[str] = 'mrsm',
1150    cwd: Optional[str] = None,
1151    env: Optional[Dict[str, str]] = None,
1152    foreground: bool = False,
1153    as_proc: bool = False,
1154    capture_output: bool = False,
1155    debug: bool = False,
1156    **kw: Any,
1157) -> Union[int, subprocess.Popen, None]:
1158    """
1159    Runs an installed python package.
1160    E.g. Translates to `/usr/bin/python -m [package]`
1161
1162    Parameters
1163    ----------
1164    package_name: str
1165        The Python module to be executed.
1166
1167    args: Optional[List[str]], default None
1168        Additional command line arguments to be appended after `-m [package]`.
1169
1170    venv: Optional[str], default 'mrsm'
1171        If specified, execute the Python interpreter from a virtual environment.
1172
1173    cwd: Optional[str], default None
1174        If specified, change directories before starting the process.
1175        Defaults to `None`.
1176
1177    env: Optional[Dict[str, str]], default None
1178        If specified, only use the provided dictionary for the environment variables.
1179        Defaults to `os.environ`.
1180
1181    as_proc: bool, default False
1182        If `True`, return a `subprocess.Popen` object.
1183
1184    capture_output: bool, default False
1185        If `as_proc` is `True`, capture stdout and stderr.
1186
1187    foreground: bool, default False
1188        If `True`, start the subprocess as a foreground process.
1189        Defaults to `False`.
1190
1191    kw: Any
1192        Additional keyword arguments to pass to `meerschaum.utils.process.run_process()`
1193        and by extension `subprocess.Popen()`.
1194
1195    Returns
1196    -------
1197    Either a return code integer or a `subprocess.Popen` object
1198    (or `None` if a `KeyboardInterrupt` occurs and as_proc is `True`).
1199    """
1200    import sys
1201    import subprocess
1202    import traceback
1203    from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
1204    from meerschaum.utils.process import run_process
1205    from meerschaum.utils.warnings import warn
1206    if args is None:
1207        args = []
1208    old_cwd = os.getcwd()
1209    if cwd is not None:
1210        os.chdir(cwd)
1211    executable = venv_executable(venv=venv)
1212    venv_path = (VIRTENV_RESOURCES_PATH / venv) if venv is not None else None
1213    env_dict = (env if isinstance(env, dict) else (os.environ or {})).copy()
1214    if venv_path is not None:
1215        env_dict.update({'VIRTUAL_ENV': venv_path.as_posix()})
1216    command = [executable, '-m', str(package_name)] + [str(a) for a in args]
1217    if debug:
1218        print(command, file=sys.stderr)
1219    try:
1220        to_return = run_process(
1221            command,
1222            foreground=foreground,
1223            as_proc=as_proc,
1224            capture_output=capture_output,
1225            **kw
1226        )
1227    except Exception:
1228        msg = f"Failed to execute {command}, will try again:\n{traceback.format_exc()}"
1229        warn(msg, color=False)
1230        stdout, stderr = (
1231            (None, None)
1232            if not capture_output
1233            else (subprocess.PIPE, subprocess.PIPE)
1234        )
1235        proc = subprocess.Popen(
1236            command,
1237            stdout=stdout,
1238            stderr=stderr,
1239            stdin=sys.stdin,
1240            env=env_dict,
1241        )
1242        to_return = proc if as_proc else proc.wait()
1243    except KeyboardInterrupt:
1244        to_return = 1 if not as_proc else None
1245    os.chdir(old_cwd)
1246    return to_return
1247
1248
1249def attempt_import(
1250    *names: str,
1251    lazy: bool = True,
1252    warn: bool = True,
1253    install: bool = True,
1254    venv: Optional[str] = 'mrsm',
1255    precheck: bool = True,
1256    split: bool = True,
1257    check_update: bool = False,
1258    check_pypi: bool = False,
1259    check_is_installed: bool = True,
1260    allow_outside_venv: bool = True,
1261    color: bool = True,
1262    debug: bool = False
1263) -> Any:
1264    """
1265    Raise a warning if packages are not installed; otherwise import and return modules.
1266    If `lazy` is `True`, return lazy-imported modules.
1267    
1268    Returns tuple of modules if multiple names are provided, else returns one module.
1269    
1270    Parameters
1271    ----------
1272    names: List[str]
1273        The packages to be imported.
1274
1275    lazy: bool, default True
1276        If `True`, lazily load packages.
1277
1278    warn: bool, default True
1279        If `True`, raise a warning if a package cannot be imported.
1280
1281    install: bool, default True
1282        If `True`, attempt to install a missing package into the designated virtual environment.
1283        If `check_update` is True, install updates if available.
1284
1285    venv: Optional[str], default 'mrsm'
1286        The virtual environment in which to search for packages and to install packages into.
1287
1288    precheck: bool, default True
1289        If `True`, attempt to find module before importing (necessary for checking if modules exist
1290        and retaining lazy imports), otherwise assume lazy is `False`.
1291
1292    split: bool, default True
1293        If `True`, split packages' names on `'.'`.
1294
1295    check_update: bool, default False
1296        If `True` and `install` is `True`, install updates if the required minimum version
1297        does not match.
1298
1299    check_pypi: bool, default False
1300        If `True` and `check_update` is `True`, check PyPI when determining whether
1301        an update is required.
1302
1303    check_is_installed: bool, default True
1304        If `True`, check if the package is contained in the virtual environment.
1305
1306    allow_outside_venv: bool, default True
1307        If `True`, search outside of the specified virtual environment
1308        if the package cannot be found.
1309        Setting to `False` will reinstall the package into a virtual environment, even if it
1310        is installed outside.
1311
1312    color: bool, default True
1313        If `False`, do not print ANSI colors.
1314
1315    Returns
1316    -------
1317    The specified modules. If they're not available and `install` is `True`, it will first
1318    download them into a virtual environment and return the modules.
1319
1320    Examples
1321    --------
1322    >>> pandas, sqlalchemy = attempt_import('pandas', 'sqlalchemy')
1323    >>> pandas = attempt_import('pandas')
1324
1325    """
1326
1327    import importlib.util
1328
1329    ### to prevent recursion, check if parent Meerschaum package is being imported
1330    if names == ('meerschaum',):
1331        return _import_module('meerschaum')
1332
1333    if venv == 'mrsm' and _import_hook_venv is not None:
1334        if debug:
1335            print(f"Import hook for virtual environment '{_import_hook_venv}' is active.")
1336        venv = _import_hook_venv
1337
1338    _warnings = _import_module('meerschaum.utils.warnings')
1339    warn_function = _warnings.warn
1340
1341    def do_import(_name: str, **kw) -> Union['ModuleType', None]:
1342        with Venv(venv=venv, debug=debug):
1343            ### determine the import method (lazy vs normal)
1344            from meerschaum.utils.misc import filter_keywords
1345            import_method = (
1346                _import_module if not lazy
1347                else lazy_import
1348            )
1349            try:
1350                mod = import_method(_name, **(filter_keywords(import_method, **kw)))
1351            except Exception as e:
1352                if warn:
1353                    import traceback
1354                    traceback.print_exception(type(e), e, e.__traceback__)
1355                    warn_function(
1356                        f"Failed to import module '{_name}'.\nException:\n{e}",
1357                        ImportWarning,
1358                        stacklevel = (5 if lazy else 4),
1359                        color = False,
1360                    )
1361                mod = None
1362        return mod
1363
1364    modules = []
1365    for name in names:
1366        ### Check if package is a declared dependency.
1367        root_name = name.split('.')[0] if split else name
1368        install_name = _import_to_install_name(root_name)
1369
1370        if install_name is None:
1371            install_name = root_name
1372            if warn and root_name != 'plugins':
1373                warn_function(
1374                    f"Package '{root_name}' is not declared in meerschaum.utils.packages.",
1375                    ImportWarning,
1376                    stacklevel = 3,
1377                    color = False
1378                )
1379
1380        ### Determine if the package exists.
1381        if precheck is False:
1382            found_module = (
1383                do_import(
1384                    name, debug=debug, warn=False, venv=venv, color=color,
1385                    check_update=False, check_pypi=False, split=split,
1386                ) is not None
1387            )
1388        else:
1389            if check_is_installed:
1390                with _locks['_is_installed_first_check']:
1391                    if not _is_installed_first_check.get(name, False):
1392                        package_is_installed = is_installed(
1393                            name,
1394                            venv = venv,
1395                            split = split,
1396                            allow_outside_venv = allow_outside_venv,
1397                            debug = debug,
1398                        )
1399                        _is_installed_first_check[name] = package_is_installed
1400                    else:
1401                        package_is_installed = _is_installed_first_check[name]
1402            else:
1403                package_is_installed = _is_installed_first_check.get(
1404                    name,
1405                    venv_contains_package(name, venv=venv, split=split, debug=debug)
1406                )
1407            found_module = package_is_installed
1408
1409        if not found_module:
1410            if install:
1411                if not pip_install(
1412                    install_name,
1413                    venv = venv,
1414                    split = False,
1415                    check_update = check_update,
1416                    color = color,
1417                    debug = debug
1418                ) and warn:
1419                    warn_function(
1420                        f"Failed to install '{install_name}'.",
1421                        ImportWarning,
1422                        stacklevel = 3,
1423                        color = False,
1424                    )
1425            elif warn:
1426                ### Raise a warning if we can't find the package and install = False.
1427                warn_function(
1428                    (f"\n\nMissing package '{name}' from virtual environment '{venv}'; "
1429                     + "some features will not work correctly."
1430                     + "\n\nSet install=True when calling attempt_import.\n"),
1431                    ImportWarning,
1432                    stacklevel = 3,
1433                    color = False,
1434                )
1435
1436        ### Do the import. Will be lazy if lazy=True.
1437        m = do_import(
1438            name, debug=debug, warn=warn, venv=venv, color=color,
1439            check_update=check_update, check_pypi=check_pypi, install=install, split=split,
1440        )
1441        modules.append(m)
1442
1443    modules = tuple(modules)
1444    if len(modules) == 1:
1445        return modules[0]
1446    return modules
1447
1448
1449def lazy_import(
1450    name: str,
1451    local_name: str = None,
1452    **kw
1453) -> meerschaum.utils.packages.lazy_loader.LazyLoader:
1454    """
1455    Lazily import a package.
1456    """
1457    from meerschaum.utils.packages.lazy_loader import LazyLoader
1458    if local_name is None:
1459        local_name = name
1460    return LazyLoader(
1461        local_name,
1462        globals(),
1463        name,
1464        **kw
1465    )
1466
1467
1468def pandas_name() -> str:
1469    """
1470    Return the configured name for `pandas`.
1471    
1472    Below are the expected possible values:
1473
1474    - 'pandas'
1475    - 'modin.pandas'
1476    - 'dask.dataframe'
1477
1478    """
1479    from meerschaum.config import get_config
1480    pandas_module_name = get_config('system', 'connectors', 'all', 'pandas', patch=True)
1481    if pandas_module_name == 'modin':
1482        pandas_module_name = 'modin.pandas'
1483    elif pandas_module_name == 'dask':
1484        pandas_module_name = 'dask.dataframe'
1485
1486    return pandas_module_name
1487
1488
1489emitted_pandas_warning: bool = False
1490def import_pandas(
1491    debug: bool = False,
1492    lazy: bool = False,
1493    **kw
1494) -> 'ModuleType':
1495    """
1496    Quality-of-life function to attempt to import the configured version of `pandas`.
1497    """
1498    pandas_module_name = pandas_name()
1499    global emitted_pandas_warning
1500
1501    if pandas_module_name != 'pandas':
1502        with _locks['emitted_pandas_warning']:
1503            if not emitted_pandas_warning:
1504                from meerschaum.utils.warnings import warn
1505                emitted_pandas_warning = True
1506                warn(
1507                    (
1508                        "You are using an alternative Pandas implementation "
1509                        + f"'{pandas_module_name}'"
1510                        + "\n   Features may not work as expected."
1511                    ),
1512                    stack=False,
1513                )
1514
1515    pytz = attempt_import('pytz', debug=debug, lazy=False, **kw)
1516    pandas, pyarrow = attempt_import('pandas', 'pyarrow', debug=debug, lazy=False, **kw)
1517    pd = attempt_import(pandas_module_name, debug=debug, lazy=lazy, **kw)
1518    return pd
1519
1520
1521def import_rich(
1522    lazy: bool = True,
1523    debug: bool = False,
1524    **kw: Any
1525) -> 'ModuleType':
1526    """
1527    Quality of life function for importing `rich`.
1528    """
1529    from meerschaum.utils.formatting import ANSI, UNICODE
1530    ## need typing_extensions for `from rich import box`
1531    typing_extensions = attempt_import(
1532        'typing_extensions', lazy=False, debug=debug
1533    )
1534    pygments = attempt_import(
1535        'pygments', lazy=False,
1536    )
1537    rich = attempt_import(
1538        'rich', lazy=lazy,
1539        **kw
1540    )
1541    return rich
1542
1543
1544def _dash_less_than_2(**kw) -> bool:
1545    dash = attempt_import('dash', **kw)
1546    if dash is None:
1547        return None
1548    packaging_version = attempt_import('packaging.version', **kw)
1549    return (
1550        packaging_version.parse(dash.__version__) < 
1551        packaging_version.parse('2.0.0')
1552    )
1553
1554
1555def import_dcc(warn=False, **kw) -> 'ModuleType':
1556    """
1557    Import Dash Core Components (`dcc`).
1558    """
1559    return (
1560        attempt_import('dash_core_components', warn=warn, **kw)
1561        if _dash_less_than_2(warn=warn, **kw) else attempt_import('dash.dcc', warn=warn, **kw)
1562    )
1563
1564
1565def import_html(warn=False, **kw) -> 'ModuleType':
1566    """
1567    Import Dash HTML Components (`html`).
1568    """
1569    return (
1570        attempt_import('dash_html_components', warn=warn, **kw)
1571        if _dash_less_than_2(warn=warn, **kw)
1572        else attempt_import('dash.html', warn=warn, **kw)
1573    )
1574
1575
1576def get_modules_from_package(
1577    package: 'package',
1578    names: bool = False,
1579    recursive: bool = False,
1580    lazy: bool = False,
1581    modules_venvs: bool = False,
1582    debug: bool = False
1583):
1584    """
1585    Find and import all modules in a package.
1586    
1587    Returns
1588    -------
1589    Either list of modules or tuple of lists.
1590    """
1591    from os.path import dirname, join, isfile, isdir, basename
1592    import glob
1593
1594    pattern = '*' if recursive else '*.py'
1595    package_path = dirname(package.__file__ or package.__path__[0])
1596    module_names = glob.glob(join(package_path, pattern), recursive=recursive)
1597    _all = [
1598        basename(f)[:-3] if isfile(f) else basename(f)
1599        for f in module_names
1600            if ((isfile(f) and f.endswith('.py')) or isdir(f))
1601               and not f.endswith('__init__.py')
1602               and not f.endswith('__pycache__')
1603    ]
1604
1605    if debug:
1606        from meerschaum.utils.debug import dprint
1607        dprint(str(_all))
1608    modules = []
1609    for module_name in [package.__name__ + "." + mod_name for mod_name in _all]:
1610        ### there's probably a better way than a try: catch but it'll do for now
1611        try:
1612            ### if specified, activate the module's virtual environment before importing.
1613            ### NOTE: this only considers the filename, so two modules from different packages
1614            ### may end up sharing virtual environments.
1615            if modules_venvs:
1616                activate_venv(module_name.split('.')[-1], debug=debug)
1617            m = lazy_import(module_name, debug=debug) if lazy else _import_module(module_name)
1618            modules.append(m)
1619        except Exception as e:
1620            if debug:
1621                dprint(str(e))
1622        finally:
1623            if modules_venvs:
1624                deactivate_venv(module_name.split('.')[-1], debug=debug)
1625    if names:
1626        return _all, modules
1627
1628    return modules
1629
1630
1631def import_children(
1632    package: Optional['ModuleType'] = None,
1633    package_name: Optional[str] = None,
1634    types : Optional[List[str]] = None,
1635    lazy: bool = True,
1636    recursive: bool = False,
1637    debug: bool = False
1638) -> List['ModuleType']:
1639    """
1640    Import all functions in a package to its `__init__`.
1641
1642    Parameters
1643    ----------
1644    package: Optional[ModuleType], default None
1645        Package to import its functions into.
1646        If `None` (default), use parent.
1647
1648    package_name: Optional[str], default None
1649        Name of package to import its functions into
1650        If None (default), use parent.
1651
1652    types: Optional[List[str]], default None
1653        Types of members to return.
1654        Defaults are `['method', 'builtin', 'class', 'function', 'package', 'module']`
1655
1656    Returns
1657    -------
1658    A list of modules.
1659    """
1660    import sys, inspect
1661
1662    if types is None:
1663        types = ['method', 'builtin', 'function', 'class', 'module']
1664
1665    ### if package_name and package are None, use parent
1666    if package is None and package_name is None:
1667        package_name = inspect.stack()[1][0].f_globals['__name__']
1668
1669    ### populate package or package_name from other other
1670    if package is None:
1671        package = sys.modules[package_name]
1672    elif package_name is None:
1673        package_name = package.__name__
1674
1675    ### Set attributes in sys module version of package.
1676    ### Kinda like setting a dictionary
1677    ###   functions[name] = func
1678    modules = get_modules_from_package(package, recursive=recursive, lazy=lazy, debug=debug)
1679    _all, members = [], []
1680    objects = []
1681    for module in modules:
1682        _objects = []
1683        for ob in inspect.getmembers(module):
1684            for t in types:
1685                ### ob is a tuple of (name, object)
1686                if getattr(inspect, 'is' + t)(ob[1]):
1687                    _objects.append(ob)
1688
1689        if 'module' in types:
1690            _objects.append((module.__name__.split('.')[0], module))
1691        objects += _objects
1692    for ob in objects:
1693        setattr(sys.modules[package_name], ob[0], ob[1])
1694        _all.append(ob[0])
1695        members.append(ob[1])
1696
1697    if debug:
1698        from meerschaum.utils.debug import dprint
1699        dprint(str(_all))
1700    ### set __all__ for import *
1701    setattr(sys.modules[package_name], '__all__', _all)
1702    return members
1703
1704
1705_reload_module_cache = {}
1706def reload_package(
1707    package: str,
1708    skip_submodules: Optional[List[str]] = None,
1709    lazy: bool = False,
1710    debug: bool = False,
1711    **kw: Any
1712):
1713    """
1714    Recursively load a package's subpackages, even if they were not previously loaded.
1715    """
1716    import sys
1717    if isinstance(package, str):
1718        package_name = package
1719    else:
1720        try:
1721            package_name = package.__name__
1722        except Exception as e:
1723            package_name = str(package)
1724
1725    skip_submodules = skip_submodules or []
1726    if 'meerschaum.utils.packages' not in skip_submodules:
1727        skip_submodules.append('meerschaum.utils.packages')
1728    def safeimport():
1729        subs = [
1730            m for m in sys.modules
1731            if m.startswith(package_name + '.')
1732        ]
1733        subs_to_skip = []
1734        for skip_mod in skip_submodules:
1735            for mod in subs:
1736                if mod.startswith(skip_mod):
1737                    subs_to_skip.append(mod)
1738                    continue
1739
1740        subs = [m for m in subs if m not in subs_to_skip]
1741        for module_name in subs:
1742            _reload_module_cache[module_name] = sys.modules.pop(module_name, None)
1743        if not subs_to_skip:
1744            _reload_module_cache[package_name] = sys.modules.pop(package_name, None)
1745
1746        return _import_module(package_name)
1747
1748    return safeimport()
1749
1750
1751def reload_meerschaum(debug: bool = False) -> SuccessTuple:
1752    """
1753    Reload the currently loaded Meercshaum modules, refreshing plugins and shell configuration.
1754    """
1755    reload_package(
1756        'meerschaum',
1757        skip_submodules = [
1758            'meerschaum._internal.shell',
1759            'meerschaum.utils.pool',
1760        ]
1761    )
1762
1763    from meerschaum.plugins import reload_plugins
1764    from meerschaum._internal.shell.Shell import _insert_shell_actions
1765    reload_plugins(debug=debug)
1766    _insert_shell_actions()
1767    return True, "Success"
1768
1769
1770def is_installed(
1771    import_name: str,
1772    venv: Optional[str] = 'mrsm',
1773    split: bool = True,
1774    allow_outside_venv: bool = True,
1775    debug: bool = False,
1776) -> bool:
1777    """
1778    Check whether a package is installed.
1779
1780    Parameters
1781    ----------
1782    import_name: str
1783        The import name of the module.
1784
1785    venv: Optional[str], default 'mrsm'
1786        The venv in which to search for the module.
1787
1788    split: bool, default True
1789        If `True`, split on periods to determine the root module name.
1790
1791    allow_outside_venv: bool, default True
1792        If `True`, search outside of the specified virtual environment
1793        if the package cannot be found.
1794
1795    Returns
1796    -------
1797    A bool indicating whether a package may be imported.
1798    """
1799    if debug:
1800        from meerschaum.utils.debug import dprint
1801    root_name = import_name.split('.')[0] if split else import_name
1802    import importlib.util
1803    with Venv(venv, debug=debug):
1804        try:
1805            spec_path = pathlib.Path(
1806                get_module_path(root_name, venv=venv, debug=debug)
1807                or
1808                (
1809                    importlib.util.find_spec(root_name).origin 
1810                    if venv is not None and allow_outside_venv
1811                    else None
1812                )
1813            )
1814        except (ModuleNotFoundError, ValueError, AttributeError, TypeError) as e:
1815            spec_path = None
1816
1817        found = (
1818            not need_update(
1819                None,
1820                import_name=root_name,
1821                _run_determine_version=False,
1822                check_pypi=False,
1823                version=determine_version(
1824                    spec_path,
1825                    venv=venv,
1826                    debug=debug,
1827                    import_name=root_name,
1828                ),
1829                debug=debug,
1830            )
1831        ) if spec_path is not None else False
1832
1833    return found
1834
1835
1836def venv_contains_package(
1837    import_name: str,
1838    venv: Optional[str] = 'mrsm',
1839    split: bool = True,
1840    debug: bool = False,
1841) -> bool:
1842    """
1843    Search the contents of a virtual environment for a package.
1844    """
1845    import site
1846    import pathlib
1847    root_name = import_name.split('.')[0] if split else import_name
1848    return get_module_path(root_name, venv=venv, debug=debug) is not None
1849
1850
1851def package_venv(package: 'ModuleType') -> Union[str, None]:
1852    """
1853    Inspect a package and return the virtual environment in which it presides.
1854    """
1855    import os
1856    from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
1857    if str(VIRTENV_RESOURCES_PATH) not in package.__file__:
1858        return None
1859    return package.__file__.split(str(VIRTENV_RESOURCES_PATH))[1].split(os.path.sep)[1]
1860
1861
1862def ensure_readline() -> 'ModuleType':
1863    """Make sure that the `readline` package is able to be imported."""
1864    import sys
1865    try:
1866        import readline
1867    except ImportError:
1868        readline = None
1869
1870    if readline is None:
1871        import platform
1872        rl_name = "gnureadline" if platform.system() != 'Windows' else "pyreadline3"
1873        try:
1874            rl = attempt_import(
1875                rl_name,
1876                lazy=False,
1877                install=True,
1878                venv=None,
1879                warn=False,
1880            )
1881        except (ImportError, ModuleNotFoundError):
1882            if not pip_install(rl_name, args=['--upgrade', '--ignore-installed'], venv=None):
1883                print(f"Unable to import {rl_name}!", file=sys.stderr)
1884                sys.exit(1)
1885
1886    sys.modules['readline'] = readline
1887    return readline
1888
1889
1890def _get_pip_os_env(color: bool = True):
1891    """
1892    Return the environment variables context in which `pip` should be run.
1893    See PEP 668 for why we are overriding the environment.
1894    """
1895    import os, sys, platform
1896    python_bin_path = pathlib.Path(sys.executable)
1897    pip_os_env = os.environ.copy()
1898    path_str = pip_os_env.get('PATH', '') or ''
1899    path_sep = ':' if platform.system() != 'Windows' else ';'
1900    pip_os_env.update({
1901        'PIP_BREAK_SYSTEM_PACKAGES': 'true',
1902        'UV_BREAK_SYSTEM_PACKAGES': 'true',
1903        ('FORCE_COLOR' if color else 'NO_COLOR'): '1',
1904    })
1905    if str(python_bin_path) not in path_str:
1906        pip_os_env['PATH'] = str(python_bin_path.parent) + path_sep + path_str
1907
1908    return pip_os_env
1909
1910
1911def use_uv() -> bool:
1912    """
1913    Return whether `uv` is available and enabled.
1914    """
1915    from meerschaum.utils.misc import is_android
1916    if is_android():
1917        return False
1918
1919    if not is_uv_enabled():
1920        return False
1921
1922    try:
1923        import uv
1924        uv_bin = uv.find_uv_bin()
1925    except (ImportError, FileNotFoundError):
1926        uv_bin = None
1927
1928    if uv_bin is None:
1929        return False
1930
1931    return True
1932
1933
1934def is_uv_enabled() -> bool:
1935    """
1936    Return whether the user has disabled `uv`.
1937    """
1938    from meerschaum.utils.misc import is_android
1939    if is_android():
1940        return False
1941
1942    try:
1943        import yaml
1944    except ImportError:
1945        return False
1946
1947    from meerschaum.config import get_config
1948    enabled = get_config('system', 'experimental', 'uv_pip')
1949    return enabled
def get_module_path( import_name: str, venv: Optional[str] = 'mrsm', debug: bool = False, _try_install_name_on_fail: bool = True) -> Optional[pathlib.Path]:
 46def get_module_path(
 47    import_name: str,
 48    venv: Optional[str] = 'mrsm',
 49    debug: bool = False,
 50    _try_install_name_on_fail: bool = True,
 51) -> Union[pathlib.Path, None]:
 52    """
 53    Get a module's path without importing.
 54    """
 55    import site
 56    if debug:
 57        from meerschaum.utils.debug import dprint
 58    if not _try_install_name_on_fail:
 59        install_name = _import_to_install_name(import_name, with_version=False)
 60        install_name_lower = install_name.lower().replace('-', '_')
 61        import_name_lower = install_name_lower
 62    else:
 63        import_name_lower = import_name.lower().replace('-', '_')
 64
 65    vtp = venv_target_path(venv, allow_nonexistent=True, debug=debug)
 66    if not vtp.exists():
 67        if debug:
 68            dprint(
 69                (
 70                    "Venv '{venv}' does not exist, cannot import "
 71                    + f"'{import_name}'."
 72                ),
 73                color = False,
 74            )
 75        return None
 76
 77    venv_target_candidate_paths = [vtp]
 78    if venv is None:
 79        site_user_packages_dirs = [
 80            pathlib.Path(site.getusersitepackages())
 81        ] if not inside_venv() else []
 82        site_packages_dirs = [pathlib.Path(path) for path in site.getsitepackages()]
 83
 84        paths_to_add = [
 85            path
 86            for path in site_user_packages_dirs + site_packages_dirs
 87            if path not in venv_target_candidate_paths
 88        ]
 89        venv_target_candidate_paths += paths_to_add
 90
 91    candidates = []
 92    for venv_target_candidate in venv_target_candidate_paths:
 93        try:
 94            file_names = os.listdir(venv_target_candidate)
 95        except FileNotFoundError:
 96            continue
 97        for file_name in file_names:
 98            file_name_lower = file_name.lower().replace('-', '_')
 99            if not file_name_lower.startswith(import_name_lower):
100                continue
101            if file_name.endswith('dist_info'):
102                continue
103            file_path = venv_target_candidate / file_name
104
105            ### Most likely: Is a directory with __init__.py
106            if file_name_lower == import_name_lower and file_path.is_dir():
107                init_path = file_path / '__init__.py'
108                if init_path.exists():
109                    candidates.append(init_path)
110
111            ### May be a standalone .py file.
112            elif file_name_lower == import_name_lower + '.py':
113                candidates.append(file_path)
114
115            ### Compiled wheels (e.g. pyodbc)
116            elif file_name_lower.startswith(import_name_lower + '.'):
117                candidates.append(file_path)
118
119    if len(candidates) == 1:
120        return candidates[0]
121
122    if not candidates:
123        if _try_install_name_on_fail:
124            return get_module_path(
125                import_name, venv=venv, debug=debug,
126                _try_install_name_on_fail=False
127            )
128        return None
129
130    specs_paths = []
131    for candidate_path in candidates:
132        spec = importlib.util.spec_from_file_location(import_name, str(candidate_path))
133        if spec is not None:
134            return candidate_path
135    
136    return None

Get a module's path without importing.

def manually_import_module( import_name: str, venv: Optional[str] = 'mrsm', check_update: bool = True, check_pypi: bool = False, install: bool = True, split: bool = True, warn: bool = True, color: bool = True, debug: bool = False, use_sys_modules: bool = True) -> "Union['ModuleType', None]":
139def manually_import_module(
140    import_name: str,
141    venv: Optional[str] = 'mrsm',
142    check_update: bool = True,
143    check_pypi: bool = False,
144    install: bool = True,
145    split: bool = True,
146    warn: bool = True,
147    color: bool = True,
148    debug: bool = False,
149    use_sys_modules: bool = True,
150) -> Union['ModuleType', None]:
151    """
152    Manually import a module from a virtual environment (or the base environment).
153
154    Parameters
155    ----------
156    import_name: str
157        The name of the module.
158        
159    venv: Optional[str], default 'mrsm'
160        The virtual environment to read from.
161
162    check_update: bool, default True
163        If `True`, examine whether the available version of the package meets the required version.
164
165    check_pypi: bool, default False
166        If `True`, check PyPI for updates before importing.
167
168    install: bool, default True
169        If `True`, install the package if it's not installed or needs an update.
170
171    split: bool, default True
172        If `True`, split `import_name` on periods to get the package name.
173
174    warn: bool, default True
175        If `True`, raise a warning if the package cannot be imported.
176
177    color: bool, default True
178        If `True`, use color output for debug and warning text.
179
180    debug: bool, default False
181        Verbosity toggle.
182
183    use_sys_modules: bool, default True
184        If `True`, return the module in `sys.modules` if it exists.
185        Otherwise continue with manually importing.
186
187    Returns
188    -------
189    The specified module or `None` if it can't be imported.
190
191    """
192    import sys
193    _previously_imported = import_name in sys.modules
194    if _previously_imported and use_sys_modules:
195        return sys.modules[import_name]
196
197    from meerschaum.utils.warnings import warn as warn_function
198    import warnings
199    root_name = import_name.split('.')[0] if split else import_name
200    install_name = _import_to_install_name(root_name)
201
202    root_path = get_module_path(root_name, venv=venv)
203    if root_path is None:
204        return None
205
206    mod_path = root_path
207    if mod_path.is_dir():
208        for _dir in import_name.split('.')[:-1]:
209            mod_path = mod_path / _dir
210            possible_end_module_filename = import_name.split('.')[-1] + '.py'
211            try:
212                mod_path = (
213                    (mod_path / possible_end_module_filename)
214                    if possible_end_module_filename in os.listdir(mod_path)
215                    else (
216                        mod_path / import_name.split('.')[-1] / '__init__.py'
217                    )
218                )
219            except Exception:
220                mod_path = None
221
222    spec = (
223        importlib.util.find_spec(import_name)
224        if mod_path is None or not mod_path.exists()
225        else importlib.util.spec_from_file_location(import_name, str(mod_path))
226    )
227    root_spec = (
228        importlib.util.find_spec(root_name)
229        if not root_path.exists()
230        else importlib.util.spec_from_file_location(root_name, str(root_path))
231    )
232
233    ### Check for updates before importing.
234    _version = (
235        determine_version(
236            pathlib.Path(root_spec.origin),
237            import_name=root_name, venv=venv, debug=debug
238        ) if root_spec is not None and root_spec.origin is not None else None
239    )
240
241    if _version is not None:
242        if check_update:
243            if need_update(
244                None,
245                import_name=root_name,
246                version=_version,
247                check_pypi=check_pypi,
248                debug=debug,
249            ):
250                if install:
251                    if not pip_install(
252                        root_name,
253                        venv=venv,
254                        split=False,
255                        check_update=check_update,
256                        color=color,
257                        debug=debug
258                    ) and warn:
259                        warn_function(
260                            f"There's an update available for '{install_name}', "
261                            + "but it failed to install. "
262                            + "Try installig via Meerschaum with "
263                            + "`install packages '{install_name}'`.",
264                            ImportWarning,
265                            stacklevel=3,
266                            color=False,
267                        )
268                elif warn:
269                    warn_function(
270                        f"There's an update available for '{root_name}'.",
271                        stack=False,
272                        color=False,
273                    )
274                spec = (
275                    importlib.util.find_spec(import_name)
276                    if mod_path is None or not mod_path.exists()
277                    else importlib.util.spec_from_file_location(import_name, str(mod_path))
278                )
279
280    if spec is None:
281        try:
282            mod = _import_module(import_name)
283        except Exception:
284            mod = None
285        return mod
286
287    with Venv(venv, debug=debug):
288        mod = importlib.util.module_from_spec(spec)
289        old_sys_mod = sys.modules.get(import_name, None)
290        sys.modules[import_name] = mod
291
292        try:
293            with warnings.catch_warnings():
294                warnings.filterwarnings('ignore', 'The NumPy')
295                spec.loader.exec_module(mod)
296        except Exception:
297            pass
298        mod = _import_module(import_name)
299        if old_sys_mod is not None:
300            sys.modules[import_name] = old_sys_mod
301        else:
302            del sys.modules[import_name]
303
304    return mod

Manually import a module from a virtual environment (or the base environment).

Parameters
  • import_name (str): The name of the module.
  • venv (Optional[str], default 'mrsm'): The virtual environment to read from.
  • check_update (bool, default True): If True, examine whether the available version of the package meets the required version.
  • check_pypi (bool, default False): If True, check PyPI for updates before importing.
  • install (bool, default True): If True, install the package if it's not installed or needs an update.
  • split (bool, default True): If True, split import_name on periods to get the package name.
  • warn (bool, default True): If True, raise a warning if the package cannot be imported.
  • color (bool, default True): If True, use color output for debug and warning text.
  • debug (bool, default False): Verbosity toggle.
  • use_sys_modules (bool, default True): If True, return the module in sys.modules if it exists. Otherwise continue with manually importing.
Returns
  • The specified module or None if it can't be imported.
def get_install_no_version(install_name: str) -> str:
335def get_install_no_version(install_name: str) -> str:
336    """
337    Strip the version information from the install name.
338    """
339    import re
340    return re.split(r'[\[=<>,! \]]', install_name)[0]

Strip the version information from the install name.

import_versions = {'mrsm': {'packaging': '25.0', 'semver': '3.0.4', 'daemon': '3.1.2', 'pandas': '3.0.0', 'prompt_toolkit': '3.0.51', 'dask': '2025.7.0'}}
def determine_version( path: pathlib.Path, import_name: Optional[str] = None, venv: Optional[str] = 'mrsm', search_for_metadata: bool = True, split: bool = True, warn: bool = False, debug: bool = False) -> Optional[str]:
344def determine_version(
345    path: pathlib.Path,
346    import_name: Optional[str] = None,
347    venv: Optional[str] = 'mrsm',
348    search_for_metadata: bool = True,
349    split: bool = True,
350    warn: bool = False,
351    debug: bool = False,
352) -> Union[str, None]:
353    """
354    Determine a module's `__version__` string from its filepath.
355    
356    First it searches for pip metadata, then it attempts to import the module in a subprocess.
357
358    Parameters
359    ----------
360    path: pathlib.Path
361        The file path of the module.
362
363    import_name: Optional[str], default None
364        The name of the module. If omitted, it will be determined from the file path.
365        Defaults to `None`.
366
367    venv: Optional[str], default 'mrsm'
368        The virtual environment of the Python interpreter to use if importing is necessary.
369
370    search_for_metadata: bool, default True
371        If `True`, search the pip site_packages directory (assumed to be the parent)
372        for the corresponding dist-info directory.
373
374    warn: bool, default True
375        If `True`, raise a warning if the module fails to import in the subprocess.
376
377    split: bool, default True
378        If `True`, split the determined import name by periods to get the room name.
379
380    Returns
381    -------
382    The package's version string if available or `None`.
383    If multiple versions are found, it will trigger an import in a subprocess.
384
385    """
386    with _locks['import_versions']:
387        if venv not in import_versions:
388            import_versions[venv] = {}
389    import os
390    old_cwd = os.getcwd()
391    from meerschaum.utils.warnings import warn as warn_function
392    if import_name is None:
393        import_name = path.parent.stem if path.stem == '__init__' else path.stem
394        import_name = import_name.split('.')[0] if split else import_name
395    if import_name in import_versions[venv]:
396        return import_versions[venv][import_name]
397    _version = None
398    module_parent_dir = (
399        path.parent.parent if path.stem == '__init__' else path.parent
400    ) if path is not None else venv_target_path(venv, allow_nonexistent=True, debug=debug)
401
402    if not module_parent_dir.exists():
403        return None
404
405    installed_dir_name = _import_to_dir_name(import_name)
406    clean_installed_dir_name = installed_dir_name.lower().replace('-', '_')
407
408    ### First, check if a dist-info directory exists.
409    _found_versions = []
410    if search_for_metadata:
411        try:
412            filenames = os.listdir(module_parent_dir)
413        except FileNotFoundError:
414            filenames = []
415        for filename in filenames:
416            if not filename.endswith('.dist-info'):
417                continue
418            filename_lower = filename.lower()
419            if not filename_lower.startswith(clean_installed_dir_name + '-'):
420                continue
421            _v = filename.replace('.dist-info', '').split("-")[-1]
422            _found_versions.append(_v)
423
424    if len(_found_versions) == 1:
425        _version = _found_versions[0]
426        with _locks['import_versions']:
427            import_versions[venv][import_name] = _version
428        return _found_versions[0]
429
430    if not _found_versions:
431        try:
432            import importlib.metadata as importlib_metadata
433        except ImportError:
434            importlib_metadata = attempt_import(
435                'importlib_metadata',
436                debug=debug, check_update=False, precheck=False,
437                color=False, check_is_installed=False, lazy=False,
438            )
439        try:
440            os.chdir(module_parent_dir)
441            _version = importlib_metadata.metadata(import_name)['Version']
442        except Exception:
443            _version = None
444        finally:
445            os.chdir(old_cwd)
446
447        if _version is not None:
448            with _locks['import_versions']:
449                import_versions[venv][import_name] = _version
450            return _version
451
452    if debug:
453        print(f'Found multiple versions for {import_name}: {_found_versions}')
454
455    module_parent_dir_str = module_parent_dir.as_posix()
456
457    ### Not a pip package, so let's try importing the module directly (in a subprocess).
458    _no_version_str = 'no-version'
459    code = (
460        f"import sys, importlib; sys.path.insert(0, '{module_parent_dir_str}');\n"
461        + f"module = importlib.import_module('{import_name}');\n"
462        + "try:\n"
463        + "  print(module.__version__ , end='')\n"
464        + "except:\n"
465        + f"  print('{_no_version_str}', end='')"
466    )
467    exit_code, stdout_bytes, stderr_bytes = venv_exec(
468        code, venv=venv, with_extras=True, debug=debug
469    )
470    stdout, stderr = stdout_bytes.decode('utf-8'), stderr_bytes.decode('utf-8')
471    _version = stdout.split('\n')[-1] if exit_code == 0 else None
472    _version = _version if _version != _no_version_str else None
473
474    if _version is None:
475        _version = _get_package_metadata(import_name, venv).get('version', None)
476    if _version is None and warn:
477        warn_function(
478            f"Failed to determine a version for '{import_name}':\n{stderr}",
479            stack = False
480        )
481
482    ### If `__version__` doesn't exist, return `None`.
483    import_versions[venv][import_name] = _version
484    return _version

Determine a module's __version__ string from its filepath.

First it searches for pip metadata, then it attempts to import the module in a subprocess.

Parameters
  • path (pathlib.Path): The file path of the module.
  • import_name (Optional[str], default None): The name of the module. If omitted, it will be determined from the file path. Defaults to None.
  • venv (Optional[str], default 'mrsm'): The virtual environment of the Python interpreter to use if importing is necessary.
  • search_for_metadata (bool, default True): If True, search the pip site_packages directory (assumed to be the parent) for the corresponding dist-info directory.
  • warn (bool, default True): If True, raise a warning if the module fails to import in the subprocess.
  • split (bool, default True): If True, split the determined import name by periods to get the room name.
Returns
  • The package's version string if available or None.
  • If multiple versions are found, it will trigger an import in a subprocess.
def need_update( package: "Optional['ModuleType']" = None, install_name: Optional[str] = None, import_name: Optional[str] = None, version: Optional[str] = None, check_pypi: bool = False, split: bool = True, color: bool = True, debug: bool = False, _run_determine_version: bool = True) -> bool:
538def need_update(
539    package: Optional['ModuleType'] = None,
540    install_name: Optional[str] = None,
541    import_name: Optional[str] = None,
542    version: Optional[str] = None,
543    check_pypi: bool = False,
544    split: bool = True,
545    color: bool = True,
546    debug: bool = False,
547    _run_determine_version: bool = True,
548) -> bool:
549    """
550    Check if a Meerschaum dependency needs an update.
551    Returns a bool for whether or not a package needs to be updated.
552
553    Parameters
554    ----------
555    package: 'ModuleType'
556        The module of the package to be updated.
557
558    install_name: Optional[str], default None
559        If provided, use this string to determine the required version.
560        Otherwise use the install name defined in `meerschaum.utils.packages._packages`.
561
562    import_name:
563        If provided, override the package's `__name__` string.
564
565    version: Optional[str], default None
566        If specified, override the package's `__version__` string.
567
568    check_pypi: bool, default False
569        If `True`, check pypi.org for updates.
570        Defaults to `False`.
571
572    split: bool, default True
573        If `True`, split the module's name on periods to detrive the root name.
574        Defaults to `True`.
575
576    color: bool, default True
577        If `True`, format debug output.
578        Defaults to `True`.
579
580    debug: bool, default True
581        Verbosity toggle.
582
583    Returns
584    -------
585    A bool indicating whether the package requires an update.
586
587    """
588    if debug:
589        from meerschaum.utils.debug import dprint
590    from meerschaum.utils.warnings import warn as warn_function
591    import re
592    root_name = (
593        package.__name__.split('.')[0] if split else package.__name__
594    ) if import_name is None else (
595        import_name.split('.')[0] if split else import_name
596    )
597    install_name = install_name or _import_to_install_name(root_name)
598    with _locks['_checked_for_updates']:
599        if install_name in _checked_for_updates:
600            return False
601        _checked_for_updates.add(install_name)
602
603    _install_no_version = get_install_no_version(install_name)
604    required_version = (
605        install_name
606        .replace(_install_no_version, '')
607    )
608    if ']' in required_version:
609        required_version = required_version.split(']')[1]
610
611    ### No minimum version was specified, and we're not going to check PyPI.
612    if not required_version and not check_pypi:
613        return False
614
615    ### NOTE: Sometimes (rarely), we depend on a development build of a package.
616    if '.dev' in required_version:
617        required_version = required_version.split('.dev')[0]
618    if version and '.dev' in version:
619        version = version.split('.dev')[0]
620
621    try:
622        if not version:
623            if not _run_determine_version:
624                version = determine_version(
625                    pathlib.Path(package.__file__),
626                    import_name=root_name, warn=False, debug=debug
627                )
628        if version is None:
629            return False
630    except Exception as e:
631        if debug:
632            dprint(str(e), color=color)
633            dprint("No version could be determined from the installed package.", color=color)
634        return False
635    split_version = version.split('.')
636    last_part = split_version[-1]
637    if len(split_version) == 2:
638        version = '.'.join(split_version) + '.0'
639    elif 'dev' in last_part or 'rc' in last_part:
640        tag = 'dev' if 'dev' in last_part else 'rc'
641        last_sep = '-'
642        if not last_part.startswith(tag):
643            last_part = f'-{tag}'.join(last_part.split(tag))
644            last_sep = '.'
645        version = '.'.join(split_version[:-1]) + last_sep + last_part
646    elif len(split_version) > 3:
647        version = '.'.join(split_version[:3])
648
649    packaging_version = attempt_import(
650        'packaging.version', check_update=False, lazy=False, debug=debug,
651    )
652
653    ### Get semver if necessary
654    if required_version:
655        semver_path = get_module_path('semver', debug=debug)
656        if semver_path is None:
657            no_venv_semver_path = get_module_path('semver', venv=None, debug=debug)
658            if no_venv_semver_path is None:
659                pip_install(_import_to_install_name('semver'), debug=debug)
660        semver = attempt_import('semver', check_update=False, lazy=False, debug=debug)
661    if check_pypi:
662        ### Check PyPI for updates
663        update_checker = attempt_import(
664            'update_checker', lazy=False, check_update=False, debug=debug
665        )
666        checker = update_checker.UpdateChecker()
667        result = checker.check(_install_no_version, version)
668    else:
669        ### Skip PyPI and assume we can't be sure.
670        result = None
671
672    ### Compare PyPI's version with our own.
673    if result is not None:
674        ### We have a result from PyPI and a stated required version.
675        if required_version:
676            try:
677                return semver.Version.parse(result.available_version).match(required_version)
678            except AttributeError as e:
679                pip_install(_import_to_install_name('semver'), venv='mrsm', debug=debug)
680                semver = manually_import_module('semver', venv='mrsm')
681                return semver.Version.parse(version).match(required_version)
682            except Exception as e:
683                if debug:
684                    dprint(f"Failed to match versions with exception:\n{e}", color=color)
685                return False
686
687        ### If `check_pypi` and we don't have a required version, check if PyPI's version
688        ### is newer than the installed version.
689        else:
690            return (
691                packaging_version.parse(result.available_version) > 
692                packaging_version.parse(version)
693            )
694
695    ### We might be depending on a prerelease.
696    ### Sanity check that the required version is not greater than the installed version. 
697    required_version = (
698        required_version.replace(_MRSM_PACKAGE_ARCHIVES_PREFIX, '')
699        .replace(' @ ', '').replace('wheels', '').replace('+mrsm', '').replace('/-', '')
700        .replace('-py3-none-any.whl', '')
701    )
702
703    if 'a' in required_version:
704        required_version = required_version.replace('a', '-pre.').replace('+mrsm', '')
705        version = version.replace('a', '-pre.').replace('+mrsm', '')
706    try:
707        return (
708            (not semver.Version.parse(version).match(required_version))
709            if required_version else False
710        )
711    except AttributeError:
712        pip_install(_import_to_install_name('semver'), venv='mrsm', debug=debug)
713        semver = manually_import_module('semver', venv='mrsm', debug=debug)
714        return (
715            (not semver.Version.parse(version).match(required_version))
716            if required_version else False
717        )
718    except Exception as e:
719        print(f"Unable to parse version ({version}) for package '{import_name}'.")
720        print(e)
721        if debug:
722            dprint(e)
723        return False
724    try:
725        return (
726            packaging_version.parse(version) > 
727            packaging_version.parse(required_version)
728        )
729    except Exception as e:
730        if debug:
731            dprint(e)
732        return False
733    return False

Check if a Meerschaum dependency needs an update. Returns a bool for whether or not a package needs to be updated.

Parameters
  • package ('ModuleType'): The module of the package to be updated.
  • install_name (Optional[str], default None): If provided, use this string to determine the required version. Otherwise use the install name defined in meerschaum.utils.packages._packages.
  • import_name:: If provided, override the package's __name__ string.
  • version (Optional[str], default None): If specified, override the package's __version__ string.
  • check_pypi (bool, default False): If True, check pypi.org for updates. Defaults to False.
  • split (bool, default True): If True, split the module's name on periods to detrive the root name. Defaults to True.
  • color (bool, default True): If True, format debug output. Defaults to True.
  • debug (bool, default True): Verbosity toggle.
Returns
  • A bool indicating whether the package requires an update.
def get_pip( venv: Optional[str] = 'mrsm', color: bool = True, debug: bool = False) -> bool:
736def get_pip(
737    venv: Optional[str] = 'mrsm',
738    color: bool = True,
739    debug: bool = False,
740) -> bool:
741    """
742    Download and run the get-pip.py script.
743
744    Parameters
745    ----------
746    venv: Optional[str], default 'mrsm'
747        The virtual environment into which to install `pip`.
748
749    color: bool, default True
750        If `True`, force color output.
751
752    debug: bool, default False
753        Verbosity toggle.
754
755    Returns
756    -------
757    A bool indicating success.
758
759    """
760    import sys
761    import subprocess
762    from meerschaum.utils.misc import wget
763    from meerschaum.config._paths import CACHE_RESOURCES_PATH
764    from meerschaum._internal.static import STATIC_CONFIG
765    url = STATIC_CONFIG['system']['urls']['get-pip.py']
766    dest = CACHE_RESOURCES_PATH / 'get-pip.py'
767    try:
768        wget(url, dest, color=False, debug=debug)
769    except Exception:
770        print(f"Failed to fetch pip from '{url}'. Please install pip and restart Meerschaum.") 
771        sys.exit(1)
772    if venv is not None:
773        init_venv(venv=venv, debug=debug)
774    cmd_list = [venv_executable(venv=venv), dest.as_posix()] 
775    return subprocess.call(cmd_list, env=_get_pip_os_env(color=color)) == 0

Download and run the get-pip.py script.

Parameters
  • venv (Optional[str], default 'mrsm'): The virtual environment into which to install pip.
  • color (bool, default True): If True, force color output.
  • debug (bool, default False): Verbosity toggle.
Returns
  • A bool indicating success.
def pip_install( *install_names: str, args: Optional[List[str]] = None, requirements_file_path: Union[pathlib.Path, str, NoneType] = None, venv: Optional[str] = 'mrsm', split: bool = False, check_update: bool = True, check_pypi: bool = True, check_wheel: bool = True, _uninstall: bool = False, _from_completely_uninstall: bool = False, _install_uv_pip: bool = True, _use_uv_pip: bool = True, color: bool = True, silent: bool = False, debug: bool = False) -> bool:
 778def pip_install(
 779    *install_names: str,
 780    args: Optional[List[str]] = None,
 781    requirements_file_path: Union[pathlib.Path, str, None] = None,
 782    venv: Optional[str] = 'mrsm',
 783    split: bool = False,
 784    check_update: bool = True,
 785    check_pypi: bool = True,
 786    check_wheel: bool = True,
 787    _uninstall: bool = False,
 788    _from_completely_uninstall: bool = False,
 789    _install_uv_pip: bool = True,
 790    _use_uv_pip: bool = True,
 791    color: bool = True,
 792    silent: bool = False,
 793    debug: bool = False,
 794) -> bool:
 795    """
 796    Install packages from PyPI with `pip`.
 797
 798    Parameters
 799    ----------
 800    *install_names: str
 801        The installation names of packages to be installed.
 802        This includes version restrictions.
 803        Use `_import_to_install_name()` to get the predefined `install_name` for a package
 804        from its import name.
 805
 806    args: Optional[List[str]], default None
 807        A list of command line arguments to pass to `pip`.
 808        If not provided, default to `['--upgrade']` if `_uninstall` is `False`, else `[]`.
 809
 810    requirements_file_path: Optional[pathlib.Path, str], default None
 811        If provided, append `['-r', '/path/to/requirements.txt']` to `args`.
 812
 813    venv: str, default 'mrsm'
 814        The virtual environment to install into.
 815
 816    split: bool, default False
 817        If `True`, split on periods and only install the root package name.
 818
 819    check_update: bool, default True
 820        If `True`, check if the package requires an update.
 821
 822    check_pypi: bool, default True
 823        If `True` and `check_update` is `True`, check PyPI for the latest version.
 824
 825    check_wheel: bool, default True
 826        If `True`, check if `wheel` is available.
 827
 828    _uninstall: bool, default False
 829        If `True`, uninstall packages instead.
 830
 831    color: bool, default True
 832        If `True`, include color in debug text.
 833
 834    silent: bool, default False
 835        If `True`, skip printing messages.
 836
 837    debug: bool, default False
 838        Verbosity toggle.
 839
 840    Returns
 841    -------
 842    A bool indicating success.
 843
 844    """
 845    from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
 846    from meerschaum._internal.static import STATIC_CONFIG
 847    from meerschaum.utils.warnings import warn
 848    if args is None:
 849        args = ['--upgrade'] if not _uninstall else []
 850    ANSI = True if color else False
 851    if check_wheel:
 852        have_wheel = venv_contains_package('wheel', venv=venv, debug=debug)
 853
 854    daemon_env_var = STATIC_CONFIG['environment']['daemon_id']
 855    inside_daemon = daemon_env_var in os.environ
 856    if inside_daemon:
 857        silent = True
 858
 859    _args = list(args)
 860    have_pip = venv_contains_package('pip', venv=None, debug=debug)
 861    pip_venv = None
 862    try:
 863        import pip
 864        have_pip = True
 865    except ImportError:
 866        have_pip = False
 867    try:
 868        import uv
 869        uv_bin = uv.find_uv_bin()
 870        have_uv_pip = True
 871    except (ImportError, FileNotFoundError):
 872        uv_bin = None
 873        have_uv_pip = False
 874
 875    if have_pip and not have_uv_pip and _install_uv_pip and is_uv_enabled():
 876        if not pip_install(
 877            'uv', 'PyYAML',
 878            venv=None,
 879            debug=debug,
 880            _install_uv_pip=False,
 881            check_update=False,
 882            check_pypi=False,
 883            check_wheel=False,
 884        ) and not silent:
 885            warn(
 886                f"Failed to install `uv` for virtual environment '{venv}'.",
 887                color=False,
 888            )
 889
 890    use_uv_pip = (
 891        _use_uv_pip
 892        and venv_contains_package('uv', venv=None, debug=debug)
 893        and uv_bin is not None
 894        and venv is not None
 895        and is_uv_enabled()
 896    )
 897
 898    import sys
 899    if not have_pip and not use_uv_pip:
 900        have_mrsm_pip = venv_contains_package('pip', venv='mrsm')
 901        if not have_mrsm_pip and not get_pip(venv=venv, color=color, debug=debug):
 902            import sys
 903            minor = sys.version_info.minor
 904            print(
 905                "\nFailed to import `pip` and `ensurepip`.\n"
 906                + "If you are running Ubuntu/Debian, "
 907                + f"you might need to install `python3.{minor}-distutils`:\n\n"
 908                + f"    sudo apt install python3.{minor}-pip python3.{minor}-venv\n\n"
 909                + "Please install pip and restart Meerschaum.\n\n"
 910                + "You can find instructions on installing `pip` here:\n"
 911                + "https://pip.pypa.io/en/stable/installing/"
 912            )
 913            sys.exit(1)
 914
 915        pip = attempt_import('pip', lazy=False)
 916        pip_venv = 'mrsm'
 917
 918    with Venv(venv, debug=debug):
 919        if venv is not None:
 920            if (
 921                '--ignore-installed' not in args
 922                and '-I' not in _args
 923                and not _uninstall
 924                and not use_uv_pip
 925            ):
 926                _args += ['--ignore-installed']
 927            if '--cache-dir' not in args and not _uninstall:
 928                cache_dir_path = VIRTENV_RESOURCES_PATH / venv / 'cache'
 929                _args += ['--cache-dir', str(cache_dir_path)]
 930
 931        if 'pip' not in ' '.join(_args) and not use_uv_pip:
 932            if check_update and not _uninstall:
 933                pip = attempt_import('pip', venv=venv, install=False, debug=debug, lazy=False)
 934                if need_update(pip, check_pypi=check_pypi, debug=debug):
 935                    _args.append(all_packages['pip'])
 936
 937        _args = (['install'] if not _uninstall else ['uninstall']) + _args
 938
 939        if check_wheel and not _uninstall and not use_uv_pip:
 940            if not have_wheel:
 941                setup_packages_to_install = (
 942                    ['setuptools', 'wheel', 'PyYAML']
 943                    + (['uv'] if is_uv_enabled() else [])
 944                )
 945                if not pip_install(
 946                    *setup_packages_to_install,
 947                    venv=venv,
 948                    check_update=False,
 949                    check_pypi=False,
 950                    check_wheel=False,
 951                    debug=debug,
 952                    _install_uv_pip=False,
 953                ) and not silent:
 954                    from meerschaum.utils.misc import items_str
 955                    warn(
 956                        (
 957                            f"Failed to install {items_str(setup_packages_to_install)} for virtual "
 958                            + f"environment '{venv}'."
 959                        ),
 960                        color=False,
 961                    )
 962
 963        if requirements_file_path is not None:
 964            _args.append('-r')
 965            _args.append(pathlib.Path(requirements_file_path).resolve().as_posix())
 966
 967        if not ANSI and '--no-color' not in _args:
 968            _args.append('--no-color')
 969
 970        if '--no-input' not in _args and not use_uv_pip:
 971            _args.append('--no-input')
 972
 973        if _uninstall and '-y' not in _args and not use_uv_pip:
 974            _args.append('-y')
 975
 976        if '--no-warn-conflicts' not in _args and not _uninstall and not use_uv_pip:
 977            _args.append('--no-warn-conflicts')
 978
 979        if '--disable-pip-version-check' not in _args and not use_uv_pip:
 980            _args.append('--disable-pip-version-check')
 981
 982        if '--target' not in _args and '-t' not in _args and not (not use_uv_pip and _uninstall):
 983            if venv is not None:
 984                vtp = venv_target_path(venv, allow_nonexistent=True, debug=debug)
 985                if not vtp.exists():
 986                    if not init_venv(venv, force=True):
 987                        vtp.mkdir(parents=True, exist_ok=True)
 988                _args += ['--target', venv_target_path(venv, debug=debug).as_posix()]
 989        elif (
 990            '--target' not in _args
 991                and '-t' not in _args
 992                and not inside_venv()
 993                and not _uninstall
 994                and not use_uv_pip
 995        ):
 996            _args.append('--user')
 997
 998        if venv is None and '--break-system-packages' not in _args:
 999            _args.append('--break-system-packages')
1000
1001        if debug:
1002            if '-v' not in _args or '-vv' not in _args or '-vvv' not in _args:
1003                if use_uv_pip:
1004                    _args.append('--verbose')
1005        else:
1006            if '-q' not in _args or '-qq' not in _args or '-qqq' not in _args:
1007                pass
1008
1009        _packages = [
1010            (
1011                get_install_no_version(install_name)
1012                if _uninstall or install_name.startswith(_MRSM_PACKAGE_ARCHIVES_PREFIX)
1013                else install_name
1014            )
1015            for install_name in install_names
1016        ]
1017        msg = "Installing packages:" if not _uninstall else "Uninstalling packages:"
1018        for p in _packages:
1019            msg += f'\n  - {p}'
1020        if not silent:
1021            print(msg)
1022
1023        if _uninstall and not _from_completely_uninstall and not use_uv_pip:
1024            for install_name in _packages:
1025                _install_no_version = get_install_no_version(install_name)
1026                if _install_no_version in ('pip', 'wheel', 'uv'):
1027                    continue
1028                if not completely_uninstall_package(
1029                    _install_no_version,
1030                    venv=venv,
1031                    debug=debug,
1032                ) and not silent:
1033                    warn(
1034                        f"Failed to clean up package '{_install_no_version}'.",
1035                    )
1036
1037        ### NOTE: Only append the `--prerelease=allow` flag if we explicitly depend on a prerelease.
1038        if use_uv_pip:
1039            _args.insert(0, 'pip')
1040            if not _uninstall and get_prerelease_dependencies(_packages):
1041                _args.append('--prerelease=allow')
1042
1043        rc = run_python_package(
1044            ('pip' if not use_uv_pip else 'uv'),
1045            _args + _packages,
1046            venv=pip_venv,
1047            env=_get_pip_os_env(color=color),
1048            debug=debug,
1049        )
1050        if debug:
1051            print(f"{rc=}")
1052        success = rc == 0
1053
1054    msg = (
1055        "Successfully " + ('un' if _uninstall else '') + "installed packages." if success 
1056        else "Failed to " + ('un' if _uninstall else '') + "install packages."
1057    )
1058    if not silent:
1059        print(msg)
1060    if debug and not silent:
1061        print('pip ' + ('un' if _uninstall else '') + 'install returned:', success)
1062    return success

Install packages from PyPI with pip.

Parameters
  • *install_names (str): The installation names of packages to be installed. This includes version restrictions. Use _import_to_install_name() to get the predefined install_name for a package from its import name.
  • args (Optional[List[str]], default None): A list of command line arguments to pass to pip. If not provided, default to ['--upgrade'] if _uninstall is False, else [].
  • requirements_file_path (Optional[pathlib.Path, str], default None): If provided, append ['-r', '/path/to/requirements.txt'] to args.
  • venv (str, default 'mrsm'): The virtual environment to install into.
  • split (bool, default False): If True, split on periods and only install the root package name.
  • check_update (bool, default True): If True, check if the package requires an update.
  • check_pypi (bool, default True): If True and check_update is True, check PyPI for the latest version.
  • check_wheel (bool, default True): If True, check if wheel is available.
  • _uninstall (bool, default False): If True, uninstall packages instead.
  • color (bool, default True): If True, include color in debug text.
  • silent (bool, default False): If True, skip printing messages.
  • debug (bool, default False): Verbosity toggle.
Returns
  • A bool indicating success.
def get_prerelease_dependencies(_packages: Optional[List[str]] = None):
1065def get_prerelease_dependencies(_packages: Optional[List[str]] = None):
1066    """
1067    Return a list of explicitly prerelease dependencies from a list of packages.
1068    """
1069    if _packages is None:
1070        _packages = list(all_packages.keys())
1071    prelrease_strings = ['dev', 'rc', 'a']
1072    prerelease_packages = []
1073    for install_name in _packages:
1074        _install_no_version = get_install_no_version(install_name)
1075        import_name = _install_to_import_name(install_name)
1076        install_with_version = _import_to_install_name(import_name)
1077        version_only = (
1078            install_with_version.lower().replace(_install_no_version.lower(), '')
1079            .split(']')[-1]
1080        )
1081
1082        is_prerelease = False
1083        for prelrease_string in prelrease_strings:
1084            if prelrease_string in version_only:
1085                is_prerelease = True
1086
1087        if is_prerelease:
1088            prerelease_packages.append(install_name)
1089    return prerelease_packages

Return a list of explicitly prerelease dependencies from a list of packages.

def completely_uninstall_package(install_name: str, venv: str = 'mrsm', debug: bool = False) -> bool:
1092def completely_uninstall_package(
1093    install_name: str,
1094    venv: str = 'mrsm',
1095    debug: bool = False,
1096) -> bool:
1097    """
1098    Continue calling `pip uninstall` until a package is completely
1099    removed from a virtual environment. 
1100    This is useful for dealing with multiple installed versions of a package.
1101    """
1102    attempts = 0
1103    _install_no_version = get_install_no_version(install_name)
1104    clean_install_no_version = _install_no_version.lower().replace('-', '_')
1105    installed_versions = []
1106    vtp = venv_target_path(venv, allow_nonexistent=True, debug=debug)
1107    if not vtp.exists():
1108        return True
1109
1110    for file_name in os.listdir(vtp):
1111        if not file_name.endswith('.dist-info'):
1112            continue
1113        clean_dist_info = file_name.replace('-', '_').lower()
1114        if not clean_dist_info.startswith(clean_install_no_version):
1115            continue
1116        installed_versions.append(file_name)
1117
1118    max_attempts = len(installed_versions)
1119    while attempts < max_attempts:
1120        if not venv_contains_package(
1121            _install_to_import_name(_install_no_version),
1122            venv=venv, debug=debug,
1123        ):
1124            return True
1125        if not pip_uninstall(
1126            _install_no_version,
1127            venv = venv,
1128            silent = (not debug),
1129            _from_completely_uninstall = True,
1130            debug = debug,
1131        ):
1132            return False
1133        attempts += 1
1134    return False

Continue calling pip uninstall until a package is completely removed from a virtual environment. This is useful for dealing with multiple installed versions of a package.

def pip_uninstall(*args, **kw) -> bool:
1137def pip_uninstall(
1138    *args, **kw
1139) -> bool:
1140    """
1141    Uninstall Python packages.
1142    This function is a wrapper around `pip_install()` but with `_uninstall` enforced as `True`.
1143    """
1144    return pip_install(*args, _uninstall=True, **{k: v for k, v in kw.items() if k != '_uninstall'})

Uninstall Python packages. This function is a wrapper around pip_install() but with _uninstall enforced as True.

def run_python_package( package_name: str, args: Optional[List[str]] = None, venv: Optional[str] = 'mrsm', cwd: Optional[str] = None, env: Optional[Dict[str, str]] = None, foreground: bool = False, as_proc: bool = False, capture_output: bool = False, debug: bool = False, **kw: Any) -> Union[int, subprocess.Popen, NoneType]:
1147def run_python_package(
1148    package_name: str,
1149    args: Optional[List[str]] = None,
1150    venv: Optional[str] = 'mrsm',
1151    cwd: Optional[str] = None,
1152    env: Optional[Dict[str, str]] = None,
1153    foreground: bool = False,
1154    as_proc: bool = False,
1155    capture_output: bool = False,
1156    debug: bool = False,
1157    **kw: Any,
1158) -> Union[int, subprocess.Popen, None]:
1159    """
1160    Runs an installed python package.
1161    E.g. Translates to `/usr/bin/python -m [package]`
1162
1163    Parameters
1164    ----------
1165    package_name: str
1166        The Python module to be executed.
1167
1168    args: Optional[List[str]], default None
1169        Additional command line arguments to be appended after `-m [package]`.
1170
1171    venv: Optional[str], default 'mrsm'
1172        If specified, execute the Python interpreter from a virtual environment.
1173
1174    cwd: Optional[str], default None
1175        If specified, change directories before starting the process.
1176        Defaults to `None`.
1177
1178    env: Optional[Dict[str, str]], default None
1179        If specified, only use the provided dictionary for the environment variables.
1180        Defaults to `os.environ`.
1181
1182    as_proc: bool, default False
1183        If `True`, return a `subprocess.Popen` object.
1184
1185    capture_output: bool, default False
1186        If `as_proc` is `True`, capture stdout and stderr.
1187
1188    foreground: bool, default False
1189        If `True`, start the subprocess as a foreground process.
1190        Defaults to `False`.
1191
1192    kw: Any
1193        Additional keyword arguments to pass to `meerschaum.utils.process.run_process()`
1194        and by extension `subprocess.Popen()`.
1195
1196    Returns
1197    -------
1198    Either a return code integer or a `subprocess.Popen` object
1199    (or `None` if a `KeyboardInterrupt` occurs and as_proc is `True`).
1200    """
1201    import sys
1202    import subprocess
1203    import traceback
1204    from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
1205    from meerschaum.utils.process import run_process
1206    from meerschaum.utils.warnings import warn
1207    if args is None:
1208        args = []
1209    old_cwd = os.getcwd()
1210    if cwd is not None:
1211        os.chdir(cwd)
1212    executable = venv_executable(venv=venv)
1213    venv_path = (VIRTENV_RESOURCES_PATH / venv) if venv is not None else None
1214    env_dict = (env if isinstance(env, dict) else (os.environ or {})).copy()
1215    if venv_path is not None:
1216        env_dict.update({'VIRTUAL_ENV': venv_path.as_posix()})
1217    command = [executable, '-m', str(package_name)] + [str(a) for a in args]
1218    if debug:
1219        print(command, file=sys.stderr)
1220    try:
1221        to_return = run_process(
1222            command,
1223            foreground=foreground,
1224            as_proc=as_proc,
1225            capture_output=capture_output,
1226            **kw
1227        )
1228    except Exception:
1229        msg = f"Failed to execute {command}, will try again:\n{traceback.format_exc()}"
1230        warn(msg, color=False)
1231        stdout, stderr = (
1232            (None, None)
1233            if not capture_output
1234            else (subprocess.PIPE, subprocess.PIPE)
1235        )
1236        proc = subprocess.Popen(
1237            command,
1238            stdout=stdout,
1239            stderr=stderr,
1240            stdin=sys.stdin,
1241            env=env_dict,
1242        )
1243        to_return = proc if as_proc else proc.wait()
1244    except KeyboardInterrupt:
1245        to_return = 1 if not as_proc else None
1246    os.chdir(old_cwd)
1247    return to_return

Runs an installed python package. E.g. Translates to /usr/bin/python -m [package]

Parameters
  • package_name (str): The Python module to be executed.
  • args (Optional[List[str]], default None): Additional command line arguments to be appended after -m [package].
  • venv (Optional[str], default 'mrsm'): If specified, execute the Python interpreter from a virtual environment.
  • cwd (Optional[str], default None): If specified, change directories before starting the process. Defaults to None.
  • env (Optional[Dict[str, str]], default None): If specified, only use the provided dictionary for the environment variables. Defaults to os.environ.
  • as_proc (bool, default False): If True, return a subprocess.Popen object.
  • capture_output (bool, default False): If as_proc is True, capture stdout and stderr.
  • foreground (bool, default False): If True, start the subprocess as a foreground process. Defaults to False.
  • kw (Any): Additional keyword arguments to pass to meerschaum.utils.process.run_process() and by extension subprocess.Popen().
Returns
  • Either a return code integer or a subprocess.Popen object
  • (or None if a KeyboardInterrupt occurs and as_proc is True).
def attempt_import( *names: str, lazy: bool = True, warn: bool = True, install: bool = True, venv: Optional[str] = 'mrsm', precheck: bool = True, split: bool = True, check_update: bool = False, check_pypi: bool = False, check_is_installed: bool = True, allow_outside_venv: bool = True, color: bool = True, debug: bool = False) -> Any:
1250def attempt_import(
1251    *names: str,
1252    lazy: bool = True,
1253    warn: bool = True,
1254    install: bool = True,
1255    venv: Optional[str] = 'mrsm',
1256    precheck: bool = True,
1257    split: bool = True,
1258    check_update: bool = False,
1259    check_pypi: bool = False,
1260    check_is_installed: bool = True,
1261    allow_outside_venv: bool = True,
1262    color: bool = True,
1263    debug: bool = False
1264) -> Any:
1265    """
1266    Raise a warning if packages are not installed; otherwise import and return modules.
1267    If `lazy` is `True`, return lazy-imported modules.
1268    
1269    Returns tuple of modules if multiple names are provided, else returns one module.
1270    
1271    Parameters
1272    ----------
1273    names: List[str]
1274        The packages to be imported.
1275
1276    lazy: bool, default True
1277        If `True`, lazily load packages.
1278
1279    warn: bool, default True
1280        If `True`, raise a warning if a package cannot be imported.
1281
1282    install: bool, default True
1283        If `True`, attempt to install a missing package into the designated virtual environment.
1284        If `check_update` is True, install updates if available.
1285
1286    venv: Optional[str], default 'mrsm'
1287        The virtual environment in which to search for packages and to install packages into.
1288
1289    precheck: bool, default True
1290        If `True`, attempt to find module before importing (necessary for checking if modules exist
1291        and retaining lazy imports), otherwise assume lazy is `False`.
1292
1293    split: bool, default True
1294        If `True`, split packages' names on `'.'`.
1295
1296    check_update: bool, default False
1297        If `True` and `install` is `True`, install updates if the required minimum version
1298        does not match.
1299
1300    check_pypi: bool, default False
1301        If `True` and `check_update` is `True`, check PyPI when determining whether
1302        an update is required.
1303
1304    check_is_installed: bool, default True
1305        If `True`, check if the package is contained in the virtual environment.
1306
1307    allow_outside_venv: bool, default True
1308        If `True`, search outside of the specified virtual environment
1309        if the package cannot be found.
1310        Setting to `False` will reinstall the package into a virtual environment, even if it
1311        is installed outside.
1312
1313    color: bool, default True
1314        If `False`, do not print ANSI colors.
1315
1316    Returns
1317    -------
1318    The specified modules. If they're not available and `install` is `True`, it will first
1319    download them into a virtual environment and return the modules.
1320
1321    Examples
1322    --------
1323    >>> pandas, sqlalchemy = attempt_import('pandas', 'sqlalchemy')
1324    >>> pandas = attempt_import('pandas')
1325
1326    """
1327
1328    import importlib.util
1329
1330    ### to prevent recursion, check if parent Meerschaum package is being imported
1331    if names == ('meerschaum',):
1332        return _import_module('meerschaum')
1333
1334    if venv == 'mrsm' and _import_hook_venv is not None:
1335        if debug:
1336            print(f"Import hook for virtual environment '{_import_hook_venv}' is active.")
1337        venv = _import_hook_venv
1338
1339    _warnings = _import_module('meerschaum.utils.warnings')
1340    warn_function = _warnings.warn
1341
1342    def do_import(_name: str, **kw) -> Union['ModuleType', None]:
1343        with Venv(venv=venv, debug=debug):
1344            ### determine the import method (lazy vs normal)
1345            from meerschaum.utils.misc import filter_keywords
1346            import_method = (
1347                _import_module if not lazy
1348                else lazy_import
1349            )
1350            try:
1351                mod = import_method(_name, **(filter_keywords(import_method, **kw)))
1352            except Exception as e:
1353                if warn:
1354                    import traceback
1355                    traceback.print_exception(type(e), e, e.__traceback__)
1356                    warn_function(
1357                        f"Failed to import module '{_name}'.\nException:\n{e}",
1358                        ImportWarning,
1359                        stacklevel = (5 if lazy else 4),
1360                        color = False,
1361                    )
1362                mod = None
1363        return mod
1364
1365    modules = []
1366    for name in names:
1367        ### Check if package is a declared dependency.
1368        root_name = name.split('.')[0] if split else name
1369        install_name = _import_to_install_name(root_name)
1370
1371        if install_name is None:
1372            install_name = root_name
1373            if warn and root_name != 'plugins':
1374                warn_function(
1375                    f"Package '{root_name}' is not declared in meerschaum.utils.packages.",
1376                    ImportWarning,
1377                    stacklevel = 3,
1378                    color = False
1379                )
1380
1381        ### Determine if the package exists.
1382        if precheck is False:
1383            found_module = (
1384                do_import(
1385                    name, debug=debug, warn=False, venv=venv, color=color,
1386                    check_update=False, check_pypi=False, split=split,
1387                ) is not None
1388            )
1389        else:
1390            if check_is_installed:
1391                with _locks['_is_installed_first_check']:
1392                    if not _is_installed_first_check.get(name, False):
1393                        package_is_installed = is_installed(
1394                            name,
1395                            venv = venv,
1396                            split = split,
1397                            allow_outside_venv = allow_outside_venv,
1398                            debug = debug,
1399                        )
1400                        _is_installed_first_check[name] = package_is_installed
1401                    else:
1402                        package_is_installed = _is_installed_first_check[name]
1403            else:
1404                package_is_installed = _is_installed_first_check.get(
1405                    name,
1406                    venv_contains_package(name, venv=venv, split=split, debug=debug)
1407                )
1408            found_module = package_is_installed
1409
1410        if not found_module:
1411            if install:
1412                if not pip_install(
1413                    install_name,
1414                    venv = venv,
1415                    split = False,
1416                    check_update = check_update,
1417                    color = color,
1418                    debug = debug
1419                ) and warn:
1420                    warn_function(
1421                        f"Failed to install '{install_name}'.",
1422                        ImportWarning,
1423                        stacklevel = 3,
1424                        color = False,
1425                    )
1426            elif warn:
1427                ### Raise a warning if we can't find the package and install = False.
1428                warn_function(
1429                    (f"\n\nMissing package '{name}' from virtual environment '{venv}'; "
1430                     + "some features will not work correctly."
1431                     + "\n\nSet install=True when calling attempt_import.\n"),
1432                    ImportWarning,
1433                    stacklevel = 3,
1434                    color = False,
1435                )
1436
1437        ### Do the import. Will be lazy if lazy=True.
1438        m = do_import(
1439            name, debug=debug, warn=warn, venv=venv, color=color,
1440            check_update=check_update, check_pypi=check_pypi, install=install, split=split,
1441        )
1442        modules.append(m)
1443
1444    modules = tuple(modules)
1445    if len(modules) == 1:
1446        return modules[0]
1447    return modules

Raise a warning if packages are not installed; otherwise import and return modules. If lazy is True, return lazy-imported modules.

Returns tuple of modules if multiple names are provided, else returns one module.

Parameters
  • names (List[str]): The packages to be imported.
  • lazy (bool, default True): If True, lazily load packages.
  • warn (bool, default True): If True, raise a warning if a package cannot be imported.
  • install (bool, default True): If True, attempt to install a missing package into the designated virtual environment. If check_update is True, install updates if available.
  • venv (Optional[str], default 'mrsm'): The virtual environment in which to search for packages and to install packages into.
  • precheck (bool, default True): If True, attempt to find module before importing (necessary for checking if modules exist and retaining lazy imports), otherwise assume lazy is False.
  • split (bool, default True): If True, split packages' names on '.'.
  • check_update (bool, default False): If True and install is True, install updates if the required minimum version does not match.
  • check_pypi (bool, default False): If True and check_update is True, check PyPI when determining whether an update is required.
  • check_is_installed (bool, default True): If True, check if the package is contained in the virtual environment.
  • allow_outside_venv (bool, default True): If True, search outside of the specified virtual environment if the package cannot be found. Setting to False will reinstall the package into a virtual environment, even if it is installed outside.
  • color (bool, default True): If False, do not print ANSI colors.
Returns
  • The specified modules. If they're not available and install is True, it will first
  • download them into a virtual environment and return the modules.
Examples
>>> pandas, sqlalchemy = attempt_import('pandas', 'sqlalchemy')
>>> pandas = attempt_import('pandas')
def lazy_import( name: str, local_name: str = None, **kw) -> meerschaum.utils.packages.lazy_loader.LazyLoader:
1450def lazy_import(
1451    name: str,
1452    local_name: str = None,
1453    **kw
1454) -> meerschaum.utils.packages.lazy_loader.LazyLoader:
1455    """
1456    Lazily import a package.
1457    """
1458    from meerschaum.utils.packages.lazy_loader import LazyLoader
1459    if local_name is None:
1460        local_name = name
1461    return LazyLoader(
1462        local_name,
1463        globals(),
1464        name,
1465        **kw
1466    )

Lazily import a package.

def pandas_name() -> str:
1469def pandas_name() -> str:
1470    """
1471    Return the configured name for `pandas`.
1472    
1473    Below are the expected possible values:
1474
1475    - 'pandas'
1476    - 'modin.pandas'
1477    - 'dask.dataframe'
1478
1479    """
1480    from meerschaum.config import get_config
1481    pandas_module_name = get_config('system', 'connectors', 'all', 'pandas', patch=True)
1482    if pandas_module_name == 'modin':
1483        pandas_module_name = 'modin.pandas'
1484    elif pandas_module_name == 'dask':
1485        pandas_module_name = 'dask.dataframe'
1486
1487    return pandas_module_name

Return the configured name for pandas.

Below are the expected possible values:

  • 'pandas'
  • 'modin.pandas'
  • 'dask.dataframe'
emitted_pandas_warning: bool = False
def import_pandas(debug: bool = False, lazy: bool = False, **kw) -> "'ModuleType'":
1491def import_pandas(
1492    debug: bool = False,
1493    lazy: bool = False,
1494    **kw
1495) -> 'ModuleType':
1496    """
1497    Quality-of-life function to attempt to import the configured version of `pandas`.
1498    """
1499    pandas_module_name = pandas_name()
1500    global emitted_pandas_warning
1501
1502    if pandas_module_name != 'pandas':
1503        with _locks['emitted_pandas_warning']:
1504            if not emitted_pandas_warning:
1505                from meerschaum.utils.warnings import warn
1506                emitted_pandas_warning = True
1507                warn(
1508                    (
1509                        "You are using an alternative Pandas implementation "
1510                        + f"'{pandas_module_name}'"
1511                        + "\n   Features may not work as expected."
1512                    ),
1513                    stack=False,
1514                )
1515
1516    pytz = attempt_import('pytz', debug=debug, lazy=False, **kw)
1517    pandas, pyarrow = attempt_import('pandas', 'pyarrow', debug=debug, lazy=False, **kw)
1518    pd = attempt_import(pandas_module_name, debug=debug, lazy=lazy, **kw)
1519    return pd

Quality-of-life function to attempt to import the configured version of pandas.

def import_rich(lazy: bool = True, debug: bool = False, **kw: Any) -> "'ModuleType'":
1522def import_rich(
1523    lazy: bool = True,
1524    debug: bool = False,
1525    **kw: Any
1526) -> 'ModuleType':
1527    """
1528    Quality of life function for importing `rich`.
1529    """
1530    from meerschaum.utils.formatting import ANSI, UNICODE
1531    ## need typing_extensions for `from rich import box`
1532    typing_extensions = attempt_import(
1533        'typing_extensions', lazy=False, debug=debug
1534    )
1535    pygments = attempt_import(
1536        'pygments', lazy=False,
1537    )
1538    rich = attempt_import(
1539        'rich', lazy=lazy,
1540        **kw
1541    )
1542    return rich

Quality of life function for importing rich.

def import_dcc(warn=False, **kw) -> "'ModuleType'":
1556def import_dcc(warn=False, **kw) -> 'ModuleType':
1557    """
1558    Import Dash Core Components (`dcc`).
1559    """
1560    return (
1561        attempt_import('dash_core_components', warn=warn, **kw)
1562        if _dash_less_than_2(warn=warn, **kw) else attempt_import('dash.dcc', warn=warn, **kw)
1563    )

Import Dash Core Components (dcc).

def import_html(warn=False, **kw) -> "'ModuleType'":
1566def import_html(warn=False, **kw) -> 'ModuleType':
1567    """
1568    Import Dash HTML Components (`html`).
1569    """
1570    return (
1571        attempt_import('dash_html_components', warn=warn, **kw)
1572        if _dash_less_than_2(warn=warn, **kw)
1573        else attempt_import('dash.html', warn=warn, **kw)
1574    )

Import Dash HTML Components (html).

def get_modules_from_package( package: "'package'", names: bool = False, recursive: bool = False, lazy: bool = False, modules_venvs: bool = False, debug: bool = False):
1577def get_modules_from_package(
1578    package: 'package',
1579    names: bool = False,
1580    recursive: bool = False,
1581    lazy: bool = False,
1582    modules_venvs: bool = False,
1583    debug: bool = False
1584):
1585    """
1586    Find and import all modules in a package.
1587    
1588    Returns
1589    -------
1590    Either list of modules or tuple of lists.
1591    """
1592    from os.path import dirname, join, isfile, isdir, basename
1593    import glob
1594
1595    pattern = '*' if recursive else '*.py'
1596    package_path = dirname(package.__file__ or package.__path__[0])
1597    module_names = glob.glob(join(package_path, pattern), recursive=recursive)
1598    _all = [
1599        basename(f)[:-3] if isfile(f) else basename(f)
1600        for f in module_names
1601            if ((isfile(f) and f.endswith('.py')) or isdir(f))
1602               and not f.endswith('__init__.py')
1603               and not f.endswith('__pycache__')
1604    ]
1605
1606    if debug:
1607        from meerschaum.utils.debug import dprint
1608        dprint(str(_all))
1609    modules = []
1610    for module_name in [package.__name__ + "." + mod_name for mod_name in _all]:
1611        ### there's probably a better way than a try: catch but it'll do for now
1612        try:
1613            ### if specified, activate the module's virtual environment before importing.
1614            ### NOTE: this only considers the filename, so two modules from different packages
1615            ### may end up sharing virtual environments.
1616            if modules_venvs:
1617                activate_venv(module_name.split('.')[-1], debug=debug)
1618            m = lazy_import(module_name, debug=debug) if lazy else _import_module(module_name)
1619            modules.append(m)
1620        except Exception as e:
1621            if debug:
1622                dprint(str(e))
1623        finally:
1624            if modules_venvs:
1625                deactivate_venv(module_name.split('.')[-1], debug=debug)
1626    if names:
1627        return _all, modules
1628
1629    return modules

Find and import all modules in a package.

Returns
  • Either list of modules or tuple of lists.
def import_children( package: "Optional['ModuleType']" = None, package_name: Optional[str] = None, types: Optional[List[str]] = None, lazy: bool = True, recursive: bool = False, debug: bool = False) -> "List['ModuleType']":
1632def import_children(
1633    package: Optional['ModuleType'] = None,
1634    package_name: Optional[str] = None,
1635    types : Optional[List[str]] = None,
1636    lazy: bool = True,
1637    recursive: bool = False,
1638    debug: bool = False
1639) -> List['ModuleType']:
1640    """
1641    Import all functions in a package to its `__init__`.
1642
1643    Parameters
1644    ----------
1645    package: Optional[ModuleType], default None
1646        Package to import its functions into.
1647        If `None` (default), use parent.
1648
1649    package_name: Optional[str], default None
1650        Name of package to import its functions into
1651        If None (default), use parent.
1652
1653    types: Optional[List[str]], default None
1654        Types of members to return.
1655        Defaults are `['method', 'builtin', 'class', 'function', 'package', 'module']`
1656
1657    Returns
1658    -------
1659    A list of modules.
1660    """
1661    import sys, inspect
1662
1663    if types is None:
1664        types = ['method', 'builtin', 'function', 'class', 'module']
1665
1666    ### if package_name and package are None, use parent
1667    if package is None and package_name is None:
1668        package_name = inspect.stack()[1][0].f_globals['__name__']
1669
1670    ### populate package or package_name from other other
1671    if package is None:
1672        package = sys.modules[package_name]
1673    elif package_name is None:
1674        package_name = package.__name__
1675
1676    ### Set attributes in sys module version of package.
1677    ### Kinda like setting a dictionary
1678    ###   functions[name] = func
1679    modules = get_modules_from_package(package, recursive=recursive, lazy=lazy, debug=debug)
1680    _all, members = [], []
1681    objects = []
1682    for module in modules:
1683        _objects = []
1684        for ob in inspect.getmembers(module):
1685            for t in types:
1686                ### ob is a tuple of (name, object)
1687                if getattr(inspect, 'is' + t)(ob[1]):
1688                    _objects.append(ob)
1689
1690        if 'module' in types:
1691            _objects.append((module.__name__.split('.')[0], module))
1692        objects += _objects
1693    for ob in objects:
1694        setattr(sys.modules[package_name], ob[0], ob[1])
1695        _all.append(ob[0])
1696        members.append(ob[1])
1697
1698    if debug:
1699        from meerschaum.utils.debug import dprint
1700        dprint(str(_all))
1701    ### set __all__ for import *
1702    setattr(sys.modules[package_name], '__all__', _all)
1703    return members

Import all functions in a package to its __init__.

Parameters
  • package (Optional[ModuleType], default None): Package to import its functions into. If None (default), use parent.
  • package_name (Optional[str], default None): Name of package to import its functions into If None (default), use parent.
  • types (Optional[List[str]], default None): Types of members to return. Defaults are ['method', 'builtin', 'class', 'function', 'package', 'module']
Returns
  • A list of modules.
def reload_package( package: str, skip_submodules: Optional[List[str]] = None, lazy: bool = False, debug: bool = False, **kw: Any):
1707def reload_package(
1708    package: str,
1709    skip_submodules: Optional[List[str]] = None,
1710    lazy: bool = False,
1711    debug: bool = False,
1712    **kw: Any
1713):
1714    """
1715    Recursively load a package's subpackages, even if they were not previously loaded.
1716    """
1717    import sys
1718    if isinstance(package, str):
1719        package_name = package
1720    else:
1721        try:
1722            package_name = package.__name__
1723        except Exception as e:
1724            package_name = str(package)
1725
1726    skip_submodules = skip_submodules or []
1727    if 'meerschaum.utils.packages' not in skip_submodules:
1728        skip_submodules.append('meerschaum.utils.packages')
1729    def safeimport():
1730        subs = [
1731            m for m in sys.modules
1732            if m.startswith(package_name + '.')
1733        ]
1734        subs_to_skip = []
1735        for skip_mod in skip_submodules:
1736            for mod in subs:
1737                if mod.startswith(skip_mod):
1738                    subs_to_skip.append(mod)
1739                    continue
1740
1741        subs = [m for m in subs if m not in subs_to_skip]
1742        for module_name in subs:
1743            _reload_module_cache[module_name] = sys.modules.pop(module_name, None)
1744        if not subs_to_skip:
1745            _reload_module_cache[package_name] = sys.modules.pop(package_name, None)
1746
1747        return _import_module(package_name)
1748
1749    return safeimport()

Recursively load a package's subpackages, even if they were not previously loaded.

def reload_meerschaum(debug: bool = False) -> Tuple[bool, str]:
1752def reload_meerschaum(debug: bool = False) -> SuccessTuple:
1753    """
1754    Reload the currently loaded Meercshaum modules, refreshing plugins and shell configuration.
1755    """
1756    reload_package(
1757        'meerschaum',
1758        skip_submodules = [
1759            'meerschaum._internal.shell',
1760            'meerschaum.utils.pool',
1761        ]
1762    )
1763
1764    from meerschaum.plugins import reload_plugins
1765    from meerschaum._internal.shell.Shell import _insert_shell_actions
1766    reload_plugins(debug=debug)
1767    _insert_shell_actions()
1768    return True, "Success"

Reload the currently loaded Meercshaum modules, refreshing plugins and shell configuration.

def is_installed( import_name: str, venv: Optional[str] = 'mrsm', split: bool = True, allow_outside_venv: bool = True, debug: bool = False) -> bool:
1771def is_installed(
1772    import_name: str,
1773    venv: Optional[str] = 'mrsm',
1774    split: bool = True,
1775    allow_outside_venv: bool = True,
1776    debug: bool = False,
1777) -> bool:
1778    """
1779    Check whether a package is installed.
1780
1781    Parameters
1782    ----------
1783    import_name: str
1784        The import name of the module.
1785
1786    venv: Optional[str], default 'mrsm'
1787        The venv in which to search for the module.
1788
1789    split: bool, default True
1790        If `True`, split on periods to determine the root module name.
1791
1792    allow_outside_venv: bool, default True
1793        If `True`, search outside of the specified virtual environment
1794        if the package cannot be found.
1795
1796    Returns
1797    -------
1798    A bool indicating whether a package may be imported.
1799    """
1800    if debug:
1801        from meerschaum.utils.debug import dprint
1802    root_name = import_name.split('.')[0] if split else import_name
1803    import importlib.util
1804    with Venv(venv, debug=debug):
1805        try:
1806            spec_path = pathlib.Path(
1807                get_module_path(root_name, venv=venv, debug=debug)
1808                or
1809                (
1810                    importlib.util.find_spec(root_name).origin 
1811                    if venv is not None and allow_outside_venv
1812                    else None
1813                )
1814            )
1815        except (ModuleNotFoundError, ValueError, AttributeError, TypeError) as e:
1816            spec_path = None
1817
1818        found = (
1819            not need_update(
1820                None,
1821                import_name=root_name,
1822                _run_determine_version=False,
1823                check_pypi=False,
1824                version=determine_version(
1825                    spec_path,
1826                    venv=venv,
1827                    debug=debug,
1828                    import_name=root_name,
1829                ),
1830                debug=debug,
1831            )
1832        ) if spec_path is not None else False
1833
1834    return found

Check whether a package is installed.

Parameters
  • import_name (str): The import name of the module.
  • venv (Optional[str], default 'mrsm'): The venv in which to search for the module.
  • split (bool, default True): If True, split on periods to determine the root module name.
  • allow_outside_venv (bool, default True): If True, search outside of the specified virtual environment if the package cannot be found.
Returns
  • A bool indicating whether a package may be imported.
def venv_contains_package( import_name: str, venv: Optional[str] = 'mrsm', split: bool = True, debug: bool = False) -> bool:
1837def venv_contains_package(
1838    import_name: str,
1839    venv: Optional[str] = 'mrsm',
1840    split: bool = True,
1841    debug: bool = False,
1842) -> bool:
1843    """
1844    Search the contents of a virtual environment for a package.
1845    """
1846    import site
1847    import pathlib
1848    root_name = import_name.split('.')[0] if split else import_name
1849    return get_module_path(root_name, venv=venv, debug=debug) is not None

Search the contents of a virtual environment for a package.

def package_venv(package: "'ModuleType'") -> Optional[str]:
1852def package_venv(package: 'ModuleType') -> Union[str, None]:
1853    """
1854    Inspect a package and return the virtual environment in which it presides.
1855    """
1856    import os
1857    from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
1858    if str(VIRTENV_RESOURCES_PATH) not in package.__file__:
1859        return None
1860    return package.__file__.split(str(VIRTENV_RESOURCES_PATH))[1].split(os.path.sep)[1]

Inspect a package and return the virtual environment in which it presides.

def ensure_readline() -> "'ModuleType'":
1863def ensure_readline() -> 'ModuleType':
1864    """Make sure that the `readline` package is able to be imported."""
1865    import sys
1866    try:
1867        import readline
1868    except ImportError:
1869        readline = None
1870
1871    if readline is None:
1872        import platform
1873        rl_name = "gnureadline" if platform.system() != 'Windows' else "pyreadline3"
1874        try:
1875            rl = attempt_import(
1876                rl_name,
1877                lazy=False,
1878                install=True,
1879                venv=None,
1880                warn=False,
1881            )
1882        except (ImportError, ModuleNotFoundError):
1883            if not pip_install(rl_name, args=['--upgrade', '--ignore-installed'], venv=None):
1884                print(f"Unable to import {rl_name}!", file=sys.stderr)
1885                sys.exit(1)
1886
1887    sys.modules['readline'] = readline
1888    return readline

Make sure that the readline package is able to be imported.

def use_uv() -> bool:
1912def use_uv() -> bool:
1913    """
1914    Return whether `uv` is available and enabled.
1915    """
1916    from meerschaum.utils.misc import is_android
1917    if is_android():
1918        return False
1919
1920    if not is_uv_enabled():
1921        return False
1922
1923    try:
1924        import uv
1925        uv_bin = uv.find_uv_bin()
1926    except (ImportError, FileNotFoundError):
1927        uv_bin = None
1928
1929    if uv_bin is None:
1930        return False
1931
1932    return True

Return whether uv is available and enabled.

def is_uv_enabled() -> bool:
1935def is_uv_enabled() -> bool:
1936    """
1937    Return whether the user has disabled `uv`.
1938    """
1939    from meerschaum.utils.misc import is_android
1940    if is_android():
1941        return False
1942
1943    try:
1944        import yaml
1945    except ImportError:
1946        return False
1947
1948    from meerschaum.config import get_config
1949    enabled = get_config('system', 'experimental', 'uv_pip')
1950    return enabled

Return whether the user has disabled uv.