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