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