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