ominfra 0.0.0.dev145__py3-none-any.whl → 0.0.0.dev146__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
ominfra/scripts/manage.py CHANGED
@@ -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__':