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

Lazily import a package.

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

Quality of life function for importing rich.

def import_dcc(warn=False, **kw) -> "'ModuleType'":
1531def import_dcc(warn=False, **kw) -> 'ModuleType':
1532    """
1533    Import Dash Core Components (`dcc`).
1534    """
1535    return (
1536        attempt_import('dash_core_components', warn=warn, **kw)
1537        if _dash_less_than_2(warn=warn, **kw) else attempt_import('dash.dcc', warn=warn, **kw)
1538    )

Import Dash Core Components (dcc).

def import_html(warn=False, **kw) -> "'ModuleType'":
1541def import_html(warn=False, **kw) -> 'ModuleType':
1542    """
1543    Import Dash HTML Components (`html`).
1544    """
1545    return (
1546        attempt_import('dash_html_components', warn=warn, **kw)
1547        if _dash_less_than_2(warn=warn, **kw)
1548        else attempt_import('dash.html', warn=warn, **kw)
1549    )

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

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

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

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

def use_uv() -> bool:
1903def use_uv() -> bool:
1904    """
1905    Return whether `uv` is available and enabled.
1906    """
1907    from meerschaum.utils.misc import is_android
1908    if is_android():
1909        return False
1910
1911    if not is_uv_enabled():
1912        return False
1913
1914    try:
1915        import uv
1916        uv_bin = uv.find_uv_bin()
1917    except (ImportError, FileNotFoundError):
1918        uv_bin = None
1919
1920    if uv_bin is None:
1921        return False
1922
1923    return True

Return whether uv is available and enabled.

def is_uv_enabled() -> bool:
1926def is_uv_enabled() -> bool:
1927    """
1928    Return whether the user has disabled `uv`.
1929    """
1930    from meerschaum.utils.misc import is_android
1931    if is_android():
1932        return False
1933
1934    try:
1935        import yaml
1936    except ImportError:
1937        return False
1938
1939    from meerschaum.config import get_config
1940    enabled = get_config('system', 'experimental', 'uv_pip')
1941    return enabled

Return whether the user has disabled uv.