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