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