omdev 0.0.0.dev112__py3-none-any.whl → 0.0.0.dev114__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.
Potentially problematic release.
This version of omdev might be problematic. Click here for more details.
- omdev/.manifests.json +3 -3
- omdev/cli/clicli.py +18 -10
- omdev/git.py +313 -14
- omdev/pip.py +77 -0
- omdev/pycharm/cli.py +4 -0
- omdev/scripts/pyproject.py +1421 -1127
- omdev/tools/git.py +54 -0
- omdev/tools/pip.py +10 -54
- {omdev-0.0.0.dev112.dist-info → omdev-0.0.0.dev114.dist-info}/METADATA +2 -2
- {omdev-0.0.0.dev112.dist-info → omdev-0.0.0.dev114.dist-info}/RECORD +14 -13
- {omdev-0.0.0.dev112.dist-info → omdev-0.0.0.dev114.dist-info}/LICENSE +0 -0
- {omdev-0.0.0.dev112.dist-info → omdev-0.0.0.dev114.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev112.dist-info → omdev-0.0.0.dev114.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev112.dist-info → omdev-0.0.0.dev114.dist-info}/top_level.txt +0 -0
omdev/.manifests.json
CHANGED
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"module": ".cli.clicli",
|
|
52
52
|
"attr": "_CLI_MODULE",
|
|
53
53
|
"file": "omdev/cli/clicli.py",
|
|
54
|
-
"line":
|
|
54
|
+
"line": 111,
|
|
55
55
|
"value": {
|
|
56
56
|
"$.cli.types.CliModule": {
|
|
57
57
|
"cmd_name": "cli",
|
|
@@ -243,7 +243,7 @@
|
|
|
243
243
|
"module": ".tools.git",
|
|
244
244
|
"attr": "_CLI_MODULE",
|
|
245
245
|
"file": "omdev/tools/git.py",
|
|
246
|
-
"line":
|
|
246
|
+
"line": 182,
|
|
247
247
|
"value": {
|
|
248
248
|
"$.cli.types.CliModule": {
|
|
249
249
|
"cmd_name": "git",
|
|
@@ -306,7 +306,7 @@
|
|
|
306
306
|
"module": ".tools.pip",
|
|
307
307
|
"attr": "_CLI_MODULE",
|
|
308
308
|
"file": "omdev/tools/pip.py",
|
|
309
|
-
"line":
|
|
309
|
+
"line": 120,
|
|
310
310
|
"value": {
|
|
311
311
|
"$.cli.types.CliModule": {
|
|
312
312
|
"cmd_name": "pip",
|
omdev/cli/clicli.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
import os
|
|
3
|
-
import subprocess
|
|
4
3
|
import sys
|
|
5
4
|
import urllib.parse
|
|
6
5
|
import urllib.request
|
|
@@ -8,6 +7,8 @@ import urllib.request
|
|
|
8
7
|
from omlish import __about__
|
|
9
8
|
from omlish import argparse as ap
|
|
10
9
|
|
|
10
|
+
from ..pip import get_root_dists
|
|
11
|
+
from ..pip import lookup_latest_package_version
|
|
11
12
|
from . import install
|
|
12
13
|
from .types import CliModule
|
|
13
14
|
|
|
@@ -50,21 +51,18 @@ class CliCli(ap.Cli):
|
|
|
50
51
|
ap.arg('extra_deps', nargs='*'),
|
|
51
52
|
)
|
|
52
53
|
def reinstall(self) -> None:
|
|
53
|
-
|
|
54
|
-
tool_name = '.'.join([mod_name.partition('.')[0], 'tools', 'pip'])
|
|
54
|
+
latest_version = lookup_latest_package_version(__package__.split('.')[0])
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
sys.executable,
|
|
58
|
-
'-m',
|
|
59
|
-
tool_name,
|
|
60
|
-
'list-root-dists',
|
|
61
|
-
]).decode()
|
|
56
|
+
#
|
|
62
57
|
|
|
58
|
+
root_dists = get_root_dists()
|
|
63
59
|
deps = sorted(
|
|
64
|
-
(
|
|
60
|
+
(set(root_dists) | set(self.args.extra_deps or []))
|
|
65
61
|
- {install.DEFAULT_CLI_PKG} # noqa
|
|
66
62
|
)
|
|
67
63
|
|
|
64
|
+
#
|
|
65
|
+
|
|
68
66
|
if deps:
|
|
69
67
|
print('Reinstalling with following additional dependencies:')
|
|
70
68
|
print('\n'.join(' ' + d for d in deps))
|
|
@@ -72,12 +70,22 @@ class CliCli(ap.Cli):
|
|
|
72
70
|
print('No additional dependencies detected.')
|
|
73
71
|
print()
|
|
74
72
|
|
|
73
|
+
#
|
|
74
|
+
|
|
75
75
|
if self.args.local:
|
|
76
76
|
print('Reinstalling from local installer.')
|
|
77
77
|
else:
|
|
78
78
|
print(f'Reinstalling from script url: {self.args.url}')
|
|
79
79
|
print()
|
|
80
80
|
|
|
81
|
+
#
|
|
82
|
+
|
|
83
|
+
print(f'Current version: {__about__.__version__}')
|
|
84
|
+
print(f'Latest version: {latest_version}')
|
|
85
|
+
print()
|
|
86
|
+
|
|
87
|
+
#
|
|
88
|
+
|
|
81
89
|
print('Continue with reinstall? (ctrl-c to cancel)')
|
|
82
90
|
input()
|
|
83
91
|
|
omdev/git.py
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
# ruff: noqa: UP007
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
|
2
2
|
# @omlish-lite
|
|
3
|
+
"""
|
|
4
|
+
git status
|
|
5
|
+
--porcelain=v1
|
|
6
|
+
--ignore-submodules
|
|
7
|
+
2>/dev/null
|
|
8
|
+
"""
|
|
9
|
+
import dataclasses as dc
|
|
10
|
+
import enum
|
|
3
11
|
import os.path
|
|
4
12
|
import subprocess
|
|
5
13
|
import typing as ta
|
|
6
14
|
|
|
15
|
+
from omlish.lite.check import check_state
|
|
16
|
+
from omlish.lite.subprocesses import subprocess_maybe_shell_wrap_exec
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
|
|
7
21
|
|
|
8
22
|
def git_clone_subtree(
|
|
9
23
|
*,
|
|
@@ -25,7 +39,7 @@ def git_clone_subtree(
|
|
|
25
39
|
]
|
|
26
40
|
|
|
27
41
|
subprocess.check_call(
|
|
28
|
-
|
|
42
|
+
subprocess_maybe_shell_wrap_exec(
|
|
29
43
|
'git',
|
|
30
44
|
*git_opts,
|
|
31
45
|
'clone',
|
|
@@ -36,30 +50,30 @@ def git_clone_subtree(
|
|
|
36
50
|
'--single-branch',
|
|
37
51
|
repo_url,
|
|
38
52
|
repo_dir,
|
|
39
|
-
|
|
53
|
+
),
|
|
40
54
|
cwd=base_dir,
|
|
41
55
|
)
|
|
42
56
|
|
|
43
57
|
rd = os.path.join(base_dir, repo_dir)
|
|
44
58
|
subprocess.check_call(
|
|
45
|
-
|
|
59
|
+
subprocess_maybe_shell_wrap_exec(
|
|
46
60
|
'git',
|
|
47
61
|
*git_opts,
|
|
48
62
|
'sparse-checkout',
|
|
49
63
|
'set',
|
|
50
64
|
'--no-cone',
|
|
51
65
|
*repo_subtrees,
|
|
52
|
-
|
|
66
|
+
),
|
|
53
67
|
cwd=rd,
|
|
54
68
|
)
|
|
55
69
|
|
|
56
70
|
subprocess.check_call(
|
|
57
|
-
|
|
71
|
+
subprocess_maybe_shell_wrap_exec(
|
|
58
72
|
'git',
|
|
59
73
|
*git_opts,
|
|
60
74
|
'checkout',
|
|
61
75
|
*([rev] if rev else []),
|
|
62
|
-
|
|
76
|
+
),
|
|
63
77
|
cwd=rd,
|
|
64
78
|
)
|
|
65
79
|
|
|
@@ -68,37 +82,322 @@ def get_git_revision(
|
|
|
68
82
|
*,
|
|
69
83
|
cwd: ta.Optional[str] = None,
|
|
70
84
|
) -> ta.Optional[str]:
|
|
71
|
-
subprocess.check_output(
|
|
85
|
+
subprocess.check_output(subprocess_maybe_shell_wrap_exec('git', '--version'))
|
|
72
86
|
|
|
73
87
|
if cwd is None:
|
|
74
88
|
cwd = os.getcwd()
|
|
75
89
|
|
|
76
90
|
if subprocess.run( # noqa
|
|
77
|
-
|
|
91
|
+
subprocess_maybe_shell_wrap_exec(
|
|
78
92
|
'git',
|
|
79
93
|
'rev-parse',
|
|
80
94
|
'--is-inside-work-tree',
|
|
81
|
-
|
|
95
|
+
),
|
|
82
96
|
stdout=subprocess.PIPE,
|
|
83
97
|
stderr=subprocess.PIPE,
|
|
84
98
|
).returncode:
|
|
85
99
|
return None
|
|
86
100
|
|
|
87
|
-
has_untracked = bool(subprocess.check_output(
|
|
101
|
+
has_untracked = bool(subprocess.check_output(subprocess_maybe_shell_wrap_exec(
|
|
88
102
|
'git',
|
|
89
103
|
'ls-files',
|
|
90
104
|
'.',
|
|
91
105
|
'--exclude-standard',
|
|
92
106
|
'--others',
|
|
93
|
-
|
|
107
|
+
), cwd=cwd).decode().strip())
|
|
94
108
|
|
|
95
|
-
dirty_rev = subprocess.check_output(
|
|
109
|
+
dirty_rev = subprocess.check_output(subprocess_maybe_shell_wrap_exec(
|
|
96
110
|
'git',
|
|
97
111
|
'describe',
|
|
98
112
|
'--match=NeVeRmAtCh',
|
|
99
113
|
'--always',
|
|
100
114
|
'--abbrev=40',
|
|
101
115
|
'--dirty',
|
|
102
|
-
|
|
116
|
+
), cwd=cwd).decode().strip()
|
|
103
117
|
|
|
104
118
|
return dirty_rev + ('-untracked' if has_untracked else '')
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
##
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
_GIT_STATUS_LINE_ESCAPE_CODES: ta.Mapping[str, str] = {
|
|
125
|
+
'\\': '\\',
|
|
126
|
+
'"': '"',
|
|
127
|
+
'n': '\n',
|
|
128
|
+
't': '\t',
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def yield_git_status_line_fields(l: str) -> ta.Iterator[str]:
|
|
133
|
+
def find_any(chars: str, start: int = 0) -> int:
|
|
134
|
+
ret = -1
|
|
135
|
+
for c in chars:
|
|
136
|
+
if (found := l.find(c, start)) >= 0 and (ret < 0 or ret > found):
|
|
137
|
+
ret = found
|
|
138
|
+
return ret
|
|
139
|
+
|
|
140
|
+
p = 0
|
|
141
|
+
while True:
|
|
142
|
+
if l[p] == '"':
|
|
143
|
+
p += 1
|
|
144
|
+
s = []
|
|
145
|
+
while (n := find_any('\\"', p)) > 0:
|
|
146
|
+
if (c := l[n]) == '\\':
|
|
147
|
+
s.append(l[p:n])
|
|
148
|
+
s.append(_GIT_STATUS_LINE_ESCAPE_CODES[l[n + 1]])
|
|
149
|
+
p = n + 2
|
|
150
|
+
elif c == '"':
|
|
151
|
+
s.append(l[p:n])
|
|
152
|
+
p = n
|
|
153
|
+
break
|
|
154
|
+
else:
|
|
155
|
+
raise ValueError(l)
|
|
156
|
+
|
|
157
|
+
if l[p] != '"':
|
|
158
|
+
raise ValueError(l)
|
|
159
|
+
|
|
160
|
+
yield ''.join(s)
|
|
161
|
+
|
|
162
|
+
p += 1
|
|
163
|
+
if p == len(l):
|
|
164
|
+
return
|
|
165
|
+
elif l[p] != ' ':
|
|
166
|
+
raise ValueError(l)
|
|
167
|
+
|
|
168
|
+
p += 1
|
|
169
|
+
|
|
170
|
+
else:
|
|
171
|
+
if (e := l.find(' ', p)) < 0:
|
|
172
|
+
yield l[p:]
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
yield l[p:e]
|
|
176
|
+
p = e + 1
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
"""
|
|
180
|
+
When merge is occurring and was successful, or outside of a merge situation, X shows the status of the index and Y shows
|
|
181
|
+
the status of the working tree:
|
|
182
|
+
-------------------------------------------------
|
|
183
|
+
X Y Meaning
|
|
184
|
+
-------------------------------------------------
|
|
185
|
+
[AMD] not updated
|
|
186
|
+
M [ MTD] updated in index
|
|
187
|
+
T [ MTD] type changed in index
|
|
188
|
+
A [ MTD] added to index
|
|
189
|
+
D deleted from index
|
|
190
|
+
R [ MTD] renamed in index
|
|
191
|
+
C [ MTD] copied in index
|
|
192
|
+
[MTARC] index and work tree matches
|
|
193
|
+
[ MTARC] M work tree changed since index
|
|
194
|
+
[ MTARC] T type changed in work tree since index
|
|
195
|
+
[ MTARC] D deleted in work tree
|
|
196
|
+
R renamed in work tree
|
|
197
|
+
C copied in work tree
|
|
198
|
+
|
|
199
|
+
When merge conflict has occurred and has not yet been resolved, X and Y show the state introduced by each head of the
|
|
200
|
+
merge, relative to the common ancestor:
|
|
201
|
+
-------------------------------------------------
|
|
202
|
+
X Y Meaning
|
|
203
|
+
-------------------------------------------------
|
|
204
|
+
D D unmerged, both deleted
|
|
205
|
+
A U unmerged, added by us
|
|
206
|
+
U D unmerged, deleted by them
|
|
207
|
+
U A unmerged, added by them
|
|
208
|
+
D U unmerged, deleted by us
|
|
209
|
+
A A unmerged, both added
|
|
210
|
+
U U unmerged, both modified
|
|
211
|
+
|
|
212
|
+
When path is untracked, X and Y are always the same, since they are unknown to the index:
|
|
213
|
+
-------------------------------------------------
|
|
214
|
+
X Y Meaning
|
|
215
|
+
-------------------------------------------------
|
|
216
|
+
? ? untracked
|
|
217
|
+
! ! ignored
|
|
218
|
+
|
|
219
|
+
Submodules have more state and instead report
|
|
220
|
+
|
|
221
|
+
- M = the submodule has a different HEAD than recorded in the index
|
|
222
|
+
- m = the submodule has modified content
|
|
223
|
+
- ? = the submodule has untracked files
|
|
224
|
+
|
|
225
|
+
This is since modified content or untracked files in a submodule cannot be added via git add in the superproject to
|
|
226
|
+
prepare a commit. m and ? are applied recursively. For example if a nested submodule in a submodule contains an
|
|
227
|
+
untracked file, this is reported as ? as well.
|
|
228
|
+
""" # noqa
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class GitStatusLineState(enum.Enum):
|
|
232
|
+
UNMODIFIED = ' '
|
|
233
|
+
MODIFIED = 'M'
|
|
234
|
+
FILE_TYPE_CHANGED = 'T'
|
|
235
|
+
ADDED = 'A'
|
|
236
|
+
DELETED = 'D'
|
|
237
|
+
RENAMED = 'R'
|
|
238
|
+
COPIED = 'C'
|
|
239
|
+
UPDATED_BUT_UNMERGED = 'U'
|
|
240
|
+
UNTRACKED = '?'
|
|
241
|
+
IGNORED = '!'
|
|
242
|
+
SUBMODULE_MODIFIED_CONTENT = 'm'
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
_EXTRA_UNMERGED_GIT_STATUS_LINE_STATES: ta.FrozenSet[ta.Tuple[GitStatusLineState, GitStatusLineState]] = frozenset([
|
|
246
|
+
(GitStatusLineState.ADDED, GitStatusLineState.ADDED),
|
|
247
|
+
(GitStatusLineState.DELETED, GitStatusLineState.DELETED),
|
|
248
|
+
])
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@dc.dataclass(frozen=True)
|
|
252
|
+
class GitStatusLine:
|
|
253
|
+
x: GitStatusLineState
|
|
254
|
+
y: GitStatusLineState
|
|
255
|
+
|
|
256
|
+
a: str
|
|
257
|
+
b: ta.Optional[str]
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def is_unmerged(self) -> bool:
|
|
261
|
+
return (
|
|
262
|
+
self.x is GitStatusLineState.UPDATED_BUT_UNMERGED or
|
|
263
|
+
self.y is GitStatusLineState.UPDATED_BUT_UNMERGED or
|
|
264
|
+
(self.x, self.y) in _EXTRA_UNMERGED_GIT_STATUS_LINE_STATES
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def __repr__(self) -> str:
|
|
268
|
+
return (
|
|
269
|
+
f'{self.__class__.__name__}('
|
|
270
|
+
f'x={self.x.name}, '
|
|
271
|
+
f'y={self.y.name}, '
|
|
272
|
+
f'a={self.a!r}' +
|
|
273
|
+
(f', b={self.b!r}' if self.b is not None else '') +
|
|
274
|
+
')'
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def parse_git_status_line(l: str) -> GitStatusLine:
|
|
279
|
+
if len(l) < 3 or l[2] != ' ':
|
|
280
|
+
raise ValueError(l)
|
|
281
|
+
x, y = l[0], l[1]
|
|
282
|
+
|
|
283
|
+
fields = list(yield_git_status_line_fields(l[3:]))
|
|
284
|
+
if len(fields) == 1:
|
|
285
|
+
a, b = fields[0], None
|
|
286
|
+
elif len(fields) == 3:
|
|
287
|
+
check_state(fields[1] == '->', l)
|
|
288
|
+
a, b = fields[0], fields[2]
|
|
289
|
+
else:
|
|
290
|
+
raise ValueError(l)
|
|
291
|
+
|
|
292
|
+
return GitStatusLine(
|
|
293
|
+
GitStatusLineState(x),
|
|
294
|
+
GitStatusLineState(y),
|
|
295
|
+
a,
|
|
296
|
+
b,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class GitStatus(ta.Sequence[GitStatusLine]):
|
|
301
|
+
def __init__(self, lines: ta.Iterable[GitStatusLine]) -> None:
|
|
302
|
+
super().__init__()
|
|
303
|
+
|
|
304
|
+
self._lst = list(lines)
|
|
305
|
+
|
|
306
|
+
by_x: ta.Dict[GitStatusLineState, list[GitStatusLine]] = {}
|
|
307
|
+
by_y: ta.Dict[GitStatusLineState, list[GitStatusLine]] = {}
|
|
308
|
+
|
|
309
|
+
by_a: ta.Dict[str, GitStatusLine] = {}
|
|
310
|
+
by_b: ta.Dict[str, GitStatusLine] = {}
|
|
311
|
+
|
|
312
|
+
for l in self._lst:
|
|
313
|
+
by_x.setdefault(l.x, []).append(l)
|
|
314
|
+
by_y.setdefault(l.y, []).append(l)
|
|
315
|
+
|
|
316
|
+
if l.a in by_a:
|
|
317
|
+
raise KeyError(l.a)
|
|
318
|
+
by_a[l.a] = l
|
|
319
|
+
|
|
320
|
+
if l.b is not None:
|
|
321
|
+
if l.b in by_b:
|
|
322
|
+
raise KeyError(l.b)
|
|
323
|
+
by_b[l.b] = l
|
|
324
|
+
|
|
325
|
+
self._by_x = by_x
|
|
326
|
+
self._by_y = by_y
|
|
327
|
+
|
|
328
|
+
self._by_a = by_a
|
|
329
|
+
self._by_b = by_b
|
|
330
|
+
|
|
331
|
+
self._has_unmerged = any(l.is_unmerged for l in self)
|
|
332
|
+
|
|
333
|
+
#
|
|
334
|
+
|
|
335
|
+
def __iter__(self) -> ta.Iterator[GitStatusLine]:
|
|
336
|
+
return iter(self._lst)
|
|
337
|
+
|
|
338
|
+
def __getitem__(self, index):
|
|
339
|
+
return self._lst[index]
|
|
340
|
+
|
|
341
|
+
def __len__(self) -> int:
|
|
342
|
+
return len(self._lst)
|
|
343
|
+
|
|
344
|
+
#
|
|
345
|
+
|
|
346
|
+
@property
|
|
347
|
+
def by_x(self) -> ta.Mapping[GitStatusLineState, ta.Sequence[GitStatusLine]]:
|
|
348
|
+
return self._by_x
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def by_y(self) -> ta.Mapping[GitStatusLineState, ta.Sequence[GitStatusLine]]:
|
|
352
|
+
return self._by_y
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def by_a(self) -> ta.Mapping[str, GitStatusLine]:
|
|
356
|
+
return self._by_a
|
|
357
|
+
|
|
358
|
+
@property
|
|
359
|
+
def by_b(self) -> ta.Mapping[str, GitStatusLine]:
|
|
360
|
+
return self._by_b
|
|
361
|
+
|
|
362
|
+
#
|
|
363
|
+
|
|
364
|
+
@property
|
|
365
|
+
def has_unmerged(self) -> bool:
|
|
366
|
+
return self._has_unmerged
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def has_staged(self) -> bool:
|
|
370
|
+
return any(l.x != GitStatusLineState.UNMODIFIED for l in self._lst)
|
|
371
|
+
|
|
372
|
+
@property
|
|
373
|
+
def has_dirty(self) -> bool:
|
|
374
|
+
return any(l.y != GitStatusLineState.UNMODIFIED for l in self._lst)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def parse_git_status(s: str) -> GitStatus:
|
|
378
|
+
return GitStatus(parse_git_status_line(l) for l in s.splitlines())
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def get_git_status(
|
|
382
|
+
*,
|
|
383
|
+
cwd: ta.Optional[str] = None,
|
|
384
|
+
ignore_submodules: bool = False,
|
|
385
|
+
verbose: bool = False,
|
|
386
|
+
) -> GitStatus:
|
|
387
|
+
if cwd is None:
|
|
388
|
+
cwd = os.getcwd()
|
|
389
|
+
|
|
390
|
+
proc = subprocess.run( # type: ignore
|
|
391
|
+
subprocess_maybe_shell_wrap_exec(
|
|
392
|
+
'git',
|
|
393
|
+
'status',
|
|
394
|
+
'--porcelain=v1',
|
|
395
|
+
*(['--ignore-submodules'] if ignore_submodules else []),
|
|
396
|
+
),
|
|
397
|
+
cwd=cwd,
|
|
398
|
+
stdout=subprocess.PIPE,
|
|
399
|
+
**(dict(stderr=subprocess.PIPE) if not verbose else {}),
|
|
400
|
+
check=True,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
return parse_git_status(proc.stdout.decode()) # noqa
|
omdev/pip.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
import io
|
|
3
|
+
import sys
|
|
4
|
+
import typing as ta
|
|
5
|
+
import urllib.request
|
|
6
|
+
|
|
7
|
+
from omlish import check
|
|
8
|
+
from omlish import lang
|
|
9
|
+
|
|
10
|
+
from .packaging.names import canonicalize_name
|
|
11
|
+
from .packaging.requires import RequiresVariable
|
|
12
|
+
from .packaging.requires import parse_requirement
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if ta.TYPE_CHECKING:
|
|
16
|
+
import xml.etree.ElementTree as ET # noqa
|
|
17
|
+
else:
|
|
18
|
+
ET = lang.proxy_import('xml.etree.ElementTree')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
DEFAULT_PYPI_URL = 'https://pypi.org/'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def lookup_latest_package_version(
|
|
28
|
+
package: str,
|
|
29
|
+
*,
|
|
30
|
+
pypi_url: str = DEFAULT_PYPI_URL,
|
|
31
|
+
) -> str:
|
|
32
|
+
pkg_name = check.non_empty_str(package)
|
|
33
|
+
with urllib.request.urlopen(f'{pypi_url}rss/project/{pkg_name}/releases.xml') as resp: # noqa
|
|
34
|
+
rss = resp.read()
|
|
35
|
+
doc = ET.parse(io.BytesIO(rss)) # noqa
|
|
36
|
+
latest = check.not_none(doc.find('./channel/item/title')).text
|
|
37
|
+
return check.non_empty_str(latest)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
##
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_root_dists(
|
|
44
|
+
*,
|
|
45
|
+
paths: ta.Iterable[str] | None = None,
|
|
46
|
+
) -> ta.Sequence[str]:
|
|
47
|
+
# FIXME: track req extras - tuple[str, str] with ('pkg', '') as 'bare'?
|
|
48
|
+
if paths is None:
|
|
49
|
+
paths = sys.path
|
|
50
|
+
|
|
51
|
+
dists: set[str] = set()
|
|
52
|
+
reqs_by_use: dict[str, set[str]] = {}
|
|
53
|
+
uses_by_req: dict[str, set[str]] = {}
|
|
54
|
+
for dist in importlib.metadata.distributions(paths=paths):
|
|
55
|
+
dist_cn = canonicalize_name(dist.metadata['Name'], validate=True)
|
|
56
|
+
if dist_cn in dists:
|
|
57
|
+
# raise NameError(dist_cn)
|
|
58
|
+
# print(f'!! duplicate dist: {dist_cn}', file=sys.stderr)
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
dists.add(dist_cn)
|
|
62
|
+
for req_str in dist.requires or []:
|
|
63
|
+
req = parse_requirement(req_str)
|
|
64
|
+
|
|
65
|
+
if any(v.value == 'extra' for m in req.marker or [] if isinstance(v := m[0], RequiresVariable)):
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
req_cn = canonicalize_name(req.name)
|
|
69
|
+
reqs_by_use.setdefault(dist_cn, set()).add(req_cn)
|
|
70
|
+
uses_by_req.setdefault(req_cn, set()).add(dist_cn)
|
|
71
|
+
|
|
72
|
+
roots: list[str] = []
|
|
73
|
+
for d in sorted(dists):
|
|
74
|
+
if not uses_by_req.get(d):
|
|
75
|
+
roots.append(d)
|
|
76
|
+
|
|
77
|
+
return roots
|