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.
- omdev/__about__.py +35 -0
- omdev/__init__.py +0 -0
- omdev/amalg/__init__.py +0 -0
- omdev/amalg/__main__.py +4 -0
- omdev/amalg/amalg.py +513 -0
- omdev/classdot.py +61 -0
- omdev/cmake.py +164 -0
- omdev/exts/__init__.py +0 -0
- omdev/exts/_distutils/__init__.py +10 -0
- omdev/exts/_distutils/build_ext.py +367 -0
- omdev/exts/_distutils/compilers/__init__.py +3 -0
- omdev/exts/_distutils/compilers/ccompiler.py +1032 -0
- omdev/exts/_distutils/compilers/options.py +80 -0
- omdev/exts/_distutils/compilers/unixccompiler.py +385 -0
- omdev/exts/_distutils/dir_util.py +76 -0
- omdev/exts/_distutils/errors.py +62 -0
- omdev/exts/_distutils/extension.py +107 -0
- omdev/exts/_distutils/file_util.py +216 -0
- omdev/exts/_distutils/modified.py +47 -0
- omdev/exts/_distutils/spawn.py +103 -0
- omdev/exts/_distutils/sysconfig.py +349 -0
- omdev/exts/_distutils/util.py +201 -0
- omdev/exts/_distutils/version.py +308 -0
- omdev/exts/build.py +43 -0
- omdev/exts/cmake.py +195 -0
- omdev/exts/importhook.py +88 -0
- omdev/exts/scan.py +74 -0
- omdev/interp/__init__.py +1 -0
- omdev/interp/__main__.py +4 -0
- omdev/interp/cli.py +63 -0
- omdev/interp/inspect.py +105 -0
- omdev/interp/providers.py +67 -0
- omdev/interp/pyenv.py +353 -0
- omdev/interp/resolvers.py +76 -0
- omdev/interp/standalone.py +187 -0
- omdev/interp/system.py +125 -0
- omdev/interp/types.py +92 -0
- omdev/mypy/__init__.py +0 -0
- omdev/mypy/debug.py +86 -0
- omdev/pyproject/__init__.py +1 -0
- omdev/pyproject/__main__.py +4 -0
- omdev/pyproject/cli.py +319 -0
- omdev/pyproject/configs.py +97 -0
- omdev/pyproject/ext.py +107 -0
- omdev/pyproject/pkg.py +196 -0
- omdev/scripts/__init__.py +0 -0
- omdev/scripts/execrss.py +19 -0
- omdev/scripts/findimports.py +62 -0
- omdev/scripts/findmagic.py +70 -0
- omdev/scripts/interp.py +2118 -0
- omdev/scripts/pyproject.py +3584 -0
- omdev/scripts/traceimport.py +502 -0
- omdev/tokens.py +42 -0
- omdev/toml/__init__.py +1 -0
- omdev/toml/parser.py +823 -0
- omdev/toml/writer.py +104 -0
- omdev/tools/__init__.py +0 -0
- omdev/tools/dockertools.py +81 -0
- omdev/tools/sqlrepl.py +193 -0
- omdev/versioning/__init__.py +1 -0
- omdev/versioning/specifiers.py +531 -0
- omdev/versioning/versions.py +416 -0
- omdev-0.0.0.dev7.dist-info/LICENSE +21 -0
- omdev-0.0.0.dev7.dist-info/METADATA +24 -0
- omdev-0.0.0.dev7.dist-info/RECORD +67 -0
- omdev-0.0.0.dev7.dist-info/WHEEL +5 -0
- omdev-0.0.0.dev7.dist-info/top_level.txt +1 -0
omdev/scripts/interp.py
ADDED
|
@@ -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()
|