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 CHANGED
@@ -51,7 +51,7 @@
51
51
  "module": ".cli.clicli",
52
52
  "attr": "_CLI_MODULE",
53
53
  "file": "omdev/cli/clicli.py",
54
- "line": 103,
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": 128,
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": 164,
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
- mod_name = globals()['__spec__'].name
54
- tool_name = '.'.join([mod_name.partition('.')[0], 'tools', 'pip'])
54
+ latest_version = lookup_latest_package_version(__package__.split('.')[0])
55
55
 
56
- out = subprocess.check_output([
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
- ({s for l in out.splitlines() if (s := l.strip())} | set(self.args.extra_deps or []))
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(['git', '--version'])
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
- ], cwd=cwd).decode().strip())
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
- ], cwd=cwd).decode().strip()
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
omdev/pycharm/cli.py CHANGED
@@ -1,3 +1,7 @@
1
+ """
2
+ TODO:
3
+ - PyCharm.app/Contents/plugins/python-ce/helpers/pydev/_pydevd_bundle/pydevd_constants.py -> USE_LOW_IMPACT_MONITORING
4
+ """
1
5
  import inspect
2
6
  import os.path
3
7
  import subprocess