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