ominfra 0.0.0.dev178__py3-none-any.whl → 0.0.0.dev180__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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),