ominfra 0.0.0.dev178__py3-none-any.whl → 0.0.0.dev180__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.
ominfra/scripts/manage.py CHANGED
@@ -111,7 +111,7 @@ CommandT = ta.TypeVar('CommandT', bound='Command')
111
111
  CommandOutputT = ta.TypeVar('CommandOutputT', bound='Command.Output')
112
112
 
113
113
  # ../../omlish/argparse/cli.py
114
- ArgparseCommandFn = ta.Callable[[], ta.Optional[int]] # ta.TypeAlias
114
+ ArgparseCmdFn = ta.Callable[[], ta.Optional[int]] # ta.TypeAlias
115
115
 
116
116
  # ../../omlish/lite/contextmanagers.py
117
117
  ExitStackedT = ta.TypeVar('ExitStackedT', bound='ExitStacked')
@@ -2766,21 +2766,6 @@ def read_package_resource_text(package: str, resource: str) -> str:
2766
2766
  return importlib.resources.read_text(package, resource)
2767
2767
 
2768
2768
 
2769
- ########################################
2770
- # ../../../omlish/lite/shlex.py
2771
-
2772
-
2773
- def shlex_needs_quote(s: str) -> bool:
2774
- return bool(s) and len(list(shlex.shlex(s))) > 1
2775
-
2776
-
2777
- def shlex_maybe_quote(s: str) -> str:
2778
- if shlex_needs_quote(s):
2779
- return shlex.quote(s)
2780
- else:
2781
- return s
2782
-
2783
-
2784
2769
  ########################################
2785
2770
  # ../../../omlish/lite/strings.py
2786
2771
 
@@ -3580,6 +3565,21 @@ def relative_symlink(
3580
3565
  )
3581
3566
 
3582
3567
 
3568
+ ########################################
3569
+ # ../../../omlish/shlex.py
3570
+
3571
+
3572
+ def shlex_needs_quote(s: str) -> bool:
3573
+ return bool(s) and len(list(shlex.shlex(s))) > 1
3574
+
3575
+
3576
+ def shlex_maybe_quote(s: str) -> str:
3577
+ if shlex_needs_quote(s):
3578
+ return shlex.quote(s)
3579
+ else:
3580
+ return s
3581
+
3582
+
3583
3583
  ########################################
3584
3584
  # ../../../omdev/packaging/specifiers.py
3585
3585
  # Copyright (c) Donald Stufft and individual contributors.
@@ -4546,15 +4546,15 @@ def argparse_arg(*args, **kwargs) -> ArgparseArg:
4546
4546
 
4547
4547
 
4548
4548
  @dc.dataclass(eq=False)
4549
- class ArgparseCommand:
4549
+ class ArgparseCmd:
4550
4550
  name: str
4551
- fn: ArgparseCommandFn
4551
+ fn: ArgparseCmdFn
4552
4552
  args: ta.Sequence[ArgparseArg] = () # noqa
4553
4553
 
4554
4554
  # _: dc.KW_ONLY
4555
4555
 
4556
4556
  aliases: ta.Optional[ta.Sequence[str]] = None
4557
- parent: ta.Optional['ArgparseCommand'] = None
4557
+ parent: ta.Optional['ArgparseCmd'] = None
4558
4558
  accepts_unknown: bool = False
4559
4559
 
4560
4560
  def __post_init__(self) -> None:
@@ -4569,7 +4569,7 @@ class ArgparseCommand:
4569
4569
 
4570
4570
  check.arg(callable(self.fn))
4571
4571
  check.arg(all(isinstance(a, ArgparseArg) for a in self.args))
4572
- check.isinstance(self.parent, (ArgparseCommand, type(None)))
4572
+ check.isinstance(self.parent, (ArgparseCmd, type(None)))
4573
4573
  check.isinstance(self.accepts_unknown, bool)
4574
4574
 
4575
4575
  functools.update_wrapper(self, self.fn)
@@ -4583,21 +4583,21 @@ class ArgparseCommand:
4583
4583
  return self.fn(*args, **kwargs)
4584
4584
 
4585
4585
 
4586
- def argparse_command(
4586
+ def argparse_cmd(
4587
4587
  *args: ArgparseArg,
4588
4588
  name: ta.Optional[str] = None,
4589
4589
  aliases: ta.Optional[ta.Iterable[str]] = None,
4590
- parent: ta.Optional[ArgparseCommand] = None,
4590
+ parent: ta.Optional[ArgparseCmd] = None,
4591
4591
  accepts_unknown: bool = False,
4592
- ) -> ta.Any: # ta.Callable[[ArgparseCommandFn], ArgparseCommand]: # FIXME
4592
+ ) -> ta.Any: # ta.Callable[[ArgparseCmdFn], ArgparseCmd]: # FIXME
4593
4593
  for arg in args:
4594
4594
  check.isinstance(arg, ArgparseArg)
4595
4595
  check.isinstance(name, (str, type(None)))
4596
- check.isinstance(parent, (ArgparseCommand, type(None)))
4596
+ check.isinstance(parent, (ArgparseCmd, type(None)))
4597
4597
  check.not_isinstance(aliases, str)
4598
4598
 
4599
4599
  def inner(fn):
4600
- return ArgparseCommand(
4600
+ return ArgparseCmd(
4601
4601
  (name if name is not None else fn.__name__).replace('_', '-'),
4602
4602
  fn,
4603
4603
  args,
@@ -4652,7 +4652,7 @@ class ArgparseCli:
4652
4652
  for bns in [bcls.__dict__ for bcls in reversed(mro)] + [ns]:
4653
4653
  bseen = set() # type: ignore
4654
4654
  for k, v in bns.items():
4655
- if isinstance(v, (ArgparseCommand, ArgparseArg)):
4655
+ if isinstance(v, (ArgparseCmd, ArgparseArg)):
4656
4656
  check.not_in(v, bseen)
4657
4657
  bseen.add(v)
4658
4658
  objs[k] = v
@@ -4679,7 +4679,7 @@ class ArgparseCli:
4679
4679
  subparsers = parser.add_subparsers()
4680
4680
 
4681
4681
  for att, obj in objs.items():
4682
- if isinstance(obj, ArgparseCommand):
4682
+ if isinstance(obj, ArgparseCmd):
4683
4683
  if obj.parent is not None:
4684
4684
  raise NotImplementedError
4685
4685
 
@@ -4741,7 +4741,7 @@ class ArgparseCli:
4741
4741
 
4742
4742
  #
4743
4743
 
4744
- def _bind_cli_cmd(self, cmd: ArgparseCommand) -> ta.Callable:
4744
+ def _bind_cli_cmd(self, cmd: ArgparseCmd) -> ta.Callable:
4745
4745
  return cmd.__get__(self, type(self))
4746
4746
 
4747
4747
  def prepare_cli_run(self) -> ta.Optional[ta.Callable]:
@@ -5126,30 +5126,6 @@ def as_injector_bindings(*args: InjectorBindingOrBindings) -> InjectorBindings:
5126
5126
  ##
5127
5127
 
5128
5128
 
5129
- @dc.dataclass(frozen=True)
5130
- class OverridesInjectorBindings(InjectorBindings):
5131
- p: InjectorBindings
5132
- m: ta.Mapping[InjectorKey, InjectorBinding]
5133
-
5134
- def bindings(self) -> ta.Iterator[InjectorBinding]:
5135
- for b in self.p.bindings():
5136
- yield self.m.get(b.key, b)
5137
-
5138
-
5139
- def injector_override(p: InjectorBindings, *args: InjectorBindingOrBindings) -> InjectorBindings:
5140
- m: ta.Dict[InjectorKey, InjectorBinding] = {}
5141
-
5142
- for b in as_injector_bindings(*args).bindings():
5143
- if b.key in m:
5144
- raise DuplicateInjectorKeyError(b.key)
5145
- m[b.key] = b
5146
-
5147
- return OverridesInjectorBindings(p, m)
5148
-
5149
-
5150
- ##
5151
-
5152
-
5153
5129
  def build_injector_provider_map(bs: InjectorBindings) -> ta.Mapping[InjectorKey, InjectorProvider]:
5154
5130
  pm: ta.Dict[InjectorKey, InjectorProvider] = {}
5155
5131
  am: ta.Dict[InjectorKey, ta.List[InjectorProvider]] = {}
@@ -5173,6 +5149,31 @@ def build_injector_provider_map(bs: InjectorBindings) -> ta.Mapping[InjectorKey,
5173
5149
  return pm
5174
5150
 
5175
5151
 
5152
+ ###
5153
+ # overrides
5154
+
5155
+
5156
+ @dc.dataclass(frozen=True)
5157
+ class OverridesInjectorBindings(InjectorBindings):
5158
+ p: InjectorBindings
5159
+ m: ta.Mapping[InjectorKey, InjectorBinding]
5160
+
5161
+ def bindings(self) -> ta.Iterator[InjectorBinding]:
5162
+ for b in self.p.bindings():
5163
+ yield self.m.get(b.key, b)
5164
+
5165
+
5166
+ def injector_override(p: InjectorBindings, *args: InjectorBindingOrBindings) -> InjectorBindings:
5167
+ m: ta.Dict[InjectorKey, InjectorBinding] = {}
5168
+
5169
+ for b in as_injector_bindings(*args).bindings():
5170
+ if b.key in m:
5171
+ raise DuplicateInjectorKeyError(b.key)
5172
+ m[b.key] = b
5173
+
5174
+ return OverridesInjectorBindings(p, m)
5175
+
5176
+
5176
5177
  ###
5177
5178
  # scopes
5178
5179
 
@@ -5197,7 +5198,7 @@ class InjectorScope(abc.ABC): # noqa
5197
5198
  @dc.dataclass(frozen=True)
5198
5199
  class State:
5199
5200
  seeds: ta.Dict[InjectorKey, ta.Any]
5200
- prvs: ta.Dict[InjectorKey, ta.Any] = dc.field(default_factory=dict)
5201
+ provisions: ta.Dict[InjectorKey, ta.Any] = dc.field(default_factory=dict)
5201
5202
 
5202
5203
  def new_state(self, vs: ta.Mapping[InjectorKey, ta.Any]) -> State:
5203
5204
  vs = dict(vs)
@@ -5277,11 +5278,11 @@ class ScopedInjectorProvider(InjectorProvider):
5277
5278
  def pfn(i: Injector) -> ta.Any:
5278
5279
  st = i[self.sc].state()
5279
5280
  try:
5280
- return st.prvs[self.k]
5281
+ return st.provisions[self.k]
5281
5282
  except KeyError:
5282
5283
  pass
5283
5284
  v = ufn(i)
5284
- st.prvs[self.k] = v
5285
+ st.provisions[self.k] = v
5285
5286
  return v
5286
5287
 
5287
5288
  ufn = self.p.provider_fn()
@@ -5305,9 +5306,7 @@ class _ScopeSeedInjectorProvider(InjectorProvider):
5305
5306
 
5306
5307
 
5307
5308
  def bind_injector_scope(sc: ta.Type[InjectorScope]) -> InjectorBindingOrBindings:
5308
- return as_injector_bindings(
5309
- InjectorBinder.bind(sc, singleton=True),
5310
- )
5309
+ return InjectorBinder.bind(sc, singleton=True)
5311
5310
 
5312
5311
 
5313
5312
  #
@@ -5847,6 +5846,8 @@ class InjectionApi:
5847
5846
  def as_bindings(self, *args: InjectorBindingOrBindings) -> InjectorBindings:
5848
5847
  return as_injector_bindings(*args)
5849
5848
 
5849
+ # overrides
5850
+
5850
5851
  def override(self, p: InjectorBindings, *args: InjectorBindingOrBindings) -> InjectorBindings:
5851
5852
  return injector_override(p, *args)
5852
5853
 
@@ -6705,6 +6706,9 @@ class TempDirAtomicPathSwapping(AtomicPathSwapping):
6705
6706
  # ../../../omdev/interp/types.py
6706
6707
 
6707
6708
 
6709
+ ##
6710
+
6711
+
6708
6712
  # See https://peps.python.org/pep-3149/
6709
6713
  INTERP_OPT_GLYPHS_BY_ATTR: ta.Mapping[str, str] = collections.OrderedDict([
6710
6714
  ('debug', 'd'),
@@ -6736,6 +6740,9 @@ class InterpOpts:
6736
6740
  return s, cls(**kw)
6737
6741
 
6738
6742
 
6743
+ ##
6744
+
6745
+
6739
6746
  @dc.dataclass(frozen=True)
6740
6747
  class InterpVersion:
6741
6748
  version: Version
@@ -6761,6 +6768,9 @@ class InterpVersion:
6761
6768
  return None
6762
6769
 
6763
6770
 
6771
+ ##
6772
+
6773
+
6764
6774
  @dc.dataclass(frozen=True)
6765
6775
  class InterpSpecifier:
6766
6776
  specifier: Specifier
@@ -6788,12 +6798,25 @@ class InterpSpecifier:
6788
6798
  return self.contains(iv)
6789
6799
 
6790
6800
 
6801
+ ##
6802
+
6803
+
6791
6804
  @dc.dataclass(frozen=True)
6792
6805
  class Interp:
6793
6806
  exe: str
6794
6807
  version: InterpVersion
6795
6808
 
6796
6809
 
6810
+ ########################################
6811
+ # ../../../omdev/interp/uv/inject.py
6812
+
6813
+
6814
+ def bind_interp_uv() -> InjectorBindings:
6815
+ lst: ta.List[InjectorBindingOrBindings] = []
6816
+
6817
+ return inj.as_bindings(*lst)
6818
+
6819
+
6797
6820
  ########################################
6798
6821
  # ../../configs.py
6799
6822
 
@@ -7707,6 +7730,50 @@ class AbstractAsyncSubprocesses(BaseSubprocesses):
7707
7730
  return ret.decode().strip()
7708
7731
 
7709
7732
 
7733
+ ########################################
7734
+ # ../../../omdev/interp/providers/base.py
7735
+ """
7736
+ TODO:
7737
+ - backends
7738
+ - local builds
7739
+ - deadsnakes?
7740
+ - uv
7741
+ - loose versions
7742
+ """
7743
+
7744
+
7745
+ ##
7746
+
7747
+
7748
+ class InterpProvider(abc.ABC):
7749
+ name: ta.ClassVar[str]
7750
+
7751
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
7752
+ super().__init_subclass__(**kwargs)
7753
+ if abc.ABC not in cls.__bases__ and 'name' not in cls.__dict__:
7754
+ sfx = 'InterpProvider'
7755
+ if not cls.__name__.endswith(sfx):
7756
+ raise NameError(cls)
7757
+ setattr(cls, 'name', snake_case(cls.__name__[:-len(sfx)]))
7758
+
7759
+ @abc.abstractmethod
7760
+ def get_installed_versions(self, spec: InterpSpecifier) -> ta.Awaitable[ta.Sequence[InterpVersion]]:
7761
+ raise NotImplementedError
7762
+
7763
+ @abc.abstractmethod
7764
+ def get_installed_version(self, version: InterpVersion) -> ta.Awaitable[Interp]:
7765
+ raise NotImplementedError
7766
+
7767
+ async def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
7768
+ return []
7769
+
7770
+ async def install_version(self, version: InterpVersion) -> Interp:
7771
+ raise TypeError
7772
+
7773
+
7774
+ InterpProviders = ta.NewType('InterpProviders', ta.Sequence[InterpProvider])
7775
+
7776
+
7710
7777
  ########################################
7711
7778
  # ../bootstrap.py
7712
7779
 
@@ -8841,7 +8908,92 @@ class InterpInspector:
8841
8908
  return ret
8842
8909
 
8843
8910
 
8844
- INTERP_INSPECTOR = InterpInspector()
8911
+ ########################################
8912
+ # ../../../omdev/interp/resolvers.py
8913
+
8914
+
8915
+ @dc.dataclass(frozen=True)
8916
+ class InterpResolverProviders:
8917
+ providers: ta.Sequence[ta.Tuple[str, InterpProvider]]
8918
+
8919
+
8920
+ class InterpResolver:
8921
+ def __init__(
8922
+ self,
8923
+ providers: InterpResolverProviders,
8924
+ ) -> None:
8925
+ super().__init__()
8926
+
8927
+ self._providers: ta.Mapping[str, InterpProvider] = collections.OrderedDict(providers.providers)
8928
+
8929
+ async def _resolve_installed(self, spec: InterpSpecifier) -> ta.Optional[ta.Tuple[InterpProvider, InterpVersion]]:
8930
+ lst = [
8931
+ (i, si)
8932
+ for i, p in enumerate(self._providers.values())
8933
+ for si in await p.get_installed_versions(spec)
8934
+ if spec.contains(si)
8935
+ ]
8936
+
8937
+ slst = sorted(lst, key=lambda t: (-t[0], t[1].version))
8938
+ if not slst:
8939
+ return None
8940
+
8941
+ bi, bv = slst[-1]
8942
+ bp = list(self._providers.values())[bi]
8943
+ return (bp, bv)
8944
+
8945
+ async def resolve(
8946
+ self,
8947
+ spec: InterpSpecifier,
8948
+ *,
8949
+ install: bool = False,
8950
+ ) -> ta.Optional[Interp]:
8951
+ tup = await self._resolve_installed(spec)
8952
+ if tup is not None:
8953
+ bp, bv = tup
8954
+ return await bp.get_installed_version(bv)
8955
+
8956
+ if not install:
8957
+ return None
8958
+
8959
+ tp = list(self._providers.values())[0] # noqa
8960
+
8961
+ sv = sorted(
8962
+ [s for s in await tp.get_installable_versions(spec) if s in spec],
8963
+ key=lambda s: s.version,
8964
+ )
8965
+ if not sv:
8966
+ return None
8967
+
8968
+ bv = sv[-1]
8969
+ return await tp.install_version(bv)
8970
+
8971
+ async def list(self, spec: InterpSpecifier) -> None:
8972
+ print('installed:')
8973
+ for n, p in self._providers.items():
8974
+ lst = [
8975
+ si
8976
+ for si in await p.get_installed_versions(spec)
8977
+ if spec.contains(si)
8978
+ ]
8979
+ if lst:
8980
+ print(f' {n}')
8981
+ for si in lst:
8982
+ print(f' {si}')
8983
+
8984
+ print()
8985
+
8986
+ print('installable:')
8987
+ for n, p in self._providers.items():
8988
+ lst = [
8989
+ si
8990
+ for si in await p.get_installable_versions(spec)
8991
+ if spec.contains(si)
8992
+ ]
8993
+ if lst:
8994
+ print(f' {n}')
8995
+ for si in lst:
8996
+ print(f' {si}')
8845
8997
 
8846
8998
 
8847
8999
  ########################################
@@ -9495,929 +9647,937 @@ class YumSystemPackageManager(SystemPackageManager):
9495
9647
 
9496
9648
 
9497
9649
  ########################################
9498
- # ../../../omdev/interp/providers.py
9650
+ # ../../../omdev/interp/providers/running.py
9651
+
9652
+
9653
+ class RunningInterpProvider(InterpProvider):
9654
+ @cached_nullary
9655
+ def version(self) -> InterpVersion:
9656
+ return InterpInspector.running().iv
9657
+
9658
+ async def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
9659
+ return [self.version()]
9660
+
9661
+ async def get_installed_version(self, version: InterpVersion) -> Interp:
9662
+ if version != self.version():
9663
+ raise KeyError(version)
9664
+ return Interp(
9665
+ exe=sys.executable,
9666
+ version=self.version(),
9667
+ )
9668
+
9669
+
9670
+ ########################################
9671
+ # ../../../omdev/interp/providers/system.py
9499
9672
  """
9500
9673
  TODO:
9501
- - backends
9502
- - local builds
9503
- - deadsnakes?
9504
- - uv
9505
- - loose versions
9674
+ - python, python3, python3.12, ...
9675
+ - check if path py's are venvs: sys.prefix != sys.base_prefix
9506
9676
  """
9507
9677
 
9508
9678
 
9509
9679
  ##
9510
9680
 
9511
9681
 
9512
- class InterpProvider(abc.ABC):
9513
- name: ta.ClassVar[str]
9682
+ class SystemInterpProvider(InterpProvider):
9683
+ @dc.dataclass(frozen=True)
9684
+ class Options:
9685
+ cmd: str = 'python3' # FIXME: unused lol
9686
+ path: ta.Optional[str] = None
9514
9687
 
9515
- def __init_subclass__(cls, **kwargs: ta.Any) -> None:
9516
- super().__init_subclass__(**kwargs)
9517
- if abc.ABC not in cls.__bases__ and 'name' not in cls.__dict__:
9518
- sfx = 'InterpProvider'
9519
- if not cls.__name__.endswith(sfx):
9520
- raise NameError(cls)
9521
- setattr(cls, 'name', snake_case(cls.__name__[:-len(sfx)]))
9522
-
9523
- @abc.abstractmethod
9524
- def get_installed_versions(self, spec: InterpSpecifier) -> ta.Awaitable[ta.Sequence[InterpVersion]]:
9525
- raise NotImplementedError
9526
-
9527
- @abc.abstractmethod
9528
- def get_installed_version(self, version: InterpVersion) -> ta.Awaitable[Interp]:
9529
- raise NotImplementedError
9530
-
9531
- async def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
9532
- return []
9533
-
9534
- async def install_version(self, version: InterpVersion) -> Interp:
9535
- raise TypeError
9688
+ inspect: bool = False
9536
9689
 
9690
+ def __init__(
9691
+ self,
9692
+ options: Options = Options(),
9693
+ *,
9694
+ inspector: ta.Optional[InterpInspector] = None,
9695
+ ) -> None:
9696
+ super().__init__()
9537
9697
 
9538
- ##
9698
+ self._options = options
9539
9699
 
9700
+ self._inspector = inspector
9540
9701
 
9541
- class RunningInterpProvider(InterpProvider):
9542
- @cached_nullary
9543
- def version(self) -> InterpVersion:
9544
- return InterpInspector.running().iv
9702
+ #
9545
9703
 
9546
- async def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
9547
- return [self.version()]
9704
+ @staticmethod
9705
+ def _re_which(
9706
+ pat: re.Pattern,
9707
+ *,
9708
+ mode: int = os.F_OK | os.X_OK,
9709
+ path: ta.Optional[str] = None,
9710
+ ) -> ta.List[str]:
9711
+ if path is None:
9712
+ path = os.environ.get('PATH', None)
9713
+ if path is None:
9714
+ try:
9715
+ path = os.confstr('CS_PATH')
9716
+ except (AttributeError, ValueError):
9717
+ path = os.defpath
9548
9718
 
9549
- async def get_installed_version(self, version: InterpVersion) -> Interp:
9550
- if version != self.version():
9551
- raise KeyError(version)
9552
- return Interp(
9553
- exe=sys.executable,
9554
- version=self.version(),
9555
- )
9719
+ if not path:
9720
+ return []
9556
9721
 
9722
+ path = os.fsdecode(path)
9723
+ pathlst = path.split(os.pathsep)
9557
9724
 
9558
- ########################################
9559
- # ../commands/inject.py
9725
+ def _access_check(fn: str, mode: int) -> bool:
9726
+ return os.path.exists(fn) and os.access(fn, mode)
9560
9727
 
9728
+ out = []
9729
+ seen = set()
9730
+ for d in pathlst:
9731
+ normdir = os.path.normcase(d)
9732
+ if normdir not in seen:
9733
+ seen.add(normdir)
9734
+ if not _access_check(normdir, mode):
9735
+ continue
9736
+ for thefile in os.listdir(d):
9737
+ name = os.path.join(d, thefile)
9738
+ if not (
9739
+ os.path.isfile(name) and
9740
+ pat.fullmatch(thefile) and
9741
+ _access_check(name, mode)
9742
+ ):
9743
+ continue
9744
+ out.append(name)
9561
9745
 
9562
- ##
9746
+ return out
9563
9747
 
9748
+ @cached_nullary
9749
+ def exes(self) -> ta.List[str]:
9750
+ return self._re_which(
9751
+ re.compile(r'python3(\.\d+)?'),
9752
+ path=self._options.path,
9753
+ )
9564
9754
 
9565
- def bind_command(
9566
- command_cls: ta.Type[Command],
9567
- executor_cls: ta.Optional[ta.Type[CommandExecutor]],
9568
- ) -> InjectorBindings:
9569
- lst: ta.List[InjectorBindingOrBindings] = [
9570
- inj.bind(CommandRegistration(command_cls), array=True),
9571
- ]
9755
+ #
9572
9756
 
9573
- if executor_cls is not None:
9574
- lst.extend([
9575
- inj.bind(executor_cls, singleton=True),
9576
- inj.bind(CommandExecutorRegistration(command_cls, executor_cls), array=True),
9577
- ])
9757
+ async def get_exe_version(self, exe: str) -> ta.Optional[InterpVersion]:
9758
+ if not self._options.inspect:
9759
+ s = os.path.basename(exe)
9760
+ if s.startswith('python'):
9761
+ s = s[len('python'):]
9762
+ if '.' in s:
9763
+ try:
9764
+ return InterpVersion.parse(s)
9765
+ except InvalidVersion:
9766
+ pass
9767
+ ii = await check.not_none(self._inspector).inspect(exe)
9768
+ return ii.iv if ii is not None else None
9578
9769
 
9579
- return inj.as_bindings(*lst)
9770
+ async def exe_versions(self) -> ta.Sequence[ta.Tuple[str, InterpVersion]]:
9771
+ lst = []
9772
+ for e in self.exes():
9773
+ if (ev := await self.get_exe_version(e)) is None:
9774
+ log.debug('Invalid system version: %s', e)
9775
+ continue
9776
+ lst.append((e, ev))
9777
+ return lst
9580
9778
 
9779
+ #
9581
9780
 
9582
- ##
9781
+ async def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
9782
+ return [ev for e, ev in await self.exe_versions()]
9583
9783
 
9784
+ async def get_installed_version(self, version: InterpVersion) -> Interp:
9785
+ for e, ev in await self.exe_versions():
9786
+ if ev != version:
9787
+ continue
9788
+ return Interp(
9789
+ exe=e,
9790
+ version=ev,
9791
+ )
9792
+ raise KeyError(version)
9584
9793
 
9585
- @dc.dataclass(frozen=True)
9586
- class _FactoryCommandExecutor(CommandExecutor):
9587
- factory: ta.Callable[[], CommandExecutor]
9588
9794
 
9589
- def execute(self, i: Command) -> ta.Awaitable[Command.Output]:
9590
- return self.factory().execute(i)
9795
+ ########################################
9796
+ # ../../../omdev/interp/pyenv/pyenv.py
9797
+ """
9798
+ TODO:
9799
+ - custom tags
9800
+ - 'aliases'
9801
+ - https://github.com/pyenv/pyenv/pull/2966
9802
+ - https://github.com/pyenv/pyenv/issues/218 (lol)
9803
+ - probably need custom (temp?) definition file
9804
+ - *or* python-build directly just into the versions dir?
9805
+ - optionally install / upgrade pyenv itself
9806
+ - new vers dont need these custom mac opts, only run on old vers
9807
+ """
9591
9808
 
9592
9809
 
9593
9810
  ##
9594
9811
 
9595
9812
 
9596
- def bind_commands(
9597
- *,
9598
- main_config: MainConfig,
9599
- ) -> InjectorBindings:
9600
- lst: ta.List[InjectorBindingOrBindings] = [
9601
- inj.bind_array(CommandRegistration),
9602
- inj.bind_array_type(CommandRegistration, CommandRegistrations),
9603
-
9604
- inj.bind_array(CommandExecutorRegistration),
9605
- inj.bind_array_type(CommandExecutorRegistration, CommandExecutorRegistrations),
9606
-
9607
- inj.bind(build_command_name_map, singleton=True),
9608
- ]
9609
-
9610
- #
9611
-
9612
- def provide_obj_marshaler_installer(cmds: CommandNameMap) -> ObjMarshalerInstaller:
9613
- return ObjMarshalerInstaller(functools.partial(install_command_marshaling, cmds))
9614
-
9615
- lst.append(inj.bind(provide_obj_marshaler_installer, array=True))
9616
-
9617
- #
9618
-
9619
- def provide_command_executor_map(
9620
- injector: Injector,
9621
- crs: CommandExecutorRegistrations,
9622
- ) -> CommandExecutorMap:
9623
- dct: ta.Dict[ta.Type[Command], CommandExecutor] = {}
9624
-
9625
- cr: CommandExecutorRegistration
9626
- for cr in crs:
9627
- if cr.command_cls in dct:
9628
- raise KeyError(cr.command_cls)
9629
-
9630
- factory = functools.partial(injector.provide, cr.executor_cls)
9631
- if main_config.debug:
9632
- ce = factory()
9633
- else:
9634
- ce = _FactoryCommandExecutor(factory)
9813
+ class Pyenv:
9814
+ def __init__(
9815
+ self,
9816
+ *,
9817
+ root: ta.Optional[str] = None,
9818
+ ) -> None:
9819
+ if root is not None and not (isinstance(root, str) and root):
9820
+ raise ValueError(f'pyenv_root: {root!r}')
9635
9821
 
9636
- dct[cr.command_cls] = ce
9822
+ super().__init__()
9637
9823
 
9638
- return CommandExecutorMap(dct)
9824
+ self._root_kw = root
9639
9825
 
9640
- lst.extend([
9641
- inj.bind(provide_command_executor_map, singleton=True),
9826
+ @async_cached_nullary
9827
+ async def root(self) -> ta.Optional[str]:
9828
+ if self._root_kw is not None:
9829
+ return self._root_kw
9642
9830
 
9643
- inj.bind(LocalCommandExecutor, singleton=True, eager=main_config.debug),
9644
- ])
9831
+ if shutil.which('pyenv'):
9832
+ return await asyncio_subprocesses.check_output_str('pyenv', 'root')
9645
9833
 
9646
- #
9834
+ d = os.path.expanduser('~/.pyenv')
9835
+ if os.path.isdir(d) and os.path.isfile(os.path.join(d, 'bin', 'pyenv')):
9836
+ return d
9647
9837
 
9648
- lst.extend([
9649
- bind_command(PingCommand, PingCommandExecutor),
9650
- bind_command(SubprocessCommand, SubprocessCommandExecutor),
9651
- ])
9838
+ return None
9652
9839
 
9653
- #
9840
+ @async_cached_nullary
9841
+ async def exe(self) -> str:
9842
+ return os.path.join(check.not_none(await self.root()), 'bin', 'pyenv')
9654
9843
 
9655
- return inj.as_bindings(*lst)
9844
+ async def version_exes(self) -> ta.List[ta.Tuple[str, str]]:
9845
+ if (root := await self.root()) is None:
9846
+ return []
9847
+ ret = []
9848
+ vp = os.path.join(root, 'versions')
9849
+ if os.path.isdir(vp):
9850
+ for dn in os.listdir(vp):
9851
+ ep = os.path.join(vp, dn, 'bin', 'python')
9852
+ if not os.path.isfile(ep):
9853
+ continue
9854
+ ret.append((dn, ep))
9855
+ return ret
9656
9856
 
9857
+ async def installable_versions(self) -> ta.List[str]:
9858
+ if await self.root() is None:
9859
+ return []
9860
+ ret = []
9861
+ s = await asyncio_subprocesses.check_output_str(await self.exe(), 'install', '--list')
9862
+ for l in s.splitlines():
9863
+ if not l.startswith(' '):
9864
+ continue
9865
+ l = l.strip()
9866
+ if not l:
9867
+ continue
9868
+ ret.append(l)
9869
+ return ret
9657
9870
 
9658
- ########################################
9659
- # ../deploy/paths/manager.py
9871
+ async def update(self) -> bool:
9872
+ if (root := await self.root()) is None:
9873
+ return False
9874
+ if not os.path.isdir(os.path.join(root, '.git')):
9875
+ return False
9876
+ await asyncio_subprocesses.check_call('git', 'pull', cwd=root)
9877
+ return True
9660
9878
 
9661
9879
 
9662
- class DeployPathsManager:
9663
- def __init__(
9664
- self,
9665
- *,
9666
- deploy_path_owners: DeployPathOwners,
9667
- ) -> None:
9668
- super().__init__()
9880
+ ##
9669
9881
 
9670
- self._deploy_path_owners = deploy_path_owners
9671
9882
 
9672
- @cached_nullary
9673
- def owners_by_path(self) -> ta.Mapping[DeployPath, DeployPathOwner]:
9674
- dct: ta.Dict[DeployPath, DeployPathOwner] = {}
9675
- for o in self._deploy_path_owners:
9676
- for p in o.get_owned_deploy_paths():
9677
- if p in dct:
9678
- raise DeployPathError(f'Duplicate deploy path owner: {p}')
9679
- dct[p] = o
9680
- return dct
9883
+ @dc.dataclass(frozen=True)
9884
+ class PyenvInstallOpts:
9885
+ opts: ta.Sequence[str] = ()
9886
+ conf_opts: ta.Sequence[str] = ()
9887
+ cflags: ta.Sequence[str] = ()
9888
+ ldflags: ta.Sequence[str] = ()
9889
+ env: ta.Mapping[str, str] = dc.field(default_factory=dict)
9681
9890
 
9682
- def validate_deploy_paths(self) -> None:
9683
- self.owners_by_path()
9891
+ def merge(self, *others: 'PyenvInstallOpts') -> 'PyenvInstallOpts':
9892
+ return PyenvInstallOpts(
9893
+ opts=list(itertools.chain.from_iterable(o.opts for o in [self, *others])),
9894
+ conf_opts=list(itertools.chain.from_iterable(o.conf_opts for o in [self, *others])),
9895
+ cflags=list(itertools.chain.from_iterable(o.cflags for o in [self, *others])),
9896
+ ldflags=list(itertools.chain.from_iterable(o.ldflags for o in [self, *others])),
9897
+ env=dict(itertools.chain.from_iterable(o.env.items() for o in [self, *others])),
9898
+ )
9684
9899
 
9685
9900
 
9686
- ########################################
9687
- # ../deploy/tmp.py
9901
+ # TODO: https://github.com/pyenv/pyenv/blob/master/plugins/python-build/README.md#building-for-maximum-performance
9902
+ DEFAULT_PYENV_INSTALL_OPTS = PyenvInstallOpts(
9903
+ opts=[
9904
+ '-s',
9905
+ '-v',
9906
+ '-k',
9907
+ ],
9908
+ conf_opts=[
9909
+ # FIXME: breaks on mac for older py's
9910
+ '--enable-loadable-sqlite-extensions',
9688
9911
 
9912
+ # '--enable-shared',
9689
9913
 
9690
- class DeployHomeAtomics(Func1[DeployHome, AtomicPathSwapping]):
9691
- pass
9914
+ '--enable-optimizations',
9915
+ '--with-lto',
9692
9916
 
9917
+ # '--enable-profiling', # ?
9693
9918
 
9694
- class DeployTmpManager(
9695
- SingleDirDeployPathOwner,
9696
- ):
9697
- def __init__(self) -> None:
9698
- super().__init__(
9699
- owned_dir='tmp',
9700
- )
9919
+ # '--enable-ipv6', # ?
9920
+ ],
9921
+ cflags=[
9922
+ # '-march=native',
9923
+ # '-mtune=native',
9924
+ ],
9925
+ )
9701
9926
 
9702
- def get_swapping(self, home: DeployHome) -> AtomicPathSwapping:
9703
- return TempDirAtomicPathSwapping(
9704
- temp_dir=self._make_dir(home),
9705
- root_dir=check.non_empty_str(home),
9706
- )
9927
+ DEBUG_PYENV_INSTALL_OPTS = PyenvInstallOpts(opts=['-g'])
9707
9928
 
9929
+ THREADED_PYENV_INSTALL_OPTS = PyenvInstallOpts(conf_opts=['--disable-gil'])
9708
9930
 
9709
- ########################################
9710
- # ../remote/connection.py
9711
9931
 
9932
+ #
9712
9933
 
9713
- ##
9714
9934
 
9935
+ class PyenvInstallOptsProvider(abc.ABC):
9936
+ @abc.abstractmethod
9937
+ def opts(self) -> ta.Awaitable[PyenvInstallOpts]:
9938
+ raise NotImplementedError
9715
9939
 
9716
- class PyremoteRemoteExecutionConnector:
9717
- def __init__(
9718
- self,
9719
- *,
9720
- spawning: RemoteSpawning,
9721
- msh: ObjMarshalerManager,
9722
- payload_file: ta.Optional[RemoteExecutionPayloadFile] = None,
9723
- ) -> None:
9724
- super().__init__()
9725
9940
 
9726
- self._spawning = spawning
9727
- self._msh = msh
9728
- self._payload_file = payload_file
9941
+ class LinuxPyenvInstallOpts(PyenvInstallOptsProvider):
9942
+ async def opts(self) -> PyenvInstallOpts:
9943
+ return PyenvInstallOpts()
9729
9944
 
9730
- #
9731
9945
 
9946
+ class DarwinPyenvInstallOpts(PyenvInstallOptsProvider):
9732
9947
  @cached_nullary
9733
- def _payload_src(self) -> str:
9734
- return get_remote_payload_src(file=self._payload_file)
9948
+ def framework_opts(self) -> PyenvInstallOpts:
9949
+ return PyenvInstallOpts(conf_opts=['--enable-framework'])
9735
9950
 
9736
9951
  @cached_nullary
9737
- def _remote_src(self) -> ta.Sequence[str]:
9738
- return [
9739
- self._payload_src(),
9740
- '_remote_execution_main()',
9741
- ]
9952
+ def has_brew(self) -> bool:
9953
+ return shutil.which('brew') is not None
9742
9954
 
9743
- @cached_nullary
9744
- def _spawn_src(self) -> str:
9745
- return pyremote_build_bootstrap_cmd(__package__ or 'manage')
9955
+ BREW_DEPS: ta.Sequence[str] = [
9956
+ 'openssl',
9957
+ 'readline',
9958
+ 'sqlite3',
9959
+ 'zlib',
9960
+ ]
9746
9961
 
9747
- #
9962
+ @async_cached_nullary
9963
+ async def brew_deps_opts(self) -> PyenvInstallOpts:
9964
+ cflags = []
9965
+ ldflags = []
9966
+ for dep in self.BREW_DEPS:
9967
+ dep_prefix = await asyncio_subprocesses.check_output_str('brew', '--prefix', dep)
9968
+ cflags.append(f'-I{dep_prefix}/include')
9969
+ ldflags.append(f'-L{dep_prefix}/lib')
9970
+ return PyenvInstallOpts(
9971
+ cflags=cflags,
9972
+ ldflags=ldflags,
9973
+ )
9748
9974
 
9749
- @contextlib.asynccontextmanager
9750
- async def connect(
9751
- self,
9752
- tgt: RemoteSpawning.Target,
9753
- bs: MainBootstrap,
9754
- ) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
9755
- spawn_src = self._spawn_src()
9756
- remote_src = self._remote_src()
9975
+ @async_cached_nullary
9976
+ async def brew_tcl_opts(self) -> PyenvInstallOpts:
9977
+ if await asyncio_subprocesses.try_output('brew', '--prefix', 'tcl-tk') is None:
9978
+ return PyenvInstallOpts()
9757
9979
 
9758
- async with self._spawning.spawn(
9759
- tgt,
9760
- spawn_src,
9761
- debug=bs.main_config.debug,
9762
- ) as proc:
9763
- res = await PyremoteBootstrapDriver( # noqa
9764
- remote_src,
9765
- PyremoteBootstrapOptions(
9766
- debug=bs.main_config.debug,
9767
- ),
9768
- ).async_run(
9769
- proc.stdout,
9770
- proc.stdin,
9771
- )
9980
+ tcl_tk_prefix = await asyncio_subprocesses.check_output_str('brew', '--prefix', 'tcl-tk')
9981
+ tcl_tk_ver_str = await asyncio_subprocesses.check_output_str('brew', 'ls', '--versions', 'tcl-tk')
9982
+ tcl_tk_ver = '.'.join(tcl_tk_ver_str.split()[1].split('.')[:2])
9772
9983
 
9773
- chan = RemoteChannelImpl(
9774
- proc.stdout,
9775
- proc.stdin,
9776
- msh=self._msh,
9777
- )
9984
+ return PyenvInstallOpts(conf_opts=[
9985
+ f"--with-tcltk-includes='-I{tcl_tk_prefix}/include'",
9986
+ f"--with-tcltk-libs='-L{tcl_tk_prefix}/lib -ltcl{tcl_tk_ver} -ltk{tcl_tk_ver}'",
9987
+ ])
9988
+
9989
+ # @cached_nullary
9990
+ # def brew_ssl_opts(self) -> PyenvInstallOpts:
9991
+ # pkg_config_path = subprocess_check_output_str('brew', '--prefix', 'openssl')
9992
+ # if 'PKG_CONFIG_PATH' in os.environ:
9993
+ # pkg_config_path += ':' + os.environ['PKG_CONFIG_PATH']
9994
+ # return PyenvInstallOpts(env={'PKG_CONFIG_PATH': pkg_config_path})
9778
9995
 
9779
- await chan.send_obj(bs)
9996
+ async def opts(self) -> PyenvInstallOpts:
9997
+ return PyenvInstallOpts().merge(
9998
+ self.framework_opts(),
9999
+ await self.brew_deps_opts(),
10000
+ await self.brew_tcl_opts(),
10001
+ # self.brew_ssl_opts(),
10002
+ )
9780
10003
 
9781
- rce: RemoteCommandExecutor
9782
- async with aclosing(RemoteCommandExecutor(chan)) as rce:
9783
- await rce.start()
9784
10004
 
9785
- yield rce
10005
+ PLATFORM_PYENV_INSTALL_OPTS: ta.Dict[str, PyenvInstallOptsProvider] = {
10006
+ 'darwin': DarwinPyenvInstallOpts(),
10007
+ 'linux': LinuxPyenvInstallOpts(),
10008
+ }
9786
10009
 
9787
10010
 
9788
10011
  ##
9789
10012
 
9790
10013
 
9791
- class InProcessRemoteExecutionConnector:
10014
+ class PyenvVersionInstaller:
10015
+ """
10016
+ Messy: can install freethreaded build with a 't' suffixed version str _or_ by THREADED_PYENV_INSTALL_OPTS - need
10017
+ latter to build custom interp with ft, need former to use canned / blessed interps. Muh.
10018
+ """
10019
+
9792
10020
  def __init__(
9793
10021
  self,
10022
+ version: str,
10023
+ opts: ta.Optional[PyenvInstallOpts] = None,
10024
+ interp_opts: InterpOpts = InterpOpts(),
9794
10025
  *,
9795
- msh: ObjMarshalerManager,
9796
- local_executor: LocalCommandExecutor,
10026
+ pyenv: Pyenv,
10027
+
10028
+ install_name: ta.Optional[str] = None,
10029
+ no_default_opts: bool = False,
9797
10030
  ) -> None:
9798
10031
  super().__init__()
9799
10032
 
9800
- self._msh = msh
9801
- self._local_executor = local_executor
10033
+ self._version = version
10034
+ self._given_opts = opts
10035
+ self._interp_opts = interp_opts
10036
+ self._given_install_name = install_name
9802
10037
 
9803
- @contextlib.asynccontextmanager
9804
- async def connect(self) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
9805
- r0, w0 = asyncio_create_bytes_channel()
9806
- r1, w1 = asyncio_create_bytes_channel()
10038
+ self._no_default_opts = no_default_opts
10039
+ self._pyenv = pyenv
9807
10040
 
9808
- remote_chan = RemoteChannelImpl(r0, w1, msh=self._msh)
9809
- local_chan = RemoteChannelImpl(r1, w0, msh=self._msh)
10041
+ @property
10042
+ def version(self) -> str:
10043
+ return self._version
9810
10044
 
9811
- rch = _RemoteCommandHandler(
9812
- remote_chan,
9813
- self._local_executor,
9814
- )
9815
- rch_task = asyncio.create_task(rch.run()) # noqa
9816
- try:
9817
- rce: RemoteCommandExecutor
9818
- async with aclosing(RemoteCommandExecutor(local_chan)) as rce:
9819
- await rce.start()
10045
+ @async_cached_nullary
10046
+ async def opts(self) -> PyenvInstallOpts:
10047
+ opts = self._given_opts
10048
+ if self._no_default_opts:
10049
+ if opts is None:
10050
+ opts = PyenvInstallOpts()
10051
+ else:
10052
+ lst = [self._given_opts if self._given_opts is not None else DEFAULT_PYENV_INSTALL_OPTS]
10053
+ if self._interp_opts.debug:
10054
+ lst.append(DEBUG_PYENV_INSTALL_OPTS)
10055
+ if self._interp_opts.threaded:
10056
+ lst.append(THREADED_PYENV_INSTALL_OPTS)
10057
+ lst.append(await PLATFORM_PYENV_INSTALL_OPTS[sys.platform].opts())
10058
+ opts = PyenvInstallOpts().merge(*lst)
10059
+ return opts
9820
10060
 
9821
- yield rce
10061
+ @cached_nullary
10062
+ def install_name(self) -> str:
10063
+ if self._given_install_name is not None:
10064
+ return self._given_install_name
10065
+ return self._version + ('-debug' if self._interp_opts.debug else '')
9822
10066
 
9823
- finally:
9824
- rch.stop()
9825
- await rch_task
10067
+ @async_cached_nullary
10068
+ async def install_dir(self) -> str:
10069
+ return str(os.path.join(check.not_none(await self._pyenv.root()), 'versions', self.install_name()))
10070
+
10071
+ @async_cached_nullary
10072
+ async def install(self) -> str:
10073
+ opts = await self.opts()
10074
+ env = {**os.environ, **opts.env}
10075
+ for k, l in [
10076
+ ('CFLAGS', opts.cflags),
10077
+ ('LDFLAGS', opts.ldflags),
10078
+ ('PYTHON_CONFIGURE_OPTS', opts.conf_opts),
10079
+ ]:
10080
+ v = ' '.join(l)
10081
+ if k in os.environ:
10082
+ v += ' ' + os.environ[k]
10083
+ env[k] = v
9826
10084
 
10085
+ conf_args = [
10086
+ *opts.opts,
10087
+ self._version,
10088
+ ]
9827
10089
 
9828
- ########################################
9829
- # ../system/commands.py
10090
+ full_args: ta.List[str]
10091
+ if self._given_install_name is not None:
10092
+ full_args = [
10093
+ os.path.join(check.not_none(await self._pyenv.root()), 'plugins', 'python-build', 'bin', 'python-build'), # noqa
10094
+ *conf_args,
10095
+ await self.install_dir(),
10096
+ ]
10097
+ else:
10098
+ full_args = [
10099
+ await self._pyenv.exe(),
10100
+ 'install',
10101
+ *conf_args,
10102
+ ]
9830
10103
 
10104
+ await asyncio_subprocesses.check_call(
10105
+ *full_args,
10106
+ env=env,
10107
+ )
9831
10108
 
9832
- ##
10109
+ exe = os.path.join(await self.install_dir(), 'bin', 'python')
10110
+ if not os.path.isfile(exe):
10111
+ raise RuntimeError(f'Interpreter not found: {exe}')
10112
+ return exe
9833
10113
 
9834
10114
 
9835
- @dc.dataclass(frozen=True)
9836
- class CheckSystemPackageCommand(Command['CheckSystemPackageCommand.Output']):
9837
- pkgs: ta.Sequence[str] = ()
10115
+ ##
9838
10116
 
9839
- def __post_init__(self) -> None:
9840
- check.not_isinstance(self.pkgs, str)
9841
10117
 
10118
+ class PyenvInterpProvider(InterpProvider):
9842
10119
  @dc.dataclass(frozen=True)
9843
- class Output(Command.Output):
9844
- pkgs: ta.Sequence[SystemPackage]
10120
+ class Options:
10121
+ inspect: bool = False
9845
10122
 
10123
+ try_update: bool = False
9846
10124
 
9847
- class CheckSystemPackageCommandExecutor(CommandExecutor[CheckSystemPackageCommand, CheckSystemPackageCommand.Output]):
9848
10125
  def __init__(
9849
10126
  self,
10127
+ options: Options = Options(),
9850
10128
  *,
9851
- mgr: SystemPackageManager,
10129
+ pyenv: Pyenv,
10130
+ inspector: InterpInspector,
9852
10131
  ) -> None:
9853
10132
  super().__init__()
9854
10133
 
9855
- self._mgr = mgr
10134
+ self._options = options
9856
10135
 
9857
- async def execute(self, cmd: CheckSystemPackageCommand) -> CheckSystemPackageCommand.Output:
9858
- log.info('Checking system package!')
10136
+ self._pyenv = pyenv
10137
+ self._inspector = inspector
9859
10138
 
9860
- ret = await self._mgr.query(*cmd.pkgs)
10139
+ #
9861
10140
 
9862
- return CheckSystemPackageCommand.Output(list(ret.values()))
10141
+ @staticmethod
10142
+ def guess_version(s: str) -> ta.Optional[InterpVersion]:
10143
+ def strip_sfx(s: str, sfx: str) -> ta.Tuple[str, bool]:
10144
+ if s.endswith(sfx):
10145
+ return s[:-len(sfx)], True
10146
+ return s, False
10147
+ ok = {}
10148
+ s, ok['debug'] = strip_sfx(s, '-debug')
10149
+ s, ok['threaded'] = strip_sfx(s, 't')
10150
+ try:
10151
+ v = Version(s)
10152
+ except InvalidVersion:
10153
+ return None
10154
+ return InterpVersion(v, InterpOpts(**ok))
9863
10155
 
10156
+ class Installed(ta.NamedTuple):
10157
+ name: str
10158
+ exe: str
10159
+ version: InterpVersion
9864
10160
 
9865
- ########################################
9866
- # ../../../omdev/interp/pyenv.py
9867
- """
9868
- TODO:
9869
- - custom tags
9870
- - 'aliases'
9871
- - https://github.com/pyenv/pyenv/pull/2966
9872
- - https://github.com/pyenv/pyenv/issues/218 (lol)
9873
- - probably need custom (temp?) definition file
9874
- - *or* python-build directly just into the versions dir?
9875
- - optionally install / upgrade pyenv itself
9876
- - new vers dont need these custom mac opts, only run on old vers
9877
- """
10161
+ async def _make_installed(self, vn: str, ep: str) -> ta.Optional[Installed]:
10162
+ iv: ta.Optional[InterpVersion]
10163
+ if self._options.inspect:
10164
+ try:
10165
+ iv = check.not_none(await self._inspector.inspect(ep)).iv
10166
+ except Exception as e: # noqa
10167
+ return None
10168
+ else:
10169
+ iv = self.guess_version(vn)
10170
+ if iv is None:
10171
+ return None
10172
+ return PyenvInterpProvider.Installed(
10173
+ name=vn,
10174
+ exe=ep,
10175
+ version=iv,
10176
+ )
9878
10177
 
10178
+ async def installed(self) -> ta.Sequence[Installed]:
10179
+ ret: ta.List[PyenvInterpProvider.Installed] = []
10180
+ for vn, ep in await self._pyenv.version_exes():
10181
+ if (i := await self._make_installed(vn, ep)) is None:
10182
+ log.debug('Invalid pyenv version: %s', vn)
10183
+ continue
10184
+ ret.append(i)
10185
+ return ret
9879
10186
 
9880
- ##
10187
+ #
9881
10188
 
10189
+ async def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
10190
+ return [i.version for i in await self.installed()]
9882
10191
 
9883
- class Pyenv:
9884
- def __init__(
9885
- self,
9886
- *,
9887
- root: ta.Optional[str] = None,
9888
- ) -> None:
9889
- if root is not None and not (isinstance(root, str) and root):
9890
- raise ValueError(f'pyenv_root: {root!r}')
10192
+ async def get_installed_version(self, version: InterpVersion) -> Interp:
10193
+ for i in await self.installed():
10194
+ if i.version == version:
10195
+ return Interp(
10196
+ exe=i.exe,
10197
+ version=i.version,
10198
+ )
10199
+ raise KeyError(version)
9891
10200
 
9892
- super().__init__()
10201
+ #
9893
10202
 
9894
- self._root_kw = root
10203
+ async def _get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
10204
+ lst = []
9895
10205
 
9896
- @async_cached_nullary
9897
- async def root(self) -> ta.Optional[str]:
9898
- if self._root_kw is not None:
9899
- return self._root_kw
10206
+ for vs in await self._pyenv.installable_versions():
10207
+ if (iv := self.guess_version(vs)) is None:
10208
+ continue
10209
+ if iv.opts.debug:
10210
+ raise Exception('Pyenv installable versions not expected to have debug suffix')
10211
+ for d in [False, True]:
10212
+ lst.append(dc.replace(iv, opts=dc.replace(iv.opts, debug=d)))
9900
10213
 
9901
- if shutil.which('pyenv'):
9902
- return await asyncio_subprocesses.check_output_str('pyenv', 'root')
10214
+ return lst
9903
10215
 
9904
- d = os.path.expanduser('~/.pyenv')
9905
- if os.path.isdir(d) and os.path.isfile(os.path.join(d, 'bin', 'pyenv')):
9906
- return d
10216
+ async def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
10217
+ lst = await self._get_installable_versions(spec)
9907
10218
 
9908
- return None
10219
+ if self._options.try_update and not any(v in spec for v in lst):
10220
+ if self._pyenv.update():
10221
+ lst = await self._get_installable_versions(spec)
10222
+
10223
+ return lst
10224
+
10225
+ async def install_version(self, version: InterpVersion) -> Interp:
10226
+ inst_version = str(version.version)
10227
+ inst_opts = version.opts
10228
+ if inst_opts.threaded:
10229
+ inst_version += 't'
10230
+ inst_opts = dc.replace(inst_opts, threaded=False)
9909
10231
 
9910
- @async_cached_nullary
9911
- async def exe(self) -> str:
9912
- return os.path.join(check.not_none(await self.root()), 'bin', 'pyenv')
10232
+ installer = PyenvVersionInstaller(
10233
+ inst_version,
10234
+ interp_opts=inst_opts,
10235
+ pyenv=self._pyenv,
10236
+ )
9913
10237
 
9914
- async def version_exes(self) -> ta.List[ta.Tuple[str, str]]:
9915
- if (root := await self.root()) is None:
9916
- return []
9917
- ret = []
9918
- vp = os.path.join(root, 'versions')
9919
- if os.path.isdir(vp):
9920
- for dn in os.listdir(vp):
9921
- ep = os.path.join(vp, dn, 'bin', 'python')
9922
- if not os.path.isfile(ep):
9923
- continue
9924
- ret.append((dn, ep))
9925
- return ret
10238
+ exe = await installer.install()
10239
+ return Interp(exe, version)
9926
10240
 
9927
- async def installable_versions(self) -> ta.List[str]:
9928
- if await self.root() is None:
9929
- return []
9930
- ret = []
9931
- s = await asyncio_subprocesses.check_output_str(await self.exe(), 'install', '--list')
9932
- for l in s.splitlines():
9933
- if not l.startswith(' '):
9934
- continue
9935
- l = l.strip()
9936
- if not l:
9937
- continue
9938
- ret.append(l)
9939
- return ret
9940
10241
 
9941
- async def update(self) -> bool:
9942
- if (root := await self.root()) is None:
9943
- return False
9944
- if not os.path.isdir(os.path.join(root, '.git')):
9945
- return False
9946
- await asyncio_subprocesses.check_call('git', 'pull', cwd=root)
9947
- return True
10242
+ ########################################
10243
+ # ../commands/inject.py
9948
10244
 
9949
10245
 
9950
10246
  ##
9951
10247
 
9952
10248
 
9953
- @dc.dataclass(frozen=True)
9954
- class PyenvInstallOpts:
9955
- opts: ta.Sequence[str] = ()
9956
- conf_opts: ta.Sequence[str] = ()
9957
- cflags: ta.Sequence[str] = ()
9958
- ldflags: ta.Sequence[str] = ()
9959
- env: ta.Mapping[str, str] = dc.field(default_factory=dict)
10249
+ def bind_command(
10250
+ command_cls: ta.Type[Command],
10251
+ executor_cls: ta.Optional[ta.Type[CommandExecutor]],
10252
+ ) -> InjectorBindings:
10253
+ lst: ta.List[InjectorBindingOrBindings] = [
10254
+ inj.bind(CommandRegistration(command_cls), array=True),
10255
+ ]
9960
10256
 
9961
- def merge(self, *others: 'PyenvInstallOpts') -> 'PyenvInstallOpts':
9962
- return PyenvInstallOpts(
9963
- opts=list(itertools.chain.from_iterable(o.opts for o in [self, *others])),
9964
- conf_opts=list(itertools.chain.from_iterable(o.conf_opts for o in [self, *others])),
9965
- cflags=list(itertools.chain.from_iterable(o.cflags for o in [self, *others])),
9966
- ldflags=list(itertools.chain.from_iterable(o.ldflags for o in [self, *others])),
9967
- env=dict(itertools.chain.from_iterable(o.env.items() for o in [self, *others])),
9968
- )
10257
+ if executor_cls is not None:
10258
+ lst.extend([
10259
+ inj.bind(executor_cls, singleton=True),
10260
+ inj.bind(CommandExecutorRegistration(command_cls, executor_cls), array=True),
10261
+ ])
9969
10262
 
10263
+ return inj.as_bindings(*lst)
9970
10264
 
9971
- # TODO: https://github.com/pyenv/pyenv/blob/master/plugins/python-build/README.md#building-for-maximum-performance
9972
- DEFAULT_PYENV_INSTALL_OPTS = PyenvInstallOpts(
9973
- opts=[
9974
- '-s',
9975
- '-v',
9976
- '-k',
9977
- ],
9978
- conf_opts=[
9979
- # FIXME: breaks on mac for older py's
9980
- '--enable-loadable-sqlite-extensions',
9981
10265
 
9982
- # '--enable-shared',
10266
+ ##
9983
10267
 
9984
- '--enable-optimizations',
9985
- '--with-lto',
9986
10268
 
9987
- # '--enable-profiling', # ?
10269
+ @dc.dataclass(frozen=True)
10270
+ class _FactoryCommandExecutor(CommandExecutor):
10271
+ factory: ta.Callable[[], CommandExecutor]
9988
10272
 
9989
- # '--enable-ipv6', # ?
9990
- ],
9991
- cflags=[
9992
- # '-march=native',
9993
- # '-mtune=native',
9994
- ],
9995
- )
10273
+ def execute(self, i: Command) -> ta.Awaitable[Command.Output]:
10274
+ return self.factory().execute(i)
9996
10275
 
9997
- DEBUG_PYENV_INSTALL_OPTS = PyenvInstallOpts(opts=['-g'])
9998
10276
 
9999
- THREADED_PYENV_INSTALL_OPTS = PyenvInstallOpts(conf_opts=['--disable-gil'])
10277
+ ##
10000
10278
 
10001
10279
 
10002
- #
10280
+ def bind_commands(
10281
+ *,
10282
+ main_config: MainConfig,
10283
+ ) -> InjectorBindings:
10284
+ lst: ta.List[InjectorBindingOrBindings] = [
10285
+ inj.bind_array(CommandRegistration),
10286
+ inj.bind_array_type(CommandRegistration, CommandRegistrations),
10003
10287
 
10288
+ inj.bind_array(CommandExecutorRegistration),
10289
+ inj.bind_array_type(CommandExecutorRegistration, CommandExecutorRegistrations),
10004
10290
 
10005
- class PyenvInstallOptsProvider(abc.ABC):
10006
- @abc.abstractmethod
10007
- def opts(self) -> ta.Awaitable[PyenvInstallOpts]:
10008
- raise NotImplementedError
10291
+ inj.bind(build_command_name_map, singleton=True),
10292
+ ]
10009
10293
 
10294
+ #
10010
10295
 
10011
- class LinuxPyenvInstallOpts(PyenvInstallOptsProvider):
10012
- async def opts(self) -> PyenvInstallOpts:
10013
- return PyenvInstallOpts()
10296
+ def provide_obj_marshaler_installer(cmds: CommandNameMap) -> ObjMarshalerInstaller:
10297
+ return ObjMarshalerInstaller(functools.partial(install_command_marshaling, cmds))
10014
10298
 
10299
+ lst.append(inj.bind(provide_obj_marshaler_installer, array=True))
10015
10300
 
10016
- class DarwinPyenvInstallOpts(PyenvInstallOptsProvider):
10017
- @cached_nullary
10018
- def framework_opts(self) -> PyenvInstallOpts:
10019
- return PyenvInstallOpts(conf_opts=['--enable-framework'])
10301
+ #
10020
10302
 
10021
- @cached_nullary
10022
- def has_brew(self) -> bool:
10023
- return shutil.which('brew') is not None
10303
+ def provide_command_executor_map(
10304
+ injector: Injector,
10305
+ crs: CommandExecutorRegistrations,
10306
+ ) -> CommandExecutorMap:
10307
+ dct: ta.Dict[ta.Type[Command], CommandExecutor] = {}
10024
10308
 
10025
- BREW_DEPS: ta.Sequence[str] = [
10026
- 'openssl',
10027
- 'readline',
10028
- 'sqlite3',
10029
- 'zlib',
10030
- ]
10309
+ cr: CommandExecutorRegistration
10310
+ for cr in crs:
10311
+ if cr.command_cls in dct:
10312
+ raise KeyError(cr.command_cls)
10031
10313
 
10032
- @async_cached_nullary
10033
- async def brew_deps_opts(self) -> PyenvInstallOpts:
10034
- cflags = []
10035
- ldflags = []
10036
- for dep in self.BREW_DEPS:
10037
- dep_prefix = await asyncio_subprocesses.check_output_str('brew', '--prefix', dep)
10038
- cflags.append(f'-I{dep_prefix}/include')
10039
- ldflags.append(f'-L{dep_prefix}/lib')
10040
- return PyenvInstallOpts(
10041
- cflags=cflags,
10042
- ldflags=ldflags,
10043
- )
10314
+ factory = functools.partial(injector.provide, cr.executor_cls)
10315
+ if main_config.debug:
10316
+ ce = factory()
10317
+ else:
10318
+ ce = _FactoryCommandExecutor(factory)
10044
10319
 
10045
- @async_cached_nullary
10046
- async def brew_tcl_opts(self) -> PyenvInstallOpts:
10047
- if await asyncio_subprocesses.try_output('brew', '--prefix', 'tcl-tk') is None:
10048
- return PyenvInstallOpts()
10320
+ dct[cr.command_cls] = ce
10049
10321
 
10050
- tcl_tk_prefix = await asyncio_subprocesses.check_output_str('brew', '--prefix', 'tcl-tk')
10051
- tcl_tk_ver_str = await asyncio_subprocesses.check_output_str('brew', 'ls', '--versions', 'tcl-tk')
10052
- tcl_tk_ver = '.'.join(tcl_tk_ver_str.split()[1].split('.')[:2])
10322
+ return CommandExecutorMap(dct)
10053
10323
 
10054
- return PyenvInstallOpts(conf_opts=[
10055
- f"--with-tcltk-includes='-I{tcl_tk_prefix}/include'",
10056
- f"--with-tcltk-libs='-L{tcl_tk_prefix}/lib -ltcl{tcl_tk_ver} -ltk{tcl_tk_ver}'",
10057
- ])
10324
+ lst.extend([
10325
+ inj.bind(provide_command_executor_map, singleton=True),
10058
10326
 
10059
- # @cached_nullary
10060
- # def brew_ssl_opts(self) -> PyenvInstallOpts:
10061
- # pkg_config_path = subprocess_check_output_str('brew', '--prefix', 'openssl')
10062
- # if 'PKG_CONFIG_PATH' in os.environ:
10063
- # pkg_config_path += ':' + os.environ['PKG_CONFIG_PATH']
10064
- # return PyenvInstallOpts(env={'PKG_CONFIG_PATH': pkg_config_path})
10327
+ inj.bind(LocalCommandExecutor, singleton=True, eager=main_config.debug),
10328
+ ])
10065
10329
 
10066
- async def opts(self) -> PyenvInstallOpts:
10067
- return PyenvInstallOpts().merge(
10068
- self.framework_opts(),
10069
- await self.brew_deps_opts(),
10070
- await self.brew_tcl_opts(),
10071
- # self.brew_ssl_opts(),
10072
- )
10330
+ #
10073
10331
 
10332
+ lst.extend([
10333
+ bind_command(PingCommand, PingCommandExecutor),
10334
+ bind_command(SubprocessCommand, SubprocessCommandExecutor),
10335
+ ])
10074
10336
 
10075
- PLATFORM_PYENV_INSTALL_OPTS: ta.Dict[str, PyenvInstallOptsProvider] = {
10076
- 'darwin': DarwinPyenvInstallOpts(),
10077
- 'linux': LinuxPyenvInstallOpts(),
10078
- }
10337
+ #
10079
10338
 
10339
+ return inj.as_bindings(*lst)
10080
10340
 
10081
- ##
10082
10341
 
10342
+ ########################################
10343
+ # ../deploy/paths/manager.py
10083
10344
 
10084
- class PyenvVersionInstaller:
10085
- """
10086
- Messy: can install freethreaded build with a 't' suffixed version str _or_ by THREADED_PYENV_INSTALL_OPTS - need
10087
- latter to build custom interp with ft, need former to use canned / blessed interps. Muh.
10088
- """
10089
10345
 
10346
+ class DeployPathsManager:
10090
10347
  def __init__(
10091
10348
  self,
10092
- version: str,
10093
- opts: ta.Optional[PyenvInstallOpts] = None,
10094
- interp_opts: InterpOpts = InterpOpts(),
10095
10349
  *,
10096
- install_name: ta.Optional[str] = None,
10097
- no_default_opts: bool = False,
10098
- pyenv: Pyenv = Pyenv(),
10350
+ deploy_path_owners: DeployPathOwners,
10099
10351
  ) -> None:
10100
10352
  super().__init__()
10101
10353
 
10102
- self._version = version
10103
- self._given_opts = opts
10104
- self._interp_opts = interp_opts
10105
- self._given_install_name = install_name
10106
-
10107
- self._no_default_opts = no_default_opts
10108
- self._pyenv = pyenv
10109
-
10110
- @property
10111
- def version(self) -> str:
10112
- return self._version
10354
+ self._deploy_path_owners = deploy_path_owners
10113
10355
 
10114
- @async_cached_nullary
10115
- async def opts(self) -> PyenvInstallOpts:
10116
- opts = self._given_opts
10117
- if self._no_default_opts:
10118
- if opts is None:
10119
- opts = PyenvInstallOpts()
10120
- else:
10121
- lst = [self._given_opts if self._given_opts is not None else DEFAULT_PYENV_INSTALL_OPTS]
10122
- if self._interp_opts.debug:
10123
- lst.append(DEBUG_PYENV_INSTALL_OPTS)
10124
- if self._interp_opts.threaded:
10125
- lst.append(THREADED_PYENV_INSTALL_OPTS)
10126
- lst.append(await PLATFORM_PYENV_INSTALL_OPTS[sys.platform].opts())
10127
- opts = PyenvInstallOpts().merge(*lst)
10128
- return opts
10356
+ @cached_nullary
10357
+ def owners_by_path(self) -> ta.Mapping[DeployPath, DeployPathOwner]:
10358
+ dct: ta.Dict[DeployPath, DeployPathOwner] = {}
10359
+ for o in self._deploy_path_owners:
10360
+ for p in o.get_owned_deploy_paths():
10361
+ if p in dct:
10362
+ raise DeployPathError(f'Duplicate deploy path owner: {p}')
10363
+ dct[p] = o
10364
+ return dct
10129
10365
 
10130
- @cached_nullary
10131
- def install_name(self) -> str:
10132
- if self._given_install_name is not None:
10133
- return self._given_install_name
10134
- return self._version + ('-debug' if self._interp_opts.debug else '')
10366
+ def validate_deploy_paths(self) -> None:
10367
+ self.owners_by_path()
10135
10368
 
10136
- @async_cached_nullary
10137
- async def install_dir(self) -> str:
10138
- return str(os.path.join(check.not_none(await self._pyenv.root()), 'versions', self.install_name()))
10139
10369
 
10140
- @async_cached_nullary
10141
- async def install(self) -> str:
10142
- opts = await self.opts()
10143
- env = {**os.environ, **opts.env}
10144
- for k, l in [
10145
- ('CFLAGS', opts.cflags),
10146
- ('LDFLAGS', opts.ldflags),
10147
- ('PYTHON_CONFIGURE_OPTS', opts.conf_opts),
10148
- ]:
10149
- v = ' '.join(l)
10150
- if k in os.environ:
10151
- v += ' ' + os.environ[k]
10152
- env[k] = v
10370
+ ########################################
10371
+ # ../deploy/tmp.py
10153
10372
 
10154
- conf_args = [
10155
- *opts.opts,
10156
- self._version,
10157
- ]
10158
10373
 
10159
- full_args: ta.List[str]
10160
- if self._given_install_name is not None:
10161
- full_args = [
10162
- os.path.join(check.not_none(await self._pyenv.root()), 'plugins', 'python-build', 'bin', 'python-build'), # noqa
10163
- *conf_args,
10164
- await self.install_dir(),
10165
- ]
10166
- else:
10167
- full_args = [
10168
- await self._pyenv.exe(),
10169
- 'install',
10170
- *conf_args,
10171
- ]
10374
+ class DeployHomeAtomics(Func1[DeployHome, AtomicPathSwapping]):
10375
+ pass
10172
10376
 
10173
- await asyncio_subprocesses.check_call(
10174
- *full_args,
10175
- env=env,
10377
+
10378
+ class DeployTmpManager(
10379
+ SingleDirDeployPathOwner,
10380
+ ):
10381
+ def __init__(self) -> None:
10382
+ super().__init__(
10383
+ owned_dir='tmp',
10176
10384
  )
10177
10385
 
10178
- exe = os.path.join(await self.install_dir(), 'bin', 'python')
10179
- if not os.path.isfile(exe):
10180
- raise RuntimeError(f'Interpreter not found: {exe}')
10181
- return exe
10386
+ def get_swapping(self, home: DeployHome) -> AtomicPathSwapping:
10387
+ return TempDirAtomicPathSwapping(
10388
+ temp_dir=self._make_dir(home),
10389
+ root_dir=check.non_empty_str(home),
10390
+ )
10391
+
10392
+
10393
+ ########################################
10394
+ # ../remote/connection.py
10182
10395
 
10183
10396
 
10184
10397
  ##
10185
10398
 
10186
10399
 
10187
- class PyenvInterpProvider(InterpProvider):
10400
+ class PyremoteRemoteExecutionConnector:
10188
10401
  def __init__(
10189
10402
  self,
10190
- pyenv: Pyenv = Pyenv(),
10191
-
10192
- inspect: bool = False,
10193
- inspector: InterpInspector = INTERP_INSPECTOR,
10194
-
10195
10403
  *,
10196
-
10197
- try_update: bool = False,
10404
+ spawning: RemoteSpawning,
10405
+ msh: ObjMarshalerManager,
10406
+ payload_file: ta.Optional[RemoteExecutionPayloadFile] = None,
10198
10407
  ) -> None:
10199
10408
  super().__init__()
10200
10409
 
10201
- self._pyenv = pyenv
10410
+ self._spawning = spawning
10411
+ self._msh = msh
10412
+ self._payload_file = payload_file
10202
10413
 
10203
- self._inspect = inspect
10204
- self._inspector = inspector
10414
+ #
10205
10415
 
10206
- self._try_update = try_update
10416
+ @cached_nullary
10417
+ def _payload_src(self) -> str:
10418
+ return get_remote_payload_src(file=self._payload_file)
10207
10419
 
10208
- #
10420
+ @cached_nullary
10421
+ def _remote_src(self) -> ta.Sequence[str]:
10422
+ return [
10423
+ self._payload_src(),
10424
+ '_remote_execution_main()',
10425
+ ]
10209
10426
 
10210
- @staticmethod
10211
- def guess_version(s: str) -> ta.Optional[InterpVersion]:
10212
- def strip_sfx(s: str, sfx: str) -> ta.Tuple[str, bool]:
10213
- if s.endswith(sfx):
10214
- return s[:-len(sfx)], True
10215
- return s, False
10216
- ok = {}
10217
- s, ok['debug'] = strip_sfx(s, '-debug')
10218
- s, ok['threaded'] = strip_sfx(s, 't')
10219
- try:
10220
- v = Version(s)
10221
- except InvalidVersion:
10222
- return None
10223
- return InterpVersion(v, InterpOpts(**ok))
10427
+ @cached_nullary
10428
+ def _spawn_src(self) -> str:
10429
+ return pyremote_build_bootstrap_cmd(__package__ or 'manage')
10224
10430
 
10225
- class Installed(ta.NamedTuple):
10226
- name: str
10227
- exe: str
10228
- version: InterpVersion
10431
+ #
10229
10432
 
10230
- async def _make_installed(self, vn: str, ep: str) -> ta.Optional[Installed]:
10231
- iv: ta.Optional[InterpVersion]
10232
- if self._inspect:
10233
- try:
10234
- iv = check.not_none(await self._inspector.inspect(ep)).iv
10235
- except Exception as e: # noqa
10236
- return None
10237
- else:
10238
- iv = self.guess_version(vn)
10239
- if iv is None:
10240
- return None
10241
- return PyenvInterpProvider.Installed(
10242
- name=vn,
10243
- exe=ep,
10244
- version=iv,
10245
- )
10433
+ @contextlib.asynccontextmanager
10434
+ async def connect(
10435
+ self,
10436
+ tgt: RemoteSpawning.Target,
10437
+ bs: MainBootstrap,
10438
+ ) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
10439
+ spawn_src = self._spawn_src()
10440
+ remote_src = self._remote_src()
10246
10441
 
10247
- async def installed(self) -> ta.Sequence[Installed]:
10248
- ret: ta.List[PyenvInterpProvider.Installed] = []
10249
- for vn, ep in await self._pyenv.version_exes():
10250
- if (i := await self._make_installed(vn, ep)) is None:
10251
- log.debug('Invalid pyenv version: %s', vn)
10252
- continue
10253
- ret.append(i)
10254
- return ret
10442
+ async with self._spawning.spawn(
10443
+ tgt,
10444
+ spawn_src,
10445
+ debug=bs.main_config.debug,
10446
+ ) as proc:
10447
+ res = await PyremoteBootstrapDriver( # noqa
10448
+ remote_src,
10449
+ PyremoteBootstrapOptions(
10450
+ debug=bs.main_config.debug,
10451
+ ),
10452
+ ).async_run(
10453
+ proc.stdout,
10454
+ proc.stdin,
10455
+ )
10255
10456
 
10256
- #
10457
+ chan = RemoteChannelImpl(
10458
+ proc.stdout,
10459
+ proc.stdin,
10460
+ msh=self._msh,
10461
+ )
10257
10462
 
10258
- async def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
10259
- return [i.version for i in await self.installed()]
10463
+ await chan.send_obj(bs)
10260
10464
 
10261
- async def get_installed_version(self, version: InterpVersion) -> Interp:
10262
- for i in await self.installed():
10263
- if i.version == version:
10264
- return Interp(
10265
- exe=i.exe,
10266
- version=i.version,
10267
- )
10268
- raise KeyError(version)
10465
+ rce: RemoteCommandExecutor
10466
+ async with aclosing(RemoteCommandExecutor(chan)) as rce:
10467
+ await rce.start()
10269
10468
 
10270
- #
10469
+ yield rce
10271
10470
 
10272
- async def _get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
10273
- lst = []
10274
10471
 
10275
- for vs in await self._pyenv.installable_versions():
10276
- if (iv := self.guess_version(vs)) is None:
10277
- continue
10278
- if iv.opts.debug:
10279
- raise Exception('Pyenv installable versions not expected to have debug suffix')
10280
- for d in [False, True]:
10281
- lst.append(dc.replace(iv, opts=dc.replace(iv.opts, debug=d)))
10472
+ ##
10282
10473
 
10283
- return lst
10284
10474
 
10285
- async def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
10286
- lst = await self._get_installable_versions(spec)
10475
+ class InProcessRemoteExecutionConnector:
10476
+ def __init__(
10477
+ self,
10478
+ *,
10479
+ msh: ObjMarshalerManager,
10480
+ local_executor: LocalCommandExecutor,
10481
+ ) -> None:
10482
+ super().__init__()
10287
10483
 
10288
- if self._try_update and not any(v in spec for v in lst):
10289
- if self._pyenv.update():
10290
- lst = await self._get_installable_versions(spec)
10484
+ self._msh = msh
10485
+ self._local_executor = local_executor
10291
10486
 
10292
- return lst
10487
+ @contextlib.asynccontextmanager
10488
+ async def connect(self) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
10489
+ r0, w0 = asyncio_create_bytes_channel()
10490
+ r1, w1 = asyncio_create_bytes_channel()
10293
10491
 
10294
- async def install_version(self, version: InterpVersion) -> Interp:
10295
- inst_version = str(version.version)
10296
- inst_opts = version.opts
10297
- if inst_opts.threaded:
10298
- inst_version += 't'
10299
- inst_opts = dc.replace(inst_opts, threaded=False)
10492
+ remote_chan = RemoteChannelImpl(r0, w1, msh=self._msh)
10493
+ local_chan = RemoteChannelImpl(r1, w0, msh=self._msh)
10300
10494
 
10301
- installer = PyenvVersionInstaller(
10302
- inst_version,
10303
- interp_opts=inst_opts,
10495
+ rch = _RemoteCommandHandler(
10496
+ remote_chan,
10497
+ self._local_executor,
10304
10498
  )
10499
+ rch_task = asyncio.create_task(rch.run()) # noqa
10500
+ try:
10501
+ rce: RemoteCommandExecutor
10502
+ async with aclosing(RemoteCommandExecutor(local_chan)) as rce:
10503
+ await rce.start()
10305
10504
 
10306
- exe = await installer.install()
10307
- return Interp(exe, version)
10505
+ yield rce
10506
+
10507
+ finally:
10508
+ rch.stop()
10509
+ await rch_task
10308
10510
 
10309
10511
 
10310
10512
  ########################################
10311
- # ../../../omdev/interp/system.py
10312
- """
10313
- TODO:
10314
- - python, python3, python3.12, ...
10315
- - check if path py's are venvs: sys.prefix != sys.base_prefix
10316
- """
10513
+ # ../system/commands.py
10514
+
10515
+
10516
+ ##
10517
+
10518
+
10519
+ @dc.dataclass(frozen=True)
10520
+ class CheckSystemPackageCommand(Command['CheckSystemPackageCommand.Output']):
10521
+ pkgs: ta.Sequence[str] = ()
10522
+
10523
+ def __post_init__(self) -> None:
10524
+ check.not_isinstance(self.pkgs, str)
10525
+
10526
+ @dc.dataclass(frozen=True)
10527
+ class Output(Command.Output):
10528
+ pkgs: ta.Sequence[SystemPackage]
10317
10529
 
10318
10530
 
10319
- ##
10531
+ class CheckSystemPackageCommandExecutor(CommandExecutor[CheckSystemPackageCommand, CheckSystemPackageCommand.Output]):
10532
+ def __init__(
10533
+ self,
10534
+ *,
10535
+ mgr: SystemPackageManager,
10536
+ ) -> None:
10537
+ super().__init__()
10320
10538
 
10539
+ self._mgr = mgr
10321
10540
 
10322
- @dc.dataclass(frozen=True)
10323
- class SystemInterpProvider(InterpProvider):
10324
- cmd: str = 'python3'
10325
- path: ta.Optional[str] = None
10541
+ async def execute(self, cmd: CheckSystemPackageCommand) -> CheckSystemPackageCommand.Output:
10542
+ log.info('Checking system package!')
10326
10543
 
10327
- inspect: bool = False
10328
- inspector: InterpInspector = INTERP_INSPECTOR
10544
+ ret = await self._mgr.query(*cmd.pkgs)
10329
10545
 
10330
- #
10546
+ return CheckSystemPackageCommand.Output(list(ret.values()))
10331
10547
 
10332
- @staticmethod
10333
- def _re_which(
10334
- pat: re.Pattern,
10335
- *,
10336
- mode: int = os.F_OK | os.X_OK,
10337
- path: ta.Optional[str] = None,
10338
- ) -> ta.List[str]:
10339
- if path is None:
10340
- path = os.environ.get('PATH', None)
10341
- if path is None:
10342
- try:
10343
- path = os.confstr('CS_PATH')
10344
- except (AttributeError, ValueError):
10345
- path = os.defpath
10346
10548
 
10347
- if not path:
10348
- return []
10549
+ ########################################
10550
+ # ../../../omdev/interp/providers/inject.py
10349
10551
 
10350
- path = os.fsdecode(path)
10351
- pathlst = path.split(os.pathsep)
10352
10552
 
10353
- def _access_check(fn: str, mode: int) -> bool:
10354
- return os.path.exists(fn) and os.access(fn, mode)
10553
+ def bind_interp_providers() -> InjectorBindings:
10554
+ lst: ta.List[InjectorBindingOrBindings] = [
10555
+ inj.bind_array(InterpProvider),
10556
+ inj.bind_array_type(InterpProvider, InterpProviders),
10355
10557
 
10356
- out = []
10357
- seen = set()
10358
- for d in pathlst:
10359
- normdir = os.path.normcase(d)
10360
- if normdir not in seen:
10361
- seen.add(normdir)
10362
- if not _access_check(normdir, mode):
10363
- continue
10364
- for thefile in os.listdir(d):
10365
- name = os.path.join(d, thefile)
10366
- if not (
10367
- os.path.isfile(name) and
10368
- pat.fullmatch(thefile) and
10369
- _access_check(name, mode)
10370
- ):
10371
- continue
10372
- out.append(name)
10558
+ inj.bind(RunningInterpProvider, singleton=True),
10559
+ inj.bind(InterpProvider, to_key=RunningInterpProvider, array=True),
10373
10560
 
10374
- return out
10561
+ inj.bind(SystemInterpProvider, singleton=True),
10562
+ inj.bind(InterpProvider, to_key=SystemInterpProvider, array=True),
10563
+ ]
10375
10564
 
10376
- @cached_nullary
10377
- def exes(self) -> ta.List[str]:
10378
- return self._re_which(
10379
- re.compile(r'python3(\.\d+)?'),
10380
- path=self.path,
10381
- )
10565
+ return inj.as_bindings(*lst)
10382
10566
 
10383
- #
10384
10567
 
10385
- async def get_exe_version(self, exe: str) -> ta.Optional[InterpVersion]:
10386
- if not self.inspect:
10387
- s = os.path.basename(exe)
10388
- if s.startswith('python'):
10389
- s = s[len('python'):]
10390
- if '.' in s:
10391
- try:
10392
- return InterpVersion.parse(s)
10393
- except InvalidVersion:
10394
- pass
10395
- ii = await self.inspector.inspect(exe)
10396
- return ii.iv if ii is not None else None
10568
+ ########################################
10569
+ # ../../../omdev/interp/pyenv/inject.py
10397
10570
 
10398
- async def exe_versions(self) -> ta.Sequence[ta.Tuple[str, InterpVersion]]:
10399
- lst = []
10400
- for e in self.exes():
10401
- if (ev := await self.get_exe_version(e)) is None:
10402
- log.debug('Invalid system version: %s', e)
10403
- continue
10404
- lst.append((e, ev))
10405
- return lst
10406
10571
 
10407
- #
10572
+ def bind_interp_pyenv() -> InjectorBindings:
10573
+ lst: ta.List[InjectorBindingOrBindings] = [
10574
+ inj.bind(Pyenv, singleton=True),
10408
10575
 
10409
- async def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
10410
- return [ev for e, ev in await self.exe_versions()]
10576
+ inj.bind(PyenvInterpProvider, singleton=True),
10577
+ inj.bind(InterpProvider, to_key=PyenvInterpProvider, array=True),
10578
+ ]
10411
10579
 
10412
- async def get_installed_version(self, version: InterpVersion) -> Interp:
10413
- for e, ev in await self.exe_versions():
10414
- if ev != version:
10415
- continue
10416
- return Interp(
10417
- exe=e,
10418
- version=ev,
10419
- )
10420
- raise KeyError(version)
10580
+ return inj.as_bindings(*lst)
10421
10581
 
10422
10582
 
10423
10583
  ########################################
@@ -10784,101 +10944,44 @@ class SshManageTargetConnector(ManageTargetConnector):
10784
10944
 
10785
10945
 
10786
10946
  ########################################
10787
- # ../../../omdev/interp/resolvers.py
10788
-
10789
-
10790
- INTERP_PROVIDER_TYPES_BY_NAME: ta.Mapping[str, ta.Type[InterpProvider]] = {
10791
- cls.name: cls for cls in deep_subclasses(InterpProvider) if abc.ABC not in cls.__bases__ # type: ignore
10792
- }
10793
-
10794
-
10795
- class InterpResolver:
10796
- def __init__(
10797
- self,
10798
- providers: ta.Sequence[ta.Tuple[str, InterpProvider]],
10799
- ) -> None:
10800
- super().__init__()
10801
-
10802
- self._providers: ta.Mapping[str, InterpProvider] = collections.OrderedDict(providers)
10803
-
10804
- async def _resolve_installed(self, spec: InterpSpecifier) -> ta.Optional[ta.Tuple[InterpProvider, InterpVersion]]:
10805
- lst = [
10806
- (i, si)
10807
- for i, p in enumerate(self._providers.values())
10808
- for si in await p.get_installed_versions(spec)
10809
- if spec.contains(si)
10810
- ]
10811
-
10812
- slst = sorted(lst, key=lambda t: (-t[0], t[1].version))
10813
- if not slst:
10814
- return None
10947
+ # ../../../omdev/interp/inject.py
10815
10948
 
10816
- bi, bv = slst[-1]
10817
- bp = list(self._providers.values())[bi]
10818
- return (bp, bv)
10819
10949
 
10820
- async def resolve(
10821
- self,
10822
- spec: InterpSpecifier,
10823
- *,
10824
- install: bool = False,
10825
- ) -> ta.Optional[Interp]:
10826
- tup = await self._resolve_installed(spec)
10827
- if tup is not None:
10828
- bp, bv = tup
10829
- return await bp.get_installed_version(bv)
10950
+ def bind_interp() -> InjectorBindings:
10951
+ lst: ta.List[InjectorBindingOrBindings] = [
10952
+ bind_interp_providers(),
10830
10953
 
10831
- if not install:
10832
- return None
10954
+ bind_interp_pyenv(),
10833
10955
 
10834
- tp = list(self._providers.values())[0] # noqa
10956
+ bind_interp_uv(),
10835
10957
 
10836
- sv = sorted(
10837
- [s for s in await tp.get_installable_versions(spec) if s in spec],
10838
- key=lambda s: s.version,
10839
- )
10840
- if not sv:
10841
- return None
10958
+ inj.bind(InterpInspector, singleton=True),
10959
+ ]
10842
10960
 
10843
- bv = sv[-1]
10844
- return await tp.install_version(bv)
10961
+ #
10845
10962
 
10846
- async def list(self, spec: InterpSpecifier) -> None:
10847
- print('installed:')
10848
- for n, p in self._providers.items():
10849
- lst = [
10850
- si
10851
- for si in await p.get_installed_versions(spec)
10852
- if spec.contains(si)
10963
+ def provide_interp_resolver_providers(injector: Injector) -> InterpResolverProviders:
10964
+ # FIXME: lol
10965
+ rps: ta.List[ta.Any] = [
10966
+ injector.provide(c)
10967
+ for c in [
10968
+ PyenvInterpProvider,
10969
+ RunningInterpProvider,
10970
+ SystemInterpProvider,
10853
10971
  ]
10854
- if lst:
10855
- print(f' {n}')
10856
- for si in lst:
10857
- print(f' {si}')
10858
-
10859
- print()
10972
+ ]
10860
10973
 
10861
- print('installable:')
10862
- for n, p in self._providers.items():
10863
- lst = [
10864
- si
10865
- for si in await p.get_installable_versions(spec)
10866
- if spec.contains(si)
10867
- ]
10868
- if lst:
10869
- print(f' {n}')
10870
- for si in lst:
10871
- print(f' {si}')
10974
+ return InterpResolverProviders([(rp.name, rp) for rp in rps])
10872
10975
 
10976
+ lst.append(inj.bind(provide_interp_resolver_providers, singleton=True))
10873
10977
 
10874
- DEFAULT_INTERP_RESOLVER = InterpResolver([(p.name, p) for p in [
10875
- # pyenv is preferred to system interpreters as it tends to have more support for things like tkinter
10876
- PyenvInterpProvider(try_update=True),
10978
+ lst.extend([
10979
+ inj.bind(InterpResolver, singleton=True),
10980
+ ])
10877
10981
 
10878
- RunningInterpProvider(),
10982
+ #
10879
10983
 
10880
- SystemInterpProvider(),
10881
- ]])
10984
+ return inj.as_bindings(*lst)
10882
10985
 
10883
10986
 
10884
10987
  ########################################
@@ -10910,6 +11013,15 @@ def bind_targets() -> InjectorBindings:
10910
11013
  return inj.as_bindings(*lst)
10911
11014
 
10912
11015
 
11016
+ ########################################
11017
+ # ../../../omdev/interp/default.py
11018
+
11019
+
11020
+ @cached_nullary
11021
+ def get_default_interp_resolver() -> InterpResolver:
11022
+ return inj.create_injector(bind_interp())[InterpResolver]
11023
+
11024
+
10913
11025
  ########################################
10914
11026
  # ../deploy/interp.py
10915
11027
 
@@ -10932,7 +11044,7 @@ class InterpCommand(Command['InterpCommand.Output']):
10932
11044
  class InterpCommandExecutor(CommandExecutor[InterpCommand, InterpCommand.Output]):
10933
11045
  async def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
10934
11046
  i = InterpSpecifier.parse(check.not_none(cmd.spec))
10935
- o = check.not_none(await DEFAULT_INTERP_RESOLVER.resolve(i, install=cmd.install))
11047
+ o = check.not_none(await get_default_interp_resolver().resolve(i, install=cmd.install))
10936
11048
  return InterpCommand.Output(
10937
11049
  exe=o.exe,
10938
11050
  version=str(o.version.version),
@@ -10959,7 +11071,7 @@ class DeployVenvManager:
10959
11071
  ) -> None:
10960
11072
  if spec.interp is not None:
10961
11073
  i = InterpSpecifier.parse(check.not_none(spec.interp))
10962
- o = check.not_none(await DEFAULT_INTERP_RESOLVER.resolve(i))
11074
+ o = check.not_none(await get_default_interp_resolver().resolve(i))
10963
11075
  sys_exe = o.exe
10964
11076
  else:
10965
11077
  sys_exe = 'python3'
@@ -11166,16 +11278,18 @@ class DeployDriver:
11166
11278
  self,
11167
11279
  *,
11168
11280
  spec: DeploySpec,
11281
+ home: DeployHome,
11282
+ time: DeployTime,
11169
11283
 
11170
- deploys: DeployManager,
11171
11284
  paths: DeployPathsManager,
11172
11285
  apps: DeployAppManager,
11173
11286
  ) -> None:
11174
11287
  super().__init__()
11175
11288
 
11176
11289
  self._spec = spec
11290
+ self._home = home
11291
+ self._time = time
11177
11292
 
11178
- self._deploys = deploys
11179
11293
  self._paths = paths
11180
11294
  self._apps = apps
11181
11295
 
@@ -11184,17 +11298,8 @@ class DeployDriver:
11184
11298
 
11185
11299
  #
11186
11300
 
11187
- hs = check.non_empty_str(self._spec.home)
11188
- hs = os.path.expanduser(hs)
11189
- hs = os.path.realpath(hs)
11190
- hs = os.path.abspath(hs)
11191
-
11192
- home = DeployHome(hs)
11193
-
11194
- #
11195
-
11196
11301
  deploy_tags = DeployTagMap(
11197
- self._deploys.make_deploy_time(),
11302
+ self._time,
11198
11303
  self._spec.key(),
11199
11304
  )
11200
11305
 
@@ -11209,7 +11314,7 @@ class DeployDriver:
11209
11314
 
11210
11315
  await self._apps.prepare_app(
11211
11316
  app,
11212
- home,
11317
+ self._home,
11213
11318
  app_tags,
11214
11319
  )
11215
11320
 
@@ -11247,10 +11352,57 @@ class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]
11247
11352
  # ../deploy/inject.py
11248
11353
 
11249
11354
 
11355
+ ##
11356
+
11357
+
11250
11358
  class DeployInjectorScope(ContextvarInjectorScope):
11251
11359
  pass
11252
11360
 
11253
11361
 
11362
+ def bind_deploy_scope() -> InjectorBindings:
11363
+ lst: ta.List[InjectorBindingOrBindings] = [
11364
+ inj.bind_scope(DeployInjectorScope),
11365
+ inj.bind_scope_seed(DeploySpec, DeployInjectorScope),
11366
+
11367
+ inj.bind(DeployDriver, in_=DeployInjectorScope),
11368
+ ]
11369
+
11370
+ #
11371
+
11372
+ def provide_deploy_driver_factory(injector: Injector, sc: DeployInjectorScope) -> DeployDriverFactory:
11373
+ @contextlib.contextmanager
11374
+ def factory(spec: DeploySpec) -> ta.Iterator[DeployDriver]:
11375
+ with sc.enter({
11376
+ inj.as_key(DeploySpec): spec,
11377
+ }):
11378
+ yield injector[DeployDriver]
11379
+ return DeployDriverFactory(factory)
11380
+ lst.append(inj.bind(provide_deploy_driver_factory, singleton=True))
11381
+
11382
+ #
11383
+
11384
+ def provide_deploy_home(deploy: DeploySpec) -> DeployHome:
11385
+ hs = check.non_empty_str(deploy.home)
11386
+ hs = os.path.expanduser(hs)
11387
+ hs = os.path.realpath(hs)
11388
+ hs = os.path.abspath(hs)
11389
+ return DeployHome(hs)
11390
+ lst.append(inj.bind(provide_deploy_home, in_=DeployInjectorScope))
11391
+
11392
+ #
11393
+
11394
+ def provide_deploy_time(deploys: DeployManager) -> DeployTime:
11395
+ return deploys.make_deploy_time()
11396
+ lst.append(inj.bind(provide_deploy_time, in_=DeployInjectorScope))
11397
+
11398
+ #
11399
+
11400
+ return inj.as_bindings(*lst)
11401
+
11402
+
11403
+ ##
11404
+
11405
+
11254
11406
  def bind_deploy(
11255
11407
  *,
11256
11408
  deploy_config: DeployConfig,
@@ -11259,6 +11411,8 @@ def bind_deploy(
11259
11411
  inj.bind(deploy_config),
11260
11412
 
11261
11413
  bind_deploy_paths(),
11414
+
11415
+ bind_deploy_scope(),
11262
11416
  ]
11263
11417
 
11264
11418
  #
@@ -11294,25 +11448,6 @@ def bind_deploy(
11294
11448
 
11295
11449
  #
11296
11450
 
11297
- def provide_deploy_driver_factory(injector: Injector, sc: DeployInjectorScope) -> DeployDriverFactory:
11298
- @contextlib.contextmanager
11299
- def factory(spec: DeploySpec) -> ta.Iterator[DeployDriver]:
11300
- with sc.enter({
11301
- inj.as_key(DeploySpec): spec,
11302
- }):
11303
- yield injector[DeployDriver]
11304
- return DeployDriverFactory(factory)
11305
- lst.append(inj.bind(provide_deploy_driver_factory, singleton=True))
11306
-
11307
- lst.extend([
11308
- inj.bind_scope(DeployInjectorScope),
11309
- inj.bind_scope_seed(DeploySpec, DeployInjectorScope),
11310
-
11311
- inj.bind(DeployDriver, in_=DeployInjectorScope),
11312
- ])
11313
-
11314
- #
11315
-
11316
11451
  lst.extend([
11317
11452
  bind_command(DeployCommand, DeployCommandExecutor),
11318
11453
  bind_command(InterpCommand, InterpCommandExecutor),
@@ -11435,7 +11570,7 @@ class MainCli(ArgparseCli):
11435
11570
 
11436
11571
  #
11437
11572
 
11438
- @argparse_command(
11573
+ @argparse_cmd(
11439
11574
  argparse_arg('--_payload-file'),
11440
11575
 
11441
11576
  argparse_arg('--pycharm-debug-port', type=int),