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

Sign up to get free protection for your applications and to get access to all the features.
ominfra/scripts/manage.py CHANGED
@@ -10,6 +10,7 @@ manage.py -s 'ssh -i /foo/bar.pem foo@bar.baz' -q --python=python3.8
10
10
  """
11
11
  import abc
12
12
  import base64
13
+ import collections
13
14
  import collections.abc
14
15
  import contextlib
15
16
  import dataclasses as dc
@@ -19,12 +20,16 @@ import enum
19
20
  import fractions
20
21
  import functools
21
22
  import inspect
23
+ import itertools
22
24
  import json
23
25
  import logging
24
26
  import os
27
+ import os.path
25
28
  import platform
26
29
  import pwd
30
+ import re
27
31
  import shlex
32
+ import shutil
28
33
  import site
29
34
  import struct
30
35
  import subprocess
@@ -49,6 +54,14 @@ if sys.version_info < (3, 8):
49
54
  ########################################
50
55
 
51
56
 
57
+ # ../../omdev/packaging/versions.py
58
+ VersionLocalType = ta.Tuple[ta.Union[int, str], ...]
59
+ VersionCmpPrePostDevType = ta.Union['InfinityVersionType', 'NegativeInfinityVersionType', ta.Tuple[str, int]]
60
+ _VersionCmpLocalType0 = ta.Tuple[ta.Union[ta.Tuple[int, str], ta.Tuple['NegativeInfinityVersionType', ta.Union[int, str]]], ...] # noqa
61
+ VersionCmpLocalType = ta.Union['NegativeInfinityVersionType', _VersionCmpLocalType0]
62
+ VersionCmpKey = ta.Tuple[int, ta.Tuple[int, ...], VersionCmpPrePostDevType, VersionCmpPrePostDevType, VersionCmpPrePostDevType, VersionCmpLocalType] # noqa
63
+ VersionComparisonMethod = ta.Callable[[VersionCmpKey, VersionCmpKey], bool]
64
+
52
65
  # ../../omlish/lite/cached.py
53
66
  T = ta.TypeVar('T')
54
67
  CallableT = ta.TypeVar('CallableT', bound=ta.Callable)
@@ -56,6 +69,11 @@ CallableT = ta.TypeVar('CallableT', bound=ta.Callable)
56
69
  # ../../omlish/lite/check.py
57
70
  SizedT = ta.TypeVar('SizedT', bound=ta.Sized)
58
71
 
72
+ # ../../omdev/packaging/specifiers.py
73
+ UnparsedVersion = ta.Union['Version', str]
74
+ UnparsedVersionVar = ta.TypeVar('UnparsedVersionVar', bound=UnparsedVersion)
75
+ CallableVersionOperator = ta.Callable[['Version', str], bool]
76
+
59
77
  # commands/base.py
60
78
  CommandT = ta.TypeVar('CommandT', bound='Command')
61
79
  CommandOutputT = ta.TypeVar('CommandOutputT', bound='Command.Output')
@@ -71,6 +89,413 @@ InjectorBindingOrBindings = ta.Union['InjectorBinding', 'InjectorBindings']
71
89
  SubprocessChannelOption = ta.Literal['pipe', 'stdout', 'devnull']
72
90
 
73
91
 
92
+ ########################################
93
+ # ../../../omdev/packaging/versions.py
94
+ # Copyright (c) Donald Stufft and individual contributors.
95
+ # All rights reserved.
96
+ #
97
+ # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
98
+ # following conditions are met:
99
+ #
100
+ # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
101
+ # following disclaimer.
102
+ #
103
+ # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
104
+ # following disclaimer in the documentation and/or other materials provided with the distribution.
105
+ #
106
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
107
+ # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
108
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
109
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
110
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
111
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
112
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. This file is dual licensed under the terms of the
113
+ # Apache License, Version 2.0, and the BSD License. See the LICENSE file in the root of this repository for complete
114
+ # details.
115
+ # https://github.com/pypa/packaging/blob/2c885fe91a54559e2382902dce28428ad2887be5/src/packaging/version.py
116
+
117
+
118
+ ##
119
+
120
+
121
+ class InfinityVersionType:
122
+ def __repr__(self) -> str:
123
+ return 'Infinity'
124
+
125
+ def __hash__(self) -> int:
126
+ return hash(repr(self))
127
+
128
+ def __lt__(self, other: object) -> bool:
129
+ return False
130
+
131
+ def __le__(self, other: object) -> bool:
132
+ return False
133
+
134
+ def __eq__(self, other: object) -> bool:
135
+ return isinstance(other, self.__class__)
136
+
137
+ def __gt__(self, other: object) -> bool:
138
+ return True
139
+
140
+ def __ge__(self, other: object) -> bool:
141
+ return True
142
+
143
+ def __neg__(self: object) -> 'NegativeInfinityVersionType':
144
+ return NegativeInfinityVersion
145
+
146
+
147
+ InfinityVersion = InfinityVersionType()
148
+
149
+
150
+ class NegativeInfinityVersionType:
151
+ def __repr__(self) -> str:
152
+ return '-Infinity'
153
+
154
+ def __hash__(self) -> int:
155
+ return hash(repr(self))
156
+
157
+ def __lt__(self, other: object) -> bool:
158
+ return True
159
+
160
+ def __le__(self, other: object) -> bool:
161
+ return True
162
+
163
+ def __eq__(self, other: object) -> bool:
164
+ return isinstance(other, self.__class__)
165
+
166
+ def __gt__(self, other: object) -> bool:
167
+ return False
168
+
169
+ def __ge__(self, other: object) -> bool:
170
+ return False
171
+
172
+ def __neg__(self: object) -> InfinityVersionType:
173
+ return InfinityVersion
174
+
175
+
176
+ NegativeInfinityVersion = NegativeInfinityVersionType()
177
+
178
+
179
+ ##
180
+
181
+
182
+ class _Version(ta.NamedTuple):
183
+ epoch: int
184
+ release: ta.Tuple[int, ...]
185
+ dev: ta.Optional[ta.Tuple[str, int]]
186
+ pre: ta.Optional[ta.Tuple[str, int]]
187
+ post: ta.Optional[ta.Tuple[str, int]]
188
+ local: ta.Optional[VersionLocalType]
189
+
190
+
191
+ class InvalidVersion(ValueError): # noqa
192
+ pass
193
+
194
+
195
+ class _BaseVersion:
196
+ _key: ta.Tuple[ta.Any, ...]
197
+
198
+ def __hash__(self) -> int:
199
+ return hash(self._key)
200
+
201
+ def __lt__(self, other: '_BaseVersion') -> bool:
202
+ if not isinstance(other, _BaseVersion):
203
+ return NotImplemented # type: ignore
204
+ return self._key < other._key
205
+
206
+ def __le__(self, other: '_BaseVersion') -> bool:
207
+ if not isinstance(other, _BaseVersion):
208
+ return NotImplemented # type: ignore
209
+ return self._key <= other._key
210
+
211
+ def __eq__(self, other: object) -> bool:
212
+ if not isinstance(other, _BaseVersion):
213
+ return NotImplemented
214
+ return self._key == other._key
215
+
216
+ def __ge__(self, other: '_BaseVersion') -> bool:
217
+ if not isinstance(other, _BaseVersion):
218
+ return NotImplemented # type: ignore
219
+ return self._key >= other._key
220
+
221
+ def __gt__(self, other: '_BaseVersion') -> bool:
222
+ if not isinstance(other, _BaseVersion):
223
+ return NotImplemented # type: ignore
224
+ return self._key > other._key
225
+
226
+ def __ne__(self, other: object) -> bool:
227
+ if not isinstance(other, _BaseVersion):
228
+ return NotImplemented
229
+ return self._key != other._key
230
+
231
+
232
+ _VERSION_PATTERN = r"""
233
+ v?
234
+ (?:
235
+ (?:(?P<epoch>[0-9]+)!)?
236
+ (?P<release>[0-9]+(?:\.[0-9]+)*)
237
+ (?P<pre>
238
+ [-_\.]?
239
+ (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc)
240
+ [-_\.]?
241
+ (?P<pre_n>[0-9]+)?
242
+ )?
243
+ (?P<post>
244
+ (?:-(?P<post_n1>[0-9]+))
245
+ |
246
+ (?:
247
+ [-_\.]?
248
+ (?P<post_l>post|rev|r)
249
+ [-_\.]?
250
+ (?P<post_n2>[0-9]+)?
251
+ )
252
+ )?
253
+ (?P<dev>
254
+ [-_\.]?
255
+ (?P<dev_l>dev)
256
+ [-_\.]?
257
+ (?P<dev_n>[0-9]+)?
258
+ )?
259
+ )
260
+ (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?
261
+ """
262
+
263
+ VERSION_PATTERN = _VERSION_PATTERN
264
+
265
+
266
+ class Version(_BaseVersion):
267
+ _regex = re.compile(r'^\s*' + VERSION_PATTERN + r'\s*$', re.VERBOSE | re.IGNORECASE)
268
+ _key: VersionCmpKey
269
+
270
+ def __init__(self, version: str) -> None:
271
+ match = self._regex.search(version)
272
+ if not match:
273
+ raise InvalidVersion(f"Invalid version: '{version}'")
274
+
275
+ self._version = _Version(
276
+ epoch=int(match.group('epoch')) if match.group('epoch') else 0,
277
+ release=tuple(int(i) for i in match.group('release').split('.')),
278
+ pre=_parse_letter_version(match.group('pre_l'), match.group('pre_n')),
279
+ post=_parse_letter_version(match.group('post_l'), match.group('post_n1') or match.group('post_n2')),
280
+ dev=_parse_letter_version(match.group('dev_l'), match.group('dev_n')),
281
+ local=_parse_local_version(match.group('local')),
282
+ )
283
+
284
+ self._key = _version_cmpkey(
285
+ self._version.epoch,
286
+ self._version.release,
287
+ self._version.pre,
288
+ self._version.post,
289
+ self._version.dev,
290
+ self._version.local,
291
+ )
292
+
293
+ def __repr__(self) -> str:
294
+ return f"<Version('{self}')>"
295
+
296
+ def __str__(self) -> str:
297
+ parts = []
298
+
299
+ if self.epoch != 0:
300
+ parts.append(f'{self.epoch}!')
301
+
302
+ parts.append('.'.join(str(x) for x in self.release))
303
+
304
+ if self.pre is not None:
305
+ parts.append(''.join(str(x) for x in self.pre))
306
+
307
+ if self.post is not None:
308
+ parts.append(f'.post{self.post}')
309
+
310
+ if self.dev is not None:
311
+ parts.append(f'.dev{self.dev}')
312
+
313
+ if self.local is not None:
314
+ parts.append(f'+{self.local}')
315
+
316
+ return ''.join(parts)
317
+
318
+ @property
319
+ def epoch(self) -> int:
320
+ return self._version.epoch
321
+
322
+ @property
323
+ def release(self) -> ta.Tuple[int, ...]:
324
+ return self._version.release
325
+
326
+ @property
327
+ def pre(self) -> ta.Optional[ta.Tuple[str, int]]:
328
+ return self._version.pre
329
+
330
+ @property
331
+ def post(self) -> ta.Optional[int]:
332
+ return self._version.post[1] if self._version.post else None
333
+
334
+ @property
335
+ def dev(self) -> ta.Optional[int]:
336
+ return self._version.dev[1] if self._version.dev else None
337
+
338
+ @property
339
+ def local(self) -> ta.Optional[str]:
340
+ if self._version.local:
341
+ return '.'.join(str(x) for x in self._version.local)
342
+ else:
343
+ return None
344
+
345
+ @property
346
+ def public(self) -> str:
347
+ return str(self).split('+', 1)[0]
348
+
349
+ @property
350
+ def base_version(self) -> str:
351
+ parts = []
352
+
353
+ if self.epoch != 0:
354
+ parts.append(f'{self.epoch}!')
355
+
356
+ parts.append('.'.join(str(x) for x in self.release))
357
+
358
+ return ''.join(parts)
359
+
360
+ @property
361
+ def is_prerelease(self) -> bool:
362
+ return self.dev is not None or self.pre is not None
363
+
364
+ @property
365
+ def is_postrelease(self) -> bool:
366
+ return self.post is not None
367
+
368
+ @property
369
+ def is_devrelease(self) -> bool:
370
+ return self.dev is not None
371
+
372
+ @property
373
+ def major(self) -> int:
374
+ return self.release[0] if len(self.release) >= 1 else 0
375
+
376
+ @property
377
+ def minor(self) -> int:
378
+ return self.release[1] if len(self.release) >= 2 else 0
379
+
380
+ @property
381
+ def micro(self) -> int:
382
+ return self.release[2] if len(self.release) >= 3 else 0
383
+
384
+
385
+ def _parse_letter_version(
386
+ letter: ta.Optional[str],
387
+ number: ta.Union[str, bytes, ta.SupportsInt, None],
388
+ ) -> ta.Optional[ta.Tuple[str, int]]:
389
+ if letter:
390
+ if number is None:
391
+ number = 0
392
+
393
+ letter = letter.lower()
394
+ if letter == 'alpha':
395
+ letter = 'a'
396
+ elif letter == 'beta':
397
+ letter = 'b'
398
+ elif letter in ['c', 'pre', 'preview']:
399
+ letter = 'rc'
400
+ elif letter in ['rev', 'r']:
401
+ letter = 'post'
402
+
403
+ return letter, int(number)
404
+ if not letter and number:
405
+ letter = 'post'
406
+ return letter, int(number)
407
+
408
+ return None
409
+
410
+
411
+ _local_version_separators = re.compile(r'[\._-]')
412
+
413
+
414
+ def _parse_local_version(local: ta.Optional[str]) -> ta.Optional[VersionLocalType]:
415
+ if local is not None:
416
+ return tuple(
417
+ part.lower() if not part.isdigit() else int(part)
418
+ for part in _local_version_separators.split(local)
419
+ )
420
+ return None
421
+
422
+
423
+ def _version_cmpkey(
424
+ epoch: int,
425
+ release: ta.Tuple[int, ...],
426
+ pre: ta.Optional[ta.Tuple[str, int]],
427
+ post: ta.Optional[ta.Tuple[str, int]],
428
+ dev: ta.Optional[ta.Tuple[str, int]],
429
+ local: ta.Optional[VersionLocalType],
430
+ ) -> VersionCmpKey:
431
+ _release = tuple(reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release)))))
432
+
433
+ if pre is None and post is None and dev is not None:
434
+ _pre: VersionCmpPrePostDevType = NegativeInfinityVersion
435
+ elif pre is None:
436
+ _pre = InfinityVersion
437
+ else:
438
+ _pre = pre
439
+
440
+ if post is None:
441
+ _post: VersionCmpPrePostDevType = NegativeInfinityVersion
442
+ else:
443
+ _post = post
444
+
445
+ if dev is None:
446
+ _dev: VersionCmpPrePostDevType = InfinityVersion
447
+ else:
448
+ _dev = dev
449
+
450
+ if local is None:
451
+ _local: VersionCmpLocalType = NegativeInfinityVersion
452
+ else:
453
+ _local = tuple((i, '') if isinstance(i, int) else (NegativeInfinityVersion, i) for i in local)
454
+
455
+ return epoch, _release, _pre, _post, _dev, _local
456
+
457
+
458
+ ##
459
+
460
+
461
+ def canonicalize_version(
462
+ version: ta.Union[Version, str],
463
+ *,
464
+ strip_trailing_zero: bool = True,
465
+ ) -> str:
466
+ if isinstance(version, str):
467
+ try:
468
+ parsed = Version(version)
469
+ except InvalidVersion:
470
+ return version
471
+ else:
472
+ parsed = version
473
+
474
+ parts = []
475
+
476
+ if parsed.epoch != 0:
477
+ parts.append(f'{parsed.epoch}!')
478
+
479
+ release_segment = '.'.join(str(x) for x in parsed.release)
480
+ if strip_trailing_zero:
481
+ release_segment = re.sub(r'(\.0)+$', '', release_segment)
482
+ parts.append(release_segment)
483
+
484
+ if parsed.pre is not None:
485
+ parts.append(''.join(str(x) for x in parsed.pre))
486
+
487
+ if parsed.post is not None:
488
+ parts.append(f'.post{parsed.post}')
489
+
490
+ if parsed.dev is not None:
491
+ parts.append(f'.dev{parsed.dev}')
492
+
493
+ if parsed.local is not None:
494
+ parts.append(f'+{parsed.local}')
495
+
496
+ return ''.join(parts)
497
+
498
+
74
499
  ########################################
75
500
  # ../config.py
76
501
 
@@ -951,143 +1376,665 @@ def format_num_bytes(num_bytes: int) -> str:
951
1376
 
952
1377
 
953
1378
  ########################################
954
- # ../commands/base.py
955
-
956
-
957
- ##
958
-
959
-
960
- @dc.dataclass(frozen=True)
961
- class Command(abc.ABC, ta.Generic[CommandOutputT]):
962
- @dc.dataclass(frozen=True)
963
- class Output(abc.ABC): # noqa
964
- pass
965
-
966
- @ta.final
967
- def execute(self, executor: 'CommandExecutor') -> CommandOutputT:
968
- return check_isinstance(executor.execute(self), self.Output) # type: ignore[return-value]
1379
+ # ../../../omdev/packaging/specifiers.py
1380
+ # Copyright (c) Donald Stufft and individual contributors.
1381
+ # All rights reserved.
1382
+ #
1383
+ # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
1384
+ # following conditions are met:
1385
+ #
1386
+ # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
1387
+ # following disclaimer.
1388
+ #
1389
+ # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
1390
+ # following disclaimer in the documentation and/or other materials provided with the distribution.
1391
+ #
1392
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
1393
+ # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
1394
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
1395
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
1396
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
1397
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
1398
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. This file is dual licensed under the terms of the
1399
+ # Apache License, Version 2.0, and the BSD License. See the LICENSE file in the root of this repository for complete
1400
+ # details.
1401
+ # https://github.com/pypa/packaging/blob/2c885fe91a54559e2382902dce28428ad2887be5/src/packaging/specifiers.py
969
1402
 
970
1403
 
971
1404
  ##
972
1405
 
973
1406
 
974
- @dc.dataclass(frozen=True)
975
- class CommandException:
976
- name: str
977
- repr: str
978
-
979
- traceback: ta.Optional[str] = None
1407
+ def _coerce_version(version: UnparsedVersion) -> Version:
1408
+ if not isinstance(version, Version):
1409
+ version = Version(version)
1410
+ return version
980
1411
 
981
- exc: ta.Optional[ta.Any] = None # Exception
982
1412
 
983
- cmd: ta.Optional[Command] = None
1413
+ class InvalidSpecifier(ValueError): # noqa
1414
+ pass
984
1415
 
985
- @classmethod
986
- def of(
987
- cls,
988
- exc: Exception,
989
- *,
990
- omit_exc_object: bool = False,
991
1416
 
992
- cmd: ta.Optional[Command] = None,
993
- ) -> 'CommandException':
994
- return CommandException(
995
- name=type(exc).__qualname__,
996
- repr=repr(exc),
1417
+ class BaseSpecifier(metaclass=abc.ABCMeta):
1418
+ @abc.abstractmethod
1419
+ def __str__(self) -> str:
1420
+ raise NotImplementedError
997
1421
 
998
- traceback=(
999
- ''.join(traceback.format_tb(exc.__traceback__))
1000
- if getattr(exc, '__traceback__', None) is not None else None
1001
- ),
1422
+ @abc.abstractmethod
1423
+ def __hash__(self) -> int:
1424
+ raise NotImplementedError
1002
1425
 
1003
- exc=None if omit_exc_object else exc,
1426
+ @abc.abstractmethod
1427
+ def __eq__(self, other: object) -> bool:
1428
+ raise NotImplementedError
1004
1429
 
1005
- cmd=cmd,
1006
- )
1430
+ @property
1431
+ @abc.abstractmethod
1432
+ def prereleases(self) -> ta.Optional[bool]:
1433
+ raise NotImplementedError
1007
1434
 
1435
+ @prereleases.setter
1436
+ def prereleases(self, value: bool) -> None:
1437
+ raise NotImplementedError
1008
1438
 
1009
- class CommandOutputOrException(abc.ABC, ta.Generic[CommandOutputT]):
1010
- @property
1011
1439
  @abc.abstractmethod
1012
- def output(self) -> ta.Optional[CommandOutputT]:
1440
+ def contains(self, item: str, prereleases: ta.Optional[bool] = None) -> bool:
1013
1441
  raise NotImplementedError
1014
1442
 
1015
- @property
1016
1443
  @abc.abstractmethod
1017
- def exception(self) -> ta.Optional[CommandException]:
1444
+ def filter(
1445
+ self,
1446
+ iterable: ta.Iterable[UnparsedVersionVar],
1447
+ prereleases: ta.Optional[bool] = None,
1448
+ ) -> ta.Iterator[UnparsedVersionVar]:
1018
1449
  raise NotImplementedError
1019
1450
 
1020
1451
 
1021
- @dc.dataclass(frozen=True)
1022
- class CommandOutputOrExceptionData(CommandOutputOrException):
1023
- output: ta.Optional[Command.Output] = None
1024
- exception: ta.Optional[CommandException] = None
1452
+ class Specifier(BaseSpecifier):
1453
+ _operator_regex_str = r"""
1454
+ (?P<operator>(~=|==|!=|<=|>=|<|>|===))
1455
+ """
1025
1456
 
1457
+ _version_regex_str = r"""
1458
+ (?P<version>
1459
+ (?:
1460
+ (?<====)
1461
+ \s*
1462
+ [^\s;)]*
1463
+ )
1464
+ |
1465
+ (?:
1466
+ (?<===|!=)
1467
+ \s*
1468
+ v?
1469
+ (?:[0-9]+!)?
1470
+ [0-9]+(?:\.[0-9]+)*
1471
+ (?:
1472
+ \.\*
1473
+ |
1474
+ (?:
1475
+ [-_\.]?
1476
+ (alpha|beta|preview|pre|a|b|c|rc)
1477
+ [-_\.]?
1478
+ [0-9]*
1479
+ )?
1480
+ (?:
1481
+ (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
1482
+ )?
1483
+ (?:[-_\.]?dev[-_\.]?[0-9]*)?
1484
+ (?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)?
1485
+ )?
1486
+ )
1487
+ |
1488
+ (?:
1489
+ (?<=~=)
1490
+ \s*
1491
+ v?
1492
+ (?:[0-9]+!)?
1493
+ [0-9]+(?:\.[0-9]+)+
1494
+ (?:
1495
+ [-_\.]?
1496
+ (alpha|beta|preview|pre|a|b|c|rc)
1497
+ [-_\.]?
1498
+ [0-9]*
1499
+ )?
1500
+ (?:
1501
+ (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
1502
+ )?
1503
+ (?:[-_\.]?dev[-_\.]?[0-9]*)?
1504
+ )
1505
+ |
1506
+ (?:
1507
+ (?<!==|!=|~=)
1508
+ \s*
1509
+ v?
1510
+ (?:[0-9]+!)?
1511
+ [0-9]+(?:\.[0-9]+)*
1512
+ (?:
1513
+ [-_\.]?
1514
+ (alpha|beta|preview|pre|a|b|c|rc)
1515
+ [-_\.]?
1516
+ [0-9]*
1517
+ )?
1518
+ (?:
1519
+ (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
1520
+ )?
1521
+ (?:[-_\.]?dev[-_\.]?[0-9]*)?
1522
+ )
1523
+ )
1524
+ """
1026
1525
 
1027
- class CommandExecutor(abc.ABC, ta.Generic[CommandT, CommandOutputT]):
1028
- @abc.abstractmethod
1029
- def execute(self, cmd: CommandT) -> CommandOutputT:
1030
- raise NotImplementedError
1526
+ _regex = re.compile(
1527
+ r'^\s*' + _operator_regex_str + _version_regex_str + r'\s*$',
1528
+ re.VERBOSE | re.IGNORECASE,
1529
+ )
1031
1530
 
1032
- def try_execute(
1531
+ OPERATORS: ta.ClassVar[ta.Mapping[str, str]] = {
1532
+ '~=': 'compatible',
1533
+ '==': 'equal',
1534
+ '!=': 'not_equal',
1535
+ '<=': 'less_than_equal',
1536
+ '>=': 'greater_than_equal',
1537
+ '<': 'less_than',
1538
+ '>': 'greater_than',
1539
+ '===': 'arbitrary',
1540
+ }
1541
+
1542
+ def __init__(
1033
1543
  self,
1034
- cmd: CommandT,
1035
- *,
1036
- log: ta.Optional[logging.Logger] = None,
1037
- omit_exc_object: bool = False,
1038
- ) -> CommandOutputOrException[CommandOutputT]:
1039
- try:
1040
- o = self.execute(cmd)
1544
+ spec: str = '',
1545
+ prereleases: ta.Optional[bool] = None,
1546
+ ) -> None:
1547
+ match = self._regex.search(spec)
1548
+ if not match:
1549
+ raise InvalidSpecifier(f"Invalid specifier: '{spec}'")
1041
1550
 
1042
- except Exception as e: # noqa
1043
- if log is not None:
1044
- log.exception('Exception executing command: %r', type(cmd))
1551
+ self._spec: ta.Tuple[str, str] = (
1552
+ match.group('operator').strip(),
1553
+ match.group('version').strip(),
1554
+ )
1045
1555
 
1046
- return CommandOutputOrExceptionData(exception=CommandException.of(
1047
- e,
1048
- omit_exc_object=omit_exc_object,
1049
- cmd=cmd,
1050
- ))
1556
+ self._prereleases = prereleases
1051
1557
 
1052
- else:
1053
- return CommandOutputOrExceptionData(output=o)
1558
+ @property # type: ignore
1559
+ def prereleases(self) -> bool:
1560
+ if self._prereleases is not None:
1561
+ return self._prereleases
1054
1562
 
1563
+ operator, version = self._spec
1564
+ if operator in ['==', '>=', '<=', '~=', '===']:
1565
+ if operator == '==' and version.endswith('.*'):
1566
+ version = version[:-2]
1055
1567
 
1056
- ##
1568
+ if Version(version).is_prerelease:
1569
+ return True
1057
1570
 
1571
+ return False
1058
1572
 
1059
- @dc.dataclass(frozen=True)
1060
- class CommandRegistration:
1061
- command_cls: ta.Type[Command]
1573
+ @prereleases.setter
1574
+ def prereleases(self, value: bool) -> None:
1575
+ self._prereleases = value
1062
1576
 
1063
- name: ta.Optional[str] = None
1577
+ @property
1578
+ def operator(self) -> str:
1579
+ return self._spec[0]
1064
1580
 
1065
1581
  @property
1066
- def name_or_default(self) -> str:
1067
- if not (cls_name := self.command_cls.__name__).endswith('Command'):
1068
- raise NameError(cls_name)
1069
- return snake_case(cls_name[:-len('Command')])
1582
+ def version(self) -> str:
1583
+ return self._spec[1]
1584
+
1585
+ def __repr__(self) -> str:
1586
+ pre = (
1587
+ f', prereleases={self.prereleases!r}'
1588
+ if self._prereleases is not None
1589
+ else ''
1590
+ )
1070
1591
 
1592
+ return f'<{self.__class__.__name__}({str(self)!r}{pre})>'
1071
1593
 
1072
- CommandRegistrations = ta.NewType('CommandRegistrations', ta.Sequence[CommandRegistration])
1594
+ def __str__(self) -> str:
1595
+ return '{}{}'.format(*self._spec)
1073
1596
 
1597
+ @property
1598
+ def _canonical_spec(self) -> ta.Tuple[str, str]:
1599
+ canonical_version = canonicalize_version(
1600
+ self._spec[1],
1601
+ strip_trailing_zero=(self._spec[0] != '~='),
1602
+ )
1603
+ return self._spec[0], canonical_version
1074
1604
 
1075
- ##
1605
+ def __hash__(self) -> int:
1606
+ return hash(self._canonical_spec)
1076
1607
 
1608
+ def __eq__(self, other: object) -> bool:
1609
+ if isinstance(other, str):
1610
+ try:
1611
+ other = self.__class__(str(other))
1612
+ except InvalidSpecifier:
1613
+ return NotImplemented
1614
+ elif not isinstance(other, self.__class__):
1615
+ return NotImplemented
1077
1616
 
1078
- @dc.dataclass(frozen=True)
1079
- class CommandExecutorRegistration:
1080
- command_cls: ta.Type[Command]
1081
- executor_cls: ta.Type[CommandExecutor]
1617
+ return self._canonical_spec == other._canonical_spec
1082
1618
 
1619
+ def _get_operator(self, op: str) -> CallableVersionOperator:
1620
+ operator_callable: CallableVersionOperator = getattr(self, f'_compare_{self.OPERATORS[op]}')
1621
+ return operator_callable
1083
1622
 
1084
- CommandExecutorRegistrations = ta.NewType('CommandExecutorRegistrations', ta.Sequence[CommandExecutorRegistration])
1623
+ def _compare_compatible(self, prospective: Version, spec: str) -> bool:
1624
+ prefix = _version_join(list(itertools.takewhile(_is_not_version_suffix, _version_split(spec)))[:-1])
1625
+ prefix += '.*'
1626
+ return self._get_operator('>=')(prospective, spec) and self._get_operator('==')(prospective, prefix)
1085
1627
 
1628
+ def _compare_equal(self, prospective: Version, spec: str) -> bool:
1629
+ if spec.endswith('.*'):
1630
+ normalized_prospective = canonicalize_version(prospective.public, strip_trailing_zero=False)
1631
+ normalized_spec = canonicalize_version(spec[:-2], strip_trailing_zero=False)
1632
+ split_spec = _version_split(normalized_spec)
1086
1633
 
1087
- ##
1634
+ split_prospective = _version_split(normalized_prospective)
1635
+ padded_prospective, _ = _pad_version(split_prospective, split_spec)
1636
+ shortened_prospective = padded_prospective[: len(split_spec)]
1088
1637
 
1638
+ return shortened_prospective == split_spec
1089
1639
 
1090
- CommandNameMap = ta.NewType('CommandNameMap', ta.Mapping[str, ta.Type[Command]])
1640
+ else:
1641
+ spec_version = Version(spec)
1642
+ if not spec_version.local:
1643
+ prospective = Version(prospective.public)
1644
+ return prospective == spec_version
1645
+
1646
+ def _compare_not_equal(self, prospective: Version, spec: str) -> bool:
1647
+ return not self._compare_equal(prospective, spec)
1648
+
1649
+ def _compare_less_than_equal(self, prospective: Version, spec: str) -> bool:
1650
+ return Version(prospective.public) <= Version(spec)
1651
+
1652
+ def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool:
1653
+ return Version(prospective.public) >= Version(spec)
1654
+
1655
+ def _compare_less_than(self, prospective: Version, spec_str: str) -> bool:
1656
+ spec = Version(spec_str)
1657
+
1658
+ if not prospective < spec:
1659
+ return False
1660
+
1661
+ if not spec.is_prerelease and prospective.is_prerelease:
1662
+ if Version(prospective.base_version) == Version(spec.base_version):
1663
+ return False
1664
+
1665
+ return True
1666
+
1667
+ def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool:
1668
+ spec = Version(spec_str)
1669
+
1670
+ if not prospective > spec:
1671
+ return False
1672
+
1673
+ if not spec.is_postrelease and prospective.is_postrelease:
1674
+ if Version(prospective.base_version) == Version(spec.base_version):
1675
+ return False
1676
+
1677
+ if prospective.local is not None:
1678
+ if Version(prospective.base_version) == Version(spec.base_version):
1679
+ return False
1680
+
1681
+ return True
1682
+
1683
+ def _compare_arbitrary(self, prospective: Version, spec: str) -> bool:
1684
+ return str(prospective).lower() == str(spec).lower()
1685
+
1686
+ def __contains__(self, item: ta.Union[str, Version]) -> bool:
1687
+ return self.contains(item)
1688
+
1689
+ def contains(self, item: UnparsedVersion, prereleases: ta.Optional[bool] = None) -> bool:
1690
+ if prereleases is None:
1691
+ prereleases = self.prereleases
1692
+
1693
+ normalized_item = _coerce_version(item)
1694
+
1695
+ if normalized_item.is_prerelease and not prereleases:
1696
+ return False
1697
+
1698
+ operator_callable: CallableVersionOperator = self._get_operator(self.operator)
1699
+ return operator_callable(normalized_item, self.version)
1700
+
1701
+ def filter(
1702
+ self,
1703
+ iterable: ta.Iterable[UnparsedVersionVar],
1704
+ prereleases: ta.Optional[bool] = None,
1705
+ ) -> ta.Iterator[UnparsedVersionVar]:
1706
+ yielded = False
1707
+ found_prereleases = []
1708
+
1709
+ kw = {'prereleases': prereleases if prereleases is not None else True}
1710
+
1711
+ for version in iterable:
1712
+ parsed_version = _coerce_version(version)
1713
+
1714
+ if self.contains(parsed_version, **kw):
1715
+ if parsed_version.is_prerelease and not (prereleases or self.prereleases):
1716
+ found_prereleases.append(version)
1717
+ else:
1718
+ yielded = True
1719
+ yield version
1720
+
1721
+ if not yielded and found_prereleases:
1722
+ for version in found_prereleases:
1723
+ yield version
1724
+
1725
+
1726
+ _version_prefix_regex = re.compile(r'^([0-9]+)((?:a|b|c|rc)[0-9]+)$')
1727
+
1728
+
1729
+ def _version_split(version: str) -> ta.List[str]:
1730
+ result: ta.List[str] = []
1731
+
1732
+ epoch, _, rest = version.rpartition('!')
1733
+ result.append(epoch or '0')
1734
+
1735
+ for item in rest.split('.'):
1736
+ match = _version_prefix_regex.search(item)
1737
+ if match:
1738
+ result.extend(match.groups())
1739
+ else:
1740
+ result.append(item)
1741
+ return result
1742
+
1743
+
1744
+ def _version_join(components: ta.List[str]) -> str:
1745
+ epoch, *rest = components
1746
+ return f"{epoch}!{'.'.join(rest)}"
1747
+
1748
+
1749
+ def _is_not_version_suffix(segment: str) -> bool:
1750
+ return not any(segment.startswith(prefix) for prefix in ('dev', 'a', 'b', 'rc', 'post'))
1751
+
1752
+
1753
+ def _pad_version(left: ta.List[str], right: ta.List[str]) -> ta.Tuple[ta.List[str], ta.List[str]]:
1754
+ left_split, right_split = [], []
1755
+
1756
+ left_split.append(list(itertools.takewhile(lambda x: x.isdigit(), left)))
1757
+ right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right)))
1758
+
1759
+ left_split.append(left[len(left_split[0]):])
1760
+ right_split.append(right[len(right_split[0]):])
1761
+
1762
+ left_split.insert(1, ['0'] * max(0, len(right_split[0]) - len(left_split[0])))
1763
+ right_split.insert(1, ['0'] * max(0, len(left_split[0]) - len(right_split[0])))
1764
+
1765
+ return (
1766
+ list(itertools.chain.from_iterable(left_split)),
1767
+ list(itertools.chain.from_iterable(right_split)),
1768
+ )
1769
+
1770
+
1771
+ class SpecifierSet(BaseSpecifier):
1772
+ def __init__(
1773
+ self,
1774
+ specifiers: str = '',
1775
+ prereleases: ta.Optional[bool] = None,
1776
+ ) -> None:
1777
+ split_specifiers = [s.strip() for s in specifiers.split(',') if s.strip()]
1778
+
1779
+ self._specs = frozenset(map(Specifier, split_specifiers))
1780
+ self._prereleases = prereleases
1781
+
1782
+ @property
1783
+ def prereleases(self) -> ta.Optional[bool]:
1784
+ if self._prereleases is not None:
1785
+ return self._prereleases
1786
+
1787
+ if not self._specs:
1788
+ return None
1789
+
1790
+ return any(s.prereleases for s in self._specs)
1791
+
1792
+ @prereleases.setter
1793
+ def prereleases(self, value: bool) -> None:
1794
+ self._prereleases = value
1795
+
1796
+ def __repr__(self) -> str:
1797
+ pre = (
1798
+ f', prereleases={self.prereleases!r}'
1799
+ if self._prereleases is not None
1800
+ else ''
1801
+ )
1802
+
1803
+ return f'<SpecifierSet({str(self)!r}{pre})>'
1804
+
1805
+ def __str__(self) -> str:
1806
+ return ','.join(sorted(str(s) for s in self._specs))
1807
+
1808
+ def __hash__(self) -> int:
1809
+ return hash(self._specs)
1810
+
1811
+ def __and__(self, other: ta.Union['SpecifierSet', str]) -> 'SpecifierSet':
1812
+ if isinstance(other, str):
1813
+ other = SpecifierSet(other)
1814
+ elif not isinstance(other, SpecifierSet):
1815
+ return NotImplemented # type: ignore
1816
+
1817
+ specifier = SpecifierSet()
1818
+ specifier._specs = frozenset(self._specs | other._specs)
1819
+
1820
+ if self._prereleases is None and other._prereleases is not None:
1821
+ specifier._prereleases = other._prereleases
1822
+ elif self._prereleases is not None and other._prereleases is None:
1823
+ specifier._prereleases = self._prereleases
1824
+ elif self._prereleases == other._prereleases:
1825
+ specifier._prereleases = self._prereleases
1826
+ else:
1827
+ raise ValueError('Cannot combine SpecifierSets with True and False prerelease overrides.')
1828
+
1829
+ return specifier
1830
+
1831
+ def __eq__(self, other: object) -> bool:
1832
+ if isinstance(other, (str, Specifier)):
1833
+ other = SpecifierSet(str(other))
1834
+ elif not isinstance(other, SpecifierSet):
1835
+ return NotImplemented
1836
+
1837
+ return self._specs == other._specs
1838
+
1839
+ def __len__(self) -> int:
1840
+ return len(self._specs)
1841
+
1842
+ def __iter__(self) -> ta.Iterator[Specifier]:
1843
+ return iter(self._specs)
1844
+
1845
+ def __contains__(self, item: UnparsedVersion) -> bool:
1846
+ return self.contains(item)
1847
+
1848
+ def contains(
1849
+ self,
1850
+ item: UnparsedVersion,
1851
+ prereleases: ta.Optional[bool] = None,
1852
+ installed: ta.Optional[bool] = None,
1853
+ ) -> bool:
1854
+ if not isinstance(item, Version):
1855
+ item = Version(item)
1856
+
1857
+ if prereleases is None:
1858
+ prereleases = self.prereleases
1859
+
1860
+ if not prereleases and item.is_prerelease:
1861
+ return False
1862
+
1863
+ if installed and item.is_prerelease:
1864
+ item = Version(item.base_version)
1865
+
1866
+ return all(s.contains(item, prereleases=prereleases) for s in self._specs)
1867
+
1868
+ def filter(
1869
+ self,
1870
+ iterable: ta.Iterable[UnparsedVersionVar],
1871
+ prereleases: ta.Optional[bool] = None,
1872
+ ) -> ta.Iterator[UnparsedVersionVar]:
1873
+ if prereleases is None:
1874
+ prereleases = self.prereleases
1875
+
1876
+ if self._specs:
1877
+ for spec in self._specs:
1878
+ iterable = spec.filter(iterable, prereleases=bool(prereleases))
1879
+ return iter(iterable)
1880
+
1881
+ else:
1882
+ filtered: ta.List[UnparsedVersionVar] = []
1883
+ found_prereleases: ta.List[UnparsedVersionVar] = []
1884
+
1885
+ for item in iterable:
1886
+ parsed_version = _coerce_version(item)
1887
+
1888
+ if parsed_version.is_prerelease and not prereleases:
1889
+ if not filtered:
1890
+ found_prereleases.append(item)
1891
+ else:
1892
+ filtered.append(item)
1893
+
1894
+ if not filtered and found_prereleases and prereleases is None:
1895
+ return iter(found_prereleases)
1896
+
1897
+ return iter(filtered)
1898
+
1899
+
1900
+ ########################################
1901
+ # ../commands/base.py
1902
+
1903
+
1904
+ ##
1905
+
1906
+
1907
+ @dc.dataclass(frozen=True)
1908
+ class Command(abc.ABC, ta.Generic[CommandOutputT]):
1909
+ @dc.dataclass(frozen=True)
1910
+ class Output(abc.ABC): # noqa
1911
+ pass
1912
+
1913
+ @ta.final
1914
+ def execute(self, executor: 'CommandExecutor') -> CommandOutputT:
1915
+ return check_isinstance(executor.execute(self), self.Output) # type: ignore[return-value]
1916
+
1917
+
1918
+ ##
1919
+
1920
+
1921
+ @dc.dataclass(frozen=True)
1922
+ class CommandException:
1923
+ name: str
1924
+ repr: str
1925
+
1926
+ traceback: ta.Optional[str] = None
1927
+
1928
+ exc: ta.Optional[ta.Any] = None # Exception
1929
+
1930
+ cmd: ta.Optional[Command] = None
1931
+
1932
+ @classmethod
1933
+ def of(
1934
+ cls,
1935
+ exc: Exception,
1936
+ *,
1937
+ omit_exc_object: bool = False,
1938
+
1939
+ cmd: ta.Optional[Command] = None,
1940
+ ) -> 'CommandException':
1941
+ return CommandException(
1942
+ name=type(exc).__qualname__,
1943
+ repr=repr(exc),
1944
+
1945
+ traceback=(
1946
+ ''.join(traceback.format_tb(exc.__traceback__))
1947
+ if getattr(exc, '__traceback__', None) is not None else None
1948
+ ),
1949
+
1950
+ exc=None if omit_exc_object else exc,
1951
+
1952
+ cmd=cmd,
1953
+ )
1954
+
1955
+
1956
+ class CommandOutputOrException(abc.ABC, ta.Generic[CommandOutputT]):
1957
+ @property
1958
+ @abc.abstractmethod
1959
+ def output(self) -> ta.Optional[CommandOutputT]:
1960
+ raise NotImplementedError
1961
+
1962
+ @property
1963
+ @abc.abstractmethod
1964
+ def exception(self) -> ta.Optional[CommandException]:
1965
+ raise NotImplementedError
1966
+
1967
+
1968
+ @dc.dataclass(frozen=True)
1969
+ class CommandOutputOrExceptionData(CommandOutputOrException):
1970
+ output: ta.Optional[Command.Output] = None
1971
+ exception: ta.Optional[CommandException] = None
1972
+
1973
+
1974
+ class CommandExecutor(abc.ABC, ta.Generic[CommandT, CommandOutputT]):
1975
+ @abc.abstractmethod
1976
+ def execute(self, cmd: CommandT) -> CommandOutputT:
1977
+ raise NotImplementedError
1978
+
1979
+ def try_execute(
1980
+ self,
1981
+ cmd: CommandT,
1982
+ *,
1983
+ log: ta.Optional[logging.Logger] = None,
1984
+ omit_exc_object: bool = False,
1985
+ ) -> CommandOutputOrException[CommandOutputT]:
1986
+ try:
1987
+ o = self.execute(cmd)
1988
+
1989
+ except Exception as e: # noqa
1990
+ if log is not None:
1991
+ log.exception('Exception executing command: %r', type(cmd))
1992
+
1993
+ return CommandOutputOrExceptionData(exception=CommandException.of(
1994
+ e,
1995
+ omit_exc_object=omit_exc_object,
1996
+ cmd=cmd,
1997
+ ))
1998
+
1999
+ else:
2000
+ return CommandOutputOrExceptionData(output=o)
2001
+
2002
+
2003
+ ##
2004
+
2005
+
2006
+ @dc.dataclass(frozen=True)
2007
+ class CommandRegistration:
2008
+ command_cls: ta.Type[Command]
2009
+
2010
+ name: ta.Optional[str] = None
2011
+
2012
+ @property
2013
+ def name_or_default(self) -> str:
2014
+ if not (cls_name := self.command_cls.__name__).endswith('Command'):
2015
+ raise NameError(cls_name)
2016
+ return snake_case(cls_name[:-len('Command')])
2017
+
2018
+
2019
+ CommandRegistrations = ta.NewType('CommandRegistrations', ta.Sequence[CommandRegistration])
2020
+
2021
+
2022
+ ##
2023
+
2024
+
2025
+ @dc.dataclass(frozen=True)
2026
+ class CommandExecutorRegistration:
2027
+ command_cls: ta.Type[Command]
2028
+ executor_cls: ta.Type[CommandExecutor]
2029
+
2030
+
2031
+ CommandExecutorRegistrations = ta.NewType('CommandExecutorRegistrations', ta.Sequence[CommandExecutorRegistration])
2032
+
2033
+
2034
+ ##
2035
+
2036
+
2037
+ CommandNameMap = ta.NewType('CommandNameMap', ta.Mapping[str, ta.Type[Command]])
1091
2038
 
1092
2039
 
1093
2040
  def build_command_name_map(crs: CommandRegistrations) -> CommandNameMap:
@@ -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__':