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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ominfra/scripts/manage.py CHANGED
@@ -10,6 +10,7 @@ manage.py -s 'ssh -i /foo/bar.pem foo@bar.baz' -q --python=python3.8
10
10
  """
11
11
  import abc
12
12
  import base64
13
+ import collections
13
14
  import collections.abc
14
15
  import contextlib
15
16
  import dataclasses as dc
@@ -19,12 +20,16 @@ import enum
19
20
  import fractions
20
21
  import functools
21
22
  import inspect
23
+ import itertools
22
24
  import json
23
25
  import logging
24
26
  import os
27
+ import os.path
25
28
  import platform
26
29
  import pwd
30
+ import re
27
31
  import shlex
32
+ import shutil
28
33
  import site
29
34
  import struct
30
35
  import subprocess
@@ -49,6 +54,14 @@ if sys.version_info < (3, 8):
49
54
  ########################################
50
55
 
51
56
 
57
+ # ../../omdev/packaging/versions.py
58
+ VersionLocalType = ta.Tuple[ta.Union[int, str], ...]
59
+ VersionCmpPrePostDevType = ta.Union['InfinityVersionType', 'NegativeInfinityVersionType', ta.Tuple[str, int]]
60
+ _VersionCmpLocalType0 = ta.Tuple[ta.Union[ta.Tuple[int, str], ta.Tuple['NegativeInfinityVersionType', ta.Union[int, str]]], ...] # noqa
61
+ VersionCmpLocalType = ta.Union['NegativeInfinityVersionType', _VersionCmpLocalType0]
62
+ VersionCmpKey = ta.Tuple[int, ta.Tuple[int, ...], VersionCmpPrePostDevType, VersionCmpPrePostDevType, VersionCmpPrePostDevType, VersionCmpLocalType] # noqa
63
+ VersionComparisonMethod = ta.Callable[[VersionCmpKey, VersionCmpKey], bool]
64
+
52
65
  # ../../omlish/lite/cached.py
53
66
  T = ta.TypeVar('T')
54
67
  CallableT = ta.TypeVar('CallableT', bound=ta.Callable)
@@ -56,6 +69,11 @@ CallableT = ta.TypeVar('CallableT', bound=ta.Callable)
56
69
  # ../../omlish/lite/check.py
57
70
  SizedT = ta.TypeVar('SizedT', bound=ta.Sized)
58
71
 
72
+ # ../../omdev/packaging/specifiers.py
73
+ UnparsedVersion = ta.Union['Version', str]
74
+ UnparsedVersionVar = ta.TypeVar('UnparsedVersionVar', bound=UnparsedVersion)
75
+ CallableVersionOperator = ta.Callable[['Version', str], bool]
76
+
59
77
  # commands/base.py
60
78
  CommandT = ta.TypeVar('CommandT', bound='Command')
61
79
  CommandOutputT = ta.TypeVar('CommandOutputT', bound='Command.Output')
@@ -71,6 +89,413 @@ InjectorBindingOrBindings = ta.Union['InjectorBinding', 'InjectorBindings']
71
89
  SubprocessChannelOption = ta.Literal['pipe', 'stdout', 'devnull']
72
90
 
73
91
 
92
+ ########################################
93
+ # ../../../omdev/packaging/versions.py
94
+ # Copyright (c) Donald Stufft and individual contributors.
95
+ # All rights reserved.
96
+ #
97
+ # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
98
+ # following conditions are met:
99
+ #
100
+ # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
101
+ # following disclaimer.
102
+ #
103
+ # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
104
+ # following disclaimer in the documentation and/or other materials provided with the distribution.
105
+ #
106
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
107
+ # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
108
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
109
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
110
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
111
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
112
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. This file is dual licensed under the terms of the
113
+ # Apache License, Version 2.0, and the BSD License. See the LICENSE file in the root of this repository for complete
114
+ # details.
115
+ # https://github.com/pypa/packaging/blob/2c885fe91a54559e2382902dce28428ad2887be5/src/packaging/version.py
116
+
117
+
118
+ ##
119
+
120
+
121
+ class InfinityVersionType:
122
+ def __repr__(self) -> str:
123
+ return 'Infinity'
124
+
125
+ def __hash__(self) -> int:
126
+ return hash(repr(self))
127
+
128
+ def __lt__(self, other: object) -> bool:
129
+ return False
130
+
131
+ def __le__(self, other: object) -> bool:
132
+ return False
133
+
134
+ def __eq__(self, other: object) -> bool:
135
+ return isinstance(other, self.__class__)
136
+
137
+ def __gt__(self, other: object) -> bool:
138
+ return True
139
+
140
+ def __ge__(self, other: object) -> bool:
141
+ return True
142
+
143
+ def __neg__(self: object) -> 'NegativeInfinityVersionType':
144
+ return NegativeInfinityVersion
145
+
146
+
147
+ InfinityVersion = InfinityVersionType()
148
+
149
+
150
+ class NegativeInfinityVersionType:
151
+ def __repr__(self) -> str:
152
+ return '-Infinity'
153
+
154
+ def __hash__(self) -> int:
155
+ return hash(repr(self))
156
+
157
+ def __lt__(self, other: object) -> bool:
158
+ return True
159
+
160
+ def __le__(self, other: object) -> bool:
161
+ return True
162
+
163
+ def __eq__(self, other: object) -> bool:
164
+ return isinstance(other, self.__class__)
165
+
166
+ def __gt__(self, other: object) -> bool:
167
+ return False
168
+
169
+ def __ge__(self, other: object) -> bool:
170
+ return False
171
+
172
+ def __neg__(self: object) -> InfinityVersionType:
173
+ return InfinityVersion
174
+
175
+
176
+ NegativeInfinityVersion = NegativeInfinityVersionType()
177
+
178
+
179
+ ##
180
+
181
+
182
+ class _Version(ta.NamedTuple):
183
+ epoch: int
184
+ release: ta.Tuple[int, ...]
185
+ dev: ta.Optional[ta.Tuple[str, int]]
186
+ pre: ta.Optional[ta.Tuple[str, int]]
187
+ post: ta.Optional[ta.Tuple[str, int]]
188
+ local: ta.Optional[VersionLocalType]
189
+
190
+
191
+ class InvalidVersion(ValueError): # noqa
192
+ pass
193
+
194
+
195
+ class _BaseVersion:
196
+ _key: ta.Tuple[ta.Any, ...]
197
+
198
+ def __hash__(self) -> int:
199
+ return hash(self._key)
200
+
201
+ def __lt__(self, other: '_BaseVersion') -> bool:
202
+ if not isinstance(other, _BaseVersion):
203
+ return NotImplemented # type: ignore
204
+ return self._key < other._key
205
+
206
+ def __le__(self, other: '_BaseVersion') -> bool:
207
+ if not isinstance(other, _BaseVersion):
208
+ return NotImplemented # type: ignore
209
+ return self._key <= other._key
210
+
211
+ def __eq__(self, other: object) -> bool:
212
+ if not isinstance(other, _BaseVersion):
213
+ return NotImplemented
214
+ return self._key == other._key
215
+
216
+ def __ge__(self, other: '_BaseVersion') -> bool:
217
+ if not isinstance(other, _BaseVersion):
218
+ return NotImplemented # type: ignore
219
+ return self._key >= other._key
220
+
221
+ def __gt__(self, other: '_BaseVersion') -> bool:
222
+ if not isinstance(other, _BaseVersion):
223
+ return NotImplemented # type: ignore
224
+ return self._key > other._key
225
+
226
+ def __ne__(self, other: object) -> bool:
227
+ if not isinstance(other, _BaseVersion):
228
+ return NotImplemented
229
+ return self._key != other._key
230
+
231
+
232
+ _VERSION_PATTERN = r"""
233
+ v?
234
+ (?:
235
+ (?:(?P<epoch>[0-9]+)!)?
236
+ (?P<release>[0-9]+(?:\.[0-9]+)*)
237
+ (?P<pre>
238
+ [-_\.]?
239
+ (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc)
240
+ [-_\.]?
241
+ (?P<pre_n>[0-9]+)?
242
+ )?
243
+ (?P<post>
244
+ (?:-(?P<post_n1>[0-9]+))
245
+ |
246
+ (?:
247
+ [-_\.]?
248
+ (?P<post_l>post|rev|r)
249
+ [-_\.]?
250
+ (?P<post_n2>[0-9]+)?
251
+ )
252
+ )?
253
+ (?P<dev>
254
+ [-_\.]?
255
+ (?P<dev_l>dev)
256
+ [-_\.]?
257
+ (?P<dev_n>[0-9]+)?
258
+ )?
259
+ )
260
+ (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?
261
+ """
262
+
263
+ VERSION_PATTERN = _VERSION_PATTERN
264
+
265
+
266
+ class Version(_BaseVersion):
267
+ _regex = re.compile(r'^\s*' + VERSION_PATTERN + r'\s*$', re.VERBOSE | re.IGNORECASE)
268
+ _key: VersionCmpKey
269
+
270
+ def __init__(self, version: str) -> None:
271
+ match = self._regex.search(version)
272
+ if not match:
273
+ raise InvalidVersion(f"Invalid version: '{version}'")
274
+
275
+ self._version = _Version(
276
+ epoch=int(match.group('epoch')) if match.group('epoch') else 0,
277
+ release=tuple(int(i) for i in match.group('release').split('.')),
278
+ pre=_parse_letter_version(match.group('pre_l'), match.group('pre_n')),
279
+ post=_parse_letter_version(match.group('post_l'), match.group('post_n1') or match.group('post_n2')),
280
+ dev=_parse_letter_version(match.group('dev_l'), match.group('dev_n')),
281
+ local=_parse_local_version(match.group('local')),
282
+ )
283
+
284
+ self._key = _version_cmpkey(
285
+ self._version.epoch,
286
+ self._version.release,
287
+ self._version.pre,
288
+ self._version.post,
289
+ self._version.dev,
290
+ self._version.local,
291
+ )
292
+
293
+ def __repr__(self) -> str:
294
+ return f"<Version('{self}')>"
295
+
296
+ def __str__(self) -> str:
297
+ parts = []
298
+
299
+ if self.epoch != 0:
300
+ parts.append(f'{self.epoch}!')
301
+
302
+ parts.append('.'.join(str(x) for x in self.release))
303
+
304
+ if self.pre is not None:
305
+ parts.append(''.join(str(x) for x in self.pre))
306
+
307
+ if self.post is not None:
308
+ parts.append(f'.post{self.post}')
309
+
310
+ if self.dev is not None:
311
+ parts.append(f'.dev{self.dev}')
312
+
313
+ if self.local is not None:
314
+ parts.append(f'+{self.local}')
315
+
316
+ return ''.join(parts)
317
+
318
+ @property
319
+ def epoch(self) -> int:
320
+ return self._version.epoch
321
+
322
+ @property
323
+ def release(self) -> ta.Tuple[int, ...]:
324
+ return self._version.release
325
+
326
+ @property
327
+ def pre(self) -> ta.Optional[ta.Tuple[str, int]]:
328
+ return self._version.pre
329
+
330
+ @property
331
+ def post(self) -> ta.Optional[int]:
332
+ return self._version.post[1] if self._version.post else None
333
+
334
+ @property
335
+ def dev(self) -> ta.Optional[int]:
336
+ return self._version.dev[1] if self._version.dev else None
337
+
338
+ @property
339
+ def local(self) -> ta.Optional[str]:
340
+ if self._version.local:
341
+ return '.'.join(str(x) for x in self._version.local)
342
+ else:
343
+ return None
344
+
345
+ @property
346
+ def public(self) -> str:
347
+ return str(self).split('+', 1)[0]
348
+
349
+ @property
350
+ def base_version(self) -> str:
351
+ parts = []
352
+
353
+ if self.epoch != 0:
354
+ parts.append(f'{self.epoch}!')
355
+
356
+ parts.append('.'.join(str(x) for x in self.release))
357
+
358
+ return ''.join(parts)
359
+
360
+ @property
361
+ def is_prerelease(self) -> bool:
362
+ return self.dev is not None or self.pre is not None
363
+
364
+ @property
365
+ def is_postrelease(self) -> bool:
366
+ return self.post is not None
367
+
368
+ @property
369
+ def is_devrelease(self) -> bool:
370
+ return self.dev is not None
371
+
372
+ @property
373
+ def major(self) -> int:
374
+ return self.release[0] if len(self.release) >= 1 else 0
375
+
376
+ @property
377
+ def minor(self) -> int:
378
+ return self.release[1] if len(self.release) >= 2 else 0
379
+
380
+ @property
381
+ def micro(self) -> int:
382
+ return self.release[2] if len(self.release) >= 3 else 0
383
+
384
+
385
+ def _parse_letter_version(
386
+ letter: ta.Optional[str],
387
+ number: ta.Union[str, bytes, ta.SupportsInt, None],
388
+ ) -> ta.Optional[ta.Tuple[str, int]]:
389
+ if letter:
390
+ if number is None:
391
+ number = 0
392
+
393
+ letter = letter.lower()
394
+ if letter == 'alpha':
395
+ letter = 'a'
396
+ elif letter == 'beta':
397
+ letter = 'b'
398
+ elif letter in ['c', 'pre', 'preview']:
399
+ letter = 'rc'
400
+ elif letter in ['rev', 'r']:
401
+ letter = 'post'
402
+
403
+ return letter, int(number)
404
+ if not letter and number:
405
+ letter = 'post'
406
+ return letter, int(number)
407
+
408
+ return None
409
+
410
+
411
+ _local_version_separators = re.compile(r'[\._-]')
412
+
413
+
414
+ def _parse_local_version(local: ta.Optional[str]) -> ta.Optional[VersionLocalType]:
415
+ if local is not None:
416
+ return tuple(
417
+ part.lower() if not part.isdigit() else int(part)
418
+ for part in _local_version_separators.split(local)
419
+ )
420
+ return None
421
+
422
+
423
+ def _version_cmpkey(
424
+ epoch: int,
425
+ release: ta.Tuple[int, ...],
426
+ pre: ta.Optional[ta.Tuple[str, int]],
427
+ post: ta.Optional[ta.Tuple[str, int]],
428
+ dev: ta.Optional[ta.Tuple[str, int]],
429
+ local: ta.Optional[VersionLocalType],
430
+ ) -> VersionCmpKey:
431
+ _release = tuple(reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release)))))
432
+
433
+ if pre is None and post is None and dev is not None:
434
+ _pre: VersionCmpPrePostDevType = NegativeInfinityVersion
435
+ elif pre is None:
436
+ _pre = InfinityVersion
437
+ else:
438
+ _pre = pre
439
+
440
+ if post is None:
441
+ _post: VersionCmpPrePostDevType = NegativeInfinityVersion
442
+ else:
443
+ _post = post
444
+
445
+ if dev is None:
446
+ _dev: VersionCmpPrePostDevType = InfinityVersion
447
+ else:
448
+ _dev = dev
449
+
450
+ if local is None:
451
+ _local: VersionCmpLocalType = NegativeInfinityVersion
452
+ else:
453
+ _local = tuple((i, '') if isinstance(i, int) else (NegativeInfinityVersion, i) for i in local)
454
+
455
+ return epoch, _release, _pre, _post, _dev, _local
456
+
457
+
458
+ ##
459
+
460
+
461
+ def canonicalize_version(
462
+ version: ta.Union[Version, str],
463
+ *,
464
+ strip_trailing_zero: bool = True,
465
+ ) -> str:
466
+ if isinstance(version, str):
467
+ try:
468
+ parsed = Version(version)
469
+ except InvalidVersion:
470
+ return version
471
+ else:
472
+ parsed = version
473
+
474
+ parts = []
475
+
476
+ if parsed.epoch != 0:
477
+ parts.append(f'{parsed.epoch}!')
478
+
479
+ release_segment = '.'.join(str(x) for x in parsed.release)
480
+ if strip_trailing_zero:
481
+ release_segment = re.sub(r'(\.0)+$', '', release_segment)
482
+ parts.append(release_segment)
483
+
484
+ if parsed.pre is not None:
485
+ parts.append(''.join(str(x) for x in parsed.pre))
486
+
487
+ if parsed.post is not None:
488
+ parts.append(f'.post{parsed.post}')
489
+
490
+ if parsed.dev is not None:
491
+ parts.append(f'.dev{parsed.dev}')
492
+
493
+ if parsed.local is not None:
494
+ parts.append(f'+{parsed.local}')
495
+
496
+ return ''.join(parts)
497
+
498
+
74
499
  ########################################
75
500
  # ../config.py
76
501
 
@@ -951,143 +1376,665 @@ def format_num_bytes(num_bytes: int) -> str:
951
1376
 
952
1377
 
953
1378
  ########################################
954
- # ../commands/base.py
955
-
956
-
957
- ##
958
-
959
-
960
- @dc.dataclass(frozen=True)
961
- class Command(abc.ABC, ta.Generic[CommandOutputT]):
962
- @dc.dataclass(frozen=True)
963
- class Output(abc.ABC): # noqa
964
- pass
965
-
966
- @ta.final
967
- def execute(self, executor: 'CommandExecutor') -> CommandOutputT:
968
- return check_isinstance(executor.execute(self), self.Output) # type: ignore[return-value]
1379
+ # ../../../omdev/packaging/specifiers.py
1380
+ # Copyright (c) Donald Stufft and individual contributors.
1381
+ # All rights reserved.
1382
+ #
1383
+ # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
1384
+ # following conditions are met:
1385
+ #
1386
+ # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
1387
+ # following disclaimer.
1388
+ #
1389
+ # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
1390
+ # following disclaimer in the documentation and/or other materials provided with the distribution.
1391
+ #
1392
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
1393
+ # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
1394
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
1395
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
1396
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
1397
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
1398
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. This file is dual licensed under the terms of the
1399
+ # Apache License, Version 2.0, and the BSD License. See the LICENSE file in the root of this repository for complete
1400
+ # details.
1401
+ # https://github.com/pypa/packaging/blob/2c885fe91a54559e2382902dce28428ad2887be5/src/packaging/specifiers.py
969
1402
 
970
1403
 
971
1404
  ##
972
1405
 
973
1406
 
974
- @dc.dataclass(frozen=True)
975
- class CommandException:
976
- name: str
977
- repr: str
978
-
979
- traceback: ta.Optional[str] = None
1407
+ def _coerce_version(version: UnparsedVersion) -> Version:
1408
+ if not isinstance(version, Version):
1409
+ version = Version(version)
1410
+ return version
980
1411
 
981
- exc: ta.Optional[ta.Any] = None # Exception
982
1412
 
983
- cmd: ta.Optional[Command] = None
1413
+ class InvalidSpecifier(ValueError): # noqa
1414
+ pass
984
1415
 
985
- @classmethod
986
- def of(
987
- cls,
988
- exc: Exception,
989
- *,
990
- omit_exc_object: bool = False,
991
1416
 
992
- cmd: ta.Optional[Command] = None,
993
- ) -> 'CommandException':
994
- return CommandException(
995
- name=type(exc).__qualname__,
996
- repr=repr(exc),
1417
+ class BaseSpecifier(metaclass=abc.ABCMeta):
1418
+ @abc.abstractmethod
1419
+ def __str__(self) -> str:
1420
+ raise NotImplementedError
997
1421
 
998
- traceback=(
999
- ''.join(traceback.format_tb(exc.__traceback__))
1000
- if getattr(exc, '__traceback__', None) is not None else None
1001
- ),
1422
+ @abc.abstractmethod
1423
+ def __hash__(self) -> int:
1424
+ raise NotImplementedError
1002
1425
 
1003
- exc=None if omit_exc_object else exc,
1426
+ @abc.abstractmethod
1427
+ def __eq__(self, other: object) -> bool:
1428
+ raise NotImplementedError
1004
1429
 
1005
- cmd=cmd,
1006
- )
1430
+ @property
1431
+ @abc.abstractmethod
1432
+ def prereleases(self) -> ta.Optional[bool]:
1433
+ raise NotImplementedError
1007
1434
 
1435
+ @prereleases.setter
1436
+ def prereleases(self, value: bool) -> None:
1437
+ raise NotImplementedError
1008
1438
 
1009
- class CommandOutputOrException(abc.ABC, ta.Generic[CommandOutputT]):
1010
- @property
1011
1439
  @abc.abstractmethod
1012
- def output(self) -> ta.Optional[CommandOutputT]:
1440
+ def contains(self, item: str, prereleases: ta.Optional[bool] = None) -> bool:
1013
1441
  raise NotImplementedError
1014
1442
 
1015
- @property
1016
1443
  @abc.abstractmethod
1017
- def exception(self) -> ta.Optional[CommandException]:
1444
+ def filter(
1445
+ self,
1446
+ iterable: ta.Iterable[UnparsedVersionVar],
1447
+ prereleases: ta.Optional[bool] = None,
1448
+ ) -> ta.Iterator[UnparsedVersionVar]:
1018
1449
  raise NotImplementedError
1019
1450
 
1020
1451
 
1021
- @dc.dataclass(frozen=True)
1022
- class CommandOutputOrExceptionData(CommandOutputOrException):
1023
- output: ta.Optional[Command.Output] = None
1024
- exception: ta.Optional[CommandException] = None
1452
+ class Specifier(BaseSpecifier):
1453
+ _operator_regex_str = r"""
1454
+ (?P<operator>(~=|==|!=|<=|>=|<|>|===))
1455
+ """
1025
1456
 
1457
+ _version_regex_str = r"""
1458
+ (?P<version>
1459
+ (?:
1460
+ (?<====)
1461
+ \s*
1462
+ [^\s;)]*
1463
+ )
1464
+ |
1465
+ (?:
1466
+ (?<===|!=)
1467
+ \s*
1468
+ v?
1469
+ (?:[0-9]+!)?
1470
+ [0-9]+(?:\.[0-9]+)*
1471
+ (?:
1472
+ \.\*
1473
+ |
1474
+ (?:
1475
+ [-_\.]?
1476
+ (alpha|beta|preview|pre|a|b|c|rc)
1477
+ [-_\.]?
1478
+ [0-9]*
1479
+ )?
1480
+ (?:
1481
+ (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
1482
+ )?
1483
+ (?:[-_\.]?dev[-_\.]?[0-9]*)?
1484
+ (?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)?
1485
+ )?
1486
+ )
1487
+ |
1488
+ (?:
1489
+ (?<=~=)
1490
+ \s*
1491
+ v?
1492
+ (?:[0-9]+!)?
1493
+ [0-9]+(?:\.[0-9]+)+
1494
+ (?:
1495
+ [-_\.]?
1496
+ (alpha|beta|preview|pre|a|b|c|rc)
1497
+ [-_\.]?
1498
+ [0-9]*
1499
+ )?
1500
+ (?:
1501
+ (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
1502
+ )?
1503
+ (?:[-_\.]?dev[-_\.]?[0-9]*)?
1504
+ )
1505
+ |
1506
+ (?:
1507
+ (?<!==|!=|~=)
1508
+ \s*
1509
+ v?
1510
+ (?:[0-9]+!)?
1511
+ [0-9]+(?:\.[0-9]+)*
1512
+ (?:
1513
+ [-_\.]?
1514
+ (alpha|beta|preview|pre|a|b|c|rc)
1515
+ [-_\.]?
1516
+ [0-9]*
1517
+ )?
1518
+ (?:
1519
+ (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
1520
+ )?
1521
+ (?:[-_\.]?dev[-_\.]?[0-9]*)?
1522
+ )
1523
+ )
1524
+ """
1026
1525
 
1027
- class CommandExecutor(abc.ABC, ta.Generic[CommandT, CommandOutputT]):
1028
- @abc.abstractmethod
1029
- def execute(self, cmd: CommandT) -> CommandOutputT:
1030
- raise NotImplementedError
1526
+ _regex = re.compile(
1527
+ r'^\s*' + _operator_regex_str + _version_regex_str + r'\s*$',
1528
+ re.VERBOSE | re.IGNORECASE,
1529
+ )
1031
1530
 
1032
- def try_execute(
1531
+ OPERATORS: ta.ClassVar[ta.Mapping[str, str]] = {
1532
+ '~=': 'compatible',
1533
+ '==': 'equal',
1534
+ '!=': 'not_equal',
1535
+ '<=': 'less_than_equal',
1536
+ '>=': 'greater_than_equal',
1537
+ '<': 'less_than',
1538
+ '>': 'greater_than',
1539
+ '===': 'arbitrary',
1540
+ }
1541
+
1542
+ def __init__(
1033
1543
  self,
1034
- cmd: CommandT,
1035
- *,
1036
- log: ta.Optional[logging.Logger] = None,
1037
- omit_exc_object: bool = False,
1038
- ) -> CommandOutputOrException[CommandOutputT]:
1039
- try:
1040
- o = self.execute(cmd)
1544
+ spec: str = '',
1545
+ prereleases: ta.Optional[bool] = None,
1546
+ ) -> None:
1547
+ match = self._regex.search(spec)
1548
+ if not match:
1549
+ raise InvalidSpecifier(f"Invalid specifier: '{spec}'")
1041
1550
 
1042
- except Exception as e: # noqa
1043
- if log is not None:
1044
- log.exception('Exception executing command: %r', type(cmd))
1551
+ self._spec: ta.Tuple[str, str] = (
1552
+ match.group('operator').strip(),
1553
+ match.group('version').strip(),
1554
+ )
1045
1555
 
1046
- return CommandOutputOrExceptionData(exception=CommandException.of(
1047
- e,
1048
- omit_exc_object=omit_exc_object,
1049
- cmd=cmd,
1050
- ))
1556
+ self._prereleases = prereleases
1051
1557
 
1052
- else:
1053
- return CommandOutputOrExceptionData(output=o)
1558
+ @property # type: ignore
1559
+ def prereleases(self) -> bool:
1560
+ if self._prereleases is not None:
1561
+ return self._prereleases
1054
1562
 
1563
+ operator, version = self._spec
1564
+ if operator in ['==', '>=', '<=', '~=', '===']:
1565
+ if operator == '==' and version.endswith('.*'):
1566
+ version = version[:-2]
1055
1567
 
1056
- ##
1568
+ if Version(version).is_prerelease:
1569
+ return True
1057
1570
 
1571
+ return False
1058
1572
 
1059
- @dc.dataclass(frozen=True)
1060
- class CommandRegistration:
1061
- command_cls: ta.Type[Command]
1573
+ @prereleases.setter
1574
+ def prereleases(self, value: bool) -> None:
1575
+ self._prereleases = value
1062
1576
 
1063
- name: ta.Optional[str] = None
1577
+ @property
1578
+ def operator(self) -> str:
1579
+ return self._spec[0]
1064
1580
 
1065
1581
  @property
1066
- def name_or_default(self) -> str:
1067
- if not (cls_name := self.command_cls.__name__).endswith('Command'):
1068
- raise NameError(cls_name)
1069
- return snake_case(cls_name[:-len('Command')])
1582
+ def version(self) -> str:
1583
+ return self._spec[1]
1584
+
1585
+ def __repr__(self) -> str:
1586
+ pre = (
1587
+ f', prereleases={self.prereleases!r}'
1588
+ if self._prereleases is not None
1589
+ else ''
1590
+ )
1070
1591
 
1592
+ return f'<{self.__class__.__name__}({str(self)!r}{pre})>'
1071
1593
 
1072
- CommandRegistrations = ta.NewType('CommandRegistrations', ta.Sequence[CommandRegistration])
1594
+ def __str__(self) -> str:
1595
+ return '{}{}'.format(*self._spec)
1073
1596
 
1597
+ @property
1598
+ def _canonical_spec(self) -> ta.Tuple[str, str]:
1599
+ canonical_version = canonicalize_version(
1600
+ self._spec[1],
1601
+ strip_trailing_zero=(self._spec[0] != '~='),
1602
+ )
1603
+ return self._spec[0], canonical_version
1074
1604
 
1075
- ##
1605
+ def __hash__(self) -> int:
1606
+ return hash(self._canonical_spec)
1076
1607
 
1608
+ def __eq__(self, other: object) -> bool:
1609
+ if isinstance(other, str):
1610
+ try:
1611
+ other = self.__class__(str(other))
1612
+ except InvalidSpecifier:
1613
+ return NotImplemented
1614
+ elif not isinstance(other, self.__class__):
1615
+ return NotImplemented
1077
1616
 
1078
- @dc.dataclass(frozen=True)
1079
- class CommandExecutorRegistration:
1080
- command_cls: ta.Type[Command]
1081
- executor_cls: ta.Type[CommandExecutor]
1617
+ return self._canonical_spec == other._canonical_spec
1082
1618
 
1619
+ def _get_operator(self, op: str) -> CallableVersionOperator:
1620
+ operator_callable: CallableVersionOperator = getattr(self, f'_compare_{self.OPERATORS[op]}')
1621
+ return operator_callable
1083
1622
 
1084
- CommandExecutorRegistrations = ta.NewType('CommandExecutorRegistrations', ta.Sequence[CommandExecutorRegistration])
1623
+ def _compare_compatible(self, prospective: Version, spec: str) -> bool:
1624
+ prefix = _version_join(list(itertools.takewhile(_is_not_version_suffix, _version_split(spec)))[:-1])
1625
+ prefix += '.*'
1626
+ return self._get_operator('>=')(prospective, spec) and self._get_operator('==')(prospective, prefix)
1085
1627
 
1628
+ def _compare_equal(self, prospective: Version, spec: str) -> bool:
1629
+ if spec.endswith('.*'):
1630
+ normalized_prospective = canonicalize_version(prospective.public, strip_trailing_zero=False)
1631
+ normalized_spec = canonicalize_version(spec[:-2], strip_trailing_zero=False)
1632
+ split_spec = _version_split(normalized_spec)
1086
1633
 
1087
- ##
1634
+ split_prospective = _version_split(normalized_prospective)
1635
+ padded_prospective, _ = _pad_version(split_prospective, split_spec)
1636
+ shortened_prospective = padded_prospective[: len(split_spec)]
1088
1637
 
1638
+ return shortened_prospective == split_spec
1089
1639
 
1090
- CommandNameMap = ta.NewType('CommandNameMap', ta.Mapping[str, ta.Type[Command]])
1640
+ else:
1641
+ spec_version = Version(spec)
1642
+ if not spec_version.local:
1643
+ prospective = Version(prospective.public)
1644
+ return prospective == spec_version
1645
+
1646
+ def _compare_not_equal(self, prospective: Version, spec: str) -> bool:
1647
+ return not self._compare_equal(prospective, spec)
1648
+
1649
+ def _compare_less_than_equal(self, prospective: Version, spec: str) -> bool:
1650
+ return Version(prospective.public) <= Version(spec)
1651
+
1652
+ def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool:
1653
+ return Version(prospective.public) >= Version(spec)
1654
+
1655
+ def _compare_less_than(self, prospective: Version, spec_str: str) -> bool:
1656
+ spec = Version(spec_str)
1657
+
1658
+ if not prospective < spec:
1659
+ return False
1660
+
1661
+ if not spec.is_prerelease and prospective.is_prerelease:
1662
+ if Version(prospective.base_version) == Version(spec.base_version):
1663
+ return False
1664
+
1665
+ return True
1666
+
1667
+ def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool:
1668
+ spec = Version(spec_str)
1669
+
1670
+ if not prospective > spec:
1671
+ return False
1672
+
1673
+ if not spec.is_postrelease and prospective.is_postrelease:
1674
+ if Version(prospective.base_version) == Version(spec.base_version):
1675
+ return False
1676
+
1677
+ if prospective.local is not None:
1678
+ if Version(prospective.base_version) == Version(spec.base_version):
1679
+ return False
1680
+
1681
+ return True
1682
+
1683
+ def _compare_arbitrary(self, prospective: Version, spec: str) -> bool:
1684
+ return str(prospective).lower() == str(spec).lower()
1685
+
1686
+ def __contains__(self, item: ta.Union[str, Version]) -> bool:
1687
+ return self.contains(item)
1688
+
1689
+ def contains(self, item: UnparsedVersion, prereleases: ta.Optional[bool] = None) -> bool:
1690
+ if prereleases is None:
1691
+ prereleases = self.prereleases
1692
+
1693
+ normalized_item = _coerce_version(item)
1694
+
1695
+ if normalized_item.is_prerelease and not prereleases:
1696
+ return False
1697
+
1698
+ operator_callable: CallableVersionOperator = self._get_operator(self.operator)
1699
+ return operator_callable(normalized_item, self.version)
1700
+
1701
+ def filter(
1702
+ self,
1703
+ iterable: ta.Iterable[UnparsedVersionVar],
1704
+ prereleases: ta.Optional[bool] = None,
1705
+ ) -> ta.Iterator[UnparsedVersionVar]:
1706
+ yielded = False
1707
+ found_prereleases = []
1708
+
1709
+ kw = {'prereleases': prereleases if prereleases is not None else True}
1710
+
1711
+ for version in iterable:
1712
+ parsed_version = _coerce_version(version)
1713
+
1714
+ if self.contains(parsed_version, **kw):
1715
+ if parsed_version.is_prerelease and not (prereleases or self.prereleases):
1716
+ found_prereleases.append(version)
1717
+ else:
1718
+ yielded = True
1719
+ yield version
1720
+
1721
+ if not yielded and found_prereleases:
1722
+ for version in found_prereleases:
1723
+ yield version
1724
+
1725
+
1726
+ _version_prefix_regex = re.compile(r'^([0-9]+)((?:a|b|c|rc)[0-9]+)$')
1727
+
1728
+
1729
+ def _version_split(version: str) -> ta.List[str]:
1730
+ result: ta.List[str] = []
1731
+
1732
+ epoch, _, rest = version.rpartition('!')
1733
+ result.append(epoch or '0')
1734
+
1735
+ for item in rest.split('.'):
1736
+ match = _version_prefix_regex.search(item)
1737
+ if match:
1738
+ result.extend(match.groups())
1739
+ else:
1740
+ result.append(item)
1741
+ return result
1742
+
1743
+
1744
+ def _version_join(components: ta.List[str]) -> str:
1745
+ epoch, *rest = components
1746
+ return f"{epoch}!{'.'.join(rest)}"
1747
+
1748
+
1749
+ def _is_not_version_suffix(segment: str) -> bool:
1750
+ return not any(segment.startswith(prefix) for prefix in ('dev', 'a', 'b', 'rc', 'post'))
1751
+
1752
+
1753
+ def _pad_version(left: ta.List[str], right: ta.List[str]) -> ta.Tuple[ta.List[str], ta.List[str]]:
1754
+ left_split, right_split = [], []
1755
+
1756
+ left_split.append(list(itertools.takewhile(lambda x: x.isdigit(), left)))
1757
+ right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right)))
1758
+
1759
+ left_split.append(left[len(left_split[0]):])
1760
+ right_split.append(right[len(right_split[0]):])
1761
+
1762
+ left_split.insert(1, ['0'] * max(0, len(right_split[0]) - len(left_split[0])))
1763
+ right_split.insert(1, ['0'] * max(0, len(left_split[0]) - len(right_split[0])))
1764
+
1765
+ return (
1766
+ list(itertools.chain.from_iterable(left_split)),
1767
+ list(itertools.chain.from_iterable(right_split)),
1768
+ )
1769
+
1770
+
1771
+ class SpecifierSet(BaseSpecifier):
1772
+ def __init__(
1773
+ self,
1774
+ specifiers: str = '',
1775
+ prereleases: ta.Optional[bool] = None,
1776
+ ) -> None:
1777
+ split_specifiers = [s.strip() for s in specifiers.split(',') if s.strip()]
1778
+
1779
+ self._specs = frozenset(map(Specifier, split_specifiers))
1780
+ self._prereleases = prereleases
1781
+
1782
+ @property
1783
+ def prereleases(self) -> ta.Optional[bool]:
1784
+ if self._prereleases is not None:
1785
+ return self._prereleases
1786
+
1787
+ if not self._specs:
1788
+ return None
1789
+
1790
+ return any(s.prereleases for s in self._specs)
1791
+
1792
+ @prereleases.setter
1793
+ def prereleases(self, value: bool) -> None:
1794
+ self._prereleases = value
1795
+
1796
+ def __repr__(self) -> str:
1797
+ pre = (
1798
+ f', prereleases={self.prereleases!r}'
1799
+ if self._prereleases is not None
1800
+ else ''
1801
+ )
1802
+
1803
+ return f'<SpecifierSet({str(self)!r}{pre})>'
1804
+
1805
+ def __str__(self) -> str:
1806
+ return ','.join(sorted(str(s) for s in self._specs))
1807
+
1808
+ def __hash__(self) -> int:
1809
+ return hash(self._specs)
1810
+
1811
+ def __and__(self, other: ta.Union['SpecifierSet', str]) -> 'SpecifierSet':
1812
+ if isinstance(other, str):
1813
+ other = SpecifierSet(other)
1814
+ elif not isinstance(other, SpecifierSet):
1815
+ return NotImplemented # type: ignore
1816
+
1817
+ specifier = SpecifierSet()
1818
+ specifier._specs = frozenset(self._specs | other._specs)
1819
+
1820
+ if self._prereleases is None and other._prereleases is not None:
1821
+ specifier._prereleases = other._prereleases
1822
+ elif self._prereleases is not None and other._prereleases is None:
1823
+ specifier._prereleases = self._prereleases
1824
+ elif self._prereleases == other._prereleases:
1825
+ specifier._prereleases = self._prereleases
1826
+ else:
1827
+ raise ValueError('Cannot combine SpecifierSets with True and False prerelease overrides.')
1828
+
1829
+ return specifier
1830
+
1831
+ def __eq__(self, other: object) -> bool:
1832
+ if isinstance(other, (str, Specifier)):
1833
+ other = SpecifierSet(str(other))
1834
+ elif not isinstance(other, SpecifierSet):
1835
+ return NotImplemented
1836
+
1837
+ return self._specs == other._specs
1838
+
1839
+ def __len__(self) -> int:
1840
+ return len(self._specs)
1841
+
1842
+ def __iter__(self) -> ta.Iterator[Specifier]:
1843
+ return iter(self._specs)
1844
+
1845
+ def __contains__(self, item: UnparsedVersion) -> bool:
1846
+ return self.contains(item)
1847
+
1848
+ def contains(
1849
+ self,
1850
+ item: UnparsedVersion,
1851
+ prereleases: ta.Optional[bool] = None,
1852
+ installed: ta.Optional[bool] = None,
1853
+ ) -> bool:
1854
+ if not isinstance(item, Version):
1855
+ item = Version(item)
1856
+
1857
+ if prereleases is None:
1858
+ prereleases = self.prereleases
1859
+
1860
+ if not prereleases and item.is_prerelease:
1861
+ return False
1862
+
1863
+ if installed and item.is_prerelease:
1864
+ item = Version(item.base_version)
1865
+
1866
+ return all(s.contains(item, prereleases=prereleases) for s in self._specs)
1867
+
1868
+ def filter(
1869
+ self,
1870
+ iterable: ta.Iterable[UnparsedVersionVar],
1871
+ prereleases: ta.Optional[bool] = None,
1872
+ ) -> ta.Iterator[UnparsedVersionVar]:
1873
+ if prereleases is None:
1874
+ prereleases = self.prereleases
1875
+
1876
+ if self._specs:
1877
+ for spec in self._specs:
1878
+ iterable = spec.filter(iterable, prereleases=bool(prereleases))
1879
+ return iter(iterable)
1880
+
1881
+ else:
1882
+ filtered: ta.List[UnparsedVersionVar] = []
1883
+ found_prereleases: ta.List[UnparsedVersionVar] = []
1884
+
1885
+ for item in iterable:
1886
+ parsed_version = _coerce_version(item)
1887
+
1888
+ if parsed_version.is_prerelease and not prereleases:
1889
+ if not filtered:
1890
+ found_prereleases.append(item)
1891
+ else:
1892
+ filtered.append(item)
1893
+
1894
+ if not filtered and found_prereleases and prereleases is None:
1895
+ return iter(found_prereleases)
1896
+
1897
+ return iter(filtered)
1898
+
1899
+
1900
+ ########################################
1901
+ # ../commands/base.py
1902
+
1903
+
1904
+ ##
1905
+
1906
+
1907
+ @dc.dataclass(frozen=True)
1908
+ class Command(abc.ABC, ta.Generic[CommandOutputT]):
1909
+ @dc.dataclass(frozen=True)
1910
+ class Output(abc.ABC): # noqa
1911
+ pass
1912
+
1913
+ @ta.final
1914
+ def execute(self, executor: 'CommandExecutor') -> CommandOutputT:
1915
+ return check_isinstance(executor.execute(self), self.Output) # type: ignore[return-value]
1916
+
1917
+
1918
+ ##
1919
+
1920
+
1921
+ @dc.dataclass(frozen=True)
1922
+ class CommandException:
1923
+ name: str
1924
+ repr: str
1925
+
1926
+ traceback: ta.Optional[str] = None
1927
+
1928
+ exc: ta.Optional[ta.Any] = None # Exception
1929
+
1930
+ cmd: ta.Optional[Command] = None
1931
+
1932
+ @classmethod
1933
+ def of(
1934
+ cls,
1935
+ exc: Exception,
1936
+ *,
1937
+ omit_exc_object: bool = False,
1938
+
1939
+ cmd: ta.Optional[Command] = None,
1940
+ ) -> 'CommandException':
1941
+ return CommandException(
1942
+ name=type(exc).__qualname__,
1943
+ repr=repr(exc),
1944
+
1945
+ traceback=(
1946
+ ''.join(traceback.format_tb(exc.__traceback__))
1947
+ if getattr(exc, '__traceback__', None) is not None else None
1948
+ ),
1949
+
1950
+ exc=None if omit_exc_object else exc,
1951
+
1952
+ cmd=cmd,
1953
+ )
1954
+
1955
+
1956
+ class CommandOutputOrException(abc.ABC, ta.Generic[CommandOutputT]):
1957
+ @property
1958
+ @abc.abstractmethod
1959
+ def output(self) -> ta.Optional[CommandOutputT]:
1960
+ raise NotImplementedError
1961
+
1962
+ @property
1963
+ @abc.abstractmethod
1964
+ def exception(self) -> ta.Optional[CommandException]:
1965
+ raise NotImplementedError
1966
+
1967
+
1968
+ @dc.dataclass(frozen=True)
1969
+ class CommandOutputOrExceptionData(CommandOutputOrException):
1970
+ output: ta.Optional[Command.Output] = None
1971
+ exception: ta.Optional[CommandException] = None
1972
+
1973
+
1974
+ class CommandExecutor(abc.ABC, ta.Generic[CommandT, CommandOutputT]):
1975
+ @abc.abstractmethod
1976
+ def execute(self, cmd: CommandT) -> CommandOutputT:
1977
+ raise NotImplementedError
1978
+
1979
+ def try_execute(
1980
+ self,
1981
+ cmd: CommandT,
1982
+ *,
1983
+ log: ta.Optional[logging.Logger] = None,
1984
+ omit_exc_object: bool = False,
1985
+ ) -> CommandOutputOrException[CommandOutputT]:
1986
+ try:
1987
+ o = self.execute(cmd)
1988
+
1989
+ except Exception as e: # noqa
1990
+ if log is not None:
1991
+ log.exception('Exception executing command: %r', type(cmd))
1992
+
1993
+ return CommandOutputOrExceptionData(exception=CommandException.of(
1994
+ e,
1995
+ omit_exc_object=omit_exc_object,
1996
+ cmd=cmd,
1997
+ ))
1998
+
1999
+ else:
2000
+ return CommandOutputOrExceptionData(output=o)
2001
+
2002
+
2003
+ ##
2004
+
2005
+
2006
+ @dc.dataclass(frozen=True)
2007
+ class CommandRegistration:
2008
+ command_cls: ta.Type[Command]
2009
+
2010
+ name: ta.Optional[str] = None
2011
+
2012
+ @property
2013
+ def name_or_default(self) -> str:
2014
+ if not (cls_name := self.command_cls.__name__).endswith('Command'):
2015
+ raise NameError(cls_name)
2016
+ return snake_case(cls_name[:-len('Command')])
2017
+
2018
+
2019
+ CommandRegistrations = ta.NewType('CommandRegistrations', ta.Sequence[CommandRegistration])
2020
+
2021
+
2022
+ ##
2023
+
2024
+
2025
+ @dc.dataclass(frozen=True)
2026
+ class CommandExecutorRegistration:
2027
+ command_cls: ta.Type[Command]
2028
+ executor_cls: ta.Type[CommandExecutor]
2029
+
2030
+
2031
+ CommandExecutorRegistrations = ta.NewType('CommandExecutorRegistrations', ta.Sequence[CommandExecutorRegistration])
2032
+
2033
+
2034
+ ##
2035
+
2036
+
2037
+ CommandNameMap = ta.NewType('CommandNameMap', ta.Mapping[str, ta.Type[Command]])
1091
2038
 
1092
2039
 
1093
2040
  def build_command_name_map(crs: CommandRegistrations) -> CommandNameMap:
@@ -2328,23 +3275,24 @@ TODO:
2328
3275
  @dc.dataclass(frozen=True)
2329
3276
  class ObjMarshalOptions:
2330
3277
  raw_bytes: bool = False
3278
+ nonstrict_dataclasses: bool = False
2331
3279
 
2332
3280
 
2333
3281
  class ObjMarshaler(abc.ABC):
2334
3282
  @abc.abstractmethod
2335
- def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3283
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2336
3284
  raise NotImplementedError
2337
3285
 
2338
3286
  @abc.abstractmethod
2339
- def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3287
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2340
3288
  raise NotImplementedError
2341
3289
 
2342
3290
 
2343
3291
  class NopObjMarshaler(ObjMarshaler):
2344
- def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3292
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2345
3293
  return o
2346
3294
 
2347
- def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3295
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2348
3296
  return o
2349
3297
 
2350
3298
 
@@ -2352,29 +3300,29 @@ class NopObjMarshaler(ObjMarshaler):
2352
3300
  class ProxyObjMarshaler(ObjMarshaler):
2353
3301
  m: ta.Optional[ObjMarshaler] = None
2354
3302
 
2355
- def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
2356
- return check_not_none(self.m).marshal(o, opts)
3303
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
3304
+ return check_not_none(self.m).marshal(o, ctx)
2357
3305
 
2358
- def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
2359
- return check_not_none(self.m).unmarshal(o, opts)
3306
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
3307
+ return check_not_none(self.m).unmarshal(o, ctx)
2360
3308
 
2361
3309
 
2362
3310
  @dc.dataclass(frozen=True)
2363
3311
  class CastObjMarshaler(ObjMarshaler):
2364
3312
  ty: type
2365
3313
 
2366
- def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3314
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2367
3315
  return o
2368
3316
 
2369
- def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3317
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2370
3318
  return self.ty(o)
2371
3319
 
2372
3320
 
2373
3321
  class DynamicObjMarshaler(ObjMarshaler):
2374
- def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
2375
- return marshal_obj(o)
3322
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
3323
+ return ctx.manager.marshal_obj(o, opts=ctx.options)
2376
3324
 
2377
- def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3325
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2378
3326
  return o
2379
3327
 
2380
3328
 
@@ -2382,10 +3330,10 @@ class DynamicObjMarshaler(ObjMarshaler):
2382
3330
  class Base64ObjMarshaler(ObjMarshaler):
2383
3331
  ty: type
2384
3332
 
2385
- def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3333
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2386
3334
  return base64.b64encode(o).decode('ascii')
2387
3335
 
2388
- def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3336
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2389
3337
  return self.ty(base64.b64decode(o))
2390
3338
 
2391
3339
 
@@ -2393,25 +3341,25 @@ class Base64ObjMarshaler(ObjMarshaler):
2393
3341
  class BytesSwitchedObjMarshaler(ObjMarshaler):
2394
3342
  m: ObjMarshaler
2395
3343
 
2396
- def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
2397
- if opts.raw_bytes:
3344
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
3345
+ if ctx.options.raw_bytes:
2398
3346
  return o
2399
- return self.m.marshal(o, opts)
3347
+ return self.m.marshal(o, ctx)
2400
3348
 
2401
- def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
2402
- if opts.raw_bytes:
3349
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
3350
+ if ctx.options.raw_bytes:
2403
3351
  return o
2404
- return self.m.unmarshal(o, opts)
3352
+ return self.m.unmarshal(o, ctx)
2405
3353
 
2406
3354
 
2407
3355
  @dc.dataclass(frozen=True)
2408
3356
  class EnumObjMarshaler(ObjMarshaler):
2409
3357
  ty: type
2410
3358
 
2411
- def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3359
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2412
3360
  return o.name
2413
3361
 
2414
- def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3362
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2415
3363
  return self.ty.__members__[o] # type: ignore
2416
3364
 
2417
3365
 
@@ -2419,15 +3367,15 @@ class EnumObjMarshaler(ObjMarshaler):
2419
3367
  class OptionalObjMarshaler(ObjMarshaler):
2420
3368
  item: ObjMarshaler
2421
3369
 
2422
- def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3370
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2423
3371
  if o is None:
2424
3372
  return None
2425
- return self.item.marshal(o, opts)
3373
+ return self.item.marshal(o, ctx)
2426
3374
 
2427
- def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3375
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2428
3376
  if o is None:
2429
3377
  return None
2430
- return self.item.unmarshal(o, opts)
3378
+ return self.item.unmarshal(o, ctx)
2431
3379
 
2432
3380
 
2433
3381
  @dc.dataclass(frozen=True)
@@ -2436,11 +3384,11 @@ class MappingObjMarshaler(ObjMarshaler):
2436
3384
  km: ObjMarshaler
2437
3385
  vm: ObjMarshaler
2438
3386
 
2439
- def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
2440
- return {self.km.marshal(k, opts): self.vm.marshal(v, opts) for k, v in o.items()}
3387
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
3388
+ return {self.km.marshal(k, ctx): self.vm.marshal(v, ctx) for k, v in o.items()}
2441
3389
 
2442
- def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
2443
- return self.ty((self.km.unmarshal(k, opts), self.vm.unmarshal(v, opts)) for k, v in o.items())
3390
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
3391
+ return self.ty((self.km.unmarshal(k, ctx), self.vm.unmarshal(v, ctx)) for k, v in o.items())
2444
3392
 
2445
3393
 
2446
3394
  @dc.dataclass(frozen=True)
@@ -2448,11 +3396,11 @@ class IterableObjMarshaler(ObjMarshaler):
2448
3396
  ty: type
2449
3397
  item: ObjMarshaler
2450
3398
 
2451
- def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
2452
- return [self.item.marshal(e, opts) for e in o]
3399
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
3400
+ return [self.item.marshal(e, ctx) for e in o]
2453
3401
 
2454
- def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
2455
- return self.ty(self.item.unmarshal(e, opts) for e in o)
3402
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
3403
+ return self.ty(self.item.unmarshal(e, ctx) for e in o)
2456
3404
 
2457
3405
 
2458
3406
  @dc.dataclass(frozen=True)
@@ -2461,11 +3409,18 @@ class DataclassObjMarshaler(ObjMarshaler):
2461
3409
  fs: ta.Mapping[str, ObjMarshaler]
2462
3410
  nonstrict: bool = False
2463
3411
 
2464
- def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
2465
- return {k: m.marshal(getattr(o, k), opts) for k, m in self.fs.items()}
3412
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
3413
+ return {
3414
+ k: m.marshal(getattr(o, k), ctx)
3415
+ for k, m in self.fs.items()
3416
+ }
2466
3417
 
2467
- def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
2468
- return self.ty(**{k: self.fs[k].unmarshal(v, opts) for k, v in o.items() if not self.nonstrict or k in self.fs})
3418
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
3419
+ return self.ty(**{
3420
+ k: self.fs[k].unmarshal(v, ctx)
3421
+ for k, v in o.items()
3422
+ if not (self.nonstrict or ctx.options.nonstrict_dataclasses) or k in self.fs
3423
+ })
2469
3424
 
2470
3425
 
2471
3426
  @dc.dataclass(frozen=True)
@@ -2485,50 +3440,50 @@ class PolymorphicObjMarshaler(ObjMarshaler):
2485
3440
  {i.tag: i for i in impls},
2486
3441
  )
2487
3442
 
2488
- def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3443
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2489
3444
  impl = self.impls_by_ty[type(o)]
2490
- return {impl.tag: impl.m.marshal(o, opts)}
3445
+ return {impl.tag: impl.m.marshal(o, ctx)}
2491
3446
 
2492
- def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3447
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2493
3448
  [(t, v)] = o.items()
2494
3449
  impl = self.impls_by_tag[t]
2495
- return impl.m.unmarshal(v, opts)
3450
+ return impl.m.unmarshal(v, ctx)
2496
3451
 
2497
3452
 
2498
3453
  @dc.dataclass(frozen=True)
2499
3454
  class DatetimeObjMarshaler(ObjMarshaler):
2500
3455
  ty: type
2501
3456
 
2502
- def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3457
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2503
3458
  return o.isoformat()
2504
3459
 
2505
- def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3460
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2506
3461
  return self.ty.fromisoformat(o) # type: ignore
2507
3462
 
2508
3463
 
2509
3464
  class DecimalObjMarshaler(ObjMarshaler):
2510
- def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3465
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2511
3466
  return str(check_isinstance(o, decimal.Decimal))
2512
3467
 
2513
- def unmarshal(self, v: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3468
+ def unmarshal(self, v: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2514
3469
  return decimal.Decimal(check_isinstance(v, str))
2515
3470
 
2516
3471
 
2517
3472
  class FractionObjMarshaler(ObjMarshaler):
2518
- def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3473
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2519
3474
  fr = check_isinstance(o, fractions.Fraction)
2520
3475
  return [fr.numerator, fr.denominator]
2521
3476
 
2522
- def unmarshal(self, v: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3477
+ def unmarshal(self, v: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2523
3478
  num, denom = check_isinstance(v, list)
2524
3479
  return fractions.Fraction(num, denom)
2525
3480
 
2526
3481
 
2527
3482
  class UuidObjMarshaler(ObjMarshaler):
2528
- def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3483
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2529
3484
  return str(o)
2530
3485
 
2531
- def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
3486
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
2532
3487
  return uuid.UUID(o)
2533
3488
 
2534
3489
 
@@ -2689,6 +3644,12 @@ class ObjMarshalerManager:
2689
3644
 
2690
3645
  #
2691
3646
 
3647
+ def _make_context(self, opts: ta.Optional[ObjMarshalOptions]) -> 'ObjMarshalContext':
3648
+ return ObjMarshalContext(
3649
+ options=opts or self._default_options,
3650
+ manager=self,
3651
+ )
3652
+
2692
3653
  def marshal_obj(
2693
3654
  self,
2694
3655
  o: ta.Any,
@@ -2696,7 +3657,7 @@ class ObjMarshalerManager:
2696
3657
  opts: ta.Optional[ObjMarshalOptions] = None,
2697
3658
  ) -> ta.Any:
2698
3659
  m = self.get_obj_marshaler(ty if ty is not None else type(o))
2699
- return m.marshal(o, opts or self._default_options)
3660
+ return m.marshal(o, self._make_context(opts))
2700
3661
 
2701
3662
  def unmarshal_obj(
2702
3663
  self,
@@ -2705,7 +3666,7 @@ class ObjMarshalerManager:
2705
3666
  opts: ta.Optional[ObjMarshalOptions] = None,
2706
3667
  ) -> T:
2707
3668
  m = self.get_obj_marshaler(ty)
2708
- return m.unmarshal(o, opts or self._default_options)
3669
+ return m.unmarshal(o, self._make_context(opts))
2709
3670
 
2710
3671
  def roundtrip_obj(
2711
3672
  self,
@@ -2720,6 +3681,12 @@ class ObjMarshalerManager:
2720
3681
  return u
2721
3682
 
2722
3683
 
3684
+ @dc.dataclass(frozen=True)
3685
+ class ObjMarshalContext:
3686
+ options: ObjMarshalOptions
3687
+ manager: ObjMarshalerManager
3688
+
3689
+
2723
3690
  ##
2724
3691
 
2725
3692
 
@@ -2750,34 +3717,125 @@ def check_runtime_version() -> None:
2750
3717
 
2751
3718
 
2752
3719
  ########################################
2753
- # ../bootstrap.py
3720
+ # ../../../omdev/interp/types.py
2754
3721
 
2755
3722
 
2756
- @dc.dataclass(frozen=True)
2757
- class MainBootstrap:
2758
- 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
+ ])
2759
3728
 
2760
- 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
+ )
2761
3732
 
2762
3733
 
2763
- ########################################
2764
- # ../commands/execution.py
3734
+ @dc.dataclass(frozen=True)
3735
+ class InterpOpts:
3736
+ threaded: bool = False
3737
+ debug: bool = False
2765
3738
 
3739
+ def __str__(self) -> str:
3740
+ return ''.join(g for a, g in INTERP_OPT_GLYPHS_BY_ATTR.items() if getattr(self, a))
2766
3741
 
2767
- 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})
2768
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)
2769
3752
 
2770
- class CommandExecutionService(CommandExecutor):
2771
- def __init__(
2772
- self,
2773
- *,
2774
- command_executors: CommandExecutorMap,
2775
- ) -> None:
2776
- super().__init__()
2777
3753
 
2778
- self._command_executors = command_executors
3754
+ @dc.dataclass(frozen=True)
3755
+ class InterpVersion:
3756
+ version: Version
3757
+ opts: InterpOpts
2779
3758
 
2780
- def execute(self, cmd: Command) -> Command.Output:
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:
2781
3839
  ce: CommandExecutor = self._command_executors[type(cmd)]
2782
3840
  return ce.execute(cmd)
2783
3841
 
@@ -2826,6 +3884,8 @@ class DeployCommand(Command['DeployCommand.Output']):
2826
3884
 
2827
3885
  class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]):
2828
3886
  def execute(self, cmd: DeployCommand) -> DeployCommand.Output:
3887
+ log.info('Deploying!')
3888
+
2829
3889
  return DeployCommand.Output()
2830
3890
 
2831
3891
 
@@ -2859,10 +3919,12 @@ class RemoteChannel:
2859
3919
  self._output = output
2860
3920
  self._msh = msh
2861
3921
 
3922
+ self._lock = threading.RLock()
3923
+
2862
3924
  def set_marshaler(self, msh: ObjMarshalerManager) -> None:
2863
3925
  self._msh = msh
2864
3926
 
2865
- 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:
2866
3928
  j = json_dumps_compact(self._msh.marshal_obj(o, ty))
2867
3929
  d = j.encode('utf-8')
2868
3930
 
@@ -2870,7 +3932,11 @@ class RemoteChannel:
2870
3932
  self._output.write(d)
2871
3933
  self._output.flush()
2872
3934
 
2873
- 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]:
2874
3940
  d = self._input.read(4)
2875
3941
  if not d:
2876
3942
  return None
@@ -2885,6 +3951,10 @@ class RemoteChannel:
2885
3951
  j = json.loads(d.decode('utf-8'))
2886
3952
  return self._msh.unmarshal_obj(j, ty)
2887
3953
 
3954
+ def recv_obj(self, ty: ta.Type[T]) -> ta.Optional[T]:
3955
+ with self._lock:
3956
+ return self._recv_obj(ty)
3957
+
2888
3958
 
2889
3959
  ########################################
2890
3960
  # ../../../omlish/lite/subprocesses.py
@@ -3020,6 +4090,102 @@ def subprocess_close(
3020
4090
  proc.wait(timeout)
3021
4091
 
3022
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
+
3023
4189
  ########################################
3024
4190
  # ../commands/subprocess.py
3025
4191
 
@@ -3042,8 +4208,7 @@ class SubprocessCommand(Command['SubprocessCommand.Output']):
3042
4208
  timeout: ta.Optional[float] = None
3043
4209
 
3044
4210
  def __post_init__(self) -> None:
3045
- if isinstance(self.cmd, str):
3046
- raise TypeError(self.cmd)
4211
+ check_not_isinstance(self.cmd, str)
3047
4212
 
3048
4213
  @dc.dataclass(frozen=True)
3049
4214
  class Output(Command.Output):
@@ -3180,111 +4345,97 @@ class RemoteSpawning:
3180
4345
 
3181
4346
 
3182
4347
  ########################################
3183
- # ../commands/inject.py
4348
+ # ../../../omdev/interp/providers.py
4349
+ """
4350
+ TODO:
4351
+ - backends
4352
+ - local builds
4353
+ - deadsnakes?
4354
+ - uv
4355
+ - loose versions
4356
+ """
3184
4357
 
3185
4358
 
3186
4359
  ##
3187
4360
 
3188
4361
 
3189
- def bind_command(
3190
- command_cls: ta.Type[Command],
3191
- executor_cls: ta.Optional[ta.Type[CommandExecutor]],
3192
- ) -> InjectorBindings:
3193
- lst: ta.List[InjectorBindingOrBindings] = [
3194
- inj.bind(CommandRegistration(command_cls), array=True),
3195
- ]
3196
-
3197
- if executor_cls is not None:
3198
- lst.extend([
3199
- inj.bind(executor_cls, singleton=True),
3200
- inj.bind(CommandExecutorRegistration(command_cls, executor_cls), array=True),
3201
- ])
3202
-
3203
- return inj.as_bindings(*lst)
4362
+ class InterpProvider(abc.ABC):
4363
+ name: ta.ClassVar[str]
3204
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)]))
3205
4372
 
3206
- ##
4373
+ @abc.abstractmethod
4374
+ def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
4375
+ raise NotImplementedError
3207
4376
 
4377
+ @abc.abstractmethod
4378
+ def get_installed_version(self, version: InterpVersion) -> Interp:
4379
+ raise NotImplementedError
3208
4380
 
3209
- @dc.dataclass(frozen=True)
3210
- class _FactoryCommandExecutor(CommandExecutor):
3211
- factory: ta.Callable[[], CommandExecutor]
4381
+ def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
4382
+ return []
3212
4383
 
3213
- def execute(self, i: Command) -> Command.Output:
3214
- return self.factory().execute(i)
4384
+ def install_version(self, version: InterpVersion) -> Interp:
4385
+ raise TypeError
3215
4386
 
3216
4387
 
3217
4388
  ##
3218
4389
 
3219
4390
 
3220
- def bind_commands(
3221
- *,
3222
- main_config: MainConfig,
3223
- ) -> InjectorBindings:
3224
- lst: ta.List[InjectorBindingOrBindings] = [
3225
- inj.bind_array(CommandRegistration),
3226
- inj.bind_array_type(CommandRegistration, CommandRegistrations),
3227
-
3228
- inj.bind_array(CommandExecutorRegistration),
3229
- inj.bind_array_type(CommandExecutorRegistration, CommandExecutorRegistrations),
3230
-
3231
- inj.bind(build_command_name_map, singleton=True),
3232
- ]
3233
-
3234
- #
3235
-
3236
- def provide_obj_marshaler_installer(cmds: CommandNameMap) -> ObjMarshalerInstaller:
3237
- return ObjMarshalerInstaller(functools.partial(install_command_marshaling, cmds))
3238
-
3239
- lst.append(inj.bind(provide_obj_marshaler_installer, array=True))
3240
-
3241
- #
3242
-
3243
- def provide_command_executor_map(
3244
- injector: Injector,
3245
- crs: CommandExecutorRegistrations,
3246
- ) -> CommandExecutorMap:
3247
- dct: ta.Dict[ta.Type[Command], CommandExecutor] = {}
3248
-
3249
- cr: CommandExecutorRegistration
3250
- for cr in crs:
3251
- if cr.command_cls in dct:
3252
- 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
+ )
3253
4406
 
3254
- factory = functools.partial(injector.provide, cr.executor_cls)
3255
- if main_config.debug:
3256
- ce = factory()
3257
- else:
3258
- ce = _FactoryCommandExecutor(factory)
3259
4407
 
3260
- dct[cr.command_cls] = ce
4408
+ ########################################
4409
+ # ../remote/execution.py
3261
4410
 
3262
- return CommandExecutorMap(dct)
3263
4411
 
3264
- lst.extend([
3265
- inj.bind(provide_command_executor_map, singleton=True),
4412
+ ##
3266
4413
 
3267
- inj.bind(CommandExecutionService, singleton=True, eager=main_config.debug),
3268
- inj.bind(CommandExecutor, to_key=CommandExecutionService),
3269
- ])
3270
4414
 
3271
- #
4415
+ class _RemoteExecutionLogHandler(logging.Handler):
4416
+ def __init__(self, fn: ta.Callable[[str], None]) -> None:
4417
+ super().__init__()
4418
+ self._fn = fn
3272
4419
 
3273
- for command_cls, executor_cls in [
3274
- (SubprocessCommand, SubprocessCommandExecutor),
3275
- ]:
3276
- lst.append(bind_command(command_cls, executor_cls))
4420
+ def emit(self, record):
4421
+ msg = self.format(record)
4422
+ self._fn(msg)
3277
4423
 
3278
- #
3279
4424
 
3280
- return inj.as_bindings(*lst)
4425
+ @dc.dataclass(frozen=True)
4426
+ class _RemoteExecutionRequest:
4427
+ c: Command
3281
4428
 
3282
4429
 
3283
- ########################################
3284
- # ../remote/execution.py
4430
+ @dc.dataclass(frozen=True)
4431
+ class _RemoteExecutionLog:
4432
+ s: str
3285
4433
 
3286
4434
 
3287
- ##
4435
+ @dc.dataclass(frozen=True)
4436
+ class _RemoteExecutionResponse:
4437
+ r: ta.Optional[CommandOutputOrExceptionData] = None
4438
+ l: ta.Optional[_RemoteExecutionLog] = None
3288
4439
 
3289
4440
 
3290
4441
  def _remote_execution_main() -> None:
@@ -3304,20 +4455,44 @@ def _remote_execution_main() -> None:
3304
4455
 
3305
4456
  chan.set_marshaler(injector[ObjMarshalerManager])
3306
4457
 
3307
- ce = injector[CommandExecutor]
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
+
4473
+ ce = injector[LocalCommandExecutor]
3308
4474
 
3309
4475
  while True:
3310
- i = chan.recv_obj(Command)
3311
- if i is None:
4476
+ req = chan.recv_obj(_RemoteExecutionRequest)
4477
+ if req is None:
3312
4478
  break
3313
4479
 
4480
+ with log_lock:
4481
+ send_logs = True
4482
+
3314
4483
  r = ce.try_execute(
3315
- i,
4484
+ req.c,
3316
4485
  log=log,
3317
4486
  omit_exc_object=True,
3318
4487
  )
3319
4488
 
3320
- 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
+ )))
3321
4496
 
3322
4497
 
3323
4498
  ##
@@ -3335,12 +4510,17 @@ class RemoteCommandExecutor(CommandExecutor):
3335
4510
  self._chan = chan
3336
4511
 
3337
4512
  def _remote_execute(self, cmd: Command) -> CommandOutputOrException:
3338
- self._chan.send_obj(cmd, Command)
4513
+ self._chan.send_obj(_RemoteExecutionRequest(cmd))
3339
4514
 
3340
- if (r := self._chan.recv_obj(CommandOutputOrExceptionData)) is None:
3341
- raise EOFError
4515
+ while True:
4516
+ if (r := self._chan.recv_obj(_RemoteExecutionResponse)) is None:
4517
+ raise EOFError
3342
4518
 
3343
- return r
4519
+ if r.l is not None:
4520
+ log.info(r.l.s)
4521
+
4522
+ if r.r is not None:
4523
+ return r.r
3344
4524
 
3345
4525
  # @ta.override
3346
4526
  def execute(self, cmd: Command) -> Command.Output:
@@ -3446,62 +4626,854 @@ class RemoteExecution:
3446
4626
 
3447
4627
 
3448
4628
  ########################################
3449
- # ../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
+ """
3450
4641
 
3451
4642
 
3452
- def bind_deploy(
3453
- ) -> InjectorBindings:
3454
- lst: ta.List[InjectorBindingOrBindings] = [
3455
- bind_command(DeployCommand, DeployCommandExecutor),
3456
- ]
4643
+ ##
3457
4644
 
3458
- return inj.as_bindings(*lst)
3459
4645
 
4646
+ class Pyenv:
3460
4647
 
3461
- ########################################
3462
- # ../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}')
3463
4655
 
4656
+ super().__init__()
3464
4657
 
3465
- def bind_remote(
3466
- *,
3467
- remote_config: RemoteConfig,
3468
- ) -> InjectorBindings:
3469
- lst: ta.List[InjectorBindingOrBindings] = [
3470
- inj.bind(remote_config),
4658
+ self._root_kw = root
3471
4659
 
3472
- 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
3473
4664
 
3474
- inj.bind(RemoteExecution, singleton=True),
3475
- ]
4665
+ if shutil.which('pyenv'):
4666
+ return subprocess_check_output_str('pyenv', 'root')
3476
4667
 
3477
- if (pf := remote_config.payload_file) is not None:
3478
- 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
3479
4671
 
3480
- return inj.as_bindings(*lst)
4672
+ return None
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
3481
4690
 
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
3482
4704
 
3483
- ########################################
3484
- # ../inject.py
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
3485
4712
 
3486
4713
 
3487
4714
  ##
3488
4715
 
3489
4716
 
3490
- def bind_main(
3491
- *,
3492
- main_config: MainConfig,
3493
- remote_config: RemoteConfig,
3494
- ) -> InjectorBindings:
3495
- lst: ta.List[InjectorBindingOrBindings] = [
3496
- 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
+ )
3497
4733
 
3498
- bind_commands(
3499
- main_config=main_config,
3500
- ),
3501
4734
 
3502
- bind_remote(
3503
- remote_config=remote_config,
3504
- ),
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,
5476
+ ),
3505
5477
 
3506
5478
  bind_deploy(),
3507
5479
  ]
@@ -3599,12 +5571,15 @@ def _main() -> None:
3599
5571
 
3600
5572
  #
3601
5573
 
5574
+ msh = injector[ObjMarshalerManager]
5575
+
3602
5576
  cmds: ta.List[Command] = []
5577
+ cmd: Command
3603
5578
  for c in args.command:
3604
- if c == 'deploy':
3605
- cmds.append(DeployCommand())
3606
- else:
3607
- 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)
3608
5583
 
3609
5584
  #
3610
5585
 
@@ -3612,7 +5587,7 @@ def _main() -> None:
3612
5587
  ce: CommandExecutor
3613
5588
 
3614
5589
  if args.local:
3615
- ce = injector[CommandExecutor]
5590
+ ce = injector[LocalCommandExecutor]
3616
5591
 
3617
5592
  else:
3618
5593
  tgt = RemoteSpawning.Target(
@@ -3624,9 +5599,13 @@ def _main() -> None:
3624
5599
  ce = es.enter_context(injector[RemoteExecution].connect(tgt, bs)) # noqa
3625
5600
 
3626
5601
  for cmd in cmds:
3627
- r = ce.try_execute(cmd)
5602
+ r = ce.try_execute(
5603
+ cmd,
5604
+ log=log,
5605
+ omit_exc_object=True,
5606
+ )
3628
5607
 
3629
- print(injector[ObjMarshalerManager].marshal_obj(r, opts=ObjMarshalOptions(raw_bytes=True)))
5608
+ print(msh.marshal_obj(r, opts=ObjMarshalOptions(raw_bytes=True)))
3630
5609
 
3631
5610
 
3632
5611
  if __name__ == '__main__':