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