meerschaum 2.2.1__py3-none-any.whl → 2.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. meerschaum/_internal/shell/Shell.py +40 -16
  2. meerschaum/_internal/term/__init__.py +3 -2
  3. meerschaum/_internal/term/tools.py +1 -1
  4. meerschaum/actions/api.py +65 -31
  5. meerschaum/actions/python.py +56 -24
  6. meerschaum/actions/start.py +2 -4
  7. meerschaum/actions/uninstall.py +5 -9
  8. meerschaum/actions/upgrade.py +11 -3
  9. meerschaum/api/__init__.py +1 -0
  10. meerschaum/api/dash/callbacks/__init__.py +4 -0
  11. meerschaum/api/dash/callbacks/custom.py +39 -0
  12. meerschaum/api/dash/callbacks/dashboard.py +39 -6
  13. meerschaum/api/dash/callbacks/login.py +3 -1
  14. meerschaum/api/dash/components.py +5 -2
  15. meerschaum/api/dash/pipes.py +145 -97
  16. meerschaum/config/_default.py +1 -0
  17. meerschaum/config/_paths.py +12 -12
  18. meerschaum/config/_version.py +1 -1
  19. meerschaum/config/paths.py +10 -0
  20. meerschaum/config/static/__init__.py +1 -1
  21. meerschaum/connectors/__init__.py +1 -1
  22. meerschaum/core/Pipe/__init__.py +5 -0
  23. meerschaum/core/Pipe/_sync.py +2 -3
  24. meerschaum/plugins/__init__.py +67 -9
  25. meerschaum/utils/daemon/Daemon.py +7 -2
  26. meerschaum/utils/misc.py +6 -0
  27. meerschaum/utils/packages/__init__.py +212 -53
  28. meerschaum/utils/packages/_packages.py +1 -0
  29. meerschaum/utils/process.py +12 -2
  30. meerschaum/utils/schedule.py +1 -1
  31. meerschaum/utils/venv/__init__.py +46 -11
  32. {meerschaum-2.2.1.dist-info → meerschaum-2.2.2.dist-info}/METADATA +5 -1
  33. {meerschaum-2.2.1.dist-info → meerschaum-2.2.2.dist-info}/RECORD +39 -37
  34. {meerschaum-2.2.1.dist-info → meerschaum-2.2.2.dist-info}/WHEEL +1 -1
  35. {meerschaum-2.2.1.dist-info → meerschaum-2.2.2.dist-info}/LICENSE +0 -0
  36. {meerschaum-2.2.1.dist-info → meerschaum-2.2.2.dist-info}/NOTICE +0 -0
  37. {meerschaum-2.2.1.dist-info → meerschaum-2.2.2.dist-info}/entry_points.txt +0 -0
  38. {meerschaum-2.2.1.dist-info → meerschaum-2.2.2.dist-info}/top_level.txt +0 -0
  39. {meerschaum-2.2.1.dist-info → meerschaum-2.2.2.dist-info}/zip-safe +0 -0
@@ -46,6 +46,7 @@ def get_module_path(
46
46
  """
47
47
  Get a module's path without importing.
48
48
  """
49
+ import site
49
50
  if debug:
50
51
  from meerschaum.utils.debug import dprint
51
52
  if not _try_install_name_on_fail:
@@ -54,33 +55,58 @@ def get_module_path(
54
55
  import_name_lower = install_name_lower
55
56
  else:
56
57
  import_name_lower = import_name.lower().replace('-', '_')
58
+
57
59
  vtp = venv_target_path(venv, allow_nonexistent=True, debug=debug)
58
60
  if not vtp.exists():
59
61
  if debug:
60
- dprint(f"Venv '{venv}' does not exist, cannot import '{import_name}'.", color=False)
62
+ dprint(
63
+ (
64
+ "Venv '{venv}' does not exist, cannot import "
65
+ + f"'{import_name}'."
66
+ ),
67
+ color = False,
68
+ )
61
69
  return None
70
+
71
+ venv_target_candidate_paths = [vtp]
72
+ if venv is None:
73
+ site_user_packages_dirs = [pathlib.Path(site.getusersitepackages())]
74
+ site_packages_dirs = [pathlib.Path(path) for path in site.getsitepackages()]
75
+
76
+ paths_to_add = [
77
+ path
78
+ for path in site_user_packages_dirs + site_packages_dirs
79
+ if path not in venv_target_candidate_paths
80
+ ]
81
+ venv_target_candidate_paths += paths_to_add
82
+
62
83
  candidates = []
63
- for file_name in os.listdir(vtp):
64
- file_name_lower = file_name.lower().replace('-', '_')
65
- if not file_name_lower.startswith(import_name_lower):
66
- continue
67
- if file_name.endswith('dist_info'):
84
+ for venv_target_candidate in venv_target_candidate_paths:
85
+ try:
86
+ file_names = os.listdir(venv_target_candidate)
87
+ except FileNotFoundError:
68
88
  continue
69
- file_path = vtp / file_name
89
+ for file_name in file_names:
90
+ file_name_lower = file_name.lower().replace('-', '_')
91
+ if not file_name_lower.startswith(import_name_lower):
92
+ continue
93
+ if file_name.endswith('dist_info'):
94
+ continue
95
+ file_path = venv_target_candidate / file_name
70
96
 
71
- ### Most likely: Is a directory with __init__.py
72
- if file_name_lower == import_name_lower and file_path.is_dir():
73
- init_path = file_path / '__init__.py'
74
- if init_path.exists():
75
- candidates.append(init_path)
97
+ ### Most likely: Is a directory with __init__.py
98
+ if file_name_lower == import_name_lower and file_path.is_dir():
99
+ init_path = file_path / '__init__.py'
100
+ if init_path.exists():
101
+ candidates.append(init_path)
76
102
 
77
- ### May be a standalone .py file.
78
- elif file_name_lower == import_name_lower + '.py':
79
- candidates.append(file_path)
103
+ ### May be a standalone .py file.
104
+ elif file_name_lower == import_name_lower + '.py':
105
+ candidates.append(file_path)
80
106
 
81
- ### Compiled wheels (e.g. pyodbc)
82
- elif file_name_lower.startswith(import_name_lower + '.'):
83
- candidates.append(file_path)
107
+ ### Compiled wheels (e.g. pyodbc)
108
+ elif file_name_lower.startswith(import_name_lower + '.'):
109
+ candidates.append(file_path)
84
110
 
85
111
  if len(candidates) == 1:
86
112
  return candidates[0]
@@ -466,12 +492,13 @@ def _get_package_metadata(import_name: str, venv: Optional[str]) -> Dict[str, st
466
492
  import re
467
493
  from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
468
494
  install_name = _import_to_install_name(import_name)
469
- _args = ['show', install_name]
495
+ _args = ['pip', 'show', install_name]
470
496
  if venv is not None:
471
497
  cache_dir_path = VIRTENV_RESOURCES_PATH / venv / 'cache'
472
- _args += ['--cache-dir', str(cache_dir_path)]
498
+ _args += ['--cache-dir', cache_dir_path.as_posix()]
499
+
473
500
  proc = run_python_package(
474
- 'pip', _args,
501
+ 'uv', _args,
475
502
  capture_output=True, as_proc=True, venv=venv, universal_newlines=True,
476
503
  )
477
504
  outs, errs = proc.communicate()
@@ -680,12 +707,22 @@ def need_update(
680
707
  return False
681
708
 
682
709
 
683
- def get_pip(venv: Optional[str] = 'mrsm', debug: bool=False) -> bool:
710
+ def get_pip(
711
+ venv: Optional[str] = 'mrsm',
712
+ color: bool = True,
713
+ debug: bool = False,
714
+ ) -> bool:
684
715
  """
685
716
  Download and run the get-pip.py script.
686
717
 
687
718
  Parameters
688
719
  ----------
720
+ venv: Optional[str], default 'mrsm'
721
+ The virtual environment into which to install `pip`.
722
+
723
+ color: bool, default True
724
+ If `True`, force color output.
725
+
689
726
  debug: bool, default False
690
727
  Verbosity toggle.
691
728
 
@@ -708,7 +745,7 @@ def get_pip(venv: Optional[str] = 'mrsm', debug: bool=False) -> bool:
708
745
  if venv is not None:
709
746
  init_venv(venv=venv, debug=debug)
710
747
  cmd_list = [venv_executable(venv=venv), dest.as_posix()]
711
- return subprocess.call(cmd_list, env=_get_pip_os_env()) == 0
748
+ return subprocess.call(cmd_list, env=_get_pip_os_env(color=color)) == 0
712
749
 
713
750
 
714
751
  def pip_install(
@@ -721,6 +758,8 @@ def pip_install(
721
758
  check_pypi: bool = True,
722
759
  check_wheel: bool = True,
723
760
  _uninstall: bool = False,
761
+ _from_completely_uninstall: bool = False,
762
+ _install_uv_pip: bool = True,
724
763
  color: bool = True,
725
764
  silent: bool = False,
726
765
  debug: bool = False,
@@ -776,7 +815,9 @@ def pip_install(
776
815
 
777
816
  """
778
817
  from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
818
+ from meerschaum.config import get_config
779
819
  from meerschaum.utils.warnings import warn
820
+ from meerschaum.utils.misc import is_android
780
821
  if args is None:
781
822
  args = ['--upgrade'] if not _uninstall else []
782
823
  if color:
@@ -787,10 +828,43 @@ def pip_install(
787
828
  have_wheel = venv_contains_package('wheel', venv=venv, debug=debug)
788
829
 
789
830
  _args = list(args)
790
- have_pip = venv_contains_package('pip', venv=venv, debug=debug)
831
+ have_pip = venv_contains_package('pip', venv=None, debug=debug)
832
+ try:
833
+ import pip
834
+ have_pip = True
835
+ except ImportError:
836
+ have_pip = False
837
+ try:
838
+ import uv
839
+ uv_bin = uv.find_uv_bin()
840
+ have_uv_pip = True
841
+ except (ImportError, FileNotFoundError):
842
+ uv_bin = None
843
+ have_uv_pip = False
844
+ if have_pip and not have_uv_pip and _install_uv_pip and not is_android():
845
+ if not pip_install(
846
+ 'uv',
847
+ venv = None,
848
+ debug = debug,
849
+ _install_uv_pip = False,
850
+ check_update = False,
851
+ check_pypi = False,
852
+ check_wheel = False,
853
+ ):
854
+ warn(
855
+ f"Failed to install `uv` for virtual environment '{venv}'.",
856
+ color = False,
857
+ )
858
+
859
+ use_uv_pip = (
860
+ venv_contains_package('uv', venv=None, debug=debug)
861
+ and uv_bin is not None
862
+ and venv is not None
863
+ )
864
+
791
865
  import sys
792
- if not have_pip:
793
- if not get_pip(venv=venv, debug=debug):
866
+ if not have_pip and not use_uv_pip:
867
+ if not get_pip(venv=venv, color=color, debug=debug):
794
868
  import sys
795
869
  minor = sys.version_info.minor
796
870
  print(
@@ -806,13 +880,18 @@ def pip_install(
806
880
 
807
881
  with Venv(venv, debug=debug):
808
882
  if venv is not None:
809
- if '--ignore-installed' not in args and '-I' not in _args and not _uninstall:
883
+ if (
884
+ '--ignore-installed' not in args
885
+ and '-I' not in _args
886
+ and not _uninstall
887
+ and not use_uv_pip
888
+ ):
810
889
  _args += ['--ignore-installed']
811
890
  if '--cache-dir' not in args and not _uninstall:
812
891
  cache_dir_path = VIRTENV_RESOURCES_PATH / venv / 'cache'
813
892
  _args += ['--cache-dir', str(cache_dir_path)]
814
893
 
815
- if 'pip' not in ' '.join(_args):
894
+ if 'pip' not in ' '.join(_args) and not use_uv_pip:
816
895
  if check_update and not _uninstall:
817
896
  pip = attempt_import('pip', venv=venv, install=False, debug=debug, lazy=False)
818
897
  if need_update(pip, check_pypi=check_pypi, debug=debug):
@@ -820,17 +899,20 @@ def pip_install(
820
899
 
821
900
  _args = (['install'] if not _uninstall else ['uninstall']) + _args
822
901
 
823
- if check_wheel and not _uninstall:
902
+ if check_wheel and not _uninstall and not use_uv_pip:
824
903
  if not have_wheel:
825
904
  if not pip_install(
826
- 'setuptools', 'wheel',
905
+ 'setuptools', 'wheel', 'uv',
827
906
  venv = venv,
828
- check_update = False, check_pypi = False,
829
- check_wheel = False, debug = debug,
907
+ check_update = False,
908
+ check_pypi = False,
909
+ check_wheel = False,
910
+ debug = debug,
911
+ _install_uv_pip = False,
830
912
  ):
831
913
  warn(
832
914
  (
833
- "Failed to install `setuptools` and `wheel` for virtual "
915
+ "Failed to install `setuptools`, `wheel`, and `uv` for virtual "
834
916
  + f"environment '{venv}'."
835
917
  ),
836
918
  color = False,
@@ -838,24 +920,24 @@ def pip_install(
838
920
 
839
921
  if requirements_file_path is not None:
840
922
  _args.append('-r')
841
- _args.append(str(pathlib.Path(requirements_file_path).resolve()))
923
+ _args.append(pathlib.Path(requirements_file_path).resolve().as_posix())
842
924
 
843
925
  if not ANSI and '--no-color' not in _args:
844
926
  _args.append('--no-color')
845
927
 
846
- if '--no-input' not in _args:
928
+ if '--no-input' not in _args and not use_uv_pip:
847
929
  _args.append('--no-input')
848
930
 
849
- if _uninstall and '-y' not in _args:
931
+ if _uninstall and '-y' not in _args and not use_uv_pip:
850
932
  _args.append('-y')
851
933
 
852
- if '--no-warn-conflicts' not in _args and not _uninstall:
934
+ if '--no-warn-conflicts' not in _args and not _uninstall and not use_uv_pip:
853
935
  _args.append('--no-warn-conflicts')
854
936
 
855
- if '--disable-pip-version-check' not in _args:
937
+ if '--disable-pip-version-check' not in _args and not use_uv_pip:
856
938
  _args.append('--disable-pip-version-check')
857
939
 
858
- if '--target' not in _args and '-t' not in _args and not _uninstall:
940
+ if '--target' not in _args and '-t' not in _args and not (not use_uv_pip and _uninstall):
859
941
  if venv is not None:
860
942
  _args += ['--target', venv_target_path(venv, debug=debug)]
861
943
  elif (
@@ -863,12 +945,14 @@ def pip_install(
863
945
  and '-t' not in _args
864
946
  and not inside_venv()
865
947
  and not _uninstall
948
+ and not use_uv_pip
866
949
  ):
867
950
  _args += ['--user']
868
951
 
869
952
  if debug:
870
953
  if '-v' not in _args or '-vv' not in _args or '-vvv' not in _args:
871
- pass
954
+ if use_uv_pip:
955
+ _args.append('--verbose')
872
956
  else:
873
957
  if '-q' not in _args or '-qq' not in _args or '-qqq' not in _args:
874
958
  pass
@@ -883,10 +967,10 @@ def pip_install(
883
967
  if not silent:
884
968
  print(msg)
885
969
 
886
- if not _uninstall:
970
+ if _uninstall and not _from_completely_uninstall and not use_uv_pip:
887
971
  for install_name in _packages:
888
972
  _install_no_version = get_install_no_version(install_name)
889
- if _install_no_version in ('pip', 'wheel'):
973
+ if _install_no_version in ('pip', 'wheel', 'uv'):
890
974
  continue
891
975
  if not completely_uninstall_package(
892
976
  _install_no_version,
@@ -896,11 +980,17 @@ def pip_install(
896
980
  f"Failed to clean up package '{_install_no_version}'.",
897
981
  )
898
982
 
983
+ ### NOTE: Only append the `--prerelease=allow` flag if we explicitly depend on a prerelease.
984
+ if use_uv_pip:
985
+ _args.insert(0, 'pip')
986
+ if not _uninstall and get_prerelease_dependencies(_packages):
987
+ _args.append('--prerelease=allow')
988
+
899
989
  rc = run_python_package(
900
- 'pip',
990
+ ('pip' if not use_uv_pip else 'uv'),
901
991
  _args + _packages,
902
- venv = venv,
903
- env = _get_pip_os_env(),
992
+ venv = None,
993
+ env = _get_pip_os_env(color=color),
904
994
  debug = debug,
905
995
  )
906
996
  if debug:
@@ -918,6 +1008,33 @@ def pip_install(
918
1008
  return success
919
1009
 
920
1010
 
1011
+ def get_prerelease_dependencies(_packages: Optional[List[str]] = None):
1012
+ """
1013
+ Return a list of explicitly prerelease dependencies from a list of packages.
1014
+ """
1015
+ if _packages is None:
1016
+ _packages = list(all_packages.keys())
1017
+ prelrease_strings = ['dev', 'rc', 'a']
1018
+ prerelease_packages = []
1019
+ for install_name in _packages:
1020
+ _install_no_version = get_install_no_version(install_name)
1021
+ import_name = _install_to_import_name(install_name)
1022
+ install_with_version = _import_to_install_name(import_name)
1023
+ version_only = (
1024
+ install_with_version.lower().replace(_install_no_version.lower(), '')
1025
+ .split(']')[-1]
1026
+ )
1027
+
1028
+ is_prerelease = False
1029
+ for prelrease_string in prelrease_strings:
1030
+ if prelrease_string in version_only:
1031
+ is_prerelease = True
1032
+
1033
+ if is_prerelease:
1034
+ prerelease_packages.append(install_name)
1035
+ return prerelease_packages
1036
+
1037
+
921
1038
  def completely_uninstall_package(
922
1039
  install_name: str,
923
1040
  venv: str = 'mrsm',
@@ -944,7 +1061,7 @@ def completely_uninstall_package(
944
1061
  continue
945
1062
  installed_versions.append(file_name)
946
1063
 
947
- max_attempts = len(installed_versions) + 1
1064
+ max_attempts = len(installed_versions)
948
1065
  while attempts < max_attempts:
949
1066
  if not venv_contains_package(
950
1067
  _install_to_import_name(_install_no_version),
@@ -953,8 +1070,10 @@ def completely_uninstall_package(
953
1070
  return True
954
1071
  if not pip_uninstall(
955
1072
  _install_no_version,
956
- venv=venv,
957
- silent=(not debug), debug=debug
1073
+ venv = venv,
1074
+ silent = (not debug),
1075
+ _from_completely_uninstall = True,
1076
+ debug = debug,
958
1077
  ):
959
1078
  return False
960
1079
  attempts += 1
@@ -1031,6 +1150,10 @@ def run_python_package(
1031
1150
  if cwd is not None:
1032
1151
  os.chdir(cwd)
1033
1152
  executable = venv_executable(venv=venv)
1153
+ venv_path = (VIRTENV_RESOURCES_PATH / venv) if venv is not None else None
1154
+ env_dict = kw.get('env', os.environ).copy()
1155
+ if venv_path is not None:
1156
+ env_dict.update({'VIRTUAL_ENV': venv_path.as_posix()})
1034
1157
  command = [executable, '-m', str(package_name)] + [str(a) for a in args]
1035
1158
  import traceback
1036
1159
  if debug:
@@ -1055,7 +1178,7 @@ def run_python_package(
1055
1178
  command,
1056
1179
  stdout = stdout,
1057
1180
  stderr = stderr,
1058
- env = kw.get('env', os.environ),
1181
+ env = env_dict,
1059
1182
  )
1060
1183
  to_return = proc if as_proc else proc.wait()
1061
1184
  except KeyboardInterrupt:
@@ -1075,9 +1198,10 @@ def attempt_import(
1075
1198
  check_update: bool = False,
1076
1199
  check_pypi: bool = False,
1077
1200
  check_is_installed: bool = True,
1201
+ allow_outside_venv: bool = True,
1078
1202
  color: bool = True,
1079
1203
  debug: bool = False
1080
- ) -> Union[Any, Tuple[Any]]:
1204
+ ) -> Any:
1081
1205
  """
1082
1206
  Raise a warning if packages are not installed; otherwise import and return modules.
1083
1207
  If `lazy` is `True`, return lazy-imported modules.
@@ -1120,6 +1244,15 @@ def attempt_import(
1120
1244
  check_is_installed: bool, default True
1121
1245
  If `True`, check if the package is contained in the virtual environment.
1122
1246
 
1247
+ allow_outside_venv: bool, default True
1248
+ If `True`, search outside of the specified virtual environment
1249
+ if the package cannot be found.
1250
+ Setting to `False` will reinstall the package into a virtual environment, even if it
1251
+ is installed outside.
1252
+
1253
+ color: bool, default True
1254
+ If `False`, do not print ANSI colors.
1255
+
1123
1256
  Returns
1124
1257
  -------
1125
1258
  The specified modules. If they're not available and `install` is `True`, it will first
@@ -1201,6 +1334,7 @@ def attempt_import(
1201
1334
  name,
1202
1335
  venv = venv,
1203
1336
  split = split,
1337
+ allow_outside_venv = allow_outside_venv,
1204
1338
  debug = debug,
1205
1339
  )
1206
1340
  _is_installed_first_check[name] = package_is_installed
@@ -1346,7 +1480,9 @@ def import_rich(
1346
1480
  'pygments', lazy=False,
1347
1481
  )
1348
1482
  rich = attempt_import(
1349
- 'rich', lazy=lazy, **kw)
1483
+ 'rich', lazy=lazy,
1484
+ **kw
1485
+ )
1350
1486
  return rich
1351
1487
 
1352
1488
 
@@ -1580,10 +1716,26 @@ def is_installed(
1580
1716
  import_name: str,
1581
1717
  venv: Optional[str] = 'mrsm',
1582
1718
  split: bool = True,
1719
+ allow_outside_venv: bool = True,
1583
1720
  debug: bool = False,
1584
1721
  ) -> bool:
1585
1722
  """
1586
1723
  Check whether a package is installed.
1724
+
1725
+ Parameters
1726
+ ----------
1727
+ import_name: str
1728
+ The import name of the module.
1729
+
1730
+ venv: Optional[str], default 'mrsm'
1731
+ The venv in which to search for the module.
1732
+
1733
+ split: bool, default True
1734
+ If `True`, split on periods to determine the root module name.
1735
+
1736
+ allow_outside_venv: bool, default True
1737
+ If `True`, search outside of the specified virtual environment
1738
+ if the package cannot be found.
1587
1739
  """
1588
1740
  if debug:
1589
1741
  from meerschaum.utils.debug import dprint
@@ -1594,7 +1746,11 @@ def is_installed(
1594
1746
  spec_path = pathlib.Path(
1595
1747
  get_module_path(root_name, venv=venv, debug=debug)
1596
1748
  or
1597
- importlib.util.find_spec(root_name).origin
1749
+ (
1750
+ importlib.util.find_spec(root_name).origin
1751
+ if venv is not None and allow_outside_venv
1752
+ else None
1753
+ )
1598
1754
  )
1599
1755
  except (ModuleNotFoundError, ValueError, AttributeError, TypeError) as e:
1600
1756
  spec_path = None
@@ -1623,6 +1779,8 @@ def venv_contains_package(
1623
1779
  """
1624
1780
  Search the contents of a virtual environment for a package.
1625
1781
  """
1782
+ import site
1783
+ import pathlib
1626
1784
  root_name = import_name.split('.')[0] if split else import_name
1627
1785
  return get_module_path(root_name, venv=venv, debug=debug) is not None
1628
1786
 
@@ -1686,7 +1844,7 @@ def _monkey_patch_get_distribution(_dist: str, _version: str) -> None:
1686
1844
  pkg_resources.get_distribution = _get_distribution
1687
1845
 
1688
1846
 
1689
- def _get_pip_os_env():
1847
+ def _get_pip_os_env(color: bool = True):
1690
1848
  """
1691
1849
  Return the environment variables context in which `pip` should be run.
1692
1850
  See PEP 668 for why we are overriding the environment.
@@ -1695,5 +1853,6 @@ def _get_pip_os_env():
1695
1853
  pip_os_env = os.environ.copy()
1696
1854
  pip_os_env.update({
1697
1855
  'PIP_BREAK_SYSTEM_PACKAGES': 'true',
1856
+ ('FORCE_COLOR' if color else 'NO_COLOR'): '1',
1698
1857
  })
1699
1858
  return pip_os_env
@@ -53,6 +53,7 @@ packages: Dict[str, Dict[str, str]] = {
53
53
  'dill' : 'dill>=0.3.3',
54
54
  'virtualenv' : 'virtualenv>=20.1.0',
55
55
  'apscheduler' : 'APScheduler>=4.0.0a5',
56
+ 'uv' : 'uv>=0.2.11',
56
57
  },
57
58
  'drivers': {
58
59
  'cryptography' : 'cryptography>=38.0.1',
@@ -9,10 +9,16 @@ See `meerschaum.utils.pool` for multiprocessing and
9
9
  """
10
10
 
11
11
  from __future__ import annotations
12
- import os, signal, subprocess, sys, platform
12
+ import os, signal, subprocess, sys, platform, traceback
13
13
  from meerschaum.utils.typing import Union, Optional, Any, Callable, Dict, Tuple
14
14
  from meerschaum.config.static import STATIC_CONFIG
15
15
 
16
+ _child_processes = []
17
+ def signal_handler(sig, frame):
18
+ for child in _child_processes:
19
+ child.send_signal(sig)
20
+ child.wait()
21
+
16
22
  def run_process(
17
23
  *args,
18
24
  foreground: bool = False,
@@ -73,6 +79,7 @@ def run_process(
73
79
  sys.stdout.write(line.decode('utf-8'))
74
80
  sys.stdout.flush()
75
81
 
82
+
76
83
  if capture_output or line_callback is not None:
77
84
  kw['stdout'] = subprocess.PIPE
78
85
  kw['stderr'] = subprocess.STDOUT
@@ -123,6 +130,7 @@ def run_process(
123
130
 
124
131
  try:
125
132
  child = subprocess.Popen(*args, **kw)
133
+ _child_processes.append(child)
126
134
 
127
135
  # we can't set the process group id from the parent since the child
128
136
  # will already have exec'd. and we can't SIGSTOP it before exec,
@@ -147,7 +155,9 @@ def run_process(
147
155
  store_proc_dict[store_proc_key] = child
148
156
  _ret = poll_process(child, line_callback) if line_callback is not None else child.wait()
149
157
  ret = _ret if not as_proc else child
150
-
158
+ except KeyboardInterrupt:
159
+ child.send_signal(signal.SIGINT)
160
+ ret = child.wait() if not as_proc else child
151
161
  finally:
152
162
  if foreground:
153
163
  # we have to mask SIGTTOU because tcsetpgrp
@@ -8,7 +8,7 @@ Schedule processes and threads.
8
8
 
9
9
  from __future__ import annotations
10
10
  import sys
11
- from datetime import datetime, timezone, timedelta, timedelta
11
+ from datetime import datetime, timezone, timedelta
12
12
  import meerschaum as mrsm
13
13
  from meerschaum.utils.typing import Callable, Any, Optional, List, Dict
14
14
 
@@ -15,7 +15,7 @@ __all__ = sorted([
15
15
  'activate_venv', 'deactivate_venv', 'init_venv',
16
16
  'inside_venv', 'is_venv_active', 'venv_exec',
17
17
  'venv_executable', 'venv_exists', 'venv_target_path',
18
- 'Venv', 'get_venvs', 'verify_venv',
18
+ 'Venv', 'get_venvs', 'verify_venv', 'get_module_venv',
19
19
  ])
20
20
  __pdoc__ = {'Venv': True}
21
21
 
@@ -79,7 +79,7 @@ def activate_venv(
79
79
  else:
80
80
  threads_active_venvs[thread_id][venv] += 1
81
81
 
82
- target = str(venv_target_path(venv, debug=debug))
82
+ target = venv_target_path(venv, debug=debug).as_posix()
83
83
  if venv in active_venvs_order:
84
84
  sys.path.remove(target)
85
85
  try:
@@ -171,7 +171,7 @@ def deactivate_venv(
171
171
  if sys.path is None:
172
172
  return False
173
173
 
174
- target = str(venv_target_path(venv, allow_nonexistent=force, debug=debug))
174
+ target = venv_target_path(venv, allow_nonexistent=force, debug=debug).as_posix()
175
175
  with LOCKS['sys.path']:
176
176
  if target in sys.path:
177
177
  sys.path.remove(target)
@@ -361,6 +361,8 @@ def init_venv(
361
361
  verified_venvs.add(venv)
362
362
  return True
363
363
 
364
+ import io
365
+ from contextlib import redirect_stdout, redirect_stderr
364
366
  import sys, platform, os, pathlib, shutil
365
367
  from meerschaum.config.static import STATIC_CONFIG
366
368
  from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
@@ -381,25 +383,34 @@ def init_venv(
381
383
  verified_venvs.add(venv)
382
384
  return True
383
385
 
384
- from meerschaum.utils.packages import run_python_package, attempt_import
386
+ from meerschaum.utils.packages import run_python_package, attempt_import, _get_pip_os_env
385
387
  global tried_virtualenv
386
388
  try:
387
389
  import venv as _venv
390
+ uv = attempt_import('uv', venv=None, debug=debug)
388
391
  virtualenv = None
389
392
  except ImportError:
390
393
  _venv = None
394
+ uv = None
391
395
  virtualenv = None
392
-
393
396
 
394
397
  _venv_success = False
395
- if _venv is not None:
396
- import io
397
- from contextlib import redirect_stdout
398
+
399
+ if uv is not None:
400
+ _venv_success = run_python_package(
401
+ 'uv',
402
+ ['venv', venv_path.as_posix(), '-q'],
403
+ venv = None,
404
+ env = _get_pip_os_env(),
405
+ debug = debug,
406
+ ) == 0
407
+
408
+ if _venv is not None and not _venv_success:
398
409
  f = io.StringIO()
399
410
  with redirect_stdout(f):
400
411
  _venv_success = run_python_package(
401
412
  'venv',
402
- [str(venv_path)] + (
413
+ [venv_path.as_posix()] + (
403
414
  ['--symlinks'] if platform.system() != 'Windows' else []
404
415
  ),
405
416
  venv=None, debug=debug
@@ -438,7 +449,7 @@ def init_venv(
438
449
  except Exception as e:
439
450
  import traceback
440
451
  traceback.print_exc()
441
- virtualenv.cli_run([str(venv_path)])
452
+ virtualenv.cli_run([venv_path.as_posix()])
442
453
  if dist_packages_path.exists():
443
454
  vtp.mkdir(exist_ok=True, parents=True)
444
455
  for file_path in dist_packages_path.glob('*'):
@@ -614,7 +625,7 @@ def venv_target_path(
614
625
  return site_path
615
626
 
616
627
  ### Allow for dist-level paths (running as root).
617
- for possible_dist in reversed(site.getsitepackages()):
628
+ for possible_dist in site.getsitepackages():
618
629
  dist_path = pathlib.Path(possible_dist)
619
630
  if not dist_path.exists():
620
631
  continue
@@ -698,4 +709,28 @@ def get_venvs() -> List[str]:
698
709
  return venvs
699
710
 
700
711
 
712
+ def get_module_venv(module) -> Union[str, None]:
713
+ """
714
+ Return the virtual environment where an imported module is installed.
715
+
716
+ Parameters
717
+ ----------
718
+ module: ModuleType
719
+ The imported module to inspect.
720
+
721
+ Returns
722
+ -------
723
+ The name of a venv or `None`.
724
+ """
725
+ import pathlib
726
+ from meerschaum.config.paths import VIRTENV_RESOURCES_PATH
727
+ module_path = pathlib.Path(module.__file__).resolve()
728
+ try:
729
+ rel_path = module_path.relative_to(VIRTENV_RESOURCES_PATH)
730
+ except ValueError:
731
+ return None
732
+
733
+ return rel_path.as_posix().split('/', maxsplit=1)[0]
734
+
735
+
701
736
  from meerschaum.utils.venv._Venv import Venv