omdev 0.0.0.dev7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. omdev/__about__.py +35 -0
  2. omdev/__init__.py +0 -0
  3. omdev/amalg/__init__.py +0 -0
  4. omdev/amalg/__main__.py +4 -0
  5. omdev/amalg/amalg.py +513 -0
  6. omdev/classdot.py +61 -0
  7. omdev/cmake.py +164 -0
  8. omdev/exts/__init__.py +0 -0
  9. omdev/exts/_distutils/__init__.py +10 -0
  10. omdev/exts/_distutils/build_ext.py +367 -0
  11. omdev/exts/_distutils/compilers/__init__.py +3 -0
  12. omdev/exts/_distutils/compilers/ccompiler.py +1032 -0
  13. omdev/exts/_distutils/compilers/options.py +80 -0
  14. omdev/exts/_distutils/compilers/unixccompiler.py +385 -0
  15. omdev/exts/_distutils/dir_util.py +76 -0
  16. omdev/exts/_distutils/errors.py +62 -0
  17. omdev/exts/_distutils/extension.py +107 -0
  18. omdev/exts/_distutils/file_util.py +216 -0
  19. omdev/exts/_distutils/modified.py +47 -0
  20. omdev/exts/_distutils/spawn.py +103 -0
  21. omdev/exts/_distutils/sysconfig.py +349 -0
  22. omdev/exts/_distutils/util.py +201 -0
  23. omdev/exts/_distutils/version.py +308 -0
  24. omdev/exts/build.py +43 -0
  25. omdev/exts/cmake.py +195 -0
  26. omdev/exts/importhook.py +88 -0
  27. omdev/exts/scan.py +74 -0
  28. omdev/interp/__init__.py +1 -0
  29. omdev/interp/__main__.py +4 -0
  30. omdev/interp/cli.py +63 -0
  31. omdev/interp/inspect.py +105 -0
  32. omdev/interp/providers.py +67 -0
  33. omdev/interp/pyenv.py +353 -0
  34. omdev/interp/resolvers.py +76 -0
  35. omdev/interp/standalone.py +187 -0
  36. omdev/interp/system.py +125 -0
  37. omdev/interp/types.py +92 -0
  38. omdev/mypy/__init__.py +0 -0
  39. omdev/mypy/debug.py +86 -0
  40. omdev/pyproject/__init__.py +1 -0
  41. omdev/pyproject/__main__.py +4 -0
  42. omdev/pyproject/cli.py +319 -0
  43. omdev/pyproject/configs.py +97 -0
  44. omdev/pyproject/ext.py +107 -0
  45. omdev/pyproject/pkg.py +196 -0
  46. omdev/scripts/__init__.py +0 -0
  47. omdev/scripts/execrss.py +19 -0
  48. omdev/scripts/findimports.py +62 -0
  49. omdev/scripts/findmagic.py +70 -0
  50. omdev/scripts/interp.py +2118 -0
  51. omdev/scripts/pyproject.py +3584 -0
  52. omdev/scripts/traceimport.py +502 -0
  53. omdev/tokens.py +42 -0
  54. omdev/toml/__init__.py +1 -0
  55. omdev/toml/parser.py +823 -0
  56. omdev/toml/writer.py +104 -0
  57. omdev/tools/__init__.py +0 -0
  58. omdev/tools/dockertools.py +81 -0
  59. omdev/tools/sqlrepl.py +193 -0
  60. omdev/versioning/__init__.py +1 -0
  61. omdev/versioning/specifiers.py +531 -0
  62. omdev/versioning/versions.py +416 -0
  63. omdev-0.0.0.dev7.dist-info/LICENSE +21 -0
  64. omdev-0.0.0.dev7.dist-info/METADATA +24 -0
  65. omdev-0.0.0.dev7.dist-info/RECORD +67 -0
  66. omdev-0.0.0.dev7.dist-info/WHEEL +5 -0
  67. omdev-0.0.0.dev7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2118 @@
1
+ #!/usr/bin/env python3
2
+ # noinspection DuplicatedCode
3
+ # @omdev-amalg-output ../interp/cli.py
4
+ # ruff: noqa: UP007
5
+ """
6
+ TODO:
7
+ - partial best-matches - '3.12'
8
+ - https://github.com/asdf-vm/asdf support (instead of pyenv) ?
9
+ - colon sep provider name prefix - pyenv:3.12
10
+ """
11
+ import abc
12
+ import argparse
13
+ import collections
14
+ import dataclasses as dc
15
+ import functools
16
+ import inspect
17
+ import itertools
18
+ import json
19
+ import logging
20
+ import os
21
+ import os.path
22
+ import re
23
+ import shlex
24
+ import shutil
25
+ import subprocess
26
+ import sys
27
+ import typing as ta
28
+
29
+
30
+ VersionLocalType = ta.Tuple[ta.Union[int, str], ...]
31
+ VersionCmpPrePostDevType = ta.Union['InfinityVersionType', 'NegativeInfinityVersionType', ta.Tuple[str, int]]
32
+ _VersionCmpLocalType0 = ta.Tuple[ta.Union[ta.Tuple[int, str], ta.Tuple['NegativeInfinityVersionType', ta.Union[int, str]]], ...] # noqa
33
+ VersionCmpLocalType = ta.Union['NegativeInfinityVersionType', _VersionCmpLocalType0]
34
+ VersionCmpKey = ta.Tuple[int, ta.Tuple[int, ...], VersionCmpPrePostDevType, VersionCmpPrePostDevType, VersionCmpPrePostDevType, VersionCmpLocalType] # noqa
35
+ VersionComparisonMethod = ta.Callable[[VersionCmpKey, VersionCmpKey], bool]
36
+ T = ta.TypeVar('T')
37
+ UnparsedVersion = ta.Union['Version', str]
38
+ UnparsedVersionVar = ta.TypeVar('UnparsedVersionVar', bound=UnparsedVersion)
39
+ CallableVersionOperator = ta.Callable[['Version', str], bool]
40
+
41
+
42
+ ########################################
43
+ # ../../versioning/versions.py
44
+ # Copyright (c) Donald Stufft and individual contributors.
45
+ # All rights reserved.
46
+ #
47
+ # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
48
+ # following conditions are met:
49
+ #
50
+ # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
51
+ # following disclaimer.
52
+ #
53
+ # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
54
+ # following disclaimer in the documentation and/or other materials provided with the distribution.
55
+ #
56
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
57
+ # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
58
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
59
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
60
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
61
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
62
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. This file is dual licensed under the terms of the
63
+ # Apache License, Version 2.0, and the BSD License. See the LICENSE file in the root of this repository for complete
64
+ # details.
65
+ # https://github.com/pypa/packaging/blob/2c885fe91a54559e2382902dce28428ad2887be5/src/packaging/version.py
66
+ # ruff: noqa: UP006 UP007
67
+
68
+
69
+ ##
70
+
71
+
72
+ class InfinityVersionType:
73
+ def __repr__(self) -> str:
74
+ return 'Infinity'
75
+
76
+ def __hash__(self) -> int:
77
+ return hash(repr(self))
78
+
79
+ def __lt__(self, other: object) -> bool:
80
+ return False
81
+
82
+ def __le__(self, other: object) -> bool:
83
+ return False
84
+
85
+ def __eq__(self, other: object) -> bool:
86
+ return isinstance(other, self.__class__)
87
+
88
+ def __gt__(self, other: object) -> bool:
89
+ return True
90
+
91
+ def __ge__(self, other: object) -> bool:
92
+ return True
93
+
94
+ def __neg__(self: object) -> 'NegativeInfinityVersionType':
95
+ return NegativeInfinityVersion
96
+
97
+
98
+ InfinityVersion = InfinityVersionType()
99
+
100
+
101
+ class NegativeInfinityVersionType:
102
+ def __repr__(self) -> str:
103
+ return '-Infinity'
104
+
105
+ def __hash__(self) -> int:
106
+ return hash(repr(self))
107
+
108
+ def __lt__(self, other: object) -> bool:
109
+ return True
110
+
111
+ def __le__(self, other: object) -> bool:
112
+ return True
113
+
114
+ def __eq__(self, other: object) -> bool:
115
+ return isinstance(other, self.__class__)
116
+
117
+ def __gt__(self, other: object) -> bool:
118
+ return False
119
+
120
+ def __ge__(self, other: object) -> bool:
121
+ return False
122
+
123
+ def __neg__(self: object) -> InfinityVersionType:
124
+ return InfinityVersion
125
+
126
+
127
+ NegativeInfinityVersion = NegativeInfinityVersionType()
128
+
129
+
130
+ ##
131
+
132
+
133
+ class _Version(ta.NamedTuple):
134
+ epoch: int
135
+ release: ta.Tuple[int, ...]
136
+ dev: ta.Optional[ta.Tuple[str, int]]
137
+ pre: ta.Optional[ta.Tuple[str, int]]
138
+ post: ta.Optional[ta.Tuple[str, int]]
139
+ local: ta.Optional[VersionLocalType]
140
+
141
+
142
+ class InvalidVersion(ValueError): # noqa
143
+ pass
144
+
145
+
146
+ class _BaseVersion:
147
+ _key: ta.Tuple[ta.Any, ...]
148
+
149
+ def __hash__(self) -> int:
150
+ return hash(self._key)
151
+
152
+ def __lt__(self, other: '_BaseVersion') -> bool:
153
+ if not isinstance(other, _BaseVersion):
154
+ return NotImplemented # type: ignore
155
+ return self._key < other._key
156
+
157
+ def __le__(self, other: '_BaseVersion') -> bool:
158
+ if not isinstance(other, _BaseVersion):
159
+ return NotImplemented # type: ignore
160
+ return self._key <= other._key
161
+
162
+ def __eq__(self, other: object) -> bool:
163
+ if not isinstance(other, _BaseVersion):
164
+ return NotImplemented
165
+ return self._key == other._key
166
+
167
+ def __ge__(self, other: '_BaseVersion') -> bool:
168
+ if not isinstance(other, _BaseVersion):
169
+ return NotImplemented # type: ignore
170
+ return self._key >= other._key
171
+
172
+ def __gt__(self, other: '_BaseVersion') -> bool:
173
+ if not isinstance(other, _BaseVersion):
174
+ return NotImplemented # type: ignore
175
+ return self._key > other._key
176
+
177
+ def __ne__(self, other: object) -> bool:
178
+ if not isinstance(other, _BaseVersion):
179
+ return NotImplemented
180
+ return self._key != other._key
181
+
182
+
183
+ _VERSION_PATTERN = r"""
184
+ v?
185
+ (?:
186
+ (?:(?P<epoch>[0-9]+)!)?
187
+ (?P<release>[0-9]+(?:\.[0-9]+)*)
188
+ (?P<pre>
189
+ [-_\.]?
190
+ (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc)
191
+ [-_\.]?
192
+ (?P<pre_n>[0-9]+)?
193
+ )?
194
+ (?P<post>
195
+ (?:-(?P<post_n1>[0-9]+))
196
+ |
197
+ (?:
198
+ [-_\.]?
199
+ (?P<post_l>post|rev|r)
200
+ [-_\.]?
201
+ (?P<post_n2>[0-9]+)?
202
+ )
203
+ )?
204
+ (?P<dev>
205
+ [-_\.]?
206
+ (?P<dev_l>dev)
207
+ [-_\.]?
208
+ (?P<dev_n>[0-9]+)?
209
+ )?
210
+ )
211
+ (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?
212
+ """
213
+
214
+ VERSION_PATTERN = _VERSION_PATTERN
215
+
216
+
217
+ class Version(_BaseVersion):
218
+ _regex = re.compile(r'^\s*' + VERSION_PATTERN + r'\s*$', re.VERBOSE | re.IGNORECASE)
219
+ _key: VersionCmpKey
220
+
221
+ def __init__(self, version: str) -> None:
222
+ match = self._regex.search(version)
223
+ if not match:
224
+ raise InvalidVersion(f"Invalid version: '{version}'")
225
+
226
+ self._version = _Version(
227
+ epoch=int(match.group('epoch')) if match.group('epoch') else 0,
228
+ release=tuple(int(i) for i in match.group('release').split('.')),
229
+ pre=_parse_letter_version(match.group('pre_l'), match.group('pre_n')),
230
+ post=_parse_letter_version(match.group('post_l'), match.group('post_n1') or match.group('post_n2')),
231
+ dev=_parse_letter_version(match.group('dev_l'), match.group('dev_n')),
232
+ local=_parse_local_version(match.group('local')),
233
+ )
234
+
235
+ self._key = _version_cmpkey(
236
+ self._version.epoch,
237
+ self._version.release,
238
+ self._version.pre,
239
+ self._version.post,
240
+ self._version.dev,
241
+ self._version.local,
242
+ )
243
+
244
+ def __repr__(self) -> str:
245
+ return f"<Version('{self}')>"
246
+
247
+ def __str__(self) -> str:
248
+ parts = []
249
+
250
+ if self.epoch != 0:
251
+ parts.append(f'{self.epoch}!')
252
+
253
+ parts.append('.'.join(str(x) for x in self.release))
254
+
255
+ if self.pre is not None:
256
+ parts.append(''.join(str(x) for x in self.pre))
257
+
258
+ if self.post is not None:
259
+ parts.append(f'.post{self.post}')
260
+
261
+ if self.dev is not None:
262
+ parts.append(f'.dev{self.dev}')
263
+
264
+ if self.local is not None:
265
+ parts.append(f'+{self.local}')
266
+
267
+ return ''.join(parts)
268
+
269
+ @property
270
+ def epoch(self) -> int:
271
+ return self._version.epoch
272
+
273
+ @property
274
+ def release(self) -> ta.Tuple[int, ...]:
275
+ return self._version.release
276
+
277
+ @property
278
+ def pre(self) -> ta.Optional[ta.Tuple[str, int]]:
279
+ return self._version.pre
280
+
281
+ @property
282
+ def post(self) -> ta.Optional[int]:
283
+ return self._version.post[1] if self._version.post else None
284
+
285
+ @property
286
+ def dev(self) -> ta.Optional[int]:
287
+ return self._version.dev[1] if self._version.dev else None
288
+
289
+ @property
290
+ def local(self) -> ta.Optional[str]:
291
+ if self._version.local:
292
+ return '.'.join(str(x) for x in self._version.local)
293
+ else:
294
+ return None
295
+
296
+ @property
297
+ def public(self) -> str:
298
+ return str(self).split('+', 1)[0]
299
+
300
+ @property
301
+ def base_version(self) -> str:
302
+ parts = []
303
+
304
+ if self.epoch != 0:
305
+ parts.append(f'{self.epoch}!')
306
+
307
+ parts.append('.'.join(str(x) for x in self.release))
308
+
309
+ return ''.join(parts)
310
+
311
+ @property
312
+ def is_prerelease(self) -> bool:
313
+ return self.dev is not None or self.pre is not None
314
+
315
+ @property
316
+ def is_postrelease(self) -> bool:
317
+ return self.post is not None
318
+
319
+ @property
320
+ def is_devrelease(self) -> bool:
321
+ return self.dev is not None
322
+
323
+ @property
324
+ def major(self) -> int:
325
+ return self.release[0] if len(self.release) >= 1 else 0
326
+
327
+ @property
328
+ def minor(self) -> int:
329
+ return self.release[1] if len(self.release) >= 2 else 0
330
+
331
+ @property
332
+ def micro(self) -> int:
333
+ return self.release[2] if len(self.release) >= 3 else 0
334
+
335
+
336
+ def _parse_letter_version(
337
+ letter: ta.Optional[str],
338
+ number: ta.Union[str, bytes, ta.SupportsInt, None],
339
+ ) -> ta.Optional[ta.Tuple[str, int]]:
340
+ if letter:
341
+ if number is None:
342
+ number = 0
343
+
344
+ letter = letter.lower()
345
+ if letter == 'alpha':
346
+ letter = 'a'
347
+ elif letter == 'beta':
348
+ letter = 'b'
349
+ elif letter in ['c', 'pre', 'preview']:
350
+ letter = 'rc'
351
+ elif letter in ['rev', 'r']:
352
+ letter = 'post'
353
+
354
+ return letter, int(number)
355
+ if not letter and number:
356
+ letter = 'post'
357
+ return letter, int(number)
358
+
359
+ return None
360
+
361
+
362
+ _local_version_separators = re.compile(r'[\._-]')
363
+
364
+
365
+ def _parse_local_version(local: ta.Optional[str]) -> ta.Optional[VersionLocalType]:
366
+ if local is not None:
367
+ return tuple(
368
+ part.lower() if not part.isdigit() else int(part)
369
+ for part in _local_version_separators.split(local)
370
+ )
371
+ return None
372
+
373
+
374
+ def _version_cmpkey(
375
+ epoch: int,
376
+ release: ta.Tuple[int, ...],
377
+ pre: ta.Optional[ta.Tuple[str, int]],
378
+ post: ta.Optional[ta.Tuple[str, int]],
379
+ dev: ta.Optional[ta.Tuple[str, int]],
380
+ local: ta.Optional[VersionLocalType],
381
+ ) -> VersionCmpKey:
382
+ _release = tuple(reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release)))))
383
+
384
+ if pre is None and post is None and dev is not None:
385
+ _pre: VersionCmpPrePostDevType = NegativeInfinityVersion
386
+ elif pre is None:
387
+ _pre = InfinityVersion
388
+ else:
389
+ _pre = pre
390
+
391
+ if post is None:
392
+ _post: VersionCmpPrePostDevType = NegativeInfinityVersion
393
+ else:
394
+ _post = post
395
+
396
+ if dev is None:
397
+ _dev: VersionCmpPrePostDevType = InfinityVersion
398
+ else:
399
+ _dev = dev
400
+
401
+ if local is None:
402
+ _local: VersionCmpLocalType = NegativeInfinityVersion
403
+ else:
404
+ _local = tuple((i, '') if isinstance(i, int) else (NegativeInfinityVersion, i) for i in local)
405
+
406
+ return epoch, _release, _pre, _post, _dev, _local
407
+
408
+
409
+ ##
410
+
411
+
412
+ def canonicalize_version(
413
+ version: ta.Union[Version, str],
414
+ *,
415
+ strip_trailing_zero: bool = True,
416
+ ) -> str:
417
+ if isinstance(version, str):
418
+ try:
419
+ parsed = Version(version)
420
+ except InvalidVersion:
421
+ return version
422
+ else:
423
+ parsed = version
424
+
425
+ parts = []
426
+
427
+ if parsed.epoch != 0:
428
+ parts.append(f'{parsed.epoch}!')
429
+
430
+ release_segment = '.'.join(str(x) for x in parsed.release)
431
+ if strip_trailing_zero:
432
+ release_segment = re.sub(r'(\.0)+$', '', release_segment)
433
+ parts.append(release_segment)
434
+
435
+ if parsed.pre is not None:
436
+ parts.append(''.join(str(x) for x in parsed.pre))
437
+
438
+ if parsed.post is not None:
439
+ parts.append(f'.post{parsed.post}')
440
+
441
+ if parsed.dev is not None:
442
+ parts.append(f'.dev{parsed.dev}')
443
+
444
+ if parsed.local is not None:
445
+ parts.append(f'+{parsed.local}')
446
+
447
+ return ''.join(parts)
448
+
449
+
450
+ ########################################
451
+ # ../../../omlish/lite/cached.py
452
+
453
+
454
+ class cached_nullary: # noqa
455
+ def __init__(self, fn):
456
+ super().__init__()
457
+ self._fn = fn
458
+ self._value = self._missing = object()
459
+ functools.update_wrapper(self, fn)
460
+
461
+ def __call__(self, *args, **kwargs): # noqa
462
+ if self._value is self._missing:
463
+ self._value = self._fn()
464
+ return self._value
465
+
466
+ def __get__(self, instance, owner): # noqa
467
+ bound = instance.__dict__[self._fn.__name__] = self.__class__(self._fn.__get__(instance, owner))
468
+ return bound
469
+
470
+
471
+ ########################################
472
+ # ../../../omlish/lite/check.py
473
+ # ruff: noqa: UP006 UP007
474
+
475
+
476
+ def check_isinstance(v: T, spec: ta.Union[ta.Type[T], tuple]) -> T:
477
+ if not isinstance(v, spec):
478
+ raise TypeError(v)
479
+ return v
480
+
481
+
482
+ def check_not_isinstance(v: T, spec: ta.Union[type, tuple]) -> T:
483
+ if isinstance(v, spec):
484
+ raise TypeError(v)
485
+ return v
486
+
487
+
488
+ def check_not_none(v: ta.Optional[T]) -> T:
489
+ if v is None:
490
+ raise ValueError
491
+ return v
492
+
493
+
494
+ def check_not(v: ta.Any) -> None:
495
+ if v:
496
+ raise ValueError(v)
497
+ return v
498
+
499
+
500
+ ########################################
501
+ # ../../../omlish/lite/json.py
502
+
503
+
504
+ ##
505
+
506
+
507
+ JSON_PRETTY_INDENT = 2
508
+
509
+ JSON_PRETTY_KWARGS: ta.Mapping[str, ta.Any] = dict(
510
+ indent=JSON_PRETTY_INDENT,
511
+ )
512
+
513
+ json_dump_pretty: ta.Callable[..., bytes] = functools.partial(json.dump, **JSON_PRETTY_KWARGS) # type: ignore
514
+ json_dumps_pretty: ta.Callable[..., str] = functools.partial(json.dumps, **JSON_PRETTY_KWARGS)
515
+
516
+
517
+ ##
518
+
519
+
520
+ JSON_COMPACT_SEPARATORS = (',', ':')
521
+
522
+ JSON_COMPACT_KWARGS: ta.Mapping[str, ta.Any] = dict(
523
+ indent=None,
524
+ separators=JSON_COMPACT_SEPARATORS,
525
+ )
526
+
527
+ json_dump_compact: ta.Callable[..., bytes] = functools.partial(json.dump, **JSON_COMPACT_KWARGS) # type: ignore
528
+ json_dumps_compact: ta.Callable[..., str] = functools.partial(json.dumps, **JSON_COMPACT_KWARGS)
529
+
530
+
531
+ ########################################
532
+ # ../../../omlish/lite/reflect.py
533
+ # ruff: noqa: UP006
534
+
535
+
536
+ _GENERIC_ALIAS_TYPES = (
537
+ ta._GenericAlias, # type: ignore # noqa
538
+ *([ta._SpecialGenericAlias] if hasattr(ta, '_SpecialGenericAlias') else []), # noqa
539
+ )
540
+
541
+
542
+ def is_generic_alias(obj, *, origin: ta.Any = None) -> bool:
543
+ return (
544
+ isinstance(obj, _GENERIC_ALIAS_TYPES) and
545
+ (origin is None or ta.get_origin(obj) is origin)
546
+ )
547
+
548
+
549
+ is_union_alias = functools.partial(is_generic_alias, origin=ta.Union)
550
+ is_callable_alias = functools.partial(is_generic_alias, origin=ta.Callable)
551
+
552
+
553
+ def is_optional_alias(spec: ta.Any) -> bool:
554
+ return (
555
+ isinstance(spec, _GENERIC_ALIAS_TYPES) and # noqa
556
+ ta.get_origin(spec) is ta.Union and
557
+ len(ta.get_args(spec)) == 2 and
558
+ any(a in (None, type(None)) for a in ta.get_args(spec))
559
+ )
560
+
561
+
562
+ def get_optional_alias_arg(spec: ta.Any) -> ta.Any:
563
+ [it] = [it for it in ta.get_args(spec) if it not in (None, type(None))]
564
+ return it
565
+
566
+
567
+ def deep_subclasses(cls: ta.Type[T]) -> ta.Iterator[ta.Type[T]]:
568
+ seen = set()
569
+ todo = list(reversed(cls.__subclasses__()))
570
+ while todo:
571
+ cur = todo.pop()
572
+ if cur in seen:
573
+ continue
574
+ seen.add(cur)
575
+ yield cur
576
+ todo.extend(reversed(cur.__subclasses__()))
577
+
578
+
579
+ ########################################
580
+ # ../../../omlish/lite/strings.py
581
+
582
+
583
+ def camel_case(name: str) -> str:
584
+ return ''.join(map(str.capitalize, name.split('_'))) # noqa
585
+
586
+
587
+ def snake_case(name: str) -> str:
588
+ uppers: list[int | None] = [i for i, c in enumerate(name) if c.isupper()]
589
+ return '_'.join([name[l:r].lower() for l, r in zip([None, *uppers], [*uppers, None])]).strip('_')
590
+
591
+
592
+ def is_dunder(name: str) -> bool:
593
+ return (
594
+ name[:2] == name[-2:] == '__' and
595
+ name[2:3] != '_' and
596
+ name[-3:-2] != '_' and
597
+ len(name) > 4
598
+ )
599
+
600
+
601
+ def is_sunder(name: str) -> bool:
602
+ return (
603
+ name[0] == name[-1] == '_' and
604
+ name[1:2] != '_' and
605
+ name[-2:-1] != '_' and
606
+ len(name) > 2
607
+ )
608
+
609
+
610
+ ########################################
611
+ # ../../versioning/specifiers.py
612
+ # Copyright (c) Donald Stufft and individual contributors.
613
+ # All rights reserved.
614
+ #
615
+ # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
616
+ # following conditions are met:
617
+ #
618
+ # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
619
+ # following disclaimer.
620
+ #
621
+ # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
622
+ # following disclaimer in the documentation and/or other materials provided with the distribution.
623
+ #
624
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
625
+ # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
626
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
627
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
628
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
629
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
630
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. This file is dual licensed under the terms of the
631
+ # Apache License, Version 2.0, and the BSD License. See the LICENSE file in the root of this repository for complete
632
+ # details.
633
+ # https://github.com/pypa/packaging/blob/2c885fe91a54559e2382902dce28428ad2887be5/src/packaging/specifiers.py
634
+ # ruff: noqa: UP006 UP007
635
+
636
+
637
+ ##
638
+
639
+
640
+ def _coerce_version(version: UnparsedVersion) -> Version:
641
+ if not isinstance(version, Version):
642
+ version = Version(version)
643
+ return version
644
+
645
+
646
+ class InvalidSpecifier(ValueError): # noqa
647
+ pass
648
+
649
+
650
+ class BaseSpecifier(metaclass=abc.ABCMeta):
651
+ @abc.abstractmethod
652
+ def __str__(self) -> str:
653
+ raise NotImplementedError
654
+
655
+ @abc.abstractmethod
656
+ def __hash__(self) -> int:
657
+ raise NotImplementedError
658
+
659
+ @abc.abstractmethod
660
+ def __eq__(self, other: object) -> bool:
661
+ raise NotImplementedError
662
+
663
+ @property
664
+ @abc.abstractmethod
665
+ def prereleases(self) -> ta.Optional[bool]:
666
+ raise NotImplementedError
667
+
668
+ @prereleases.setter
669
+ def prereleases(self, value: bool) -> None:
670
+ raise NotImplementedError
671
+
672
+ @abc.abstractmethod
673
+ def contains(self, item: str, prereleases: ta.Optional[bool] = None) -> bool:
674
+ raise NotImplementedError
675
+
676
+ @abc.abstractmethod
677
+ def filter(
678
+ self,
679
+ iterable: ta.Iterable[UnparsedVersionVar],
680
+ prereleases: ta.Optional[bool] = None,
681
+ ) -> ta.Iterator[UnparsedVersionVar]:
682
+ raise NotImplementedError
683
+
684
+
685
+ class Specifier(BaseSpecifier):
686
+ _operator_regex_str = r"""
687
+ (?P<operator>(~=|==|!=|<=|>=|<|>|===))
688
+ """
689
+
690
+ _version_regex_str = r"""
691
+ (?P<version>
692
+ (?:
693
+ (?<====)
694
+ \s*
695
+ [^\s;)]*
696
+ )
697
+ |
698
+ (?:
699
+ (?<===|!=)
700
+ \s*
701
+ v?
702
+ (?:[0-9]+!)?
703
+ [0-9]+(?:\.[0-9]+)*
704
+ (?:
705
+ \.\*
706
+ |
707
+ (?:
708
+ [-_\.]?
709
+ (alpha|beta|preview|pre|a|b|c|rc)
710
+ [-_\.]?
711
+ [0-9]*
712
+ )?
713
+ (?:
714
+ (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
715
+ )?
716
+ (?:[-_\.]?dev[-_\.]?[0-9]*)?
717
+ (?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)?
718
+ )?
719
+ )
720
+ |
721
+ (?:
722
+ (?<=~=)
723
+ \s*
724
+ v?
725
+ (?:[0-9]+!)?
726
+ [0-9]+(?:\.[0-9]+)+
727
+ (?:
728
+ [-_\.]?
729
+ (alpha|beta|preview|pre|a|b|c|rc)
730
+ [-_\.]?
731
+ [0-9]*
732
+ )?
733
+ (?:
734
+ (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
735
+ )?
736
+ (?:[-_\.]?dev[-_\.]?[0-9]*)?
737
+ )
738
+ |
739
+ (?:
740
+ (?<!==|!=|~=)
741
+ \s*
742
+ v?
743
+ (?:[0-9]+!)?
744
+ [0-9]+(?:\.[0-9]+)*
745
+ (?:
746
+ [-_\.]?
747
+ (alpha|beta|preview|pre|a|b|c|rc)
748
+ [-_\.]?
749
+ [0-9]*
750
+ )?
751
+ (?:
752
+ (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
753
+ )?
754
+ (?:[-_\.]?dev[-_\.]?[0-9]*)?
755
+ )
756
+ )
757
+ """
758
+
759
+ _regex = re.compile(
760
+ r'^\s*' + _operator_regex_str + _version_regex_str + r'\s*$',
761
+ re.VERBOSE | re.IGNORECASE,
762
+ )
763
+
764
+ OPERATORS: ta.ClassVar[ta.Mapping[str, str]] = {
765
+ '~=': 'compatible',
766
+ '==': 'equal',
767
+ '!=': 'not_equal',
768
+ '<=': 'less_than_equal',
769
+ '>=': 'greater_than_equal',
770
+ '<': 'less_than',
771
+ '>': 'greater_than',
772
+ '===': 'arbitrary',
773
+ }
774
+
775
+ def __init__(
776
+ self,
777
+ spec: str = '',
778
+ prereleases: ta.Optional[bool] = None,
779
+ ) -> None:
780
+ match = self._regex.search(spec)
781
+ if not match:
782
+ raise InvalidSpecifier(f"Invalid specifier: '{spec}'")
783
+
784
+ self._spec: ta.Tuple[str, str] = (
785
+ match.group('operator').strip(),
786
+ match.group('version').strip(),
787
+ )
788
+
789
+ self._prereleases = prereleases
790
+
791
+ @property # type: ignore
792
+ def prereleases(self) -> bool:
793
+ if self._prereleases is not None:
794
+ return self._prereleases
795
+
796
+ operator, version = self._spec
797
+ if operator in ['==', '>=', '<=', '~=', '===']:
798
+ if operator == '==' and version.endswith('.*'):
799
+ version = version[:-2]
800
+
801
+ if Version(version).is_prerelease:
802
+ return True
803
+
804
+ return False
805
+
806
+ @prereleases.setter
807
+ def prereleases(self, value: bool) -> None:
808
+ self._prereleases = value
809
+
810
+ @property
811
+ def operator(self) -> str:
812
+ return self._spec[0]
813
+
814
+ @property
815
+ def version(self) -> str:
816
+ return self._spec[1]
817
+
818
+ def __repr__(self) -> str:
819
+ pre = (
820
+ f', prereleases={self.prereleases!r}'
821
+ if self._prereleases is not None
822
+ else ''
823
+ )
824
+
825
+ return f'<{self.__class__.__name__}({str(self)!r}{pre})>'
826
+
827
+ def __str__(self) -> str:
828
+ return '{}{}'.format(*self._spec)
829
+
830
+ @property
831
+ def _canonical_spec(self) -> ta.Tuple[str, str]:
832
+ canonical_version = canonicalize_version(
833
+ self._spec[1],
834
+ strip_trailing_zero=(self._spec[0] != '~='),
835
+ )
836
+ return self._spec[0], canonical_version
837
+
838
+ def __hash__(self) -> int:
839
+ return hash(self._canonical_spec)
840
+
841
+ def __eq__(self, other: object) -> bool:
842
+ if isinstance(other, str):
843
+ try:
844
+ other = self.__class__(str(other))
845
+ except InvalidSpecifier:
846
+ return NotImplemented
847
+ elif not isinstance(other, self.__class__):
848
+ return NotImplemented
849
+
850
+ return self._canonical_spec == other._canonical_spec
851
+
852
+ def _get_operator(self, op: str) -> CallableVersionOperator:
853
+ operator_callable: CallableVersionOperator = getattr(self, f'_compare_{self.OPERATORS[op]}')
854
+ return operator_callable
855
+
856
+ def _compare_compatible(self, prospective: Version, spec: str) -> bool:
857
+ prefix = _version_join(list(itertools.takewhile(_is_not_version_suffix, _version_split(spec)))[:-1])
858
+ prefix += '.*'
859
+ return self._get_operator('>=')(prospective, spec) and self._get_operator('==')(prospective, prefix)
860
+
861
+ def _compare_equal(self, prospective: Version, spec: str) -> bool:
862
+ if spec.endswith('.*'):
863
+ normalized_prospective = canonicalize_version(prospective.public, strip_trailing_zero=False)
864
+ normalized_spec = canonicalize_version(spec[:-2], strip_trailing_zero=False)
865
+ split_spec = _version_split(normalized_spec)
866
+
867
+ split_prospective = _version_split(normalized_prospective)
868
+ padded_prospective, _ = _pad_version(split_prospective, split_spec)
869
+ shortened_prospective = padded_prospective[: len(split_spec)]
870
+
871
+ return shortened_prospective == split_spec
872
+
873
+ else:
874
+ spec_version = Version(spec)
875
+ if not spec_version.local:
876
+ prospective = Version(prospective.public)
877
+ return prospective == spec_version
878
+
879
+ def _compare_not_equal(self, prospective: Version, spec: str) -> bool:
880
+ return not self._compare_equal(prospective, spec)
881
+
882
+ def _compare_less_than_equal(self, prospective: Version, spec: str) -> bool:
883
+ return Version(prospective.public) <= Version(spec)
884
+
885
+ def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool:
886
+ return Version(prospective.public) >= Version(spec)
887
+
888
+ def _compare_less_than(self, prospective: Version, spec_str: str) -> bool:
889
+ spec = Version(spec_str)
890
+
891
+ if not prospective < spec:
892
+ return False
893
+
894
+ if not spec.is_prerelease and prospective.is_prerelease:
895
+ if Version(prospective.base_version) == Version(spec.base_version):
896
+ return False
897
+
898
+ return True
899
+
900
+ def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool:
901
+ spec = Version(spec_str)
902
+
903
+ if not prospective > spec:
904
+ return False
905
+
906
+ if not spec.is_postrelease and prospective.is_postrelease:
907
+ if Version(prospective.base_version) == Version(spec.base_version):
908
+ return False
909
+
910
+ if prospective.local is not None:
911
+ if Version(prospective.base_version) == Version(spec.base_version):
912
+ return False
913
+
914
+ return True
915
+
916
+ def _compare_arbitrary(self, prospective: Version, spec: str) -> bool:
917
+ return str(prospective).lower() == str(spec).lower()
918
+
919
+ def __contains__(self, item: ta.Union[str, Version]) -> bool:
920
+ return self.contains(item)
921
+
922
+ def contains(self, item: UnparsedVersion, prereleases: ta.Optional[bool] = None) -> bool:
923
+ if prereleases is None:
924
+ prereleases = self.prereleases
925
+
926
+ normalized_item = _coerce_version(item)
927
+
928
+ if normalized_item.is_prerelease and not prereleases:
929
+ return False
930
+
931
+ operator_callable: CallableVersionOperator = self._get_operator(self.operator)
932
+ return operator_callable(normalized_item, self.version)
933
+
934
+ def filter(
935
+ self,
936
+ iterable: ta.Iterable[UnparsedVersionVar],
937
+ prereleases: ta.Optional[bool] = None,
938
+ ) -> ta.Iterator[UnparsedVersionVar]:
939
+ yielded = False
940
+ found_prereleases = []
941
+
942
+ kw = {'prereleases': prereleases if prereleases is not None else True}
943
+
944
+ for version in iterable:
945
+ parsed_version = _coerce_version(version)
946
+
947
+ if self.contains(parsed_version, **kw):
948
+ if parsed_version.is_prerelease and not (prereleases or self.prereleases):
949
+ found_prereleases.append(version)
950
+ else:
951
+ yielded = True
952
+ yield version
953
+
954
+ if not yielded and found_prereleases:
955
+ for version in found_prereleases:
956
+ yield version
957
+
958
+
959
+ _version_prefix_regex = re.compile(r'^([0-9]+)((?:a|b|c|rc)[0-9]+)$')
960
+
961
+
962
+ def _version_split(version: str) -> ta.List[str]:
963
+ result: ta.List[str] = []
964
+
965
+ epoch, _, rest = version.rpartition('!')
966
+ result.append(epoch or '0')
967
+
968
+ for item in rest.split('.'):
969
+ match = _version_prefix_regex.search(item)
970
+ if match:
971
+ result.extend(match.groups())
972
+ else:
973
+ result.append(item)
974
+ return result
975
+
976
+
977
+ def _version_join(components: ta.List[str]) -> str:
978
+ epoch, *rest = components
979
+ return f"{epoch}!{'.'.join(rest)}"
980
+
981
+
982
+ def _is_not_version_suffix(segment: str) -> bool:
983
+ return not any(segment.startswith(prefix) for prefix in ('dev', 'a', 'b', 'rc', 'post'))
984
+
985
+
986
+ def _pad_version(left: ta.List[str], right: ta.List[str]) -> ta.Tuple[ta.List[str], ta.List[str]]:
987
+ left_split, right_split = [], []
988
+
989
+ left_split.append(list(itertools.takewhile(lambda x: x.isdigit(), left)))
990
+ right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right)))
991
+
992
+ left_split.append(left[len(left_split[0]):])
993
+ right_split.append(right[len(right_split[0]):])
994
+
995
+ left_split.insert(1, ['0'] * max(0, len(right_split[0]) - len(left_split[0])))
996
+ right_split.insert(1, ['0'] * max(0, len(left_split[0]) - len(right_split[0])))
997
+
998
+ return (
999
+ list(itertools.chain.from_iterable(left_split)),
1000
+ list(itertools.chain.from_iterable(right_split)),
1001
+ )
1002
+
1003
+
1004
+ class SpecifierSet(BaseSpecifier):
1005
+ def __init__(
1006
+ self,
1007
+ specifiers: str = '',
1008
+ prereleases: ta.Optional[bool] = None,
1009
+ ) -> None:
1010
+ split_specifiers = [s.strip() for s in specifiers.split(',') if s.strip()]
1011
+
1012
+ self._specs = frozenset(map(Specifier, split_specifiers))
1013
+ self._prereleases = prereleases
1014
+
1015
+ @property
1016
+ def prereleases(self) -> ta.Optional[bool]:
1017
+ if self._prereleases is not None:
1018
+ return self._prereleases
1019
+
1020
+ if not self._specs:
1021
+ return None
1022
+
1023
+ return any(s.prereleases for s in self._specs)
1024
+
1025
+ @prereleases.setter
1026
+ def prereleases(self, value: bool) -> None:
1027
+ self._prereleases = value
1028
+
1029
+ def __repr__(self) -> str:
1030
+ pre = (
1031
+ f', prereleases={self.prereleases!r}'
1032
+ if self._prereleases is not None
1033
+ else ''
1034
+ )
1035
+
1036
+ return f'<SpecifierSet({str(self)!r}{pre})>'
1037
+
1038
+ def __str__(self) -> str:
1039
+ return ','.join(sorted(str(s) for s in self._specs))
1040
+
1041
+ def __hash__(self) -> int:
1042
+ return hash(self._specs)
1043
+
1044
+ def __and__(self, other: ta.Union['SpecifierSet', str]) -> 'SpecifierSet':
1045
+ if isinstance(other, str):
1046
+ other = SpecifierSet(other)
1047
+ elif not isinstance(other, SpecifierSet):
1048
+ return NotImplemented # type: ignore
1049
+
1050
+ specifier = SpecifierSet()
1051
+ specifier._specs = frozenset(self._specs | other._specs)
1052
+
1053
+ if self._prereleases is None and other._prereleases is not None:
1054
+ specifier._prereleases = other._prereleases
1055
+ elif self._prereleases is not None and other._prereleases is None:
1056
+ specifier._prereleases = self._prereleases
1057
+ elif self._prereleases == other._prereleases:
1058
+ specifier._prereleases = self._prereleases
1059
+ else:
1060
+ raise ValueError('Cannot combine SpecifierSets with True and False prerelease overrides.')
1061
+
1062
+ return specifier
1063
+
1064
+ def __eq__(self, other: object) -> bool:
1065
+ if isinstance(other, (str, Specifier)):
1066
+ other = SpecifierSet(str(other))
1067
+ elif not isinstance(other, SpecifierSet):
1068
+ return NotImplemented
1069
+
1070
+ return self._specs == other._specs
1071
+
1072
+ def __len__(self) -> int:
1073
+ return len(self._specs)
1074
+
1075
+ def __iter__(self) -> ta.Iterator[Specifier]:
1076
+ return iter(self._specs)
1077
+
1078
+ def __contains__(self, item: UnparsedVersion) -> bool:
1079
+ return self.contains(item)
1080
+
1081
+ def contains(
1082
+ self,
1083
+ item: UnparsedVersion,
1084
+ prereleases: ta.Optional[bool] = None,
1085
+ installed: ta.Optional[bool] = None,
1086
+ ) -> bool:
1087
+ if not isinstance(item, Version):
1088
+ item = Version(item)
1089
+
1090
+ if prereleases is None:
1091
+ prereleases = self.prereleases
1092
+
1093
+ if not prereleases and item.is_prerelease:
1094
+ return False
1095
+
1096
+ if installed and item.is_prerelease:
1097
+ item = Version(item.base_version)
1098
+
1099
+ return all(s.contains(item, prereleases=prereleases) for s in self._specs)
1100
+
1101
+ def filter(
1102
+ self,
1103
+ iterable: ta.Iterable[UnparsedVersionVar],
1104
+ prereleases: ta.Optional[bool] = None,
1105
+ ) -> ta.Iterator[UnparsedVersionVar]:
1106
+ if prereleases is None:
1107
+ prereleases = self.prereleases
1108
+
1109
+ if self._specs:
1110
+ for spec in self._specs:
1111
+ iterable = spec.filter(iterable, prereleases=bool(prereleases))
1112
+ return iter(iterable)
1113
+
1114
+ else:
1115
+ filtered: ta.List[UnparsedVersionVar] = []
1116
+ found_prereleases: ta.List[UnparsedVersionVar] = []
1117
+
1118
+ for item in iterable:
1119
+ parsed_version = _coerce_version(item)
1120
+
1121
+ if parsed_version.is_prerelease and not prereleases:
1122
+ if not filtered:
1123
+ found_prereleases.append(item)
1124
+ else:
1125
+ filtered.append(item)
1126
+
1127
+ if not filtered and found_prereleases and prereleases is None:
1128
+ return iter(found_prereleases)
1129
+
1130
+ return iter(filtered)
1131
+
1132
+
1133
+ ########################################
1134
+ # ../../../omlish/lite/logs.py
1135
+ """
1136
+ TODO:
1137
+ - debug
1138
+ """
1139
+ # ruff: noqa: UP007
1140
+
1141
+
1142
+ log = logging.getLogger(__name__)
1143
+
1144
+
1145
+ class JsonLogFormatter(logging.Formatter):
1146
+
1147
+ KEYS: ta.Mapping[str, bool] = {
1148
+ 'name': False,
1149
+ 'msg': False,
1150
+ 'args': False,
1151
+ 'levelname': False,
1152
+ 'levelno': False,
1153
+ 'pathname': False,
1154
+ 'filename': False,
1155
+ 'module': False,
1156
+ 'exc_info': True,
1157
+ 'exc_text': True,
1158
+ 'stack_info': True,
1159
+ 'lineno': False,
1160
+ 'funcName': False,
1161
+ 'created': False,
1162
+ 'msecs': False,
1163
+ 'relativeCreated': False,
1164
+ 'thread': False,
1165
+ 'threadName': False,
1166
+ 'processName': False,
1167
+ 'process': False,
1168
+ }
1169
+
1170
+ def format(self, record: logging.LogRecord) -> str:
1171
+ dct = {
1172
+ k: v
1173
+ for k, o in self.KEYS.items()
1174
+ for v in [getattr(record, k)]
1175
+ if not (o and v is None)
1176
+ }
1177
+ return json_dumps_compact(dct)
1178
+
1179
+
1180
+ def configure_standard_logging(level: ta.Union[int, str] = logging.INFO) -> None:
1181
+ logging.root.addHandler(logging.StreamHandler())
1182
+ logging.root.setLevel(level)
1183
+
1184
+
1185
+ ########################################
1186
+ # ../../../omlish/lite/runtime.py
1187
+
1188
+
1189
+ @cached_nullary
1190
+ def is_debugger_attached() -> bool:
1191
+ return any(frame[1].endswith('pydevd.py') for frame in inspect.stack())
1192
+
1193
+
1194
+ REQUIRED_PYTHON_VERSION = (3, 8)
1195
+
1196
+
1197
+ def check_runtime_version() -> None:
1198
+ if sys.version_info < REQUIRED_PYTHON_VERSION:
1199
+ raise OSError(
1200
+ f'Requires python {REQUIRED_PYTHON_VERSION}, got {sys.version_info} from {sys.executable}') # noqa
1201
+
1202
+
1203
+ ########################################
1204
+ # ../types.py
1205
+ # ruff: noqa: UP006
1206
+
1207
+
1208
+ # See https://peps.python.org/pep-3149/
1209
+ INTERP_OPT_GLYPHS_BY_ATTR: ta.Mapping[str, str] = collections.OrderedDict([
1210
+ ('debug', 'd'),
1211
+ ('threaded', 't'),
1212
+ ])
1213
+
1214
+ INTERP_OPT_ATTRS_BY_GLYPH: ta.Mapping[str, str] = collections.OrderedDict(
1215
+ (g, a) for a, g in INTERP_OPT_GLYPHS_BY_ATTR.items()
1216
+ )
1217
+
1218
+
1219
+ @dc.dataclass(frozen=True)
1220
+ class InterpOpts:
1221
+ threaded: bool = False
1222
+ debug: bool = False
1223
+
1224
+ def __str__(self) -> str:
1225
+ return ''.join(g for a, g in INTERP_OPT_GLYPHS_BY_ATTR.items() if getattr(self, a))
1226
+
1227
+ @classmethod
1228
+ def parse(cls, s: str) -> 'InterpOpts':
1229
+ return cls(**{INTERP_OPT_ATTRS_BY_GLYPH[g]: True for g in s})
1230
+
1231
+ @classmethod
1232
+ def parse_suffix(cls, s: str) -> ta.Tuple[str, 'InterpOpts']:
1233
+ kw = {}
1234
+ while s and (a := INTERP_OPT_ATTRS_BY_GLYPH.get(s[-1])):
1235
+ s, kw[a] = s[:-1], True
1236
+ return s, cls(**kw)
1237
+
1238
+
1239
+ @dc.dataclass(frozen=True)
1240
+ class InterpVersion:
1241
+ version: Version
1242
+ opts: InterpOpts
1243
+
1244
+ def __str__(self) -> str:
1245
+ return str(self.version) + str(self.opts)
1246
+
1247
+ @classmethod
1248
+ def parse(cls, s: str) -> 'InterpVersion':
1249
+ s, o = InterpOpts.parse_suffix(s)
1250
+ v = Version(s)
1251
+ return cls(
1252
+ version=v,
1253
+ opts=o,
1254
+ )
1255
+
1256
+ @classmethod
1257
+ def try_parse(cls, s: str) -> ta.Optional['InterpVersion']:
1258
+ try:
1259
+ return cls.parse(s)
1260
+ except (KeyError, InvalidVersion):
1261
+ return None
1262
+
1263
+
1264
+ @dc.dataclass(frozen=True)
1265
+ class InterpSpecifier:
1266
+ specifier: Specifier
1267
+ opts: InterpOpts
1268
+
1269
+ def __str__(self) -> str:
1270
+ return str(self.specifier) + str(self.opts)
1271
+
1272
+ @classmethod
1273
+ def parse(cls, s: str) -> 'InterpSpecifier':
1274
+ s, o = InterpOpts.parse_suffix(s)
1275
+ if not any(s.startswith(o) for o in Specifier.OPERATORS):
1276
+ s = '~=' + s
1277
+ return cls(
1278
+ specifier=Specifier(s),
1279
+ opts=o,
1280
+ )
1281
+
1282
+ def contains(self, iv: InterpVersion) -> bool:
1283
+ return self.specifier.contains(iv.version) and self.opts == iv.opts
1284
+
1285
+
1286
+ @dc.dataclass(frozen=True)
1287
+ class Interp:
1288
+ exe: str
1289
+ version: InterpVersion
1290
+
1291
+
1292
+ ########################################
1293
+ # ../../../omlish/lite/subprocesses.py
1294
+ # ruff: noqa: UP006 UP007
1295
+
1296
+
1297
+ ##
1298
+
1299
+
1300
+ _SUBPROCESS_SHELL_WRAP_EXECS = False
1301
+
1302
+
1303
+ def subprocess_shell_wrap_exec(*args: str) -> ta.Tuple[str, ...]:
1304
+ return ('sh', '-c', ' '.join(map(shlex.quote, args)))
1305
+
1306
+
1307
+ def subprocess_maybe_shell_wrap_exec(*args: str) -> ta.Tuple[str, ...]:
1308
+ if _SUBPROCESS_SHELL_WRAP_EXECS or is_debugger_attached():
1309
+ return subprocess_shell_wrap_exec(*args)
1310
+ else:
1311
+ return args
1312
+
1313
+
1314
+ def _prepare_subprocess_invocation(
1315
+ *args: str,
1316
+ env: ta.Optional[ta.Mapping[str, ta.Any]] = None,
1317
+ extra_env: ta.Optional[ta.Mapping[str, ta.Any]] = None,
1318
+ quiet: bool = False,
1319
+ shell: bool = False,
1320
+ **kwargs: ta.Any,
1321
+ ) -> ta.Tuple[ta.Tuple[ta.Any, ...], ta.Dict[str, ta.Any]]:
1322
+ log.debug(args)
1323
+ if extra_env:
1324
+ log.debug(extra_env)
1325
+
1326
+ if extra_env:
1327
+ env = {**(env if env is not None else os.environ), **extra_env}
1328
+
1329
+ if quiet and 'stderr' not in kwargs:
1330
+ if not log.isEnabledFor(logging.DEBUG):
1331
+ kwargs['stderr'] = subprocess.DEVNULL
1332
+
1333
+ if not shell:
1334
+ args = subprocess_maybe_shell_wrap_exec(*args)
1335
+
1336
+ return args, dict(
1337
+ env=env,
1338
+ shell=shell,
1339
+ **kwargs,
1340
+ )
1341
+
1342
+
1343
+ def subprocess_check_call(*args: str, stdout=sys.stderr, **kwargs: ta.Any) -> None:
1344
+ args, kwargs = _prepare_subprocess_invocation(*args, stdout=stdout, **kwargs)
1345
+ return subprocess.check_call(args, **kwargs) # type: ignore
1346
+
1347
+
1348
+ def subprocess_check_output(*args: str, **kwargs: ta.Any) -> bytes:
1349
+ args, kwargs = _prepare_subprocess_invocation(*args, **kwargs)
1350
+ return subprocess.check_output(args, **kwargs)
1351
+
1352
+
1353
+ def subprocess_check_output_str(*args: str, **kwargs: ta.Any) -> str:
1354
+ return subprocess_check_output(*args, **kwargs).decode().strip()
1355
+
1356
+
1357
+ ##
1358
+
1359
+
1360
+ DEFAULT_SUBPROCESS_TRY_EXCEPTIONS: ta.Tuple[ta.Type[Exception], ...] = (
1361
+ FileNotFoundError,
1362
+ subprocess.CalledProcessError,
1363
+ )
1364
+
1365
+
1366
+ def subprocess_try_call(
1367
+ *args: str,
1368
+ try_exceptions: ta.Tuple[ta.Type[Exception], ...] = DEFAULT_SUBPROCESS_TRY_EXCEPTIONS,
1369
+ **kwargs: ta.Any,
1370
+ ) -> bool:
1371
+ try:
1372
+ subprocess_check_call(*args, **kwargs)
1373
+ except try_exceptions as e: # noqa
1374
+ if log.isEnabledFor(logging.DEBUG):
1375
+ log.exception('command failed')
1376
+ return False
1377
+ else:
1378
+ return True
1379
+
1380
+
1381
+ def subprocess_try_output(
1382
+ *args: str,
1383
+ try_exceptions: ta.Tuple[ta.Type[Exception], ...] = DEFAULT_SUBPROCESS_TRY_EXCEPTIONS,
1384
+ **kwargs: ta.Any,
1385
+ ) -> ta.Optional[bytes]:
1386
+ try:
1387
+ return subprocess_check_output(*args, **kwargs)
1388
+ except try_exceptions as e: # noqa
1389
+ if log.isEnabledFor(logging.DEBUG):
1390
+ log.exception('command failed')
1391
+ return None
1392
+
1393
+
1394
+ def subprocess_try_output_str(*args: str, **kwargs: ta.Any) -> ta.Optional[str]:
1395
+ out = subprocess_try_output(*args, **kwargs)
1396
+ return out.decode().strip() if out is not None else None
1397
+
1398
+
1399
+ ########################################
1400
+ # ../inspect.py
1401
+ # ruff: noqa: UP006 UP007
1402
+
1403
+
1404
+ @dc.dataclass(frozen=True)
1405
+ class InterpInspection:
1406
+ exe: str
1407
+ version: Version
1408
+
1409
+ version_str: str
1410
+ config_vars: ta.Mapping[str, str]
1411
+ prefix: str
1412
+ base_prefix: str
1413
+
1414
+ @property
1415
+ def opts(self) -> InterpOpts:
1416
+ return InterpOpts(
1417
+ threaded=bool(self.config_vars.get('Py_GIL_DISABLED')),
1418
+ debug=bool(self.config_vars.get('Py_DEBUG')),
1419
+ )
1420
+
1421
+ @property
1422
+ def iv(self) -> InterpVersion:
1423
+ return InterpVersion(
1424
+ version=self.version,
1425
+ opts=self.opts,
1426
+ )
1427
+
1428
+ @property
1429
+ def is_venv(self) -> bool:
1430
+ return self.prefix != self.base_prefix
1431
+
1432
+
1433
+ class InterpInspector:
1434
+
1435
+ def __init__(self) -> None:
1436
+ super().__init__()
1437
+
1438
+ self._cache: ta.Dict[str, ta.Optional[InterpInspection]] = {}
1439
+
1440
+ _RAW_INSPECTION_CODE = """
1441
+ __import__('json').dumps(dict(
1442
+ version_str=__import__('sys').version,
1443
+ prefix=__import__('sys').prefix,
1444
+ base_prefix=__import__('sys').base_prefix,
1445
+ config_vars=__import__('sysconfig').get_config_vars(),
1446
+ ))"""
1447
+
1448
+ _INSPECTION_CODE = ''.join(l.strip() for l in _RAW_INSPECTION_CODE.splitlines())
1449
+
1450
+ @staticmethod
1451
+ def _build_inspection(
1452
+ exe: str,
1453
+ output: str,
1454
+ ) -> InterpInspection:
1455
+ dct = json.loads(output)
1456
+
1457
+ version = Version(dct['version_str'].split()[0])
1458
+
1459
+ return InterpInspection(
1460
+ exe=exe,
1461
+ version=version,
1462
+ **{k: dct[k] for k in (
1463
+ 'version_str',
1464
+ 'prefix',
1465
+ 'base_prefix',
1466
+ 'config_vars',
1467
+ )},
1468
+ )
1469
+
1470
+ @classmethod
1471
+ def running(cls) -> 'InterpInspection':
1472
+ return cls._build_inspection(sys.executable, eval(cls._INSPECTION_CODE)) # noqa
1473
+
1474
+ def _inspect(self, exe: str) -> InterpInspection:
1475
+ output = subprocess_check_output(exe, '-c', f'print({self._INSPECTION_CODE})', quiet=True)
1476
+ return self._build_inspection(exe, output.decode())
1477
+
1478
+ def inspect(self, exe: str) -> ta.Optional[InterpInspection]:
1479
+ try:
1480
+ return self._cache[exe]
1481
+ except KeyError:
1482
+ ret: ta.Optional[InterpInspection]
1483
+ try:
1484
+ ret = self._inspect(exe)
1485
+ except Exception as e: # noqa
1486
+ if log.isEnabledFor(logging.DEBUG):
1487
+ log.exception('Failed to inspect interp: %s', exe)
1488
+ ret = None
1489
+ self._cache[exe] = ret
1490
+ return ret
1491
+
1492
+
1493
+ INTERP_INSPECTOR = InterpInspector()
1494
+
1495
+
1496
+ ########################################
1497
+ # ../providers.py
1498
+ """
1499
+ TODO:
1500
+ - backends
1501
+ - local builds
1502
+ - deadsnakes?
1503
+ - loose versions
1504
+ """
1505
+
1506
+
1507
+ ##
1508
+
1509
+
1510
+ class InterpProvider(abc.ABC):
1511
+ name: ta.ClassVar[str]
1512
+
1513
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
1514
+ super().__init_subclass__(**kwargs)
1515
+ if abc.ABC not in cls.__bases__ and 'name' not in cls.__dict__:
1516
+ sfx = 'InterpProvider'
1517
+ if not cls.__name__.endswith(sfx):
1518
+ raise NameError(cls)
1519
+ setattr(cls, 'name', snake_case(cls.__name__[:-len(sfx)]))
1520
+
1521
+ @abc.abstractmethod
1522
+ def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
1523
+ raise NotImplementedError
1524
+
1525
+ @abc.abstractmethod
1526
+ def get_installed_version(self, version: InterpVersion) -> Interp:
1527
+ raise NotImplementedError
1528
+
1529
+ def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
1530
+ return []
1531
+
1532
+ def install_version(self, version: InterpVersion) -> Interp:
1533
+ raise TypeError
1534
+
1535
+
1536
+ ##
1537
+
1538
+
1539
+ class RunningInterpProvider(InterpProvider):
1540
+ @cached_nullary
1541
+ def version(self) -> InterpVersion:
1542
+ return InterpInspector.running().iv
1543
+
1544
+ def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
1545
+ return [self.version()]
1546
+
1547
+ def get_installed_version(self, version: InterpVersion) -> Interp:
1548
+ if version != self.version():
1549
+ raise KeyError(version)
1550
+ return Interp(
1551
+ exe=sys.executable,
1552
+ version=self.version(),
1553
+ )
1554
+
1555
+
1556
+ ########################################
1557
+ # ../pyenv.py
1558
+ """
1559
+ TODO:
1560
+ - custom tags
1561
+ - optionally install / upgrade pyenv itself
1562
+ - new vers dont need these custom mac opts, only run on old vers
1563
+ """
1564
+ # ruff: noqa: UP006 UP007
1565
+
1566
+
1567
+ ##
1568
+
1569
+
1570
+ class Pyenv:
1571
+
1572
+ def __init__(
1573
+ self,
1574
+ *,
1575
+ root: ta.Optional[str] = None,
1576
+ ) -> None:
1577
+ if root is not None and not (isinstance(root, str) and root):
1578
+ raise ValueError(f'pyenv_root: {root!r}')
1579
+
1580
+ super().__init__()
1581
+
1582
+ self._root_kw = root
1583
+
1584
+ @cached_nullary
1585
+ def root(self) -> ta.Optional[str]:
1586
+ if self._root_kw is not None:
1587
+ return self._root_kw
1588
+
1589
+ if shutil.which('pyenv'):
1590
+ return subprocess_check_output_str('pyenv', 'root')
1591
+
1592
+ d = os.path.expanduser('~/.pyenv')
1593
+ if os.path.isdir(d) and os.path.isfile(os.path.join(d, 'bin', 'pyenv')):
1594
+ return d
1595
+
1596
+ return None
1597
+
1598
+ @cached_nullary
1599
+ def exe(self) -> str:
1600
+ return os.path.join(check_not_none(self.root()), 'bin', 'pyenv')
1601
+
1602
+ def version_exes(self) -> ta.List[ta.Tuple[str, str]]:
1603
+ ret = []
1604
+ vp = os.path.join(self.root(), 'versions')
1605
+ for dn in os.listdir(vp):
1606
+ ep = os.path.join(vp, dn, 'bin', 'python')
1607
+ if not os.path.isfile(ep):
1608
+ continue
1609
+ ret.append((dn, ep))
1610
+ return ret
1611
+
1612
+ def installable_versions(self) -> ta.List[str]:
1613
+ ret = []
1614
+ s = subprocess_check_output_str(self.exe(), 'install', '--list')
1615
+ for l in s.splitlines():
1616
+ if not l.startswith(' '):
1617
+ continue
1618
+ l = l.strip()
1619
+ if not l:
1620
+ continue
1621
+ ret.append(l)
1622
+ return ret
1623
+
1624
+
1625
+ ##
1626
+
1627
+
1628
+ @dc.dataclass(frozen=True)
1629
+ class PyenvInstallOpts:
1630
+ opts: ta.Sequence[str] = ()
1631
+ conf_opts: ta.Sequence[str] = ()
1632
+ cflags: ta.Sequence[str] = ()
1633
+ ldflags: ta.Sequence[str] = ()
1634
+ env: ta.Mapping[str, str] = dc.field(default_factory=dict)
1635
+
1636
+ def merge(self, *others: 'PyenvInstallOpts') -> 'PyenvInstallOpts':
1637
+ return PyenvInstallOpts(
1638
+ opts=list(itertools.chain.from_iterable(o.opts for o in [self, *others])),
1639
+ conf_opts=list(itertools.chain.from_iterable(o.conf_opts for o in [self, *others])),
1640
+ cflags=list(itertools.chain.from_iterable(o.cflags for o in [self, *others])),
1641
+ ldflags=list(itertools.chain.from_iterable(o.ldflags for o in [self, *others])),
1642
+ env=dict(itertools.chain.from_iterable(o.env.items() for o in [self, *others])),
1643
+ )
1644
+
1645
+
1646
+ DEFAULT_PYENV_INSTALL_OPTS = PyenvInstallOpts(opts=['-s', '-v'])
1647
+ DEBUG_PYENV_INSTALL_OPTS = PyenvInstallOpts(opts=['-g'])
1648
+
1649
+
1650
+ #
1651
+
1652
+
1653
+ class PyenvInstallOptsProvider(abc.ABC):
1654
+ @abc.abstractmethod
1655
+ def opts(self) -> PyenvInstallOpts:
1656
+ raise NotImplementedError
1657
+
1658
+
1659
+ class LinuxPyenvInstallOpts(PyenvInstallOptsProvider):
1660
+ def opts(self) -> PyenvInstallOpts:
1661
+ return PyenvInstallOpts()
1662
+
1663
+
1664
+ class DarwinPyenvInstallOpts(PyenvInstallOptsProvider):
1665
+
1666
+ @cached_nullary
1667
+ def framework_opts(self) -> PyenvInstallOpts:
1668
+ return PyenvInstallOpts(conf_opts=['--enable-framework'])
1669
+
1670
+ @cached_nullary
1671
+ def has_brew(self) -> bool:
1672
+ return shutil.which('brew') is not None
1673
+
1674
+ BREW_DEPS: ta.Sequence[str] = [
1675
+ 'openssl',
1676
+ 'readline',
1677
+ 'sqlite3',
1678
+ 'zlib',
1679
+ ]
1680
+
1681
+ @cached_nullary
1682
+ def brew_deps_opts(self) -> PyenvInstallOpts:
1683
+ cflags = []
1684
+ ldflags = []
1685
+ for dep in self.BREW_DEPS:
1686
+ dep_prefix = subprocess_check_output_str('brew', '--prefix', dep)
1687
+ cflags.append(f'-I{dep_prefix}/include')
1688
+ ldflags.append(f'-L{dep_prefix}/lib')
1689
+ return PyenvInstallOpts(
1690
+ cflags=cflags,
1691
+ ldflags=ldflags,
1692
+ )
1693
+
1694
+ @cached_nullary
1695
+ def brew_tcl_opts(self) -> PyenvInstallOpts:
1696
+ if subprocess_try_output('brew', '--prefix', 'tcl-tk') is None:
1697
+ return PyenvInstallOpts()
1698
+
1699
+ tcl_tk_prefix = subprocess_check_output_str('brew', '--prefix', 'tcl-tk')
1700
+ tcl_tk_ver_str = subprocess_check_output_str('brew', 'ls', '--versions', 'tcl-tk')
1701
+ tcl_tk_ver = '.'.join(tcl_tk_ver_str.split()[1].split('.')[:2])
1702
+
1703
+ return PyenvInstallOpts(conf_opts=[
1704
+ f"--with-tcltk-includes='-I{tcl_tk_prefix}/include'",
1705
+ f"--with-tcltk-libs='-L{tcl_tk_prefix}/lib -ltcl{tcl_tk_ver} -ltk{tcl_tk_ver}'",
1706
+ ])
1707
+
1708
+ @cached_nullary
1709
+ def brew_ssl_opts(self) -> PyenvInstallOpts:
1710
+ pkg_config_path = subprocess_check_output_str('brew', '--prefix', 'openssl')
1711
+ if 'PKG_CONFIG_PATH' in os.environ:
1712
+ pkg_config_path += ':' + os.environ['PKG_CONFIG_PATH']
1713
+ return PyenvInstallOpts(env={'PKG_CONFIG_PATH': pkg_config_path})
1714
+
1715
+ def opts(self) -> PyenvInstallOpts:
1716
+ return PyenvInstallOpts().merge(
1717
+ self.framework_opts(),
1718
+ self.brew_deps_opts(),
1719
+ self.brew_tcl_opts(),
1720
+ self.brew_ssl_opts(),
1721
+ )
1722
+
1723
+
1724
+ PLATFORM_PYENV_INSTALL_OPTS: ta.Dict[str, PyenvInstallOptsProvider] = {
1725
+ 'darwin': DarwinPyenvInstallOpts(),
1726
+ 'linux': LinuxPyenvInstallOpts(),
1727
+ }
1728
+
1729
+
1730
+ ##
1731
+
1732
+
1733
+ class PyenvVersionInstaller:
1734
+
1735
+ def __init__(
1736
+ self,
1737
+ version: str,
1738
+ opts: ta.Optional[PyenvInstallOpts] = None,
1739
+ *,
1740
+ debug: bool = False,
1741
+ pyenv: Pyenv = Pyenv(),
1742
+ ) -> None:
1743
+ super().__init__()
1744
+
1745
+ if opts is None:
1746
+ lst = [DEFAULT_PYENV_INSTALL_OPTS]
1747
+ if debug:
1748
+ lst.append(DEBUG_PYENV_INSTALL_OPTS)
1749
+ lst.append(PLATFORM_PYENV_INSTALL_OPTS[sys.platform].opts())
1750
+ opts = PyenvInstallOpts().merge(*lst)
1751
+
1752
+ self._version = version
1753
+ self._opts = opts
1754
+ self._debug = debug
1755
+ self._pyenv = pyenv
1756
+
1757
+ @property
1758
+ def version(self) -> str:
1759
+ return self._version
1760
+
1761
+ @property
1762
+ def opts(self) -> PyenvInstallOpts:
1763
+ return self._opts
1764
+
1765
+ @cached_nullary
1766
+ def install_name(self) -> str:
1767
+ return self._version + ('-debug' if self._debug else '')
1768
+
1769
+ @cached_nullary
1770
+ def install_dir(self) -> str:
1771
+ return str(os.path.join(check_not_none(self._pyenv.root()), 'versions', self.install_name()))
1772
+
1773
+ @cached_nullary
1774
+ def install(self) -> str:
1775
+ env = dict(self._opts.env)
1776
+ for k, l in [
1777
+ ('CFLAGS', self._opts.cflags),
1778
+ ('LDFLAGS', self._opts.ldflags),
1779
+ ('PYTHON_CONFIGURE_OPTS', self._opts.conf_opts),
1780
+ ]:
1781
+ v = ' '.join(l)
1782
+ if k in os.environ:
1783
+ v += ' ' + os.environ[k]
1784
+ env[k] = v
1785
+
1786
+ subprocess_check_call(self._pyenv.exe(), 'install', *self._opts.opts, self._version, env=env)
1787
+
1788
+ exe = os.path.join(self.install_dir(), 'bin', 'python')
1789
+ if not os.path.isfile(exe):
1790
+ raise RuntimeError(f'Interpreter not found: {exe}')
1791
+ return exe
1792
+
1793
+
1794
+ ##
1795
+
1796
+
1797
+ class PyenvInterpProvider(InterpProvider):
1798
+
1799
+ def __init__(
1800
+ self,
1801
+ pyenv: Pyenv = Pyenv(),
1802
+
1803
+ inspect: bool = False,
1804
+ inspector: InterpInspector = INTERP_INSPECTOR,
1805
+ ) -> None:
1806
+ super().__init__()
1807
+
1808
+ self._pyenv = pyenv
1809
+
1810
+ self._inspect = inspect
1811
+ self._inspector = inspector
1812
+
1813
+ #
1814
+
1815
+ @staticmethod
1816
+ def guess_version(s: str) -> ta.Optional[InterpVersion]:
1817
+ def strip_sfx(s: str, sfx: str) -> ta.Tuple[str, bool]:
1818
+ if s.endswith(sfx):
1819
+ return s[:-len(sfx)], True
1820
+ return s, False
1821
+ ok = {}
1822
+ s, ok['debug'] = strip_sfx(s, '-debug')
1823
+ s, ok['threaded'] = strip_sfx(s, 't')
1824
+ try:
1825
+ v = Version(s)
1826
+ except InvalidVersion:
1827
+ return None
1828
+ return InterpVersion(v, InterpOpts(**ok))
1829
+
1830
+ class Installed(ta.NamedTuple):
1831
+ name: str
1832
+ exe: str
1833
+ version: InterpVersion
1834
+
1835
+ def _make_installed(self, vn: str, ep: str) -> ta.Optional[Installed]:
1836
+ iv: ta.Optional[InterpVersion]
1837
+ if self._inspect:
1838
+ try:
1839
+ iv = check_not_none(self._inspector.inspect(ep)).iv
1840
+ except Exception as e: # noqa
1841
+ return None
1842
+ else:
1843
+ iv = self.guess_version(vn)
1844
+ if iv is None:
1845
+ return None
1846
+ return PyenvInterpProvider.Installed(
1847
+ name=vn,
1848
+ exe=ep,
1849
+ version=iv,
1850
+ )
1851
+
1852
+ def installed(self) -> ta.Sequence[Installed]:
1853
+ ret: ta.List[PyenvInterpProvider.Installed] = []
1854
+ for vn, ep in self._pyenv.version_exes():
1855
+ if (i := self._make_installed(vn, ep)) is None:
1856
+ log.debug('Invalid pyenv version: %s', vn)
1857
+ continue
1858
+ ret.append(i)
1859
+ return ret
1860
+
1861
+ #
1862
+
1863
+ def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
1864
+ return [i.version for i in self.installed()]
1865
+
1866
+ def get_installed_version(self, version: InterpVersion) -> Interp:
1867
+ for i in self.installed():
1868
+ if i.version == version:
1869
+ return Interp(
1870
+ exe=i.exe,
1871
+ version=i.version,
1872
+ )
1873
+ raise KeyError(version)
1874
+
1875
+ #
1876
+
1877
+ def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
1878
+ lst = []
1879
+ for vs in self._pyenv.installable_versions():
1880
+ if (iv := self.guess_version(vs)) is None:
1881
+ continue
1882
+ if iv.opts.debug:
1883
+ raise Exception('Pyenv installable versions not expected to have debug suffix')
1884
+ for d in [False, True]:
1885
+ lst.append(dc.replace(iv, opts=dc.replace(iv.opts, debug=d)))
1886
+ return lst
1887
+
1888
+
1889
+ ########################################
1890
+ # ../system.py
1891
+ """
1892
+ TODO:
1893
+ - python, python3, python3.12, ...
1894
+ - check if path py's are venvs: sys.prefix != sys.base_prefix
1895
+ """
1896
+ # ruff: noqa: UP006 UP007
1897
+
1898
+
1899
+ ##
1900
+
1901
+
1902
+ @dc.dataclass(frozen=True)
1903
+ class SystemInterpProvider(InterpProvider):
1904
+ cmd: str = 'python3'
1905
+ path: ta.Optional[str] = None
1906
+
1907
+ inspect: bool = False
1908
+ inspector: InterpInspector = INTERP_INSPECTOR
1909
+
1910
+ #
1911
+
1912
+ @staticmethod
1913
+ def _re_which(
1914
+ pat: re.Pattern,
1915
+ *,
1916
+ mode: int = os.F_OK | os.X_OK,
1917
+ path: ta.Optional[str] = None,
1918
+ ) -> ta.List[str]:
1919
+ if path is None:
1920
+ path = os.environ.get('PATH', None)
1921
+ if path is None:
1922
+ try:
1923
+ path = os.confstr('CS_PATH')
1924
+ except (AttributeError, ValueError):
1925
+ path = os.defpath
1926
+
1927
+ if not path:
1928
+ return []
1929
+
1930
+ path = os.fsdecode(path)
1931
+ pathlst = path.split(os.pathsep)
1932
+
1933
+ def _access_check(fn: str, mode: int) -> bool:
1934
+ return os.path.exists(fn) and os.access(fn, mode)
1935
+
1936
+ out = []
1937
+ seen = set()
1938
+ for d in pathlst:
1939
+ normdir = os.path.normcase(d)
1940
+ if normdir not in seen:
1941
+ seen.add(normdir)
1942
+ if not _access_check(normdir, mode):
1943
+ continue
1944
+ for thefile in os.listdir(d):
1945
+ name = os.path.join(d, thefile)
1946
+ if not (
1947
+ os.path.isfile(name) and
1948
+ pat.fullmatch(thefile) and
1949
+ _access_check(name, mode)
1950
+ ):
1951
+ continue
1952
+ out.append(name)
1953
+
1954
+ return out
1955
+
1956
+ @cached_nullary
1957
+ def exes(self) -> ta.List[str]:
1958
+ return self._re_which(
1959
+ re.compile(r'python3(\.\d+)?'),
1960
+ path=self.path,
1961
+ )
1962
+
1963
+ #
1964
+
1965
+ def get_exe_version(self, exe: str) -> ta.Optional[InterpVersion]:
1966
+ if not self.inspect:
1967
+ s = os.path.basename(exe)
1968
+ if s.startswith('python'):
1969
+ s = s[len('python'):]
1970
+ if '.' in s:
1971
+ try:
1972
+ return InterpVersion.parse(s)
1973
+ except InvalidVersion:
1974
+ pass
1975
+ ii = self.inspector.inspect(exe)
1976
+ return ii.iv if ii is not None else None
1977
+
1978
+ def exe_versions(self) -> ta.Sequence[ta.Tuple[str, InterpVersion]]:
1979
+ lst = []
1980
+ for e in self.exes():
1981
+ if (ev := self.get_exe_version(e)) is None:
1982
+ log.debug('Invalid system version: %s', e)
1983
+ continue
1984
+ lst.append((e, ev))
1985
+ return lst
1986
+
1987
+ #
1988
+
1989
+ def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
1990
+ return [ev for e, ev in self.exe_versions()]
1991
+
1992
+ def get_installed_version(self, version: InterpVersion) -> Interp:
1993
+ for e, ev in self.exe_versions():
1994
+ if ev != version:
1995
+ continue
1996
+ return Interp(
1997
+ exe=e,
1998
+ version=ev,
1999
+ )
2000
+ raise KeyError(version)
2001
+
2002
+
2003
+ ########################################
2004
+ # ../resolvers.py
2005
+ # ruff: noqa: UP006
2006
+
2007
+
2008
+ INTERP_PROVIDER_TYPES_BY_NAME: ta.Mapping[str, ta.Type[InterpProvider]] = {
2009
+ cls.name: cls for cls in deep_subclasses(InterpProvider) if abc.ABC not in cls.__bases__ # type: ignore
2010
+ }
2011
+
2012
+
2013
+ class InterpResolver:
2014
+ def __init__(
2015
+ self,
2016
+ providers: ta.Sequence[ta.Tuple[str, InterpProvider]],
2017
+ ) -> None:
2018
+ super().__init__()
2019
+ self._providers: ta.Mapping[str, InterpProvider] = collections.OrderedDict(providers)
2020
+
2021
+ def resolve(self, spec: InterpSpecifier) -> Interp:
2022
+ lst = [
2023
+ (i, si)
2024
+ for i, p in enumerate(self._providers.values())
2025
+ for si in p.get_installed_versions(spec)
2026
+ if spec.contains(si)
2027
+ ]
2028
+ best = sorted(lst, key=lambda t: (-t[0], t[1]))[-1]
2029
+ bi, bv = best
2030
+ bp = list(self._providers.values())[bi]
2031
+ return bp.get_installed_version(bv)
2032
+
2033
+ def list(self, spec: InterpSpecifier) -> None:
2034
+ print('installed:')
2035
+ for n, p in self._providers.items():
2036
+ lst = [
2037
+ si
2038
+ for si in p.get_installed_versions(spec)
2039
+ if spec.contains(si)
2040
+ ]
2041
+ if lst:
2042
+ print(f' {n}')
2043
+ for si in lst:
2044
+ print(f' {si}')
2045
+
2046
+ print()
2047
+
2048
+ print('installable:')
2049
+ for n, p in self._providers.items():
2050
+ lst = [
2051
+ si
2052
+ for si in p.get_installable_versions(spec)
2053
+ if spec.contains(si)
2054
+ ]
2055
+ if lst:
2056
+ print(f' {n}')
2057
+ for si in lst:
2058
+ print(f' {si}')
2059
+
2060
+
2061
+ DEFAULT_INTERP_RESOLVER = InterpResolver([(p.name, p) for p in [
2062
+ # pyenv is preferred to system interpreters as it tends to have more support for things like tkinter
2063
+ PyenvInterpProvider(),
2064
+
2065
+ RunningInterpProvider(),
2066
+
2067
+ SystemInterpProvider(),
2068
+ ]])
2069
+
2070
+
2071
+ ########################################
2072
+ # cli.py
2073
+
2074
+
2075
+ def _list_cmd(args) -> None:
2076
+ r = DEFAULT_INTERP_RESOLVER
2077
+ s = InterpSpecifier.parse(args.version)
2078
+ r.list(s)
2079
+
2080
+
2081
+ def _resolve_cmd(args) -> None:
2082
+ r = DEFAULT_INTERP_RESOLVER
2083
+ s = InterpSpecifier.parse(args.version)
2084
+ print(r.resolve(s).exe)
2085
+
2086
+
2087
+ def _build_parser() -> argparse.ArgumentParser:
2088
+ parser = argparse.ArgumentParser()
2089
+
2090
+ subparsers = parser.add_subparsers()
2091
+
2092
+ parser_list = subparsers.add_parser('list')
2093
+ parser_list.add_argument('version')
2094
+ parser_list.add_argument('--debug', action='store_true')
2095
+ parser_list.set_defaults(func=_list_cmd)
2096
+
2097
+ parser_resolve = subparsers.add_parser('resolve')
2098
+ parser_resolve.add_argument('version')
2099
+ parser_resolve.add_argument('--debug', action='store_true')
2100
+ parser_resolve.set_defaults(func=_resolve_cmd)
2101
+
2102
+ return parser
2103
+
2104
+
2105
+ def _main(argv: ta.Optional[ta.Sequence[str]] = None) -> None:
2106
+ check_runtime_version()
2107
+ configure_standard_logging()
2108
+
2109
+ parser = _build_parser()
2110
+ args = parser.parse_args(argv)
2111
+ if not getattr(args, 'func', None):
2112
+ parser.print_help()
2113
+ else:
2114
+ args.func(args)
2115
+
2116
+
2117
+ if __name__ == '__main__':
2118
+ _main()