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

Strip the version information from the install name.

import_versions = {'mrsm': {'daemon': '3.1.0', 'packaging': '24.2', 'semver': '3.0.2', 'pandas': '2.2.3', 'prompt_toolkit': '3.0.48', 'dask': '2024.12.1'}}
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]:
340def determine_version(
341    path: pathlib.Path,
342    import_name: Optional[str] = None,
343    venv: Optional[str] = 'mrsm',
344    search_for_metadata: bool = True,
345    split: bool = True,
346    warn: bool = False,
347    debug: bool = False,
348) -> Union[str, None]:
349    """
350    Determine a module's `__version__` string from its filepath.
351    
352    First it searches for pip metadata, then it attempts to import the module in a subprocess.
353
354    Parameters
355    ----------
356    path: pathlib.Path
357        The file path of the module.
358
359    import_name: Optional[str], default None
360        The name of the module. If omitted, it will be determined from the file path.
361        Defaults to `None`.
362
363    venv: Optional[str], default 'mrsm'
364        The virtual environment of the Python interpreter to use if importing is necessary.
365
366    search_for_metadata: bool, default True
367        If `True`, search the pip site_packages directory (assumed to be the parent)
368        for the corresponding dist-info directory.
369
370    warn: bool, default True
371        If `True`, raise a warning if the module fails to import in the subprocess.
372
373    split: bool, default True
374        If `True`, split the determined import name by periods to get the room name.
375
376    Returns
377    -------
378    The package's version string if available or `None`.
379    If multiple versions are found, it will trigger an import in a subprocess.
380
381    """
382    with _locks['import_versions']:
383        if venv not in import_versions:
384            import_versions[venv] = {}
385    import os
386    old_cwd = os.getcwd()
387    from meerschaum.utils.warnings import warn as warn_function
388    if import_name is None:
389        import_name = path.parent.stem if path.stem == '__init__' else path.stem
390        import_name = import_name.split('.')[0] if split else import_name
391    if import_name in import_versions[venv]:
392        return import_versions[venv][import_name]
393    _version = None
394    module_parent_dir = (
395        path.parent.parent if path.stem == '__init__' else path.parent
396    ) if path is not None else venv_target_path(venv, allow_nonexistent=True, debug=debug)
397
398    if not module_parent_dir.exists():
399        return None
400
401    installed_dir_name = _import_to_dir_name(import_name)
402    clean_installed_dir_name = installed_dir_name.lower().replace('-', '_')
403
404    ### First, check if a dist-info directory exists.
405    _found_versions = []
406    if search_for_metadata:
407        try:
408            filenames = os.listdir(module_parent_dir)
409        except FileNotFoundError:
410            filenames = []
411        for filename in filenames:
412            if not filename.endswith('.dist-info'):
413                continue
414            filename_lower = filename.lower()
415            if not filename_lower.startswith(clean_installed_dir_name + '-'):
416                continue
417            _v = filename.replace('.dist-info', '').split("-")[-1]
418            _found_versions.append(_v)
419
420    if len(_found_versions) == 1:
421        _version = _found_versions[0]
422        with _locks['import_versions']:
423            import_versions[venv][import_name] = _version
424        return _found_versions[0]
425
426    if not _found_versions:
427        try:
428            import importlib.metadata as importlib_metadata
429        except ImportError:
430            importlib_metadata = attempt_import(
431                'importlib_metadata',
432                debug=debug, check_update=False, precheck=False,
433                color=False, check_is_installed=False, lazy=False,
434            )
435        try:
436            os.chdir(module_parent_dir)
437            _version = importlib_metadata.metadata(import_name)['Version']
438        except Exception:
439            _version = None
440        finally:
441            os.chdir(old_cwd)
442
443        if _version is not None:
444            with _locks['import_versions']:
445                import_versions[venv][import_name] = _version
446            return _version
447
448    if debug:
449        print(f'Found multiple versions for {import_name}: {_found_versions}')
450
451    module_parent_dir_str = module_parent_dir.as_posix()
452
453    ### Not a pip package, so let's try importing the module directly (in a subprocess).
454    _no_version_str = 'no-version'
455    code = (
456        f"import sys, importlib; sys.path.insert(0, '{module_parent_dir_str}');\n"
457        + f"module = importlib.import_module('{import_name}');\n"
458        + "try:\n"
459        + "  print(module.__version__ , end='')\n"
460        + "except:\n"
461        + f"  print('{_no_version_str}', end='')"
462    )
463    exit_code, stdout_bytes, stderr_bytes = venv_exec(
464        code, venv=venv, with_extras=True, debug=debug
465    )
466    stdout, stderr = stdout_bytes.decode('utf-8'), stderr_bytes.decode('utf-8')
467    _version = stdout.split('\n')[-1] if exit_code == 0 else None
468    _version = _version if _version != _no_version_str else None
469
470    if _version is None:
471        _version = _get_package_metadata(import_name, venv).get('version', None)
472    if _version is None and warn:
473        warn_function(
474            f"Failed to determine a version for '{import_name}':\n{stderr}",
475            stack = False
476        )
477
478    ### If `__version__` doesn't exist, return `None`.
479    import_versions[venv][import_name] = _version
480    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:
535def need_update(
536    package: Optional['ModuleType'] = None,
537    install_name: Optional[str] = None,
538    import_name: Optional[str] = None,
539    version: Optional[str] = None,
540    check_pypi: bool = False,
541    split: bool = True,
542    color: bool = True,
543    debug: bool = False,
544    _run_determine_version: bool = True,
545) -> bool:
546    """
547    Check if a Meerschaum dependency needs an update.
548    Returns a bool for whether or not a package needs to be updated.
549
550    Parameters
551    ----------
552    package: 'ModuleType'
553        The module of the package to be updated.
554
555    install_name: Optional[str], default None
556        If provided, use this string to determine the required version.
557        Otherwise use the install name defined in `meerschaum.utils.packages._packages`.
558
559    import_name:
560        If provided, override the package's `__name__` string.
561
562    version: Optional[str], default None
563        If specified, override the package's `__version__` string.
564
565    check_pypi: bool, default False
566        If `True`, check pypi.org for updates.
567        Defaults to `False`.
568
569    split: bool, default True
570        If `True`, split the module's name on periods to detrive the root name.
571        Defaults to `True`.
572
573    color: bool, default True
574        If `True`, format debug output.
575        Defaults to `True`.
576
577    debug: bool, default True
578        Verbosity toggle.
579
580    Returns
581    -------
582    A bool indicating whether the package requires an update.
583
584    """
585    if debug:
586        from meerschaum.utils.debug import dprint
587    from meerschaum.utils.warnings import warn as warn_function
588    import re
589    root_name = (
590        package.__name__.split('.')[0] if split else package.__name__
591    ) if import_name is None else (
592        import_name.split('.')[0] if split else import_name
593    )
594    install_name = install_name or _import_to_install_name(root_name)
595    with _locks['_checked_for_updates']:
596        if install_name in _checked_for_updates:
597            return False
598        _checked_for_updates.add(install_name)
599
600    _install_no_version = get_install_no_version(install_name)
601    required_version = (
602        install_name
603        .replace(_install_no_version, '')
604    )
605    if ']' in required_version:
606        required_version = required_version.split(']')[1]
607
608    ### No minimum version was specified, and we're not going to check PyPI.
609    if not required_version and not check_pypi:
610        return False
611
612    ### NOTE: Sometimes (rarely), we depend on a development build of a package.
613    if '.dev' in required_version:
614        required_version = required_version.split('.dev')[0]
615    if version and '.dev' in version:
616        version = version.split('.dev')[0]
617
618    try:
619        if not version:
620            if not _run_determine_version:
621                version = determine_version(
622                    pathlib.Path(package.__file__),
623                    import_name=root_name, warn=False, debug=debug
624                )
625        if version is None:
626            return False
627    except Exception as e:
628        if debug:
629            dprint(str(e), color=color)
630            dprint("No version could be determined from the installed package.", color=color)
631        return False
632    split_version = version.split('.')
633    last_part = split_version[-1]
634    if len(split_version) == 2:
635        version = '.'.join(split_version) + '.0'
636    elif 'dev' in last_part or 'rc' in last_part:
637        tag = 'dev' if 'dev' in last_part else 'rc'
638        last_sep = '-'
639        if not last_part.startswith(tag):
640            last_part = f'-{tag}'.join(last_part.split(tag))
641            last_sep = '.'
642        version = '.'.join(split_version[:-1]) + last_sep + last_part
643    elif len(split_version) > 3:
644        version = '.'.join(split_version[:3])
645
646    packaging_version = attempt_import(
647        'packaging.version', check_update=False, lazy=False, debug=debug,
648    )
649
650    ### Get semver if necessary
651    if required_version:
652        semver_path = get_module_path('semver', debug=debug)
653        if semver_path is None:
654            pip_install(_import_to_install_name('semver'), debug=debug)
655        semver = attempt_import('semver', check_update=False, lazy=False, debug=debug)
656    if check_pypi:
657        ### Check PyPI for updates
658        update_checker = attempt_import(
659            'update_checker', lazy=False, check_update=False, debug=debug
660        )
661        checker = update_checker.UpdateChecker()
662        result = checker.check(_install_no_version, version)
663    else:
664        ### Skip PyPI and assume we can't be sure.
665        result = None
666
667    ### Compare PyPI's version with our own.
668    if result is not None:
669        ### We have a result from PyPI and a stated required version.
670        if required_version:
671            try:
672                return semver.Version.parse(result.available_version).match(required_version)
673            except AttributeError as e:
674                pip_install(_import_to_install_name('semver'), venv='mrsm', debug=debug)
675                semver = manually_import_module('semver', venv='mrsm')
676                return semver.Version.parse(version).match(required_version)
677            except Exception as e:
678                if debug:
679                    dprint(f"Failed to match versions with exception:\n{e}", color=color)
680                return False
681
682        ### If `check_pypi` and we don't have a required version, check if PyPI's version
683        ### is newer than the installed version.
684        else:
685            return (
686                packaging_version.parse(result.available_version) > 
687                packaging_version.parse(version)
688            )
689
690    ### We might be depending on a prerelease.
691    ### Sanity check that the required version is not greater than the installed version. 
692    required_version = (
693        required_version.replace(_MRSM_PACKAGE_ARCHIVES_PREFIX, '')
694        .replace(' @ ', '').replace('wheels', '').replace('+mrsm', '').replace('/-', '')
695        .replace('-py3-none-any.whl', '')
696    )
697
698    if 'a' in required_version:
699        required_version = required_version.replace('a', '-pre.').replace('+mrsm', '')
700        version = version.replace('a', '-pre.').replace('+mrsm', '')
701    try:
702        return (
703            (not semver.Version.parse(version).match(required_version))
704            if required_version else False
705        )
706    except AttributeError:
707        pip_install(_import_to_install_name('semver'), venv='mrsm', debug=debug)
708        semver = manually_import_module('semver', venv='mrsm', debug=debug)
709        return (
710            (not semver.Version.parse(version).match(required_version))
711            if required_version else False
712        )
713    except Exception as e:
714        print(f"Unable to parse version ({version}) for package '{import_name}'.")
715        print(e)
716        if debug:
717            dprint(e)
718        return False
719    try:
720        return (
721            packaging_version.parse(version) > 
722            packaging_version.parse(required_version)
723        )
724    except Exception as e:
725        if debug:
726            dprint(e)
727        return False
728    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:
731def get_pip(
732    venv: Optional[str] = 'mrsm',
733    color: bool = True,
734    debug: bool = False,
735) -> bool:
736    """
737    Download and run the get-pip.py script.
738
739    Parameters
740    ----------
741    venv: Optional[str], default 'mrsm'
742        The virtual environment into which to install `pip`.
743
744    color: bool, default True
745        If `True`, force color output.
746
747    debug: bool, default False
748        Verbosity toggle.
749
750    Returns
751    -------
752    A bool indicating success.
753
754    """
755    import sys
756    import subprocess
757    from meerschaum.utils.misc import wget
758    from meerschaum.config._paths import CACHE_RESOURCES_PATH
759    from meerschaum.config.static import STATIC_CONFIG
760    url = STATIC_CONFIG['system']['urls']['get-pip.py']
761    dest = CACHE_RESOURCES_PATH / 'get-pip.py'
762    try:
763        wget(url, dest, color=False, debug=debug)
764    except Exception:
765        print(f"Failed to fetch pip from '{url}'. Please install pip and restart Meerschaum.") 
766        sys.exit(1)
767    if venv is not None:
768        init_venv(venv=venv, debug=debug)
769    cmd_list = [venv_executable(venv=venv), dest.as_posix()] 
770    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:
 773def pip_install(
 774    *install_names: str,
 775    args: Optional[List[str]] = None,
 776    requirements_file_path: Union[pathlib.Path, str, None] = None,
 777    venv: Optional[str] = 'mrsm',
 778    split: bool = False,
 779    check_update: bool = True,
 780    check_pypi: bool = True,
 781    check_wheel: bool = True,
 782    _uninstall: bool = False,
 783    _from_completely_uninstall: bool = False,
 784    _install_uv_pip: bool = True,
 785    _use_uv_pip: bool = True,
 786    color: bool = True,
 787    silent: bool = False,
 788    debug: bool = False,
 789) -> bool:
 790    """
 791    Install packages from PyPI with `pip`.
 792
 793    Parameters
 794    ----------
 795    *install_names: str
 796        The installation names of packages to be installed.
 797        This includes version restrictions.
 798        Use `_import_to_install_name()` to get the predefined `install_name` for a package
 799        from its import name.
 800
 801    args: Optional[List[str]], default None
 802        A list of command line arguments to pass to `pip`.
 803        If not provided, default to `['--upgrade']` if `_uninstall` is `False`, else `[]`.
 804
 805    requirements_file_path: Optional[pathlib.Path, str], default None
 806        If provided, append `['-r', '/path/to/requirements.txt']` to `args`.
 807
 808    venv: str, default 'mrsm'
 809        The virtual environment to install into.
 810
 811    split: bool, default False
 812        If `True`, split on periods and only install the root package name.
 813
 814    check_update: bool, default True
 815        If `True`, check if the package requires an update.
 816
 817    check_pypi: bool, default True
 818        If `True` and `check_update` is `True`, check PyPI for the latest version.
 819
 820    check_wheel: bool, default True
 821        If `True`, check if `wheel` is available.
 822
 823    _uninstall: bool, default False
 824        If `True`, uninstall packages instead.
 825
 826    color: bool, default True
 827        If `True`, include color in debug text.
 828
 829    silent: bool, default False
 830        If `True`, skip printing messages.
 831
 832    debug: bool, default False
 833        Verbosity toggle.
 834
 835    Returns
 836    -------
 837    A bool indicating success.
 838
 839    """
 840    from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
 841    from meerschaum.config.static import STATIC_CONFIG
 842    from meerschaum.utils.warnings import warn
 843    if args is None:
 844        args = ['--upgrade'] if not _uninstall else []
 845    ANSI = True if color else False
 846    if check_wheel:
 847        have_wheel = venv_contains_package('wheel', venv=venv, debug=debug)
 848
 849    daemon_env_var = STATIC_CONFIG['environment']['daemon_id']
 850    inside_daemon = daemon_env_var in os.environ
 851    if inside_daemon:
 852        silent = True
 853
 854    _args = list(args)
 855    have_pip = venv_contains_package('pip', venv=None, debug=debug)
 856    try:
 857        import pip
 858        have_pip = True
 859    except ImportError:
 860        have_pip = False
 861    try:
 862        import uv
 863        uv_bin = uv.find_uv_bin()
 864        have_uv_pip = True
 865    except (ImportError, FileNotFoundError):
 866        uv_bin = None
 867        have_uv_pip = False
 868    if have_pip and not have_uv_pip and _install_uv_pip and is_uv_enabled():
 869        if not pip_install(
 870            'uv',
 871            venv=None,
 872            debug=debug,
 873            _install_uv_pip=False,
 874            check_update=False,
 875            check_pypi=False,
 876            check_wheel=False,
 877        ) and not silent:
 878            warn(
 879                f"Failed to install `uv` for virtual environment '{venv}'.",
 880                color=False,
 881            )
 882
 883    use_uv_pip = (
 884        _use_uv_pip
 885        and venv_contains_package('uv', venv=None, debug=debug)
 886        and uv_bin is not None
 887        and venv is not None
 888        and is_uv_enabled()
 889    )
 890
 891    import sys
 892    if not have_pip and not use_uv_pip:
 893        if not get_pip(venv=venv, color=color, debug=debug):
 894            import sys
 895            minor = sys.version_info.minor
 896            print(
 897                "\nFailed to import `pip` and `ensurepip`.\n"
 898                + "If you are running Ubuntu/Debian, "
 899                + "you might need to install `python3.{minor}-distutils`:\n\n"
 900                + f"    sudo apt install python3.{minor}-pip python3.{minor}-venv\n\n"
 901                + "Please install pip and restart Meerschaum.\n\n"
 902                + "You can find instructions on installing `pip` here:\n"
 903                + "https://pip.pypa.io/en/stable/installing/"
 904            )
 905            sys.exit(1)
 906
 907    with Venv(venv, debug=debug):
 908        if venv is not None:
 909            if (
 910                '--ignore-installed' not in args
 911                and '-I' not in _args
 912                and not _uninstall
 913                and not use_uv_pip
 914            ):
 915                _args += ['--ignore-installed']
 916            if '--cache-dir' not in args and not _uninstall:
 917                cache_dir_path = VIRTENV_RESOURCES_PATH / venv / 'cache'
 918                _args += ['--cache-dir', str(cache_dir_path)]
 919
 920        if 'pip' not in ' '.join(_args) and not use_uv_pip:
 921            if check_update and not _uninstall:
 922                pip = attempt_import('pip', venv=venv, install=False, debug=debug, lazy=False)
 923                if need_update(pip, check_pypi=check_pypi, debug=debug):
 924                    _args.append(all_packages['pip'])
 925
 926        _args = (['install'] if not _uninstall else ['uninstall']) + _args
 927
 928        if check_wheel and not _uninstall and not use_uv_pip:
 929            if not have_wheel:
 930                setup_packages_to_install = (
 931                    ['setuptools', 'wheel']
 932                    + (['uv'] if is_uv_enabled() else [])
 933                )
 934                if not pip_install(
 935                    *setup_packages_to_install,
 936                    venv=venv,
 937                    check_update=False,
 938                    check_pypi=False,
 939                    check_wheel=False,
 940                    debug=debug,
 941                    _install_uv_pip=False,
 942                ) and not silent:
 943                    from meerschaum.utils.misc import items_str
 944                    warn(
 945                        (
 946                            f"Failed to install {items_str(setup_packages_to_install)} for virtual "
 947                            + f"environment '{venv}'."
 948                        ),
 949                        color=False,
 950                    )
 951
 952        if requirements_file_path is not None:
 953            _args.append('-r')
 954            _args.append(pathlib.Path(requirements_file_path).resolve().as_posix())
 955
 956        if not ANSI and '--no-color' not in _args:
 957            _args.append('--no-color')
 958
 959        if '--no-input' not in _args and not use_uv_pip:
 960            _args.append('--no-input')
 961
 962        if _uninstall and '-y' not in _args and not use_uv_pip:
 963            _args.append('-y')
 964
 965        if '--no-warn-conflicts' not in _args and not _uninstall and not use_uv_pip:
 966            _args.append('--no-warn-conflicts')
 967
 968        if '--disable-pip-version-check' not in _args and not use_uv_pip:
 969            _args.append('--disable-pip-version-check')
 970
 971        if '--target' not in _args and '-t' not in _args and not (not use_uv_pip and _uninstall):
 972            if venv is not None:
 973                vtp = venv_target_path(venv, allow_nonexistent=True, debug=debug)
 974                if not vtp.exists():
 975                    if not init_venv(venv, force=True):
 976                        vtp.mkdir(parents=True, exist_ok=True)
 977                _args += ['--target', venv_target_path(venv, debug=debug)]
 978        elif (
 979            '--target' not in _args
 980                and '-t' not in _args
 981                and not inside_venv()
 982                and not _uninstall
 983                and not use_uv_pip
 984        ):
 985            _args += ['--user']
 986
 987        if debug:
 988            if '-v' not in _args or '-vv' not in _args or '-vvv' not in _args:
 989                if use_uv_pip:
 990                    _args.append('--verbose')
 991        else:
 992            if '-q' not in _args or '-qq' not in _args or '-qqq' not in _args:
 993                pass
 994
 995        _packages = [
 996            (
 997                get_install_no_version(install_name)
 998                if _uninstall or install_name.startswith(_MRSM_PACKAGE_ARCHIVES_PREFIX)
 999                else install_name
1000            )
1001            for install_name in install_names
1002        ]
1003        msg = "Installing packages:" if not _uninstall else "Uninstalling packages:"
1004        for p in _packages:
1005            msg += f'\n  - {p}'
1006        if not silent:
1007            print(msg)
1008
1009        if _uninstall and not _from_completely_uninstall and not use_uv_pip:
1010            for install_name in _packages:
1011                _install_no_version = get_install_no_version(install_name)
1012                if _install_no_version in ('pip', 'wheel', 'uv'):
1013                    continue
1014                if not completely_uninstall_package(
1015                    _install_no_version,
1016                    venv=venv, debug=debug,
1017                ) and not silent:
1018                    warn(
1019                        f"Failed to clean up package '{_install_no_version}'.",
1020                    )
1021
1022        ### NOTE: Only append the `--prerelease=allow` flag if we explicitly depend on a prerelease.
1023        if use_uv_pip:
1024            _args.insert(0, 'pip')
1025            if not _uninstall and get_prerelease_dependencies(_packages):
1026                _args.append('--prerelease=allow')
1027
1028        rc = run_python_package(
1029            ('pip' if not use_uv_pip else 'uv'),
1030            _args + _packages,
1031            venv=None,
1032            env=_get_pip_os_env(color=color),
1033            debug=debug,
1034        )
1035        if debug:
1036            print(f"{rc=}")
1037        success = rc == 0
1038
1039    msg = (
1040        "Successfully " + ('un' if _uninstall else '') + "installed packages." if success 
1041        else "Failed to " + ('un' if _uninstall else '') + "install packages."
1042    )
1043    if not silent:
1044        print(msg)
1045    if debug and not silent:
1046        print('pip ' + ('un' if _uninstall else '') + 'install returned:', success)
1047    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):
1050def get_prerelease_dependencies(_packages: Optional[List[str]] = None):
1051    """
1052    Return a list of explicitly prerelease dependencies from a list of packages.
1053    """
1054    if _packages is None:
1055        _packages = list(all_packages.keys())
1056    prelrease_strings = ['dev', 'rc', 'a']
1057    prerelease_packages = []
1058    for install_name in _packages:
1059        _install_no_version = get_install_no_version(install_name)
1060        import_name = _install_to_import_name(install_name)
1061        install_with_version = _import_to_install_name(import_name)
1062        version_only = (
1063            install_with_version.lower().replace(_install_no_version.lower(), '')
1064            .split(']')[-1]
1065        )
1066
1067        is_prerelease = False
1068        for prelrease_string in prelrease_strings:
1069            if prelrease_string in version_only:
1070                is_prerelease = True
1071
1072        if is_prerelease:
1073            prerelease_packages.append(install_name)
1074    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:
1077def completely_uninstall_package(
1078    install_name: str,
1079    venv: str = 'mrsm',
1080    debug: bool = False,
1081) -> bool:
1082    """
1083    Continue calling `pip uninstall` until a package is completely
1084    removed from a virtual environment. 
1085    This is useful for dealing with multiple installed versions of a package.
1086    """
1087    attempts = 0
1088    _install_no_version = get_install_no_version(install_name)
1089    clean_install_no_version = _install_no_version.lower().replace('-', '_')
1090    installed_versions = []
1091    vtp = venv_target_path(venv, allow_nonexistent=True, debug=debug)
1092    if not vtp.exists():
1093        return True
1094
1095    for file_name in os.listdir(vtp):
1096        if not file_name.endswith('.dist-info'):
1097            continue
1098        clean_dist_info = file_name.replace('-', '_').lower()
1099        if not clean_dist_info.startswith(clean_install_no_version):
1100            continue
1101        installed_versions.append(file_name)
1102
1103    max_attempts = len(installed_versions)
1104    while attempts < max_attempts:
1105        if not venv_contains_package(
1106            _install_to_import_name(_install_no_version),
1107            venv=venv, debug=debug,
1108        ):
1109            return True
1110        if not pip_uninstall(
1111            _install_no_version,
1112            venv = venv,
1113            silent = (not debug),
1114            _from_completely_uninstall = True,
1115            debug = debug,
1116        ):
1117            return False
1118        attempts += 1
1119    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:
1122def pip_uninstall(
1123    *args, **kw
1124) -> bool:
1125    """
1126    Uninstall Python packages.
1127    This function is a wrapper around `pip_install()` but with `_uninstall` enforced as `True`.
1128    """
1129    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, foreground: bool = False, as_proc: bool = False, capture_output: bool = False, debug: bool = False, **kw: Any) -> Union[int, subprocess.Popen, NoneType]:
1132def run_python_package(
1133    package_name: str,
1134    args: Optional[List[str]] = None,
1135    venv: Optional[str] = 'mrsm',
1136    cwd: Optional[str] = None,
1137    foreground: bool = False,
1138    as_proc: bool = False,
1139    capture_output: bool = False,
1140    debug: bool = False,
1141    **kw: Any,
1142) -> Union[int, subprocess.Popen, None]:
1143    """
1144    Runs an installed python package.
1145    E.g. Translates to `/usr/bin/python -m [package]`
1146
1147    Parameters
1148    ----------
1149    package_name: str
1150        The Python module to be executed.
1151
1152    args: Optional[List[str]], default None
1153        Additional command line arguments to be appended after `-m [package]`.
1154
1155    venv: Optional[str], default 'mrsm'
1156        If specified, execute the Python interpreter from a virtual environment.
1157
1158    cwd: Optional[str], default None
1159        If specified, change directories before starting the process.
1160        Defaults to `None`.
1161
1162    as_proc: bool, default False
1163        If `True`, return a `subprocess.Popen` object.
1164
1165    capture_output: bool, default False
1166        If `as_proc` is `True`, capture stdout and stderr.
1167
1168    foreground: bool, default False
1169        If `True`, start the subprocess as a foreground process.
1170        Defaults to `False`.
1171
1172    kw: Any
1173        Additional keyword arguments to pass to `meerschaum.utils.process.run_process()`
1174        and by extension `subprocess.Popen()`.
1175
1176    Returns
1177    -------
1178    Either a return code integer or a `subprocess.Popen` object
1179    (or `None` if a `KeyboardInterrupt` occurs and as_proc is `True`).
1180    """
1181    import sys, platform
1182    import subprocess
1183    from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
1184    from meerschaum.utils.process import run_process
1185    from meerschaum.utils.warnings import warn
1186    if args is None:
1187        args = []
1188    old_cwd = os.getcwd()
1189    if cwd is not None:
1190        os.chdir(cwd)
1191    executable = venv_executable(venv=venv)
1192    venv_path = (VIRTENV_RESOURCES_PATH / venv) if venv is not None else None
1193    env_dict = kw.get('env', os.environ).copy()
1194    if venv_path is not None:
1195        env_dict.update({'VIRTUAL_ENV': venv_path.as_posix()})
1196    command = [executable, '-m', str(package_name)] + [str(a) for a in args]
1197    import traceback
1198    if debug:
1199        print(command, file=sys.stderr)
1200    try:
1201        to_return = run_process(
1202            command,
1203            foreground=foreground,
1204            as_proc=as_proc,
1205            capture_output=capture_output,
1206            **kw
1207        )
1208    except Exception as e:
1209        msg = f"Failed to execute {command}, will try again:\n{traceback.format_exc()}"
1210        warn(msg, color=False)
1211        stdout, stderr = (
1212            (None, None)
1213            if not capture_output
1214            else (subprocess.PIPE, subprocess.PIPE)
1215        )
1216        proc = subprocess.Popen(
1217            command,
1218            stdout=stdout,
1219            stderr=stderr,
1220            env=env_dict,
1221        )
1222        to_return = proc if as_proc else proc.wait()
1223    except KeyboardInterrupt:
1224        to_return = 1 if not as_proc else None
1225    os.chdir(old_cwd)
1226    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.
  • 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:
1229def attempt_import(
1230    *names: str,
1231    lazy: bool = True,
1232    warn: bool = True,
1233    install: bool = True,
1234    venv: Optional[str] = 'mrsm',
1235    precheck: bool = True,
1236    split: bool = True,
1237    check_update: bool = False,
1238    check_pypi: bool = False,
1239    check_is_installed: bool = True,
1240    allow_outside_venv: bool = True,
1241    color: bool = True,
1242    debug: bool = False
1243) -> Any:
1244    """
1245    Raise a warning if packages are not installed; otherwise import and return modules.
1246    If `lazy` is `True`, return lazy-imported modules.
1247    
1248    Returns tuple of modules if multiple names are provided, else returns one module.
1249    
1250    Parameters
1251    ----------
1252    names: List[str]
1253        The packages to be imported.
1254
1255    lazy: bool, default True
1256        If `True`, lazily load packages.
1257
1258    warn: bool, default True
1259        If `True`, raise a warning if a package cannot be imported.
1260
1261    install: bool, default True
1262        If `True`, attempt to install a missing package into the designated virtual environment.
1263        If `check_update` is True, install updates if available.
1264
1265    venv: Optional[str], default 'mrsm'
1266        The virtual environment in which to search for packages and to install packages into.
1267
1268    precheck: bool, default True
1269        If `True`, attempt to find module before importing (necessary for checking if modules exist
1270        and retaining lazy imports), otherwise assume lazy is `False`.
1271
1272    split: bool, default True
1273        If `True`, split packages' names on `'.'`.
1274
1275    check_update: bool, default False
1276        If `True` and `install` is `True`, install updates if the required minimum version
1277        does not match.
1278
1279    check_pypi: bool, default False
1280        If `True` and `check_update` is `True`, check PyPI when determining whether
1281        an update is required.
1282
1283    check_is_installed: bool, default True
1284        If `True`, check if the package is contained in the virtual environment.
1285
1286    allow_outside_venv: bool, default True
1287        If `True`, search outside of the specified virtual environment
1288        if the package cannot be found.
1289        Setting to `False` will reinstall the package into a virtual environment, even if it
1290        is installed outside.
1291
1292    color: bool, default True
1293        If `False`, do not print ANSI colors.
1294
1295    Returns
1296    -------
1297    The specified modules. If they're not available and `install` is `True`, it will first
1298    download them into a virtual environment and return the modules.
1299
1300    Examples
1301    --------
1302    >>> pandas, sqlalchemy = attempt_import('pandas', 'sqlalchemy')
1303    >>> pandas = attempt_import('pandas')
1304
1305    """
1306
1307    import importlib.util
1308
1309    ### to prevent recursion, check if parent Meerschaum package is being imported
1310    if names == ('meerschaum',):
1311        return _import_module('meerschaum')
1312
1313    if venv == 'mrsm' and _import_hook_venv is not None:
1314        if debug:
1315            print(f"Import hook for virtual environment '{_import_hook_venv}' is active.")
1316        venv = _import_hook_venv
1317
1318    _warnings = _import_module('meerschaum.utils.warnings')
1319    warn_function = _warnings.warn
1320
1321    def do_import(_name: str, **kw) -> Union['ModuleType', None]:
1322        with Venv(venv=venv, debug=debug):
1323            ### determine the import method (lazy vs normal)
1324            from meerschaum.utils.misc import filter_keywords
1325            import_method = (
1326                _import_module if not lazy
1327                else lazy_import
1328            )
1329            try:
1330                mod = import_method(_name, **(filter_keywords(import_method, **kw)))
1331            except Exception as e:
1332                if warn:
1333                    import traceback
1334                    traceback.print_exception(type(e), e, e.__traceback__)
1335                    warn_function(
1336                        f"Failed to import module '{_name}'.\nException:\n{e}",
1337                        ImportWarning,
1338                        stacklevel = (5 if lazy else 4),
1339                        color = False,
1340                    )
1341                mod = None
1342        return mod
1343
1344    modules = []
1345    for name in names:
1346        ### Check if package is a declared dependency.
1347        root_name = name.split('.')[0] if split else name
1348        install_name = _import_to_install_name(root_name)
1349
1350        if install_name is None:
1351            install_name = root_name
1352            if warn and root_name != 'plugins':
1353                warn_function(
1354                    f"Package '{root_name}' is not declared in meerschaum.utils.packages.",
1355                    ImportWarning,
1356                    stacklevel = 3,
1357                    color = False
1358                )
1359
1360        ### Determine if the package exists.
1361        if precheck is False:
1362            found_module = (
1363                do_import(
1364                    name, debug=debug, warn=False, venv=venv, color=color,
1365                    check_update=False, check_pypi=False, split=split,
1366                ) is not None
1367            )
1368        else:
1369            if check_is_installed:
1370                with _locks['_is_installed_first_check']:
1371                    if not _is_installed_first_check.get(name, False):
1372                        package_is_installed = is_installed(
1373                            name,
1374                            venv = venv,
1375                            split = split,
1376                            allow_outside_venv = allow_outside_venv,
1377                            debug = debug,
1378                        )
1379                        _is_installed_first_check[name] = package_is_installed
1380                    else:
1381                        package_is_installed = _is_installed_first_check[name]
1382            else:
1383                package_is_installed = _is_installed_first_check.get(
1384                    name,
1385                    venv_contains_package(name, venv=venv, split=split, debug=debug)
1386                )
1387            found_module = package_is_installed
1388
1389        if not found_module:
1390            if install:
1391                if not pip_install(
1392                    install_name,
1393                    venv = venv,
1394                    split = False,
1395                    check_update = check_update,
1396                    color = color,
1397                    debug = debug
1398                ) and warn:
1399                    warn_function(
1400                        f"Failed to install '{install_name}'.",
1401                        ImportWarning,
1402                        stacklevel = 3,
1403                        color = False,
1404                    )
1405            elif warn:
1406                ### Raise a warning if we can't find the package and install = False.
1407                warn_function(
1408                    (f"\n\nMissing package '{name}' from virtual environment '{venv}'; "
1409                     + "some features will not work correctly."
1410                     + f"\n\nSet install=True when calling attempt_import.\n"),
1411                    ImportWarning,
1412                    stacklevel = 3,
1413                    color = False,
1414                )
1415
1416        ### Do the import. Will be lazy if lazy=True.
1417        m = do_import(
1418            name, debug=debug, warn=warn, venv=venv, color=color,
1419            check_update=check_update, check_pypi=check_pypi, install=install, split=split,
1420        )
1421        modules.append(m)
1422
1423    modules = tuple(modules)
1424    if len(modules) == 1:
1425        return modules[0]
1426    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:
1429def lazy_import(
1430    name: str,
1431    local_name: str = None,
1432    **kw
1433) -> meerschaum.utils.packages.lazy_loader.LazyLoader:
1434    """
1435    Lazily import a package.
1436    """
1437    from meerschaum.utils.packages.lazy_loader import LazyLoader
1438    if local_name is None:
1439        local_name = name
1440    return LazyLoader(
1441        local_name,
1442        globals(),
1443        name,
1444        **kw
1445    )

Lazily import a package.

def pandas_name() -> str:
1448def pandas_name() -> str:
1449    """
1450    Return the configured name for `pandas`.
1451    
1452    Below are the expected possible values:
1453
1454    - 'pandas'
1455    - 'modin.pandas'
1456    - 'dask.dataframe'
1457
1458    """
1459    from meerschaum.config import get_config
1460    pandas_module_name = get_config('system', 'connectors', 'all', 'pandas', patch=True)
1461    if pandas_module_name == 'modin':
1462        pandas_module_name = 'modin.pandas'
1463    elif pandas_module_name == 'dask':
1464        pandas_module_name = 'dask.dataframe'
1465
1466    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'":
1470def import_pandas(
1471    debug: bool = False,
1472    lazy: bool = False,
1473    **kw
1474) -> 'ModuleType':
1475    """
1476    Quality-of-life function to attempt to import the configured version of `pandas`.
1477    """
1478    pandas_module_name = pandas_name()
1479    global emitted_pandas_warning
1480
1481    if pandas_module_name != 'pandas':
1482        with _locks['emitted_pandas_warning']:
1483            if not emitted_pandas_warning:
1484                from meerschaum.utils.warnings import warn
1485                emitted_pandas_warning = True
1486                warn(
1487                    (
1488                        "You are using an alternative Pandas implementation "
1489                        + f"'{pandas_module_name}'"
1490                        + "\n   Features may not work as expected."
1491                    ),
1492                    stack=False,
1493                )
1494
1495    pytz = attempt_import('pytz', debug=debug, lazy=False, **kw)
1496    pandas, pyarrow = attempt_import('pandas', 'pyarrow', debug=debug, lazy=False, **kw)
1497    pd = attempt_import(pandas_module_name, debug=debug, lazy=lazy, **kw)
1498    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'":
1501def import_rich(
1502    lazy: bool = True,
1503    debug: bool = False,
1504    **kw : Any
1505) -> 'ModuleType':
1506    """
1507    Quality of life function for importing `rich`.
1508    """
1509    from meerschaum.utils.formatting import ANSI, UNICODE
1510    if not ANSI and not UNICODE:
1511        return None
1512
1513    ## need typing_extensions for `from rich import box`
1514    typing_extensions = attempt_import(
1515        'typing_extensions', lazy=False, debug=debug
1516    )
1517    pygments = attempt_import(
1518        'pygments', lazy=False,
1519    )
1520    rich = attempt_import(
1521        'rich', lazy=lazy,
1522        **kw
1523    )
1524    return rich

Quality of life function for importing rich.

def import_dcc(warn=False, **kw) -> "'ModuleType'":
1538def import_dcc(warn=False, **kw) -> 'ModuleType':
1539    """
1540    Import Dash Core Components (`dcc`).
1541    """
1542    return (
1543        attempt_import('dash_core_components', warn=warn, **kw)
1544        if _dash_less_than_2(warn=warn, **kw) else attempt_import('dash.dcc', warn=warn, **kw)
1545    )

Import Dash Core Components (dcc).

def import_html(warn=False, **kw) -> "'ModuleType'":
1548def import_html(warn=False, **kw) -> 'ModuleType':
1549    """
1550    Import Dash HTML Components (`html`).
1551    """
1552    return (
1553        attempt_import('dash_html_components', warn=warn, **kw)
1554        if _dash_less_than_2(warn=warn, **kw)
1555        else attempt_import('dash.html', warn=warn, **kw)
1556    )

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):
1559def get_modules_from_package(
1560    package: 'package',
1561    names: bool = False,
1562    recursive: bool = False,
1563    lazy: bool = False,
1564    modules_venvs: bool = False,
1565    debug: bool = False
1566):
1567    """
1568    Find and import all modules in a package.
1569    
1570    Returns
1571    -------
1572    Either list of modules or tuple of lists.
1573    """
1574    from os.path import dirname, join, isfile, isdir, basename
1575    import glob
1576
1577    pattern = '*' if recursive else '*.py'
1578    package_path = dirname(package.__file__ or package.__path__[0])
1579    module_names = glob.glob(join(package_path, pattern), recursive=recursive)
1580    _all = [
1581        basename(f)[:-3] if isfile(f) else basename(f)
1582        for f in module_names
1583            if ((isfile(f) and f.endswith('.py')) or isdir(f))
1584               and not f.endswith('__init__.py')
1585               and not f.endswith('__pycache__')
1586    ]
1587
1588    if debug:
1589        from meerschaum.utils.debug import dprint
1590        dprint(str(_all))
1591    modules = []
1592    for module_name in [package.__name__ + "." + mod_name for mod_name in _all]:
1593        ### there's probably a better way than a try: catch but it'll do for now
1594        try:
1595            ### if specified, activate the module's virtual environment before importing.
1596            ### NOTE: this only considers the filename, so two modules from different packages
1597            ### may end up sharing virtual environments.
1598            if modules_venvs:
1599                activate_venv(module_name.split('.')[-1], debug=debug)
1600            m = lazy_import(module_name, debug=debug) if lazy else _import_module(module_name)
1601            modules.append(m)
1602        except Exception as e:
1603            if debug:
1604                dprint(str(e))
1605        finally:
1606            if modules_venvs:
1607                deactivate_venv(module_name.split('.')[-1], debug=debug)
1608    if names:
1609        return _all, modules
1610
1611    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']":
1614def import_children(
1615    package: Optional['ModuleType'] = None,
1616    package_name: Optional[str] = None,
1617    types : Optional[List[str]] = None,
1618    lazy: bool = True,
1619    recursive: bool = False,
1620    debug: bool = False
1621) -> List['ModuleType']:
1622    """
1623    Import all functions in a package to its `__init__`.
1624
1625    Parameters
1626    ----------
1627    package: Optional[ModuleType], default None
1628        Package to import its functions into.
1629        If `None` (default), use parent.
1630
1631    package_name: Optional[str], default None
1632        Name of package to import its functions into
1633        If None (default), use parent.
1634
1635    types: Optional[List[str]], default None
1636        Types of members to return.
1637        Defaults are `['method', 'builtin', 'class', 'function', 'package', 'module']`
1638
1639    Returns
1640    -------
1641    A list of modules.
1642    """
1643    import sys, inspect
1644
1645    if types is None:
1646        types = ['method', 'builtin', 'function', 'class', 'module']
1647
1648    ### if package_name and package are None, use parent
1649    if package is None and package_name is None:
1650        package_name = inspect.stack()[1][0].f_globals['__name__']
1651
1652    ### populate package or package_name from other other
1653    if package is None:
1654        package = sys.modules[package_name]
1655    elif package_name is None:
1656        package_name = package.__name__
1657
1658    ### Set attributes in sys module version of package.
1659    ### Kinda like setting a dictionary
1660    ###   functions[name] = func
1661    modules = get_modules_from_package(package, recursive=recursive, lazy=lazy, debug=debug)
1662    _all, members = [], []
1663    objects = []
1664    for module in modules:
1665        _objects = []
1666        for ob in inspect.getmembers(module):
1667            for t in types:
1668                ### ob is a tuple of (name, object)
1669                if getattr(inspect, 'is' + t)(ob[1]):
1670                    _objects.append(ob)
1671
1672        if 'module' in types:
1673            _objects.append((module.__name__.split('.')[0], module))
1674        objects += _objects
1675    for ob in objects:
1676        setattr(sys.modules[package_name], ob[0], ob[1])
1677        _all.append(ob[0])
1678        members.append(ob[1])
1679
1680    if debug:
1681        from meerschaum.utils.debug import dprint
1682        dprint(str(_all))
1683    ### set __all__ for import *
1684    setattr(sys.modules[package_name], '__all__', _all)
1685    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):
1689def reload_package(
1690    package: str,
1691    skip_submodules: Optional[List[str]] = None,
1692    lazy: bool = False,
1693    debug: bool = False,
1694    **kw: Any
1695):
1696    """
1697    Recursively load a package's subpackages, even if they were not previously loaded.
1698    """
1699    import sys
1700    if isinstance(package, str):
1701        package_name = package
1702    else:
1703        try:
1704            package_name = package.__name__
1705        except Exception as e:
1706            package_name = str(package)
1707
1708    skip_submodules = skip_submodules or []
1709    if 'meerschaum.utils.packages' not in skip_submodules:
1710        skip_submodules.append('meerschaum.utils.packages')
1711    def safeimport():
1712        subs = [
1713            m for m in sys.modules
1714            if m.startswith(package_name + '.')
1715        ]
1716        subs_to_skip = []
1717        for skip_mod in skip_submodules:
1718            for mod in subs:
1719                if mod.startswith(skip_mod):
1720                    subs_to_skip.append(mod)
1721                    continue
1722
1723        subs = [m for m in subs if m not in subs_to_skip]
1724        for module_name in subs:
1725            _reload_module_cache[module_name] = sys.modules.pop(module_name, None)
1726        if not subs_to_skip:
1727            _reload_module_cache[package_name] = sys.modules.pop(package_name, None)
1728
1729        return _import_module(package_name)
1730
1731    return safeimport()

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

def reload_meerschaum(debug: bool = False) -> Tuple[bool, str]:
1734def reload_meerschaum(debug: bool = False) -> SuccessTuple:
1735    """
1736    Reload the currently loaded Meercshaum modules, refreshing plugins and shell configuration.
1737    """
1738    reload_package(
1739        'meerschaum',
1740        skip_submodules = [
1741            'meerschaum._internal.shell',
1742            'meerschaum.utils.pool',
1743        ]
1744    )
1745
1746    from meerschaum.plugins import reload_plugins
1747    from meerschaum._internal.shell.Shell import _insert_shell_actions
1748    reload_plugins(debug=debug)
1749    _insert_shell_actions()
1750    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:
1753def is_installed(
1754    import_name: str,
1755    venv: Optional[str] = 'mrsm',
1756    split: bool = True,
1757    allow_outside_venv: bool = True,
1758    debug: bool = False,
1759) -> bool:
1760    """
1761    Check whether a package is installed.
1762
1763    Parameters
1764    ----------
1765    import_name: str
1766        The import name of the module.
1767
1768    venv: Optional[str], default 'mrsm'
1769        The venv in which to search for the module.
1770
1771    split: bool, default True
1772        If `True`, split on periods to determine the root module name.
1773
1774    allow_outside_venv: bool, default True
1775        If `True`, search outside of the specified virtual environment
1776        if the package cannot be found.
1777    """
1778    if debug:
1779        from meerschaum.utils.debug import dprint
1780    root_name = import_name.split('.')[0] if split else import_name
1781    import importlib.util
1782    with Venv(venv, debug=debug):
1783        try:
1784            spec_path = pathlib.Path(
1785                get_module_path(root_name, venv=venv, debug=debug)
1786                or
1787                (
1788                    importlib.util.find_spec(root_name).origin 
1789                    if venv is not None and allow_outside_venv
1790                    else None
1791                )
1792            )
1793        except (ModuleNotFoundError, ValueError, AttributeError, TypeError) as e:
1794            spec_path = None
1795
1796        found = (
1797            not need_update(
1798                None,
1799                import_name=root_name,
1800                _run_determine_version=False,
1801                check_pypi=False,
1802                version=determine_version(
1803                    spec_path,
1804                    venv=venv,
1805                    debug=debug,
1806                    import_name=root_name,
1807                ),
1808                debug=debug,
1809            )
1810        ) if spec_path is not None else False
1811
1812    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.
def venv_contains_package( import_name: str, venv: Optional[str] = 'mrsm', split: bool = True, debug: bool = False) -> bool:
1815def venv_contains_package(
1816    import_name: str,
1817    venv: Optional[str] = 'mrsm',
1818    split: bool = True,
1819    debug: bool = False,
1820) -> bool:
1821    """
1822    Search the contents of a virtual environment for a package.
1823    """
1824    import site
1825    import pathlib
1826    root_name = import_name.split('.')[0] if split else import_name
1827    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]:
1830def package_venv(package: 'ModuleType') -> Union[str, None]:
1831    """
1832    Inspect a package and return the virtual environment in which it presides.
1833    """
1834    import os
1835    from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
1836    if str(VIRTENV_RESOURCES_PATH) not in package.__file__:
1837        return None
1838    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'":
1841def ensure_readline() -> 'ModuleType':
1842    """Make sure that the `readline` package is able to be imported."""
1843    import sys
1844    try:
1845        import readline
1846    except ImportError:
1847        readline = None
1848
1849    if readline is None:
1850        import platform
1851        rl_name = "gnureadline" if platform.system() != 'Windows' else "pyreadline3"
1852        try:
1853            rl = attempt_import(
1854                rl_name,
1855                lazy=False,
1856                install=True,
1857                venv=None,
1858                warn=False,
1859            )
1860        except (ImportError, ModuleNotFoundError):
1861            if not pip_install(rl_name, args=['--upgrade', '--ignore-installed'], venv=None):
1862                print(f"Unable to import {rl_name}!", file=sys.stderr)
1863                sys.exit(1)
1864
1865    sys.modules['readline'] = readline
1866    return readline

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

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

Return whether uv is available and enabled.

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

Return whether the user has disabled uv.