ominfra 0.0.0.dev145__py3-none-any.whl → 0.0.0.dev147__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
@@ -10,6 +10,7 @@ manage.py -s 'ssh -i /foo/bar.pem foo@bar.baz' -q --python=python3.8
10
10
  """
11
11
  import abc
12
12
  import base64
13
+ import collections
13
14
  import collections.abc
14
15
  import contextlib
15
16
  import dataclasses as dc
@@ -19,12 +20,16 @@ import enum
19
20
  import fractions
20
21
  import functools
21
22
  import inspect
23
+ import itertools
22
24
  import json
23
25
  import logging
24
26
  import os
27
+ import os.path
25
28
  import platform
26
29
  import pwd
30
+ import re
27
31
  import shlex
32
+ import shutil
28
33
  import site
29
34
  import struct
30
35
  import subprocess
@@ -49,6 +54,14 @@ if sys.version_info < (3, 8):
49
54
  ########################################
50
55
 
51
56
 
57
+ # ../../omdev/packaging/versions.py
58
+ VersionLocalType = ta.Tuple[ta.Union[int, str], ...]
59
+ VersionCmpPrePostDevType = ta.Union['InfinityVersionType', 'NegativeInfinityVersionType', ta.Tuple[str, int]]
60
+ _VersionCmpLocalType0 = ta.Tuple[ta.Union[ta.Tuple[int, str], ta.Tuple['NegativeInfinityVersionType', ta.Union[int, str]]], ...] # noqa
61
+ VersionCmpLocalType = ta.Union['NegativeInfinityVersionType', _VersionCmpLocalType0]
62
+ VersionCmpKey = ta.Tuple[int, ta.Tuple[int, ...], VersionCmpPrePostDevType, VersionCmpPrePostDevType, VersionCmpPrePostDevType, VersionCmpLocalType] # noqa
63
+ VersionComparisonMethod = ta.Callable[[VersionCmpKey, VersionCmpKey], bool]
64
+
52
65
  # ../../omlish/lite/cached.py
53
66
  T = ta.TypeVar('T')
54
67
  CallableT = ta.TypeVar('CallableT', bound=ta.Callable)
@@ -56,6 +69,11 @@ CallableT = ta.TypeVar('CallableT', bound=ta.Callable)
56
69
  # ../../omlish/lite/check.py
57
70
  SizedT = ta.TypeVar('SizedT', bound=ta.Sized)
58
71
 
72
+ # ../../omdev/packaging/specifiers.py
73
+ UnparsedVersion = ta.Union['Version', str]
74
+ UnparsedVersionVar = ta.TypeVar('UnparsedVersionVar', bound=UnparsedVersion)
75
+ CallableVersionOperator = ta.Callable[['Version', str], bool]
76
+
59
77
  # commands/base.py
60
78
  CommandT = ta.TypeVar('CommandT', bound='Command')
61
79
  CommandOutputT = ta.TypeVar('CommandOutputT', bound='Command.Output')
@@ -71,6 +89,413 @@ InjectorBindingOrBindings = ta.Union['InjectorBinding', 'InjectorBindings']
71
89
  SubprocessChannelOption = ta.Literal['pipe', 'stdout', 'devnull']
72
90
 
73
91
 
92
+ ########################################
93
+ # ../../../omdev/packaging/versions.py
94
+ # Copyright (c) Donald Stufft and individual contributors.
95
+ # All rights reserved.
96
+ #
97
+ # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
98
+ # following conditions are met:
99
+ #
100
+ # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
101
+ # following disclaimer.
102
+ #
103
+ # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
104
+ # following disclaimer in the documentation and/or other materials provided with the distribution.
105
+ #
106
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
107
+ # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
108
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
109
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
110
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
111
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
112
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. This file is dual licensed under the terms of the
113
+ # Apache License, Version 2.0, and the BSD License. See the LICENSE file in the root of this repository for complete
114
+ # details.
115
+ # https://github.com/pypa/packaging/blob/2c885fe91a54559e2382902dce28428ad2887be5/src/packaging/version.py
116
+
117
+
118
+ ##
119
+
120
+
121
+ class InfinityVersionType:
122
+ def __repr__(self) -> str:
123
+ return 'Infinity'
124
+
125
+ def __hash__(self) -> int:
126
+ return hash(repr(self))
127
+
128
+ def __lt__(self, other: object) -> bool:
129
+ return False
130
+
131
+ def __le__(self, other: object) -> bool:
132
+ return False
133
+
134
+ def __eq__(self, other: object) -> bool:
135
+ return isinstance(other, self.__class__)
136
+
137
+ def __gt__(self, other: object) -> bool:
138
+ return True
139
+
140
+ def __ge__(self, other: object) -> bool:
141
+ return True
142
+
143
+ def __neg__(self: object) -> 'NegativeInfinityVersionType':
144
+ return NegativeInfinityVersion
145
+
146
+
147
+ InfinityVersion = InfinityVersionType()
148
+
149
+
150
+ class NegativeInfinityVersionType:
151
+ def __repr__(self) -> str:
152
+ return '-Infinity'
153
+
154
+ def __hash__(self) -> int:
155
+ return hash(repr(self))
156
+
157
+ def __lt__(self, other: object) -> bool:
158
+ return True
159
+
160
+ def __le__(self, other: object) -> bool:
161
+ return True
162
+
163
+ def __eq__(self, other: object) -> bool:
164
+ return isinstance(other, self.__class__)
165
+
166
+ def __gt__(self, other: object) -> bool:
167
+ return False
168
+
169
+ def __ge__(self, other: object) -> bool:
170
+ return False
171
+
172
+ def __neg__(self: object) -> InfinityVersionType:
173
+ return InfinityVersion
174
+
175
+
176
+ NegativeInfinityVersion = NegativeInfinityVersionType()
177
+
178
+
179
+ ##
180
+
181
+
182
+ class _Version(ta.NamedTuple):
183
+ epoch: int
184
+ release: ta.Tuple[int, ...]
185
+ dev: ta.Optional[ta.Tuple[str, int]]
186
+ pre: ta.Optional[ta.Tuple[str, int]]
187
+ post: ta.Optional[ta.Tuple[str, int]]
188
+ local: ta.Optional[VersionLocalType]
189
+
190
+
191
+ class InvalidVersion(ValueError): # noqa
192
+ pass
193
+
194
+
195
+ class _BaseVersion:
196
+ _key: ta.Tuple[ta.Any, ...]
197
+
198
+ def __hash__(self) -> int:
199
+ return hash(self._key)
200
+
201
+ def __lt__(self, other: '_BaseVersion') -> bool:
202
+ if not isinstance(other, _BaseVersion):
203
+ return NotImplemented # type: ignore
204
+ return self._key < other._key
205
+
206
+ def __le__(self, other: '_BaseVersion') -> bool:
207
+ if not isinstance(other, _BaseVersion):
208
+ return NotImplemented # type: ignore
209
+ return self._key <= other._key
210
+
211
+ def __eq__(self, other: object) -> bool:
212
+ if not isinstance(other, _BaseVersion):
213
+ return NotImplemented
214
+ return self._key == other._key
215
+
216
+ def __ge__(self, other: '_BaseVersion') -> bool:
217
+ if not isinstance(other, _BaseVersion):
218
+ return NotImplemented # type: ignore
219
+ return self._key >= other._key
220
+
221
+ def __gt__(self, other: '_BaseVersion') -> bool:
222
+ if not isinstance(other, _BaseVersion):
223
+ return NotImplemented # type: ignore
224
+ return self._key > other._key
225
+
226
+ def __ne__(self, other: object) -> bool:
227
+ if not isinstance(other, _BaseVersion):
228
+ return NotImplemented
229
+ return self._key != other._key
230
+
231
+
232
+ _VERSION_PATTERN = r"""
233
+ v?
234
+ (?:
235
+ (?:(?P<epoch>[0-9]+)!)?
236
+ (?P<release>[0-9]+(?:\.[0-9]+)*)
237
+ (?P<pre>
238
+ [-_\.]?
239
+ (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc)
240
+ [-_\.]?
241
+ (?P<pre_n>[0-9]+)?
242
+ )?
243
+ (?P<post>
244
+ (?:-(?P<post_n1>[0-9]+))
245
+ |
246
+ (?:
247
+ [-_\.]?
248
+ (?P<post_l>post|rev|r)
249
+ [-_\.]?
250
+ (?P<post_n2>[0-9]+)?
251
+ )
252
+ )?
253
+ (?P<dev>
254
+ [-_\.]?
255
+ (?P<dev_l>dev)
256
+ [-_\.]?
257
+ (?P<dev_n>[0-9]+)?
258
+ )?
259
+ )
260
+ (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?
261
+ """
262
+
263
+ VERSION_PATTERN = _VERSION_PATTERN
264
+
265
+
266
+ class Version(_BaseVersion):
267
+ _regex = re.compile(r'^\s*' + VERSION_PATTERN + r'\s*$', re.VERBOSE | re.IGNORECASE)
268
+ _key: VersionCmpKey
269
+
270
+ def __init__(self, version: str) -> None:
271
+ match = self._regex.search(version)
272
+ if not match:
273
+ raise InvalidVersion(f"Invalid version: '{version}'")
274
+
275
+ self._version = _Version(
276
+ epoch=int(match.group('epoch')) if match.group('epoch') else 0,
277
+ release=tuple(int(i) for i in match.group('release').split('.')),
278
+ pre=_parse_letter_version(match.group('pre_l'), match.group('pre_n')),
279
+ post=_parse_letter_version(match.group('post_l'), match.group('post_n1') or match.group('post_n2')),
280
+ dev=_parse_letter_version(match.group('dev_l'), match.group('dev_n')),
281
+ local=_parse_local_version(match.group('local')),
282
+ )
283
+
284
+ self._key = _version_cmpkey(
285
+ self._version.epoch,
286
+ self._version.release,
287
+ self._version.pre,
288
+ self._version.post,
289
+ self._version.dev,
290
+ self._version.local,
291
+ )
292
+
293
+ def __repr__(self) -> str:
294
+ return f"<Version('{self}')>"
295
+
296
+ def __str__(self) -> str:
297
+ parts = []
298
+
299
+ if self.epoch != 0:
300
+ parts.append(f'{self.epoch}!')
301
+
302
+ parts.append('.'.join(str(x) for x in self.release))
303
+
304
+ if self.pre is not None:
305
+ parts.append(''.join(str(x) for x in self.pre))
306
+
307
+ if self.post is not None:
308
+ parts.append(f'.post{self.post}')
309
+
310
+ if self.dev is not None:
311
+ parts.append(f'.dev{self.dev}')
312
+
313
+ if self.local is not None:
314
+ parts.append(f'+{self.local}')
315
+
316
+ return ''.join(parts)
317
+
318
+ @property
319
+ def epoch(self) -> int:
320
+ return self._version.epoch
321
+
322
+ @property
323
+ def release(self) -> ta.Tuple[int, ...]:
324
+ return self._version.release
325
+
326
+ @property
327
+ def pre(self) -> ta.Optional[ta.Tuple[str, int]]:
328
+ return self._version.pre
329
+
330
+ @property
331
+ def post(self) -> ta.Optional[int]:
332
+ return self._version.post[1] if self._version.post else None
333
+
334
+ @property
335
+ def dev(self) -> ta.Optional[int]:
336
+ return self._version.dev[1] if self._version.dev else None
337
+
338
+ @property
339
+ def local(self) -> ta.Optional[str]:
340
+ if self._version.local:
341
+ return '.'.join(str(x) for x in self._version.local)
342
+ else:
343
+ return None
344
+
345
+ @property
346
+ def public(self) -> str:
347
+ return str(self).split('+', 1)[0]
348
+
349
+ @property
350
+ def base_version(self) -> str:
351
+ parts = []
352
+
353
+ if self.epoch != 0:
354
+ parts.append(f'{self.epoch}!')
355
+
356
+ parts.append('.'.join(str(x) for x in self.release))
357
+
358
+ return ''.join(parts)
359
+
360
+ @property
361
+ def is_prerelease(self) -> bool:
362
+ return self.dev is not None or self.pre is not None
363
+
364
+ @property
365
+ def is_postrelease(self) -> bool:
366
+ return self.post is not None
367
+
368
+ @property
369
+ def is_devrelease(self) -> bool:
370
+ return self.dev is not None
371
+
372
+ @property
373
+ def major(self) -> int:
374
+ return self.release[0] if len(self.release) >= 1 else 0
375
+
376
+ @property
377
+ def minor(self) -> int:
378
+ return self.release[1] if len(self.release) >= 2 else 0
379
+
380
+ @property
381
+ def micro(self) -> int:
382
+ return self.release[2] if len(self.release) >= 3 else 0
383
+
384
+
385
+ def _parse_letter_version(
386
+ letter: ta.Optional[str],
387
+ number: ta.Union[str, bytes, ta.SupportsInt, None],
388
+ ) -> ta.Optional[ta.Tuple[str, int]]:
389
+ if letter:
390
+ if number is None:
391
+ number = 0
392
+
393
+ letter = letter.lower()
394
+ if letter == 'alpha':
395
+ letter = 'a'
396
+ elif letter == 'beta':
397
+ letter = 'b'
398
+ elif letter in ['c', 'pre', 'preview']:
399
+ letter = 'rc'
400
+ elif letter in ['rev', 'r']:
401
+ letter = 'post'
402
+
403
+ return letter, int(number)
404
+ if not letter and number:
405
+ letter = 'post'
406
+ return letter, int(number)
407
+
408
+ return None
409
+
410
+
411
+ _local_version_separators = re.compile(r'[\._-]')
412
+
413
+
414
+ def _parse_local_version(local: ta.Optional[str]) -> ta.Optional[VersionLocalType]:
415
+ if local is not None:
416
+ return tuple(
417
+ part.lower() if not part.isdigit() else int(part)
418
+ for part in _local_version_separators.split(local)
419
+ )
420
+ return None
421
+
422
+
423
+ def _version_cmpkey(
424
+ epoch: int,
425
+ release: ta.Tuple[int, ...],
426
+ pre: ta.Optional[ta.Tuple[str, int]],
427
+ post: ta.Optional[ta.Tuple[str, int]],
428
+ dev: ta.Optional[ta.Tuple[str, int]],
429
+ local: ta.Optional[VersionLocalType],
430
+ ) -> VersionCmpKey:
431
+ _release = tuple(reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release)))))
432
+
433
+ if pre is None and post is None and dev is not None:
434
+ _pre: VersionCmpPrePostDevType = NegativeInfinityVersion
435
+ elif pre is None:
436
+ _pre = InfinityVersion
437
+ else:
438
+ _pre = pre
439
+
440
+ if post is None:
441
+ _post: VersionCmpPrePostDevType = NegativeInfinityVersion
442
+ else:
443
+ _post = post
444
+
445
+ if dev is None:
446
+ _dev: VersionCmpPrePostDevType = InfinityVersion
447
+ else:
448
+ _dev = dev
449
+
450
+ if local is None:
451
+ _local: VersionCmpLocalType = NegativeInfinityVersion
452
+ else:
453
+ _local = tuple((i, '') if isinstance(i, int) else (NegativeInfinityVersion, i) for i in local)
454
+
455
+ return epoch, _release, _pre, _post, _dev, _local
456
+
457
+
458
+ ##
459
+
460
+
461
+ def canonicalize_version(
462
+ version: ta.Union[Version, str],
463
+ *,
464
+ strip_trailing_zero: bool = True,
465
+ ) -> str:
466
+ if isinstance(version, str):
467
+ try:
468
+ parsed = Version(version)
469
+ except InvalidVersion:
470
+ return version
471
+ else:
472
+ parsed = version
473
+
474
+ parts = []
475
+
476
+ if parsed.epoch != 0:
477
+ parts.append(f'{parsed.epoch}!')
478
+
479
+ release_segment = '.'.join(str(x) for x in parsed.release)
480
+ if strip_trailing_zero:
481
+ release_segment = re.sub(r'(\.0)+$', '', release_segment)
482
+ parts.append(release_segment)
483
+
484
+ if parsed.pre is not None:
485
+ parts.append(''.join(str(x) for x in parsed.pre))
486
+
487
+ if parsed.post is not None:
488
+ parts.append(f'.post{parsed.post}')
489
+
490
+ if parsed.dev is not None:
491
+ parts.append(f'.dev{parsed.dev}')
492
+
493
+ if parsed.local is not None:
494
+ parts.append(f'+{parsed.local}')
495
+
496
+ return ''.join(parts)
497
+
498
+
74
499
  ########################################
75
500
  # ../config.py
76
501
 
@@ -816,7 +1241,6 @@ def pycharm_debug_connect(prd: PycharmRemoteDebug) -> None:
816
1241
  def pycharm_debug_preamble(prd: PycharmRemoteDebug) -> str:
817
1242
  import inspect
818
1243
  import textwrap
819
-
820
1244
  return textwrap.dedent(f"""
821
1245
  {inspect.getsource(pycharm_debug_connect)}
822
1246
 
@@ -951,143 +1375,665 @@ def format_num_bytes(num_bytes: int) -> str:
951
1375
 
952
1376
 
953
1377
  ########################################
954
- # ../commands/base.py
955
-
956
-
957
- ##
958
-
959
-
960
- @dc.dataclass(frozen=True)
961
- class Command(abc.ABC, ta.Generic[CommandOutputT]):
962
- @dc.dataclass(frozen=True)
963
- class Output(abc.ABC): # noqa
964
- pass
965
-
966
- @ta.final
967
- def execute(self, executor: 'CommandExecutor') -> CommandOutputT:
968
- return check_isinstance(executor.execute(self), self.Output) # type: ignore[return-value]
1378
+ # ../../../omdev/packaging/specifiers.py
1379
+ # Copyright (c) Donald Stufft and individual contributors.
1380
+ # All rights reserved.
1381
+ #
1382
+ # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
1383
+ # following conditions are met:
1384
+ #
1385
+ # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
1386
+ # following disclaimer.
1387
+ #
1388
+ # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
1389
+ # following disclaimer in the documentation and/or other materials provided with the distribution.
1390
+ #
1391
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
1392
+ # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
1393
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
1394
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
1395
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
1396
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
1397
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. This file is dual licensed under the terms of the
1398
+ # Apache License, Version 2.0, and the BSD License. See the LICENSE file in the root of this repository for complete
1399
+ # details.
1400
+ # https://github.com/pypa/packaging/blob/2c885fe91a54559e2382902dce28428ad2887be5/src/packaging/specifiers.py
969
1401
 
970
1402
 
971
1403
  ##
972
1404
 
973
1405
 
974
- @dc.dataclass(frozen=True)
975
- class CommandException:
976
- name: str
977
- repr: str
1406
+ def _coerce_version(version: UnparsedVersion) -> Version:
1407
+ if not isinstance(version, Version):
1408
+ version = Version(version)
1409
+ return version
978
1410
 
979
- traceback: ta.Optional[str] = None
980
-
981
- exc: ta.Optional[ta.Any] = None # Exception
982
1411
 
983
- cmd: ta.Optional[Command] = None
1412
+ class InvalidSpecifier(ValueError): # noqa
1413
+ pass
984
1414
 
985
- @classmethod
986
- def of(
987
- cls,
988
- exc: Exception,
989
- *,
990
- omit_exc_object: bool = False,
991
1415
 
992
- cmd: ta.Optional[Command] = None,
993
- ) -> 'CommandException':
994
- return CommandException(
995
- name=type(exc).__qualname__,
996
- repr=repr(exc),
1416
+ class BaseSpecifier(metaclass=abc.ABCMeta):
1417
+ @abc.abstractmethod
1418
+ def __str__(self) -> str:
1419
+ raise NotImplementedError
997
1420
 
998
- traceback=(
999
- ''.join(traceback.format_tb(exc.__traceback__))
1000
- if getattr(exc, '__traceback__', None) is not None else None
1001
- ),
1421
+ @abc.abstractmethod
1422
+ def __hash__(self) -> int:
1423
+ raise NotImplementedError
1002
1424
 
1003
- exc=None if omit_exc_object else exc,
1425
+ @abc.abstractmethod
1426
+ def __eq__(self, other: object) -> bool:
1427
+ raise NotImplementedError
1004
1428
 
1005
- cmd=cmd,
1006
- )
1429
+ @property
1430
+ @abc.abstractmethod
1431
+ def prereleases(self) -> ta.Optional[bool]:
1432
+ raise NotImplementedError
1007
1433
 
1434
+ @prereleases.setter
1435
+ def prereleases(self, value: bool) -> None:
1436
+ raise NotImplementedError
1008
1437
 
1009
- class CommandOutputOrException(abc.ABC, ta.Generic[CommandOutputT]):
1010
- @property
1011
1438
  @abc.abstractmethod
1012
- def output(self) -> ta.Optional[CommandOutputT]:
1439
+ def contains(self, item: str, prereleases: ta.Optional[bool] = None) -> bool:
1013
1440
  raise NotImplementedError
1014
1441
 
1015
- @property
1016
1442
  @abc.abstractmethod
1017
- def exception(self) -> ta.Optional[CommandException]:
1443
+ def filter(
1444
+ self,
1445
+ iterable: ta.Iterable[UnparsedVersionVar],
1446
+ prereleases: ta.Optional[bool] = None,
1447
+ ) -> ta.Iterator[UnparsedVersionVar]:
1018
1448
  raise NotImplementedError
1019
1449
 
1020
1450
 
1021
- @dc.dataclass(frozen=True)
1022
- class CommandOutputOrExceptionData(CommandOutputOrException):
1023
- output: ta.Optional[Command.Output] = None
1024
- exception: ta.Optional[CommandException] = None
1451
+ class Specifier(BaseSpecifier):
1452
+ _operator_regex_str = r"""
1453
+ (?P<operator>(~=|==|!=|<=|>=|<|>|===))
1454
+ """
1025
1455
 
1456
+ _version_regex_str = r"""
1457
+ (?P<version>
1458
+ (?:
1459
+ (?<====)
1460
+ \s*
1461
+ [^\s;)]*
1462
+ )
1463
+ |
1464
+ (?:
1465
+ (?<===|!=)
1466
+ \s*
1467
+ v?
1468
+ (?:[0-9]+!)?
1469
+ [0-9]+(?:\.[0-9]+)*
1470
+ (?:
1471
+ \.\*
1472
+ |
1473
+ (?:
1474
+ [-_\.]?
1475
+ (alpha|beta|preview|pre|a|b|c|rc)
1476
+ [-_\.]?
1477
+ [0-9]*
1478
+ )?
1479
+ (?:
1480
+ (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
1481
+ )?
1482
+ (?:[-_\.]?dev[-_\.]?[0-9]*)?
1483
+ (?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)?
1484
+ )?
1485
+ )
1486
+ |
1487
+ (?:
1488
+ (?<=~=)
1489
+ \s*
1490
+ v?
1491
+ (?:[0-9]+!)?
1492
+ [0-9]+(?:\.[0-9]+)+
1493
+ (?:
1494
+ [-_\.]?
1495
+ (alpha|beta|preview|pre|a|b|c|rc)
1496
+ [-_\.]?
1497
+ [0-9]*
1498
+ )?
1499
+ (?:
1500
+ (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
1501
+ )?
1502
+ (?:[-_\.]?dev[-_\.]?[0-9]*)?
1503
+ )
1504
+ |
1505
+ (?:
1506
+ (?<!==|!=|~=)
1507
+ \s*
1508
+ v?
1509
+ (?:[0-9]+!)?
1510
+ [0-9]+(?:\.[0-9]+)*
1511
+ (?:
1512
+ [-_\.]?
1513
+ (alpha|beta|preview|pre|a|b|c|rc)
1514
+ [-_\.]?
1515
+ [0-9]*
1516
+ )?
1517
+ (?:
1518
+ (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
1519
+ )?
1520
+ (?:[-_\.]?dev[-_\.]?[0-9]*)?
1521
+ )
1522
+ )
1523
+ """
1026
1524
 
1027
- class CommandExecutor(abc.ABC, ta.Generic[CommandT, CommandOutputT]):
1028
- @abc.abstractmethod
1029
- def execute(self, cmd: CommandT) -> CommandOutputT:
1030
- raise NotImplementedError
1525
+ _regex = re.compile(
1526
+ r'^\s*' + _operator_regex_str + _version_regex_str + r'\s*$',
1527
+ re.VERBOSE | re.IGNORECASE,
1528
+ )
1031
1529
 
1032
- def try_execute(
1530
+ OPERATORS: ta.ClassVar[ta.Mapping[str, str]] = {
1531
+ '~=': 'compatible',
1532
+ '==': 'equal',
1533
+ '!=': 'not_equal',
1534
+ '<=': 'less_than_equal',
1535
+ '>=': 'greater_than_equal',
1536
+ '<': 'less_than',
1537
+ '>': 'greater_than',
1538
+ '===': 'arbitrary',
1539
+ }
1540
+
1541
+ def __init__(
1033
1542
  self,
1034
- cmd: CommandT,
1035
- *,
1036
- log: ta.Optional[logging.Logger] = None,
1037
- omit_exc_object: bool = False,
1038
- ) -> CommandOutputOrException[CommandOutputT]:
1039
- try:
1040
- o = self.execute(cmd)
1543
+ spec: str = '',
1544
+ prereleases: ta.Optional[bool] = None,
1545
+ ) -> None:
1546
+ match = self._regex.search(spec)
1547
+ if not match:
1548
+ raise InvalidSpecifier(f"Invalid specifier: '{spec}'")
1041
1549
 
1042
- except Exception as e: # noqa
1043
- if log is not None:
1044
- log.exception('Exception executing command: %r', type(cmd))
1550
+ self._spec: ta.Tuple[str, str] = (
1551
+ match.group('operator').strip(),
1552
+ match.group('version').strip(),
1553
+ )
1045
1554
 
1046
- return CommandOutputOrExceptionData(exception=CommandException.of(
1047
- e,
1048
- omit_exc_object=omit_exc_object,
1049
- cmd=cmd,
1050
- ))
1555
+ self._prereleases = prereleases
1051
1556
 
1052
- else:
1053
- return CommandOutputOrExceptionData(output=o)
1557
+ @property # type: ignore
1558
+ def prereleases(self) -> bool:
1559
+ if self._prereleases is not None:
1560
+ return self._prereleases
1054
1561
 
1562
+ operator, version = self._spec
1563
+ if operator in ['==', '>=', '<=', '~=', '===']:
1564
+ if operator == '==' and version.endswith('.*'):
1565
+ version = version[:-2]
1055
1566
 
1056
- ##
1567
+ if Version(version).is_prerelease:
1568
+ return True
1057
1569
 
1570
+ return False
1058
1571
 
1059
- @dc.dataclass(frozen=True)
1060
- class CommandRegistration:
1061
- command_cls: ta.Type[Command]
1572
+ @prereleases.setter
1573
+ def prereleases(self, value: bool) -> None:
1574
+ self._prereleases = value
1062
1575
 
1063
- name: ta.Optional[str] = None
1576
+ @property
1577
+ def operator(self) -> str:
1578
+ return self._spec[0]
1064
1579
 
1065
1580
  @property
1066
- def name_or_default(self) -> str:
1067
- if not (cls_name := self.command_cls.__name__).endswith('Command'):
1068
- raise NameError(cls_name)
1069
- return snake_case(cls_name[:-len('Command')])
1581
+ def version(self) -> str:
1582
+ return self._spec[1]
1583
+
1584
+ def __repr__(self) -> str:
1585
+ pre = (
1586
+ f', prereleases={self.prereleases!r}'
1587
+ if self._prereleases is not None
1588
+ else ''
1589
+ )
1070
1590
 
1591
+ return f'<{self.__class__.__name__}({str(self)!r}{pre})>'
1071
1592
 
1072
- CommandRegistrations = ta.NewType('CommandRegistrations', ta.Sequence[CommandRegistration])
1593
+ def __str__(self) -> str:
1594
+ return '{}{}'.format(*self._spec)
1073
1595
 
1596
+ @property
1597
+ def _canonical_spec(self) -> ta.Tuple[str, str]:
1598
+ canonical_version = canonicalize_version(
1599
+ self._spec[1],
1600
+ strip_trailing_zero=(self._spec[0] != '~='),
1601
+ )
1602
+ return self._spec[0], canonical_version
1074
1603
 
1075
- ##
1604
+ def __hash__(self) -> int:
1605
+ return hash(self._canonical_spec)
1076
1606
 
1607
+ def __eq__(self, other: object) -> bool:
1608
+ if isinstance(other, str):
1609
+ try:
1610
+ other = self.__class__(str(other))
1611
+ except InvalidSpecifier:
1612
+ return NotImplemented
1613
+ elif not isinstance(other, self.__class__):
1614
+ return NotImplemented
1077
1615
 
1078
- @dc.dataclass(frozen=True)
1079
- class CommandExecutorRegistration:
1080
- command_cls: ta.Type[Command]
1081
- executor_cls: ta.Type[CommandExecutor]
1616
+ return self._canonical_spec == other._canonical_spec
1082
1617
 
1618
+ def _get_operator(self, op: str) -> CallableVersionOperator:
1619
+ operator_callable: CallableVersionOperator = getattr(self, f'_compare_{self.OPERATORS[op]}')
1620
+ return operator_callable
1083
1621
 
1084
- CommandExecutorRegistrations = ta.NewType('CommandExecutorRegistrations', ta.Sequence[CommandExecutorRegistration])
1622
+ def _compare_compatible(self, prospective: Version, spec: str) -> bool:
1623
+ prefix = _version_join(list(itertools.takewhile(_is_not_version_suffix, _version_split(spec)))[:-1])
1624
+ prefix += '.*'
1625
+ return self._get_operator('>=')(prospective, spec) and self._get_operator('==')(prospective, prefix)
1085
1626
 
1627
+ def _compare_equal(self, prospective: Version, spec: str) -> bool:
1628
+ if spec.endswith('.*'):
1629
+ normalized_prospective = canonicalize_version(prospective.public, strip_trailing_zero=False)
1630
+ normalized_spec = canonicalize_version(spec[:-2], strip_trailing_zero=False)
1631
+ split_spec = _version_split(normalized_spec)
1086
1632
 
1087
- ##
1633
+ split_prospective = _version_split(normalized_prospective)
1634
+ padded_prospective, _ = _pad_version(split_prospective, split_spec)
1635
+ shortened_prospective = padded_prospective[: len(split_spec)]
1088
1636
 
1637
+ return shortened_prospective == split_spec
1089
1638
 
1090
- CommandNameMap = ta.NewType('CommandNameMap', ta.Mapping[str, ta.Type[Command]])
1639
+ else:
1640
+ spec_version = Version(spec)
1641
+ if not spec_version.local:
1642
+ prospective = Version(prospective.public)
1643
+ return prospective == spec_version
1644
+
1645
+ def _compare_not_equal(self, prospective: Version, spec: str) -> bool:
1646
+ return not self._compare_equal(prospective, spec)
1647
+
1648
+ def _compare_less_than_equal(self, prospective: Version, spec: str) -> bool:
1649
+ return Version(prospective.public) <= Version(spec)
1650
+
1651
+ def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool:
1652
+ return Version(prospective.public) >= Version(spec)
1653
+
1654
+ def _compare_less_than(self, prospective: Version, spec_str: str) -> bool:
1655
+ spec = Version(spec_str)
1656
+
1657
+ if not prospective < spec:
1658
+ return False
1659
+
1660
+ if not spec.is_prerelease and prospective.is_prerelease:
1661
+ if Version(prospective.base_version) == Version(spec.base_version):
1662
+ return False
1663
+
1664
+ return True
1665
+
1666
+ def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool:
1667
+ spec = Version(spec_str)
1668
+
1669
+ if not prospective > spec:
1670
+ return False
1671
+
1672
+ if not spec.is_postrelease and prospective.is_postrelease:
1673
+ if Version(prospective.base_version) == Version(spec.base_version):
1674
+ return False
1675
+
1676
+ if prospective.local is not None:
1677
+ if Version(prospective.base_version) == Version(spec.base_version):
1678
+ return False
1679
+
1680
+ return True
1681
+
1682
+ def _compare_arbitrary(self, prospective: Version, spec: str) -> bool:
1683
+ return str(prospective).lower() == str(spec).lower()
1684
+
1685
+ def __contains__(self, item: ta.Union[str, Version]) -> bool:
1686
+ return self.contains(item)
1687
+
1688
+ def contains(self, item: UnparsedVersion, prereleases: ta.Optional[bool] = None) -> bool:
1689
+ if prereleases is None:
1690
+ prereleases = self.prereleases
1691
+
1692
+ normalized_item = _coerce_version(item)
1693
+
1694
+ if normalized_item.is_prerelease and not prereleases:
1695
+ return False
1696
+
1697
+ operator_callable: CallableVersionOperator = self._get_operator(self.operator)
1698
+ return operator_callable(normalized_item, self.version)
1699
+
1700
+ def filter(
1701
+ self,
1702
+ iterable: ta.Iterable[UnparsedVersionVar],
1703
+ prereleases: ta.Optional[bool] = None,
1704
+ ) -> ta.Iterator[UnparsedVersionVar]:
1705
+ yielded = False
1706
+ found_prereleases = []
1707
+
1708
+ kw = {'prereleases': prereleases if prereleases is not None else True}
1709
+
1710
+ for version in iterable:
1711
+ parsed_version = _coerce_version(version)
1712
+
1713
+ if self.contains(parsed_version, **kw):
1714
+ if parsed_version.is_prerelease and not (prereleases or self.prereleases):
1715
+ found_prereleases.append(version)
1716
+ else:
1717
+ yielded = True
1718
+ yield version
1719
+
1720
+ if not yielded and found_prereleases:
1721
+ for version in found_prereleases:
1722
+ yield version
1723
+
1724
+
1725
+ _version_prefix_regex = re.compile(r'^([0-9]+)((?:a|b|c|rc)[0-9]+)$')
1726
+
1727
+
1728
+ def _version_split(version: str) -> ta.List[str]:
1729
+ result: ta.List[str] = []
1730
+
1731
+ epoch, _, rest = version.rpartition('!')
1732
+ result.append(epoch or '0')
1733
+
1734
+ for item in rest.split('.'):
1735
+ match = _version_prefix_regex.search(item)
1736
+ if match:
1737
+ result.extend(match.groups())
1738
+ else:
1739
+ result.append(item)
1740
+ return result
1741
+
1742
+
1743
+ def _version_join(components: ta.List[str]) -> str:
1744
+ epoch, *rest = components
1745
+ return f"{epoch}!{'.'.join(rest)}"
1746
+
1747
+
1748
+ def _is_not_version_suffix(segment: str) -> bool:
1749
+ return not any(segment.startswith(prefix) for prefix in ('dev', 'a', 'b', 'rc', 'post'))
1750
+
1751
+
1752
+ def _pad_version(left: ta.List[str], right: ta.List[str]) -> ta.Tuple[ta.List[str], ta.List[str]]:
1753
+ left_split, right_split = [], []
1754
+
1755
+ left_split.append(list(itertools.takewhile(lambda x: x.isdigit(), left)))
1756
+ right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right)))
1757
+
1758
+ left_split.append(left[len(left_split[0]):])
1759
+ right_split.append(right[len(right_split[0]):])
1760
+
1761
+ left_split.insert(1, ['0'] * max(0, len(right_split[0]) - len(left_split[0])))
1762
+ right_split.insert(1, ['0'] * max(0, len(left_split[0]) - len(right_split[0])))
1763
+
1764
+ return (
1765
+ list(itertools.chain.from_iterable(left_split)),
1766
+ list(itertools.chain.from_iterable(right_split)),
1767
+ )
1768
+
1769
+
1770
+ class SpecifierSet(BaseSpecifier):
1771
+ def __init__(
1772
+ self,
1773
+ specifiers: str = '',
1774
+ prereleases: ta.Optional[bool] = None,
1775
+ ) -> None:
1776
+ split_specifiers = [s.strip() for s in specifiers.split(',') if s.strip()]
1777
+
1778
+ self._specs = frozenset(map(Specifier, split_specifiers))
1779
+ self._prereleases = prereleases
1780
+
1781
+ @property
1782
+ def prereleases(self) -> ta.Optional[bool]:
1783
+ if self._prereleases is not None:
1784
+ return self._prereleases
1785
+
1786
+ if not self._specs:
1787
+ return None
1788
+
1789
+ return any(s.prereleases for s in self._specs)
1790
+
1791
+ @prereleases.setter
1792
+ def prereleases(self, value: bool) -> None:
1793
+ self._prereleases = value
1794
+
1795
+ def __repr__(self) -> str:
1796
+ pre = (
1797
+ f', prereleases={self.prereleases!r}'
1798
+ if self._prereleases is not None
1799
+ else ''
1800
+ )
1801
+
1802
+ return f'<SpecifierSet({str(self)!r}{pre})>'
1803
+
1804
+ def __str__(self) -> str:
1805
+ return ','.join(sorted(str(s) for s in self._specs))
1806
+
1807
+ def __hash__(self) -> int:
1808
+ return hash(self._specs)
1809
+
1810
+ def __and__(self, other: ta.Union['SpecifierSet', str]) -> 'SpecifierSet':
1811
+ if isinstance(other, str):
1812
+ other = SpecifierSet(other)
1813
+ elif not isinstance(other, SpecifierSet):
1814
+ return NotImplemented # type: ignore
1815
+
1816
+ specifier = SpecifierSet()
1817
+ specifier._specs = frozenset(self._specs | other._specs)
1818
+
1819
+ if self._prereleases is None and other._prereleases is not None:
1820
+ specifier._prereleases = other._prereleases
1821
+ elif self._prereleases is not None and other._prereleases is None:
1822
+ specifier._prereleases = self._prereleases
1823
+ elif self._prereleases == other._prereleases:
1824
+ specifier._prereleases = self._prereleases
1825
+ else:
1826
+ raise ValueError('Cannot combine SpecifierSets with True and False prerelease overrides.')
1827
+
1828
+ return specifier
1829
+
1830
+ def __eq__(self, other: object) -> bool:
1831
+ if isinstance(other, (str, Specifier)):
1832
+ other = SpecifierSet(str(other))
1833
+ elif not isinstance(other, SpecifierSet):
1834
+ return NotImplemented
1835
+
1836
+ return self._specs == other._specs
1837
+
1838
+ def __len__(self) -> int:
1839
+ return len(self._specs)
1840
+
1841
+ def __iter__(self) -> ta.Iterator[Specifier]:
1842
+ return iter(self._specs)
1843
+
1844
+ def __contains__(self, item: UnparsedVersion) -> bool:
1845
+ return self.contains(item)
1846
+
1847
+ def contains(
1848
+ self,
1849
+ item: UnparsedVersion,
1850
+ prereleases: ta.Optional[bool] = None,
1851
+ installed: ta.Optional[bool] = None,
1852
+ ) -> bool:
1853
+ if not isinstance(item, Version):
1854
+ item = Version(item)
1855
+
1856
+ if prereleases is None:
1857
+ prereleases = self.prereleases
1858
+
1859
+ if not prereleases and item.is_prerelease:
1860
+ return False
1861
+
1862
+ if installed and item.is_prerelease:
1863
+ item = Version(item.base_version)
1864
+
1865
+ return all(s.contains(item, prereleases=prereleases) for s in self._specs)
1866
+
1867
+ def filter(
1868
+ self,
1869
+ iterable: ta.Iterable[UnparsedVersionVar],
1870
+ prereleases: ta.Optional[bool] = None,
1871
+ ) -> ta.Iterator[UnparsedVersionVar]:
1872
+ if prereleases is None:
1873
+ prereleases = self.prereleases
1874
+
1875
+ if self._specs:
1876
+ for spec in self._specs:
1877
+ iterable = spec.filter(iterable, prereleases=bool(prereleases))
1878
+ return iter(iterable)
1879
+
1880
+ else:
1881
+ filtered: ta.List[UnparsedVersionVar] = []
1882
+ found_prereleases: ta.List[UnparsedVersionVar] = []
1883
+
1884
+ for item in iterable:
1885
+ parsed_version = _coerce_version(item)
1886
+
1887
+ if parsed_version.is_prerelease and not prereleases:
1888
+ if not filtered:
1889
+ found_prereleases.append(item)
1890
+ else:
1891
+ filtered.append(item)
1892
+
1893
+ if not filtered and found_prereleases and prereleases is None:
1894
+ return iter(found_prereleases)
1895
+
1896
+ return iter(filtered)
1897
+
1898
+
1899
+ ########################################
1900
+ # ../commands/base.py
1901
+
1902
+
1903
+ ##
1904
+
1905
+
1906
+ @dc.dataclass(frozen=True)
1907
+ class Command(abc.ABC, ta.Generic[CommandOutputT]):
1908
+ @dc.dataclass(frozen=True)
1909
+ class Output(abc.ABC): # noqa
1910
+ pass
1911
+
1912
+ @ta.final
1913
+ def execute(self, executor: 'CommandExecutor') -> CommandOutputT:
1914
+ return check_isinstance(executor.execute(self), self.Output) # type: ignore[return-value]
1915
+
1916
+
1917
+ ##
1918
+
1919
+
1920
+ @dc.dataclass(frozen=True)
1921
+ class CommandException:
1922
+ name: str
1923
+ repr: str
1924
+
1925
+ traceback: ta.Optional[str] = None
1926
+
1927
+ exc: ta.Optional[ta.Any] = None # Exception
1928
+
1929
+ cmd: ta.Optional[Command] = None
1930
+
1931
+ @classmethod
1932
+ def of(
1933
+ cls,
1934
+ exc: Exception,
1935
+ *,
1936
+ omit_exc_object: bool = False,
1937
+
1938
+ cmd: ta.Optional[Command] = None,
1939
+ ) -> 'CommandException':
1940
+ return CommandException(
1941
+ name=type(exc).__qualname__,
1942
+ repr=repr(exc),
1943
+
1944
+ traceback=(
1945
+ ''.join(traceback.format_tb(exc.__traceback__))
1946
+ if getattr(exc, '__traceback__', None) is not None else None
1947
+ ),
1948
+
1949
+ exc=None if omit_exc_object else exc,
1950
+
1951
+ cmd=cmd,
1952
+ )
1953
+
1954
+
1955
+ class CommandOutputOrException(abc.ABC, ta.Generic[CommandOutputT]):
1956
+ @property
1957
+ @abc.abstractmethod
1958
+ def output(self) -> ta.Optional[CommandOutputT]:
1959
+ raise NotImplementedError
1960
+
1961
+ @property
1962
+ @abc.abstractmethod
1963
+ def exception(self) -> ta.Optional[CommandException]:
1964
+ raise NotImplementedError
1965
+
1966
+
1967
+ @dc.dataclass(frozen=True)
1968
+ class CommandOutputOrExceptionData(CommandOutputOrException):
1969
+ output: ta.Optional[Command.Output] = None
1970
+ exception: ta.Optional[CommandException] = None
1971
+
1972
+
1973
+ class CommandExecutor(abc.ABC, ta.Generic[CommandT, CommandOutputT]):
1974
+ @abc.abstractmethod
1975
+ def execute(self, cmd: CommandT) -> CommandOutputT:
1976
+ raise NotImplementedError
1977
+
1978
+ def try_execute(
1979
+ self,
1980
+ cmd: CommandT,
1981
+ *,
1982
+ log: ta.Optional[logging.Logger] = None,
1983
+ omit_exc_object: bool = False,
1984
+ ) -> CommandOutputOrException[CommandOutputT]:
1985
+ try:
1986
+ o = self.execute(cmd)
1987
+
1988
+ except Exception as e: # noqa
1989
+ if log is not None:
1990
+ log.exception('Exception executing command: %r', type(cmd))
1991
+
1992
+ return CommandOutputOrExceptionData(exception=CommandException.of(
1993
+ e,
1994
+ omit_exc_object=omit_exc_object,
1995
+ cmd=cmd,
1996
+ ))
1997
+
1998
+ else:
1999
+ return CommandOutputOrExceptionData(output=o)
2000
+
2001
+
2002
+ ##
2003
+
2004
+
2005
+ @dc.dataclass(frozen=True)
2006
+ class CommandRegistration:
2007
+ command_cls: ta.Type[Command]
2008
+
2009
+ name: ta.Optional[str] = None
2010
+
2011
+ @property
2012
+ def name_or_default(self) -> str:
2013
+ if not (cls_name := self.command_cls.__name__).endswith('Command'):
2014
+ raise NameError(cls_name)
2015
+ return snake_case(cls_name[:-len('Command')])
2016
+
2017
+
2018
+ CommandRegistrations = ta.NewType('CommandRegistrations', ta.Sequence[CommandRegistration])
2019
+
2020
+
2021
+ ##
2022
+
2023
+
2024
+ @dc.dataclass(frozen=True)
2025
+ class CommandExecutorRegistration:
2026
+ command_cls: ta.Type[Command]
2027
+ executor_cls: ta.Type[CommandExecutor]
2028
+
2029
+
2030
+ CommandExecutorRegistrations = ta.NewType('CommandExecutorRegistrations', ta.Sequence[CommandExecutorRegistration])
2031
+
2032
+
2033
+ ##
2034
+
2035
+
2036
+ CommandNameMap = ta.NewType('CommandNameMap', ta.Mapping[str, ta.Type[Command]])
1091
2037
 
1092
2038
 
1093
2039
  def build_command_name_map(crs: CommandRegistrations) -> CommandNameMap:
@@ -2770,39 +3716,130 @@ def check_runtime_version() -> None:
2770
3716
 
2771
3717
 
2772
3718
  ########################################
2773
- # ../bootstrap.py
3719
+ # ../../../omdev/interp/types.py
2774
3720
 
2775
3721
 
2776
- @dc.dataclass(frozen=True)
2777
- class MainBootstrap:
2778
- main_config: MainConfig = MainConfig()
2779
-
2780
- remote_config: RemoteConfig = RemoteConfig()
3722
+ # See https://peps.python.org/pep-3149/
3723
+ INTERP_OPT_GLYPHS_BY_ATTR: ta.Mapping[str, str] = collections.OrderedDict([
3724
+ ('debug', 'd'),
3725
+ ('threaded', 't'),
3726
+ ])
2781
3727
 
3728
+ INTERP_OPT_ATTRS_BY_GLYPH: ta.Mapping[str, str] = collections.OrderedDict(
3729
+ (g, a) for a, g in INTERP_OPT_GLYPHS_BY_ATTR.items()
3730
+ )
2782
3731
 
2783
- ########################################
2784
- # ../commands/execution.py
2785
3732
 
3733
+ @dc.dataclass(frozen=True)
3734
+ class InterpOpts:
3735
+ threaded: bool = False
3736
+ debug: bool = False
2786
3737
 
2787
- CommandExecutorMap = ta.NewType('CommandExecutorMap', ta.Mapping[ta.Type[Command], CommandExecutor])
3738
+ def __str__(self) -> str:
3739
+ return ''.join(g for a, g in INTERP_OPT_GLYPHS_BY_ATTR.items() if getattr(self, a))
2788
3740
 
3741
+ @classmethod
3742
+ def parse(cls, s: str) -> 'InterpOpts':
3743
+ return cls(**{INTERP_OPT_ATTRS_BY_GLYPH[g]: True for g in s})
2789
3744
 
2790
- class LocalCommandExecutor(CommandExecutor):
2791
- def __init__(
2792
- self,
2793
- *,
2794
- command_executors: CommandExecutorMap,
2795
- ) -> None:
2796
- super().__init__()
3745
+ @classmethod
3746
+ def parse_suffix(cls, s: str) -> ta.Tuple[str, 'InterpOpts']:
3747
+ kw = {}
3748
+ while s and (a := INTERP_OPT_ATTRS_BY_GLYPH.get(s[-1])):
3749
+ s, kw[a] = s[:-1], True
3750
+ return s, cls(**kw)
2797
3751
 
2798
- self._command_executors = command_executors
2799
3752
 
2800
- def execute(self, cmd: Command) -> Command.Output:
2801
- ce: CommandExecutor = self._command_executors[type(cmd)]
2802
- return ce.execute(cmd)
3753
+ @dc.dataclass(frozen=True)
3754
+ class InterpVersion:
3755
+ version: Version
3756
+ opts: InterpOpts
2803
3757
 
3758
+ def __str__(self) -> str:
3759
+ return str(self.version) + str(self.opts)
2804
3760
 
2805
- ########################################
3761
+ @classmethod
3762
+ def parse(cls, s: str) -> 'InterpVersion':
3763
+ s, o = InterpOpts.parse_suffix(s)
3764
+ v = Version(s)
3765
+ return cls(
3766
+ version=v,
3767
+ opts=o,
3768
+ )
3769
+
3770
+ @classmethod
3771
+ def try_parse(cls, s: str) -> ta.Optional['InterpVersion']:
3772
+ try:
3773
+ return cls.parse(s)
3774
+ except (KeyError, InvalidVersion):
3775
+ return None
3776
+
3777
+
3778
+ @dc.dataclass(frozen=True)
3779
+ class InterpSpecifier:
3780
+ specifier: Specifier
3781
+ opts: InterpOpts
3782
+
3783
+ def __str__(self) -> str:
3784
+ return str(self.specifier) + str(self.opts)
3785
+
3786
+ @classmethod
3787
+ def parse(cls, s: str) -> 'InterpSpecifier':
3788
+ s, o = InterpOpts.parse_suffix(s)
3789
+ if not any(s.startswith(o) for o in Specifier.OPERATORS):
3790
+ s = '~=' + s
3791
+ return cls(
3792
+ specifier=Specifier(s),
3793
+ opts=o,
3794
+ )
3795
+
3796
+ def contains(self, iv: InterpVersion) -> bool:
3797
+ return self.specifier.contains(iv.version) and self.opts == iv.opts
3798
+
3799
+ def __contains__(self, iv: InterpVersion) -> bool:
3800
+ return self.contains(iv)
3801
+
3802
+
3803
+ @dc.dataclass(frozen=True)
3804
+ class Interp:
3805
+ exe: str
3806
+ version: InterpVersion
3807
+
3808
+
3809
+ ########################################
3810
+ # ../bootstrap.py
3811
+
3812
+
3813
+ @dc.dataclass(frozen=True)
3814
+ class MainBootstrap:
3815
+ main_config: MainConfig = MainConfig()
3816
+
3817
+ remote_config: RemoteConfig = RemoteConfig()
3818
+
3819
+
3820
+ ########################################
3821
+ # ../commands/execution.py
3822
+
3823
+
3824
+ CommandExecutorMap = ta.NewType('CommandExecutorMap', ta.Mapping[ta.Type[Command], CommandExecutor])
3825
+
3826
+
3827
+ class LocalCommandExecutor(CommandExecutor):
3828
+ def __init__(
3829
+ self,
3830
+ *,
3831
+ command_executors: CommandExecutorMap,
3832
+ ) -> None:
3833
+ super().__init__()
3834
+
3835
+ self._command_executors = command_executors
3836
+
3837
+ def execute(self, cmd: Command) -> Command.Output:
3838
+ ce: CommandExecutor = self._command_executors[type(cmd)]
3839
+ return ce.execute(cmd)
3840
+
3841
+
3842
+ ########################################
2806
3843
  # ../commands/marshal.py
2807
3844
 
2808
3845
 
@@ -2846,6 +3883,8 @@ class DeployCommand(Command['DeployCommand.Output']):
2846
3883
 
2847
3884
  class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]):
2848
3885
  def execute(self, cmd: DeployCommand) -> DeployCommand.Output:
3886
+ log.info('Deploying!')
3887
+
2849
3888
  return DeployCommand.Output()
2850
3889
 
2851
3890
 
@@ -2879,10 +3918,12 @@ class RemoteChannel:
2879
3918
  self._output = output
2880
3919
  self._msh = msh
2881
3920
 
3921
+ self._lock = threading.RLock()
3922
+
2882
3923
  def set_marshaler(self, msh: ObjMarshalerManager) -> None:
2883
3924
  self._msh = msh
2884
3925
 
2885
- def send_obj(self, o: ta.Any, ty: ta.Any = None) -> None:
3926
+ def _send_obj(self, o: ta.Any, ty: ta.Any = None) -> None:
2886
3927
  j = json_dumps_compact(self._msh.marshal_obj(o, ty))
2887
3928
  d = j.encode('utf-8')
2888
3929
 
@@ -2890,7 +3931,11 @@ class RemoteChannel:
2890
3931
  self._output.write(d)
2891
3932
  self._output.flush()
2892
3933
 
2893
- def recv_obj(self, ty: ta.Type[T]) -> ta.Optional[T]:
3934
+ def send_obj(self, o: ta.Any, ty: ta.Any = None) -> None:
3935
+ with self._lock:
3936
+ return self._send_obj(o, ty)
3937
+
3938
+ def _recv_obj(self, ty: ta.Type[T]) -> ta.Optional[T]:
2894
3939
  d = self._input.read(4)
2895
3940
  if not d:
2896
3941
  return None
@@ -2905,6 +3950,10 @@ class RemoteChannel:
2905
3950
  j = json.loads(d.decode('utf-8'))
2906
3951
  return self._msh.unmarshal_obj(j, ty)
2907
3952
 
3953
+ def recv_obj(self, ty: ta.Type[T]) -> ta.Optional[T]:
3954
+ with self._lock:
3955
+ return self._recv_obj(ty)
3956
+
2908
3957
 
2909
3958
  ########################################
2910
3959
  # ../../../omlish/lite/subprocesses.py
@@ -3040,6 +4089,102 @@ def subprocess_close(
3040
4089
  proc.wait(timeout)
3041
4090
 
3042
4091
 
4092
+ ########################################
4093
+ # ../../../omdev/interp/inspect.py
4094
+
4095
+
4096
+ @dc.dataclass(frozen=True)
4097
+ class InterpInspection:
4098
+ exe: str
4099
+ version: Version
4100
+
4101
+ version_str: str
4102
+ config_vars: ta.Mapping[str, str]
4103
+ prefix: str
4104
+ base_prefix: str
4105
+
4106
+ @property
4107
+ def opts(self) -> InterpOpts:
4108
+ return InterpOpts(
4109
+ threaded=bool(self.config_vars.get('Py_GIL_DISABLED')),
4110
+ debug=bool(self.config_vars.get('Py_DEBUG')),
4111
+ )
4112
+
4113
+ @property
4114
+ def iv(self) -> InterpVersion:
4115
+ return InterpVersion(
4116
+ version=self.version,
4117
+ opts=self.opts,
4118
+ )
4119
+
4120
+ @property
4121
+ def is_venv(self) -> bool:
4122
+ return self.prefix != self.base_prefix
4123
+
4124
+
4125
+ class InterpInspector:
4126
+
4127
+ def __init__(self) -> None:
4128
+ super().__init__()
4129
+
4130
+ self._cache: ta.Dict[str, ta.Optional[InterpInspection]] = {}
4131
+
4132
+ _RAW_INSPECTION_CODE = """
4133
+ __import__('json').dumps(dict(
4134
+ version_str=__import__('sys').version,
4135
+ prefix=__import__('sys').prefix,
4136
+ base_prefix=__import__('sys').base_prefix,
4137
+ config_vars=__import__('sysconfig').get_config_vars(),
4138
+ ))"""
4139
+
4140
+ _INSPECTION_CODE = ''.join(l.strip() for l in _RAW_INSPECTION_CODE.splitlines())
4141
+
4142
+ @staticmethod
4143
+ def _build_inspection(
4144
+ exe: str,
4145
+ output: str,
4146
+ ) -> InterpInspection:
4147
+ dct = json.loads(output)
4148
+
4149
+ version = Version(dct['version_str'].split()[0])
4150
+
4151
+ return InterpInspection(
4152
+ exe=exe,
4153
+ version=version,
4154
+ **{k: dct[k] for k in (
4155
+ 'version_str',
4156
+ 'prefix',
4157
+ 'base_prefix',
4158
+ 'config_vars',
4159
+ )},
4160
+ )
4161
+
4162
+ @classmethod
4163
+ def running(cls) -> 'InterpInspection':
4164
+ return cls._build_inspection(sys.executable, eval(cls._INSPECTION_CODE)) # noqa
4165
+
4166
+ def _inspect(self, exe: str) -> InterpInspection:
4167
+ output = subprocess_check_output(exe, '-c', f'print({self._INSPECTION_CODE})', quiet=True)
4168
+ return self._build_inspection(exe, output.decode())
4169
+
4170
+ def inspect(self, exe: str) -> ta.Optional[InterpInspection]:
4171
+ try:
4172
+ return self._cache[exe]
4173
+ except KeyError:
4174
+ ret: ta.Optional[InterpInspection]
4175
+ try:
4176
+ ret = self._inspect(exe)
4177
+ except Exception as e: # noqa
4178
+ if log.isEnabledFor(logging.DEBUG):
4179
+ log.exception('Failed to inspect interp: %s', exe)
4180
+ ret = None
4181
+ self._cache[exe] = ret
4182
+ return ret
4183
+
4184
+
4185
+ INTERP_INSPECTOR = InterpInspector()
4186
+
4187
+
3043
4188
  ########################################
3044
4189
  # ../commands/subprocess.py
3045
4190
 
@@ -3062,8 +4207,7 @@ class SubprocessCommand(Command['SubprocessCommand.Output']):
3062
4207
  timeout: ta.Optional[float] = None
3063
4208
 
3064
4209
  def __post_init__(self) -> None:
3065
- if isinstance(self.cmd, str):
3066
- raise TypeError(self.cmd)
4210
+ check_not_isinstance(self.cmd, str)
3067
4211
 
3068
4212
  @dc.dataclass(frozen=True)
3069
4213
  class Output(Command.Output):
@@ -3200,110 +4344,97 @@ class RemoteSpawning:
3200
4344
 
3201
4345
 
3202
4346
  ########################################
3203
- # ../commands/inject.py
4347
+ # ../../../omdev/interp/providers.py
4348
+ """
4349
+ TODO:
4350
+ - backends
4351
+ - local builds
4352
+ - deadsnakes?
4353
+ - uv
4354
+ - loose versions
4355
+ """
3204
4356
 
3205
4357
 
3206
4358
  ##
3207
4359
 
3208
4360
 
3209
- def bind_command(
3210
- command_cls: ta.Type[Command],
3211
- executor_cls: ta.Optional[ta.Type[CommandExecutor]],
3212
- ) -> InjectorBindings:
3213
- lst: ta.List[InjectorBindingOrBindings] = [
3214
- inj.bind(CommandRegistration(command_cls), array=True),
3215
- ]
3216
-
3217
- if executor_cls is not None:
3218
- lst.extend([
3219
- inj.bind(executor_cls, singleton=True),
3220
- inj.bind(CommandExecutorRegistration(command_cls, executor_cls), array=True),
3221
- ])
3222
-
3223
- return inj.as_bindings(*lst)
4361
+ class InterpProvider(abc.ABC):
4362
+ name: ta.ClassVar[str]
3224
4363
 
4364
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
4365
+ super().__init_subclass__(**kwargs)
4366
+ if abc.ABC not in cls.__bases__ and 'name' not in cls.__dict__:
4367
+ sfx = 'InterpProvider'
4368
+ if not cls.__name__.endswith(sfx):
4369
+ raise NameError(cls)
4370
+ setattr(cls, 'name', snake_case(cls.__name__[:-len(sfx)]))
3225
4371
 
3226
- ##
4372
+ @abc.abstractmethod
4373
+ def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
4374
+ raise NotImplementedError
3227
4375
 
4376
+ @abc.abstractmethod
4377
+ def get_installed_version(self, version: InterpVersion) -> Interp:
4378
+ raise NotImplementedError
3228
4379
 
3229
- @dc.dataclass(frozen=True)
3230
- class _FactoryCommandExecutor(CommandExecutor):
3231
- factory: ta.Callable[[], CommandExecutor]
4380
+ def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
4381
+ return []
3232
4382
 
3233
- def execute(self, i: Command) -> Command.Output:
3234
- return self.factory().execute(i)
4383
+ def install_version(self, version: InterpVersion) -> Interp:
4384
+ raise TypeError
3235
4385
 
3236
4386
 
3237
4387
  ##
3238
4388
 
3239
4389
 
3240
- def bind_commands(
3241
- *,
3242
- main_config: MainConfig,
3243
- ) -> InjectorBindings:
3244
- lst: ta.List[InjectorBindingOrBindings] = [
3245
- inj.bind_array(CommandRegistration),
3246
- inj.bind_array_type(CommandRegistration, CommandRegistrations),
3247
-
3248
- inj.bind_array(CommandExecutorRegistration),
3249
- inj.bind_array_type(CommandExecutorRegistration, CommandExecutorRegistrations),
3250
-
3251
- inj.bind(build_command_name_map, singleton=True),
3252
- ]
3253
-
3254
- #
3255
-
3256
- def provide_obj_marshaler_installer(cmds: CommandNameMap) -> ObjMarshalerInstaller:
3257
- return ObjMarshalerInstaller(functools.partial(install_command_marshaling, cmds))
3258
-
3259
- lst.append(inj.bind(provide_obj_marshaler_installer, array=True))
3260
-
3261
- #
3262
-
3263
- def provide_command_executor_map(
3264
- injector: Injector,
3265
- crs: CommandExecutorRegistrations,
3266
- ) -> CommandExecutorMap:
3267
- dct: ta.Dict[ta.Type[Command], CommandExecutor] = {}
3268
-
3269
- cr: CommandExecutorRegistration
3270
- for cr in crs:
3271
- if cr.command_cls in dct:
3272
- raise KeyError(cr.command_cls)
4390
+ class RunningInterpProvider(InterpProvider):
4391
+ @cached_nullary
4392
+ def version(self) -> InterpVersion:
4393
+ return InterpInspector.running().iv
4394
+
4395
+ def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
4396
+ return [self.version()]
4397
+
4398
+ def get_installed_version(self, version: InterpVersion) -> Interp:
4399
+ if version != self.version():
4400
+ raise KeyError(version)
4401
+ return Interp(
4402
+ exe=sys.executable,
4403
+ version=self.version(),
4404
+ )
3273
4405
 
3274
- factory = functools.partial(injector.provide, cr.executor_cls)
3275
- if main_config.debug:
3276
- ce = factory()
3277
- else:
3278
- ce = _FactoryCommandExecutor(factory)
3279
4406
 
3280
- dct[cr.command_cls] = ce
4407
+ ########################################
4408
+ # ../remote/execution.py
3281
4409
 
3282
- return CommandExecutorMap(dct)
3283
4410
 
3284
- lst.extend([
3285
- inj.bind(provide_command_executor_map, singleton=True),
4411
+ ##
3286
4412
 
3287
- inj.bind(LocalCommandExecutor, singleton=True, eager=main_config.debug),
3288
- ])
3289
4413
 
3290
- #
4414
+ class _RemoteExecutionLogHandler(logging.Handler):
4415
+ def __init__(self, fn: ta.Callable[[str], None]) -> None:
4416
+ super().__init__()
4417
+ self._fn = fn
3291
4418
 
3292
- for command_cls, executor_cls in [
3293
- (SubprocessCommand, SubprocessCommandExecutor),
3294
- ]:
3295
- lst.append(bind_command(command_cls, executor_cls))
4419
+ def emit(self, record):
4420
+ msg = self.format(record)
4421
+ self._fn(msg)
3296
4422
 
3297
- #
3298
4423
 
3299
- return inj.as_bindings(*lst)
4424
+ @dc.dataclass(frozen=True)
4425
+ class _RemoteExecutionRequest:
4426
+ c: Command
3300
4427
 
3301
4428
 
3302
- ########################################
3303
- # ../remote/execution.py
4429
+ @dc.dataclass(frozen=True)
4430
+ class _RemoteExecutionLog:
4431
+ s: str
3304
4432
 
3305
4433
 
3306
- ##
4434
+ @dc.dataclass(frozen=True)
4435
+ class _RemoteExecutionResponse:
4436
+ r: ta.Optional[CommandOutputOrExceptionData] = None
4437
+ l: ta.Optional[_RemoteExecutionLog] = None
3307
4438
 
3308
4439
 
3309
4440
  def _remote_execution_main() -> None:
@@ -3323,20 +4454,44 @@ def _remote_execution_main() -> None:
3323
4454
 
3324
4455
  chan.set_marshaler(injector[ObjMarshalerManager])
3325
4456
 
4457
+ #
4458
+
4459
+ log_lock = threading.RLock()
4460
+ send_logs = False
4461
+
4462
+ def log_fn(s: str) -> None:
4463
+ with log_lock:
4464
+ if send_logs:
4465
+ chan.send_obj(_RemoteExecutionResponse(l=_RemoteExecutionLog(s)))
4466
+
4467
+ log_handler = _RemoteExecutionLogHandler(log_fn)
4468
+ logging.root.addHandler(log_handler)
4469
+
4470
+ #
4471
+
3326
4472
  ce = injector[LocalCommandExecutor]
3327
4473
 
3328
4474
  while True:
3329
- i = chan.recv_obj(Command)
3330
- if i is None:
4475
+ req = chan.recv_obj(_RemoteExecutionRequest)
4476
+ if req is None:
3331
4477
  break
3332
4478
 
4479
+ with log_lock:
4480
+ send_logs = True
4481
+
3333
4482
  r = ce.try_execute(
3334
- i,
4483
+ req.c,
3335
4484
  log=log,
3336
4485
  omit_exc_object=True,
3337
4486
  )
3338
4487
 
3339
- chan.send_obj(r)
4488
+ with log_lock:
4489
+ send_logs = False
4490
+
4491
+ chan.send_obj(_RemoteExecutionResponse(r=CommandOutputOrExceptionData(
4492
+ output=r.output,
4493
+ exception=r.exception,
4494
+ )))
3340
4495
 
3341
4496
 
3342
4497
  ##
@@ -3354,12 +4509,17 @@ class RemoteCommandExecutor(CommandExecutor):
3354
4509
  self._chan = chan
3355
4510
 
3356
4511
  def _remote_execute(self, cmd: Command) -> CommandOutputOrException:
3357
- self._chan.send_obj(cmd, Command)
4512
+ self._chan.send_obj(_RemoteExecutionRequest(cmd))
3358
4513
 
3359
- if (r := self._chan.recv_obj(CommandOutputOrExceptionData)) is None:
3360
- raise EOFError
4514
+ while True:
4515
+ if (r := self._chan.recv_obj(_RemoteExecutionResponse)) is None:
4516
+ raise EOFError
3361
4517
 
3362
- return r
4518
+ if r.l is not None:
4519
+ log.info(r.l.s)
4520
+
4521
+ if r.r is not None:
4522
+ return r.r
3363
4523
 
3364
4524
  # @ta.override
3365
4525
  def execute(self, cmd: Command) -> Command.Output:
@@ -3465,54 +4625,846 @@ class RemoteExecution:
3465
4625
 
3466
4626
 
3467
4627
  ########################################
3468
- # ../deploy/inject.py
4628
+ # ../../../omdev/interp/pyenv.py
4629
+ """
4630
+ TODO:
4631
+ - custom tags
4632
+ - 'aliases'
4633
+ - https://github.com/pyenv/pyenv/pull/2966
4634
+ - https://github.com/pyenv/pyenv/issues/218 (lol)
4635
+ - probably need custom (temp?) definition file
4636
+ - *or* python-build directly just into the versions dir?
4637
+ - optionally install / upgrade pyenv itself
4638
+ - new vers dont need these custom mac opts, only run on old vers
4639
+ """
3469
4640
 
3470
4641
 
3471
- def bind_deploy(
3472
- ) -> InjectorBindings:
3473
- lst: ta.List[InjectorBindingOrBindings] = [
3474
- bind_command(DeployCommand, DeployCommandExecutor),
3475
- ]
4642
+ ##
3476
4643
 
3477
- return inj.as_bindings(*lst)
3478
4644
 
4645
+ class Pyenv:
3479
4646
 
3480
- ########################################
3481
- # ../remote/inject.py
4647
+ def __init__(
4648
+ self,
4649
+ *,
4650
+ root: ta.Optional[str] = None,
4651
+ ) -> None:
4652
+ if root is not None and not (isinstance(root, str) and root):
4653
+ raise ValueError(f'pyenv_root: {root!r}')
3482
4654
 
4655
+ super().__init__()
3483
4656
 
3484
- def bind_remote(
3485
- *,
3486
- remote_config: RemoteConfig,
3487
- ) -> InjectorBindings:
3488
- lst: ta.List[InjectorBindingOrBindings] = [
3489
- inj.bind(remote_config),
4657
+ self._root_kw = root
3490
4658
 
3491
- inj.bind(RemoteSpawning, singleton=True),
4659
+ @cached_nullary
4660
+ def root(self) -> ta.Optional[str]:
4661
+ if self._root_kw is not None:
4662
+ return self._root_kw
3492
4663
 
3493
- inj.bind(RemoteExecution, singleton=True),
3494
- ]
4664
+ if shutil.which('pyenv'):
4665
+ return subprocess_check_output_str('pyenv', 'root')
3495
4666
 
3496
- if (pf := remote_config.payload_file) is not None:
3497
- lst.append(inj.bind(pf, to_key=RemoteExecutionPayloadFile))
4667
+ d = os.path.expanduser('~/.pyenv')
4668
+ if os.path.isdir(d) and os.path.isfile(os.path.join(d, 'bin', 'pyenv')):
4669
+ return d
3498
4670
 
3499
- return inj.as_bindings(*lst)
4671
+ return None
4672
+
4673
+ @cached_nullary
4674
+ def exe(self) -> str:
4675
+ return os.path.join(check_not_none(self.root()), 'bin', 'pyenv')
4676
+
4677
+ def version_exes(self) -> ta.List[ta.Tuple[str, str]]:
4678
+ if (root := self.root()) is None:
4679
+ return []
4680
+ ret = []
4681
+ vp = os.path.join(root, 'versions')
4682
+ if os.path.isdir(vp):
4683
+ for dn in os.listdir(vp):
4684
+ ep = os.path.join(vp, dn, 'bin', 'python')
4685
+ if not os.path.isfile(ep):
4686
+ continue
4687
+ ret.append((dn, ep))
4688
+ return ret
3500
4689
 
4690
+ def installable_versions(self) -> ta.List[str]:
4691
+ if self.root() is None:
4692
+ return []
4693
+ ret = []
4694
+ s = subprocess_check_output_str(self.exe(), 'install', '--list')
4695
+ for l in s.splitlines():
4696
+ if not l.startswith(' '):
4697
+ continue
4698
+ l = l.strip()
4699
+ if not l:
4700
+ continue
4701
+ ret.append(l)
4702
+ return ret
3501
4703
 
3502
- ########################################
3503
- # ../inject.py
4704
+ def update(self) -> bool:
4705
+ if (root := self.root()) is None:
4706
+ return False
4707
+ if not os.path.isdir(os.path.join(root, '.git')):
4708
+ return False
4709
+ subprocess_check_call('git', 'pull', cwd=root)
4710
+ return True
3504
4711
 
3505
4712
 
3506
4713
  ##
3507
4714
 
3508
4715
 
3509
- def bind_main(
3510
- *,
3511
- main_config: MainConfig,
3512
- remote_config: RemoteConfig,
3513
- ) -> InjectorBindings:
3514
- lst: ta.List[InjectorBindingOrBindings] = [
3515
- inj.bind(main_config),
4716
+ @dc.dataclass(frozen=True)
4717
+ class PyenvInstallOpts:
4718
+ opts: ta.Sequence[str] = ()
4719
+ conf_opts: ta.Sequence[str] = ()
4720
+ cflags: ta.Sequence[str] = ()
4721
+ ldflags: ta.Sequence[str] = ()
4722
+ env: ta.Mapping[str, str] = dc.field(default_factory=dict)
4723
+
4724
+ def merge(self, *others: 'PyenvInstallOpts') -> 'PyenvInstallOpts':
4725
+ return PyenvInstallOpts(
4726
+ opts=list(itertools.chain.from_iterable(o.opts for o in [self, *others])),
4727
+ conf_opts=list(itertools.chain.from_iterable(o.conf_opts for o in [self, *others])),
4728
+ cflags=list(itertools.chain.from_iterable(o.cflags for o in [self, *others])),
4729
+ ldflags=list(itertools.chain.from_iterable(o.ldflags for o in [self, *others])),
4730
+ env=dict(itertools.chain.from_iterable(o.env.items() for o in [self, *others])),
4731
+ )
4732
+
4733
+
4734
+ # TODO: https://github.com/pyenv/pyenv/blob/master/plugins/python-build/README.md#building-for-maximum-performance
4735
+ DEFAULT_PYENV_INSTALL_OPTS = PyenvInstallOpts(
4736
+ opts=[
4737
+ '-s',
4738
+ '-v',
4739
+ '-k',
4740
+ ],
4741
+ conf_opts=[
4742
+ # FIXME: breaks on mac for older py's
4743
+ '--enable-loadable-sqlite-extensions',
4744
+
4745
+ # '--enable-shared',
4746
+
4747
+ '--enable-optimizations',
4748
+ '--with-lto',
4749
+
4750
+ # '--enable-profiling', # ?
4751
+
4752
+ # '--enable-ipv6', # ?
4753
+ ],
4754
+ cflags=[
4755
+ # '-march=native',
4756
+ # '-mtune=native',
4757
+ ],
4758
+ )
4759
+
4760
+ DEBUG_PYENV_INSTALL_OPTS = PyenvInstallOpts(opts=['-g'])
4761
+
4762
+ THREADED_PYENV_INSTALL_OPTS = PyenvInstallOpts(conf_opts=['--disable-gil'])
4763
+
4764
+
4765
+ #
4766
+
4767
+
4768
+ class PyenvInstallOptsProvider(abc.ABC):
4769
+ @abc.abstractmethod
4770
+ def opts(self) -> PyenvInstallOpts:
4771
+ raise NotImplementedError
4772
+
4773
+
4774
+ class LinuxPyenvInstallOpts(PyenvInstallOptsProvider):
4775
+ def opts(self) -> PyenvInstallOpts:
4776
+ return PyenvInstallOpts()
4777
+
4778
+
4779
+ class DarwinPyenvInstallOpts(PyenvInstallOptsProvider):
4780
+
4781
+ @cached_nullary
4782
+ def framework_opts(self) -> PyenvInstallOpts:
4783
+ return PyenvInstallOpts(conf_opts=['--enable-framework'])
4784
+
4785
+ @cached_nullary
4786
+ def has_brew(self) -> bool:
4787
+ return shutil.which('brew') is not None
4788
+
4789
+ BREW_DEPS: ta.Sequence[str] = [
4790
+ 'openssl',
4791
+ 'readline',
4792
+ 'sqlite3',
4793
+ 'zlib',
4794
+ ]
4795
+
4796
+ @cached_nullary
4797
+ def brew_deps_opts(self) -> PyenvInstallOpts:
4798
+ cflags = []
4799
+ ldflags = []
4800
+ for dep in self.BREW_DEPS:
4801
+ dep_prefix = subprocess_check_output_str('brew', '--prefix', dep)
4802
+ cflags.append(f'-I{dep_prefix}/include')
4803
+ ldflags.append(f'-L{dep_prefix}/lib')
4804
+ return PyenvInstallOpts(
4805
+ cflags=cflags,
4806
+ ldflags=ldflags,
4807
+ )
4808
+
4809
+ @cached_nullary
4810
+ def brew_tcl_opts(self) -> PyenvInstallOpts:
4811
+ if subprocess_try_output('brew', '--prefix', 'tcl-tk') is None:
4812
+ return PyenvInstallOpts()
4813
+
4814
+ tcl_tk_prefix = subprocess_check_output_str('brew', '--prefix', 'tcl-tk')
4815
+ tcl_tk_ver_str = subprocess_check_output_str('brew', 'ls', '--versions', 'tcl-tk')
4816
+ tcl_tk_ver = '.'.join(tcl_tk_ver_str.split()[1].split('.')[:2])
4817
+
4818
+ return PyenvInstallOpts(conf_opts=[
4819
+ f"--with-tcltk-includes='-I{tcl_tk_prefix}/include'",
4820
+ f"--with-tcltk-libs='-L{tcl_tk_prefix}/lib -ltcl{tcl_tk_ver} -ltk{tcl_tk_ver}'",
4821
+ ])
4822
+
4823
+ # @cached_nullary
4824
+ # def brew_ssl_opts(self) -> PyenvInstallOpts:
4825
+ # pkg_config_path = subprocess_check_output_str('brew', '--prefix', 'openssl')
4826
+ # if 'PKG_CONFIG_PATH' in os.environ:
4827
+ # pkg_config_path += ':' + os.environ['PKG_CONFIG_PATH']
4828
+ # return PyenvInstallOpts(env={'PKG_CONFIG_PATH': pkg_config_path})
4829
+
4830
+ def opts(self) -> PyenvInstallOpts:
4831
+ return PyenvInstallOpts().merge(
4832
+ self.framework_opts(),
4833
+ self.brew_deps_opts(),
4834
+ self.brew_tcl_opts(),
4835
+ # self.brew_ssl_opts(),
4836
+ )
4837
+
4838
+
4839
+ PLATFORM_PYENV_INSTALL_OPTS: ta.Dict[str, PyenvInstallOptsProvider] = {
4840
+ 'darwin': DarwinPyenvInstallOpts(),
4841
+ 'linux': LinuxPyenvInstallOpts(),
4842
+ }
4843
+
4844
+
4845
+ ##
4846
+
4847
+
4848
+ class PyenvVersionInstaller:
4849
+ """
4850
+ Messy: can install freethreaded build with a 't' suffixed version str _or_ by THREADED_PYENV_INSTALL_OPTS - need
4851
+ latter to build custom interp with ft, need former to use canned / blessed interps. Muh.
4852
+ """
4853
+
4854
+ def __init__(
4855
+ self,
4856
+ version: str,
4857
+ opts: ta.Optional[PyenvInstallOpts] = None,
4858
+ interp_opts: InterpOpts = InterpOpts(),
4859
+ *,
4860
+ install_name: ta.Optional[str] = None,
4861
+ no_default_opts: bool = False,
4862
+ pyenv: Pyenv = Pyenv(),
4863
+ ) -> None:
4864
+ super().__init__()
4865
+
4866
+ if no_default_opts:
4867
+ if opts is None:
4868
+ opts = PyenvInstallOpts()
4869
+ else:
4870
+ lst = [opts if opts is not None else DEFAULT_PYENV_INSTALL_OPTS]
4871
+ if interp_opts.debug:
4872
+ lst.append(DEBUG_PYENV_INSTALL_OPTS)
4873
+ if interp_opts.threaded:
4874
+ lst.append(THREADED_PYENV_INSTALL_OPTS)
4875
+ lst.append(PLATFORM_PYENV_INSTALL_OPTS[sys.platform].opts())
4876
+ opts = PyenvInstallOpts().merge(*lst)
4877
+
4878
+ self._version = version
4879
+ self._opts = opts
4880
+ self._interp_opts = interp_opts
4881
+ self._given_install_name = install_name
4882
+
4883
+ self._no_default_opts = no_default_opts
4884
+ self._pyenv = pyenv
4885
+
4886
+ @property
4887
+ def version(self) -> str:
4888
+ return self._version
4889
+
4890
+ @property
4891
+ def opts(self) -> PyenvInstallOpts:
4892
+ return self._opts
4893
+
4894
+ @cached_nullary
4895
+ def install_name(self) -> str:
4896
+ if self._given_install_name is not None:
4897
+ return self._given_install_name
4898
+ return self._version + ('-debug' if self._interp_opts.debug else '')
4899
+
4900
+ @cached_nullary
4901
+ def install_dir(self) -> str:
4902
+ return str(os.path.join(check_not_none(self._pyenv.root()), 'versions', self.install_name()))
4903
+
4904
+ @cached_nullary
4905
+ def install(self) -> str:
4906
+ env = {**os.environ, **self._opts.env}
4907
+ for k, l in [
4908
+ ('CFLAGS', self._opts.cflags),
4909
+ ('LDFLAGS', self._opts.ldflags),
4910
+ ('PYTHON_CONFIGURE_OPTS', self._opts.conf_opts),
4911
+ ]:
4912
+ v = ' '.join(l)
4913
+ if k in os.environ:
4914
+ v += ' ' + os.environ[k]
4915
+ env[k] = v
4916
+
4917
+ conf_args = [
4918
+ *self._opts.opts,
4919
+ self._version,
4920
+ ]
4921
+
4922
+ if self._given_install_name is not None:
4923
+ full_args = [
4924
+ os.path.join(check_not_none(self._pyenv.root()), 'plugins', 'python-build', 'bin', 'python-build'),
4925
+ *conf_args,
4926
+ self.install_dir(),
4927
+ ]
4928
+ else:
4929
+ full_args = [
4930
+ self._pyenv.exe(),
4931
+ 'install',
4932
+ *conf_args,
4933
+ ]
4934
+
4935
+ subprocess_check_call(
4936
+ *full_args,
4937
+ env=env,
4938
+ )
4939
+
4940
+ exe = os.path.join(self.install_dir(), 'bin', 'python')
4941
+ if not os.path.isfile(exe):
4942
+ raise RuntimeError(f'Interpreter not found: {exe}')
4943
+ return exe
4944
+
4945
+
4946
+ ##
4947
+
4948
+
4949
+ class PyenvInterpProvider(InterpProvider):
4950
+
4951
+ def __init__(
4952
+ self,
4953
+ pyenv: Pyenv = Pyenv(),
4954
+
4955
+ inspect: bool = False,
4956
+ inspector: InterpInspector = INTERP_INSPECTOR,
4957
+
4958
+ *,
4959
+
4960
+ try_update: bool = False,
4961
+ ) -> None:
4962
+ super().__init__()
4963
+
4964
+ self._pyenv = pyenv
4965
+
4966
+ self._inspect = inspect
4967
+ self._inspector = inspector
4968
+
4969
+ self._try_update = try_update
4970
+
4971
+ #
4972
+
4973
+ @staticmethod
4974
+ def guess_version(s: str) -> ta.Optional[InterpVersion]:
4975
+ def strip_sfx(s: str, sfx: str) -> ta.Tuple[str, bool]:
4976
+ if s.endswith(sfx):
4977
+ return s[:-len(sfx)], True
4978
+ return s, False
4979
+ ok = {}
4980
+ s, ok['debug'] = strip_sfx(s, '-debug')
4981
+ s, ok['threaded'] = strip_sfx(s, 't')
4982
+ try:
4983
+ v = Version(s)
4984
+ except InvalidVersion:
4985
+ return None
4986
+ return InterpVersion(v, InterpOpts(**ok))
4987
+
4988
+ class Installed(ta.NamedTuple):
4989
+ name: str
4990
+ exe: str
4991
+ version: InterpVersion
4992
+
4993
+ def _make_installed(self, vn: str, ep: str) -> ta.Optional[Installed]:
4994
+ iv: ta.Optional[InterpVersion]
4995
+ if self._inspect:
4996
+ try:
4997
+ iv = check_not_none(self._inspector.inspect(ep)).iv
4998
+ except Exception as e: # noqa
4999
+ return None
5000
+ else:
5001
+ iv = self.guess_version(vn)
5002
+ if iv is None:
5003
+ return None
5004
+ return PyenvInterpProvider.Installed(
5005
+ name=vn,
5006
+ exe=ep,
5007
+ version=iv,
5008
+ )
5009
+
5010
+ def installed(self) -> ta.Sequence[Installed]:
5011
+ ret: ta.List[PyenvInterpProvider.Installed] = []
5012
+ for vn, ep in self._pyenv.version_exes():
5013
+ if (i := self._make_installed(vn, ep)) is None:
5014
+ log.debug('Invalid pyenv version: %s', vn)
5015
+ continue
5016
+ ret.append(i)
5017
+ return ret
5018
+
5019
+ #
5020
+
5021
+ def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
5022
+ return [i.version for i in self.installed()]
5023
+
5024
+ def get_installed_version(self, version: InterpVersion) -> Interp:
5025
+ for i in self.installed():
5026
+ if i.version == version:
5027
+ return Interp(
5028
+ exe=i.exe,
5029
+ version=i.version,
5030
+ )
5031
+ raise KeyError(version)
5032
+
5033
+ #
5034
+
5035
+ def _get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
5036
+ lst = []
5037
+
5038
+ for vs in self._pyenv.installable_versions():
5039
+ if (iv := self.guess_version(vs)) is None:
5040
+ continue
5041
+ if iv.opts.debug:
5042
+ raise Exception('Pyenv installable versions not expected to have debug suffix')
5043
+ for d in [False, True]:
5044
+ lst.append(dc.replace(iv, opts=dc.replace(iv.opts, debug=d)))
5045
+
5046
+ return lst
5047
+
5048
+ def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
5049
+ lst = self._get_installable_versions(spec)
5050
+
5051
+ if self._try_update and not any(v in spec for v in lst):
5052
+ if self._pyenv.update():
5053
+ lst = self._get_installable_versions(spec)
5054
+
5055
+ return lst
5056
+
5057
+ def install_version(self, version: InterpVersion) -> Interp:
5058
+ inst_version = str(version.version)
5059
+ inst_opts = version.opts
5060
+ if inst_opts.threaded:
5061
+ inst_version += 't'
5062
+ inst_opts = dc.replace(inst_opts, threaded=False)
5063
+
5064
+ installer = PyenvVersionInstaller(
5065
+ inst_version,
5066
+ interp_opts=inst_opts,
5067
+ )
5068
+
5069
+ exe = installer.install()
5070
+ return Interp(exe, version)
5071
+
5072
+
5073
+ ########################################
5074
+ # ../../../omdev/interp/system.py
5075
+ """
5076
+ TODO:
5077
+ - python, python3, python3.12, ...
5078
+ - check if path py's are venvs: sys.prefix != sys.base_prefix
5079
+ """
5080
+
5081
+
5082
+ ##
5083
+
5084
+
5085
+ @dc.dataclass(frozen=True)
5086
+ class SystemInterpProvider(InterpProvider):
5087
+ cmd: str = 'python3'
5088
+ path: ta.Optional[str] = None
5089
+
5090
+ inspect: bool = False
5091
+ inspector: InterpInspector = INTERP_INSPECTOR
5092
+
5093
+ #
5094
+
5095
+ @staticmethod
5096
+ def _re_which(
5097
+ pat: re.Pattern,
5098
+ *,
5099
+ mode: int = os.F_OK | os.X_OK,
5100
+ path: ta.Optional[str] = None,
5101
+ ) -> ta.List[str]:
5102
+ if path is None:
5103
+ path = os.environ.get('PATH', None)
5104
+ if path is None:
5105
+ try:
5106
+ path = os.confstr('CS_PATH')
5107
+ except (AttributeError, ValueError):
5108
+ path = os.defpath
5109
+
5110
+ if not path:
5111
+ return []
5112
+
5113
+ path = os.fsdecode(path)
5114
+ pathlst = path.split(os.pathsep)
5115
+
5116
+ def _access_check(fn: str, mode: int) -> bool:
5117
+ return os.path.exists(fn) and os.access(fn, mode)
5118
+
5119
+ out = []
5120
+ seen = set()
5121
+ for d in pathlst:
5122
+ normdir = os.path.normcase(d)
5123
+ if normdir not in seen:
5124
+ seen.add(normdir)
5125
+ if not _access_check(normdir, mode):
5126
+ continue
5127
+ for thefile in os.listdir(d):
5128
+ name = os.path.join(d, thefile)
5129
+ if not (
5130
+ os.path.isfile(name) and
5131
+ pat.fullmatch(thefile) and
5132
+ _access_check(name, mode)
5133
+ ):
5134
+ continue
5135
+ out.append(name)
5136
+
5137
+ return out
5138
+
5139
+ @cached_nullary
5140
+ def exes(self) -> ta.List[str]:
5141
+ return self._re_which(
5142
+ re.compile(r'python3(\.\d+)?'),
5143
+ path=self.path,
5144
+ )
5145
+
5146
+ #
5147
+
5148
+ def get_exe_version(self, exe: str) -> ta.Optional[InterpVersion]:
5149
+ if not self.inspect:
5150
+ s = os.path.basename(exe)
5151
+ if s.startswith('python'):
5152
+ s = s[len('python'):]
5153
+ if '.' in s:
5154
+ try:
5155
+ return InterpVersion.parse(s)
5156
+ except InvalidVersion:
5157
+ pass
5158
+ ii = self.inspector.inspect(exe)
5159
+ return ii.iv if ii is not None else None
5160
+
5161
+ def exe_versions(self) -> ta.Sequence[ta.Tuple[str, InterpVersion]]:
5162
+ lst = []
5163
+ for e in self.exes():
5164
+ if (ev := self.get_exe_version(e)) is None:
5165
+ log.debug('Invalid system version: %s', e)
5166
+ continue
5167
+ lst.append((e, ev))
5168
+ return lst
5169
+
5170
+ #
5171
+
5172
+ def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
5173
+ return [ev for e, ev in self.exe_versions()]
5174
+
5175
+ def get_installed_version(self, version: InterpVersion) -> Interp:
5176
+ for e, ev in self.exe_versions():
5177
+ if ev != version:
5178
+ continue
5179
+ return Interp(
5180
+ exe=e,
5181
+ version=ev,
5182
+ )
5183
+ raise KeyError(version)
5184
+
5185
+
5186
+ ########################################
5187
+ # ../remote/inject.py
5188
+
5189
+
5190
+ def bind_remote(
5191
+ *,
5192
+ remote_config: RemoteConfig,
5193
+ ) -> InjectorBindings:
5194
+ lst: ta.List[InjectorBindingOrBindings] = [
5195
+ inj.bind(remote_config),
5196
+
5197
+ inj.bind(RemoteSpawning, singleton=True),
5198
+
5199
+ inj.bind(RemoteExecution, singleton=True),
5200
+ ]
5201
+
5202
+ if (pf := remote_config.payload_file) is not None:
5203
+ lst.append(inj.bind(pf, to_key=RemoteExecutionPayloadFile))
5204
+
5205
+ return inj.as_bindings(*lst)
5206
+
5207
+
5208
+ ########################################
5209
+ # ../../../omdev/interp/resolvers.py
5210
+
5211
+
5212
+ INTERP_PROVIDER_TYPES_BY_NAME: ta.Mapping[str, ta.Type[InterpProvider]] = {
5213
+ cls.name: cls for cls in deep_subclasses(InterpProvider) if abc.ABC not in cls.__bases__ # type: ignore
5214
+ }
5215
+
5216
+
5217
+ class InterpResolver:
5218
+ def __init__(
5219
+ self,
5220
+ providers: ta.Sequence[ta.Tuple[str, InterpProvider]],
5221
+ ) -> None:
5222
+ super().__init__()
5223
+ self._providers: ta.Mapping[str, InterpProvider] = collections.OrderedDict(providers)
5224
+
5225
+ def _resolve_installed(self, spec: InterpSpecifier) -> ta.Optional[ta.Tuple[InterpProvider, InterpVersion]]:
5226
+ lst = [
5227
+ (i, si)
5228
+ for i, p in enumerate(self._providers.values())
5229
+ for si in p.get_installed_versions(spec)
5230
+ if spec.contains(si)
5231
+ ]
5232
+
5233
+ slst = sorted(lst, key=lambda t: (-t[0], t[1].version))
5234
+ if not slst:
5235
+ return None
5236
+
5237
+ bi, bv = slst[-1]
5238
+ bp = list(self._providers.values())[bi]
5239
+ return (bp, bv)
5240
+
5241
+ def resolve(
5242
+ self,
5243
+ spec: InterpSpecifier,
5244
+ *,
5245
+ install: bool = False,
5246
+ ) -> ta.Optional[Interp]:
5247
+ tup = self._resolve_installed(spec)
5248
+ if tup is not None:
5249
+ bp, bv = tup
5250
+ return bp.get_installed_version(bv)
5251
+
5252
+ if not install:
5253
+ return None
5254
+
5255
+ tp = list(self._providers.values())[0] # noqa
5256
+
5257
+ sv = sorted(
5258
+ [s for s in tp.get_installable_versions(spec) if s in spec],
5259
+ key=lambda s: s.version,
5260
+ )
5261
+ if not sv:
5262
+ return None
5263
+
5264
+ bv = sv[-1]
5265
+ return tp.install_version(bv)
5266
+
5267
+ def list(self, spec: InterpSpecifier) -> None:
5268
+ print('installed:')
5269
+ for n, p in self._providers.items():
5270
+ lst = [
5271
+ si
5272
+ for si in p.get_installed_versions(spec)
5273
+ if spec.contains(si)
5274
+ ]
5275
+ if lst:
5276
+ print(f' {n}')
5277
+ for si in lst:
5278
+ print(f' {si}')
5279
+
5280
+ print()
5281
+
5282
+ print('installable:')
5283
+ for n, p in self._providers.items():
5284
+ lst = [
5285
+ si
5286
+ for si in p.get_installable_versions(spec)
5287
+ if spec.contains(si)
5288
+ ]
5289
+ if lst:
5290
+ print(f' {n}')
5291
+ for si in lst:
5292
+ print(f' {si}')
5293
+
5294
+
5295
+ DEFAULT_INTERP_RESOLVER = InterpResolver([(p.name, p) for p in [
5296
+ # pyenv is preferred to system interpreters as it tends to have more support for things like tkinter
5297
+ PyenvInterpProvider(try_update=True),
5298
+
5299
+ RunningInterpProvider(),
5300
+
5301
+ SystemInterpProvider(),
5302
+ ]])
5303
+
5304
+
5305
+ ########################################
5306
+ # ../commands/interp.py
5307
+
5308
+
5309
+ ##
5310
+
5311
+
5312
+ @dc.dataclass(frozen=True)
5313
+ class InterpCommand(Command['InterpCommand.Output']):
5314
+ spec: str
5315
+ install: bool = False
5316
+
5317
+ @dc.dataclass(frozen=True)
5318
+ class Output(Command.Output):
5319
+ exe: str
5320
+ version: str
5321
+ opts: InterpOpts
5322
+
5323
+
5324
+ ##
5325
+
5326
+
5327
+ class InterpCommandExecutor(CommandExecutor[InterpCommand, InterpCommand.Output]):
5328
+ def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
5329
+ i = InterpSpecifier.parse(check_not_none(cmd.spec))
5330
+ o = check_not_none(DEFAULT_INTERP_RESOLVER.resolve(i, install=cmd.install))
5331
+ return InterpCommand.Output(
5332
+ exe=o.exe,
5333
+ version=str(o.version.version),
5334
+ opts=o.version.opts,
5335
+ )
5336
+
5337
+
5338
+ ########################################
5339
+ # ../commands/inject.py
5340
+
5341
+
5342
+ ##
5343
+
5344
+
5345
+ def bind_command(
5346
+ command_cls: ta.Type[Command],
5347
+ executor_cls: ta.Optional[ta.Type[CommandExecutor]],
5348
+ ) -> InjectorBindings:
5349
+ lst: ta.List[InjectorBindingOrBindings] = [
5350
+ inj.bind(CommandRegistration(command_cls), array=True),
5351
+ ]
5352
+
5353
+ if executor_cls is not None:
5354
+ lst.extend([
5355
+ inj.bind(executor_cls, singleton=True),
5356
+ inj.bind(CommandExecutorRegistration(command_cls, executor_cls), array=True),
5357
+ ])
5358
+
5359
+ return inj.as_bindings(*lst)
5360
+
5361
+
5362
+ ##
5363
+
5364
+
5365
+ @dc.dataclass(frozen=True)
5366
+ class _FactoryCommandExecutor(CommandExecutor):
5367
+ factory: ta.Callable[[], CommandExecutor]
5368
+
5369
+ def execute(self, i: Command) -> Command.Output:
5370
+ return self.factory().execute(i)
5371
+
5372
+
5373
+ ##
5374
+
5375
+
5376
+ def bind_commands(
5377
+ *,
5378
+ main_config: MainConfig,
5379
+ ) -> InjectorBindings:
5380
+ lst: ta.List[InjectorBindingOrBindings] = [
5381
+ inj.bind_array(CommandRegistration),
5382
+ inj.bind_array_type(CommandRegistration, CommandRegistrations),
5383
+
5384
+ inj.bind_array(CommandExecutorRegistration),
5385
+ inj.bind_array_type(CommandExecutorRegistration, CommandExecutorRegistrations),
5386
+
5387
+ inj.bind(build_command_name_map, singleton=True),
5388
+ ]
5389
+
5390
+ #
5391
+
5392
+ def provide_obj_marshaler_installer(cmds: CommandNameMap) -> ObjMarshalerInstaller:
5393
+ return ObjMarshalerInstaller(functools.partial(install_command_marshaling, cmds))
5394
+
5395
+ lst.append(inj.bind(provide_obj_marshaler_installer, array=True))
5396
+
5397
+ #
5398
+
5399
+ def provide_command_executor_map(
5400
+ injector: Injector,
5401
+ crs: CommandExecutorRegistrations,
5402
+ ) -> CommandExecutorMap:
5403
+ dct: ta.Dict[ta.Type[Command], CommandExecutor] = {}
5404
+
5405
+ cr: CommandExecutorRegistration
5406
+ for cr in crs:
5407
+ if cr.command_cls in dct:
5408
+ raise KeyError(cr.command_cls)
5409
+
5410
+ factory = functools.partial(injector.provide, cr.executor_cls)
5411
+ if main_config.debug:
5412
+ ce = factory()
5413
+ else:
5414
+ ce = _FactoryCommandExecutor(factory)
5415
+
5416
+ dct[cr.command_cls] = ce
5417
+
5418
+ return CommandExecutorMap(dct)
5419
+
5420
+ lst.extend([
5421
+ inj.bind(provide_command_executor_map, singleton=True),
5422
+
5423
+ inj.bind(LocalCommandExecutor, singleton=True, eager=main_config.debug),
5424
+ ])
5425
+
5426
+ #
5427
+
5428
+ command_cls: ta.Any
5429
+ executor_cls: ta.Any
5430
+ for command_cls, executor_cls in [
5431
+ (SubprocessCommand, SubprocessCommandExecutor),
5432
+ (InterpCommand, InterpCommandExecutor),
5433
+ ]:
5434
+ lst.append(bind_command(command_cls, executor_cls))
5435
+
5436
+ #
5437
+
5438
+ return inj.as_bindings(*lst)
5439
+
5440
+
5441
+ ########################################
5442
+ # ../deploy/inject.py
5443
+
5444
+
5445
+ def bind_deploy(
5446
+ ) -> InjectorBindings:
5447
+ lst: ta.List[InjectorBindingOrBindings] = [
5448
+ bind_command(DeployCommand, DeployCommandExecutor),
5449
+ ]
5450
+
5451
+ return inj.as_bindings(*lst)
5452
+
5453
+
5454
+ ########################################
5455
+ # ../inject.py
5456
+
5457
+
5458
+ ##
5459
+
5460
+
5461
+ def bind_main(
5462
+ *,
5463
+ main_config: MainConfig,
5464
+ remote_config: RemoteConfig,
5465
+ ) -> InjectorBindings:
5466
+ lst: ta.List[InjectorBindingOrBindings] = [
5467
+ inj.bind(main_config),
3516
5468
 
3517
5469
  bind_commands(
3518
5470
  main_config=main_config,
@@ -3618,12 +5570,15 @@ def _main() -> None:
3618
5570
 
3619
5571
  #
3620
5572
 
5573
+ msh = injector[ObjMarshalerManager]
5574
+
3621
5575
  cmds: ta.List[Command] = []
5576
+ cmd: Command
3622
5577
  for c in args.command:
3623
- if c == 'deploy':
3624
- cmds.append(DeployCommand())
3625
- else:
3626
- cmds.append(SubprocessCommand([c]))
5578
+ if not c.startswith('{'):
5579
+ c = json.dumps({c: {}})
5580
+ cmd = msh.unmarshal_obj(json.loads(c), Command)
5581
+ cmds.append(cmd)
3627
5582
 
3628
5583
  #
3629
5584
 
@@ -3643,9 +5598,13 @@ def _main() -> None:
3643
5598
  ce = es.enter_context(injector[RemoteExecution].connect(tgt, bs)) # noqa
3644
5599
 
3645
5600
  for cmd in cmds:
3646
- r = ce.try_execute(cmd)
5601
+ r = ce.try_execute(
5602
+ cmd,
5603
+ log=log,
5604
+ omit_exc_object=True,
5605
+ )
3647
5606
 
3648
- print(injector[ObjMarshalerManager].marshal_obj(r, opts=ObjMarshalOptions(raw_bytes=True)))
5607
+ print(msh.marshal_obj(r, opts=ObjMarshalOptions(raw_bytes=True)))
3649
5608
 
3650
5609
 
3651
5610
  if __name__ == '__main__':