omdev 0.0.0.dev440__py3-none-any.whl → 0.0.0.dev495__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.

Files changed (148) hide show
  1. omdev/.omlish-manifests.json +18 -30
  2. omdev/README.md +51 -0
  3. omdev/__about__.py +11 -7
  4. omdev/amalg/gen/gen.py +49 -6
  5. omdev/amalg/gen/imports.py +1 -1
  6. omdev/amalg/gen/manifests.py +1 -1
  7. omdev/amalg/gen/resources.py +1 -1
  8. omdev/amalg/gen/srcfiles.py +13 -3
  9. omdev/amalg/gen/strip.py +1 -1
  10. omdev/amalg/gen/types.py +1 -1
  11. omdev/amalg/gen/typing.py +1 -1
  12. omdev/amalg/info.py +32 -0
  13. omdev/cache/data/actions.py +1 -1
  14. omdev/cache/data/specs.py +1 -1
  15. omdev/cexts/_boilerplate.cc +2 -3
  16. omdev/cexts/cmake.py +4 -1
  17. omdev/ci/cli.py +2 -3
  18. omdev/cli/clicli.py +37 -7
  19. omdev/cmdlog/cli.py +1 -2
  20. omdev/dataclasses/_dumping.py +1960 -0
  21. omdev/dataclasses/_template.py +22 -0
  22. omdev/dataclasses/cli.py +7 -2
  23. omdev/dataclasses/codegen.py +340 -60
  24. omdev/dataclasses/dumping.py +200 -0
  25. omdev/interp/cli.py +1 -1
  26. omdev/interp/types.py +3 -2
  27. omdev/interp/uv/provider.py +37 -0
  28. omdev/interp/venvs.py +1 -0
  29. omdev/irc/messages/base.py +50 -0
  30. omdev/irc/messages/formats.py +92 -0
  31. omdev/irc/messages/messages.py +775 -0
  32. omdev/irc/messages/parsing.py +99 -0
  33. omdev/irc/numerics/__init__.py +0 -0
  34. omdev/irc/numerics/formats.py +97 -0
  35. omdev/irc/numerics/numerics.py +865 -0
  36. omdev/irc/numerics/types.py +59 -0
  37. omdev/irc/protocol/LICENSE +11 -0
  38. omdev/irc/protocol/__init__.py +61 -0
  39. omdev/irc/protocol/consts.py +6 -0
  40. omdev/irc/protocol/errors.py +30 -0
  41. omdev/irc/protocol/message.py +21 -0
  42. omdev/irc/protocol/nuh.py +55 -0
  43. omdev/irc/protocol/parsing.py +158 -0
  44. omdev/irc/protocol/rendering.py +153 -0
  45. omdev/irc/protocol/tags.py +102 -0
  46. omdev/irc/protocol/utils.py +30 -0
  47. omdev/manifests/_dumping.py +125 -25
  48. omdev/manifests/main.py +1 -1
  49. omdev/markdown/__init__.py +0 -0
  50. omdev/markdown/incparse.py +116 -0
  51. omdev/markdown/tokens.py +51 -0
  52. omdev/packaging/marshal.py +8 -8
  53. omdev/packaging/requires.py +6 -6
  54. omdev/packaging/revisions.py +1 -1
  55. omdev/packaging/specifiers.py +2 -1
  56. omdev/packaging/versions.py +4 -4
  57. omdev/packaging/wheelfile.py +2 -0
  58. omdev/precheck/blanklines.py +66 -0
  59. omdev/precheck/caches.py +1 -1
  60. omdev/precheck/imports.py +14 -1
  61. omdev/precheck/main.py +4 -3
  62. omdev/precheck/unicode.py +39 -15
  63. omdev/py/asts/__init__.py +0 -0
  64. omdev/py/asts/parents.py +28 -0
  65. omdev/py/asts/toplevel.py +123 -0
  66. omdev/py/asts/visitors.py +18 -0
  67. omdev/py/attrdocs.py +1 -1
  68. omdev/py/bracepy.py +12 -4
  69. omdev/py/reprs.py +32 -0
  70. omdev/py/srcheaders.py +1 -1
  71. omdev/py/tokens/__init__.py +0 -0
  72. omdev/py/tools/mkrelimp.py +1 -1
  73. omdev/py/tools/pipdepup.py +686 -0
  74. omdev/pyproject/cli.py +1 -1
  75. omdev/pyproject/pkg.py +190 -45
  76. omdev/pyproject/reqs.py +31 -9
  77. omdev/pyproject/tools/__init__.py +0 -0
  78. omdev/pyproject/tools/aboutdeps.py +60 -0
  79. omdev/pyproject/venvs.py +8 -1
  80. omdev/rs/__init__.py +0 -0
  81. omdev/scripts/ci.py +752 -98
  82. omdev/scripts/interp.py +232 -39
  83. omdev/scripts/lib/inject.py +74 -27
  84. omdev/scripts/lib/logs.py +187 -43
  85. omdev/scripts/lib/marshal.py +67 -25
  86. omdev/scripts/pyproject.py +1369 -143
  87. omdev/tools/git/cli.py +10 -0
  88. omdev/tools/json/formats.py +2 -0
  89. omdev/tools/json/processing.py +5 -2
  90. omdev/tools/jsonview/cli.py +49 -65
  91. omdev/tools/jsonview/resources/jsonview.html.j2 +43 -0
  92. omdev/tools/pawk/README.md +195 -0
  93. omdev/tools/pawk/pawk.py +2 -2
  94. omdev/tools/pip.py +8 -0
  95. omdev/tui/__init__.py +0 -0
  96. omdev/tui/apps/__init__.py +0 -0
  97. omdev/tui/apps/edit/__init__.py +0 -0
  98. omdev/tui/apps/edit/main.py +167 -0
  99. omdev/tui/apps/irc/__init__.py +0 -0
  100. omdev/tui/apps/irc/__main__.py +4 -0
  101. omdev/tui/apps/irc/app.py +286 -0
  102. omdev/tui/apps/irc/client.py +187 -0
  103. omdev/tui/apps/irc/commands.py +175 -0
  104. omdev/tui/apps/irc/main.py +26 -0
  105. omdev/tui/apps/markdown/__init__.py +0 -0
  106. omdev/tui/apps/markdown/__main__.py +11 -0
  107. omdev/{ptk → tui/apps}/markdown/cli.py +5 -7
  108. omdev/tui/rich/__init__.py +46 -0
  109. omdev/tui/rich/console2.py +20 -0
  110. omdev/tui/rich/markdown2.py +186 -0
  111. omdev/tui/textual/__init__.py +265 -0
  112. omdev/tui/textual/app2.py +16 -0
  113. omdev/tui/textual/autocomplete/LICENSE +21 -0
  114. omdev/tui/textual/autocomplete/__init__.py +33 -0
  115. omdev/tui/textual/autocomplete/matching.py +226 -0
  116. omdev/tui/textual/autocomplete/paths.py +202 -0
  117. omdev/tui/textual/autocomplete/widget.py +612 -0
  118. omdev/tui/textual/debug/__init__.py +10 -0
  119. omdev/tui/textual/debug/dominfo.py +151 -0
  120. omdev/tui/textual/debug/screen.py +24 -0
  121. omdev/tui/textual/devtools.py +187 -0
  122. omdev/tui/textual/drivers2.py +55 -0
  123. omdev/tui/textual/logging2.py +20 -0
  124. omdev/tui/textual/types.py +45 -0
  125. {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/METADATA +15 -9
  126. {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/RECORD +135 -80
  127. omdev/ptk/__init__.py +0 -103
  128. omdev/ptk/apps/ncdu.py +0 -167
  129. omdev/ptk/confirm.py +0 -60
  130. omdev/ptk/markdown/LICENSE +0 -22
  131. omdev/ptk/markdown/__init__.py +0 -10
  132. omdev/ptk/markdown/__main__.py +0 -11
  133. omdev/ptk/markdown/border.py +0 -94
  134. omdev/ptk/markdown/markdown.py +0 -390
  135. omdev/ptk/markdown/parser.py +0 -42
  136. omdev/ptk/markdown/styles.py +0 -29
  137. omdev/ptk/markdown/tags.py +0 -299
  138. omdev/ptk/markdown/utils.py +0 -366
  139. omdev/pyproject/cexts.py +0 -110
  140. /omdev/{ptk/apps → irc}/__init__.py +0 -0
  141. /omdev/{tokens → irc/messages}/__init__.py +0 -0
  142. /omdev/{tokens → py/tokens}/all.py +0 -0
  143. /omdev/{tokens → py/tokens}/tokenizert.py +0 -0
  144. /omdev/{tokens → py/tokens}/utils.py +0 -0
  145. {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/WHEEL +0 -0
  146. {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/entry_points.txt +0 -0
  147. {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/licenses/LICENSE +0 -0
  148. {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,686 @@
1
+ """
2
+ TODO:
3
+ - output 2 tables lol
4
+ - min_time_since_prev_version
5
+ - without this the min age is moot lol, can still catch a bad release at the same time of day just n days later
6
+ - * at least show 'suggested', 'suggested age', 'latest', 'latest age', 'number of releases between the 2' *
7
+ - how to handle non-linearity? new minor vers come out in parallel for diff major vers
8
+ - trie?
9
+ - find which reqs file + lineno to update
10
+ - auto update
11
+
12
+ #
13
+
14
+ https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns
15
+ https://news.ycombinator.com/item?id=46005111
16
+ """
17
+ # Copyright (c) 2008-present The pip developers (see AUTHORS.txt file)
18
+ #
19
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
20
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
21
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
22
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
23
+ #
24
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
25
+ # Software.
26
+ #
27
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
28
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
29
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
30
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
31
+ # ~> https://github.com/pypa/pip/blob/a52069365063ea813fe3a3f8bac90397c9426d35/src/pip/_internal/commands/list.py (25.3)
32
+ import dataclasses as dc
33
+ import datetime
34
+ import itertools
35
+ import os.path
36
+ import ssl
37
+ import typing as ta
38
+
39
+ from pip._internal.index.package_finder import LinkEvaluator # noqa
40
+ from pip._internal.index.package_finder import PackageFinder # noqa
41
+ from pip._internal.metadata import BaseDistribution # noqa
42
+ from pip._internal.models.candidate import InstallationCandidate # noqa
43
+ from pip._internal.models.link import Link # noqa
44
+ from pip._internal.network.session import PipSession # noqa
45
+ from pip._vendor.packaging.version import Version # noqa
46
+
47
+ from omlish import cached
48
+ from omlish import check
49
+ from omlish import collections as col
50
+ from omlish.concurrent import all as conc
51
+ from omlish.formats import json
52
+ from omlish.sync import ObjectPool
53
+
54
+
55
+ ##
56
+
57
+
58
+ @cached.function
59
+ def now_utc() -> datetime.datetime:
60
+ return datetime.datetime.now(datetime.UTC)
61
+
62
+
63
+ ##
64
+
65
+
66
+ @dc.dataclass(frozen=True, kw_only=True)
67
+ class IndexOptions:
68
+ no_index: bool = False
69
+ index_url: str | None = None
70
+ extra_index_urls: ta.Sequence[str] | None = None
71
+
72
+ def get_index_urls(self) -> list[str] | None:
73
+ if self.no_index:
74
+ return []
75
+
76
+ index_urls: list[str] = []
77
+ if (index_url := self.index_url) is None:
78
+ from pip._internal.models.index import PyPI # noqa
79
+ index_url = PyPI.simple_url
80
+ if index_url:
81
+ index_urls.append(index_url)
82
+
83
+ if (ext := self.extra_index_urls):
84
+ index_urls.extend(ext)
85
+
86
+ return index_urls or None
87
+
88
+
89
+ ##
90
+
91
+
92
+ @dc.dataclass(frozen=True, kw_only=True)
93
+ class CacheOpts:
94
+ no_cache: bool = False
95
+ cache_dir: str | None = None
96
+
97
+ def get_cache_dir(self) -> str | None:
98
+ if self.no_cache:
99
+ return None
100
+
101
+ if (cache_dir := self.cache_dir) is None:
102
+ from pip._internal.locations.base import USER_CACHE_DIR # noqa
103
+ cache_dir = USER_CACHE_DIR
104
+
105
+ check.state(not cache_dir or os.path.isabs(cache_dir))
106
+ return cache_dir
107
+
108
+
109
+ ##
110
+
111
+
112
+ @dc.dataclass(frozen=True, kw_only=True)
113
+ class SessionOpts:
114
+ DEFAULT_RETRIES: ta.ClassVar[int] = 5
115
+ retries: int | None = DEFAULT_RETRIES
116
+
117
+ DEFAULT_TIMEOUT: ta.ClassVar[int | None] = 15
118
+ timeout: int | None = DEFAULT_TIMEOUT
119
+
120
+ cert: str | None = None
121
+ client_cert: str | None = None
122
+ trusted_hosts: ta.Sequence[str] | None = None
123
+ proxy: str | None = None
124
+
125
+ keyring_provider: ta.Literal['auto', 'disabled', 'import', 'subprocess'] = 'auto'
126
+ no_input: bool = False
127
+
128
+
129
+ def _create_truststore_ssl_context() -> ssl.SSLContext:
130
+ from pip._vendor import certifi # noqa
131
+ from pip._vendor import truststore # noqa
132
+
133
+ ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
134
+ ctx.load_verify_locations(certifi.where())
135
+ return ctx
136
+
137
+
138
+ def build_session(
139
+ session_opts: SessionOpts = SessionOpts(),
140
+ *,
141
+ cache_opts: CacheOpts = CacheOpts(),
142
+ index_opts: IndexOptions = IndexOptions(),
143
+ ) -> PipSession:
144
+ ssl_context = _create_truststore_ssl_context()
145
+
146
+ session = PipSession(
147
+ cache=os.path.join(cache_dir, 'http-v2') if (cache_dir := cache_opts.get_cache_dir()) else None,
148
+ retries=session_opts.retries or 0,
149
+ trusted_hosts=session_opts.trusted_hosts or [],
150
+ index_urls=index_opts.get_index_urls(),
151
+ ssl_context=ssl_context,
152
+ )
153
+
154
+ # Handle custom ca-bundles from the user
155
+ if session_opts.cert:
156
+ session.verify = session_opts.cert # type: ignore[assignment]
157
+
158
+ # Handle SSL client certificate
159
+ if session_opts.client_cert:
160
+ session.cert = session_opts.client_cert
161
+
162
+ # Handle timeouts
163
+ if session_opts.timeout:
164
+ session.timeout = session_opts.timeout
165
+
166
+ # Handle configured proxies
167
+ if session_opts.proxy:
168
+ session.proxies = {
169
+ 'http': session_opts.proxy,
170
+ 'https': session_opts.proxy,
171
+ }
172
+ session.trust_env = False
173
+ session.pip_proxy = session_opts.proxy # type: ignore[assignment]
174
+
175
+ # Determine if we can prompt the user for authentication or not
176
+ auth: ta.Any = session.auth
177
+ auth.prompting = not session_opts.no_input
178
+ auth.keyring_provider = session_opts.keyring_provider
179
+
180
+ return session
181
+
182
+
183
+ ##
184
+
185
+
186
+ class MyPackageFinder(PackageFinder):
187
+ def __init__(self, *args: ta.Any, **kwargs: ta.Any) -> None:
188
+ super().__init__(*args, **kwargs)
189
+
190
+ self._link_pypi_dict_by_hash: dict[str, dict[str, ta.Any]] = {}
191
+
192
+ def get_link_pypi_dict(self, link: Link) -> dict[str, ta.Any] | None:
193
+ if link.hash is None:
194
+ return None
195
+ return self._link_pypi_dict_by_hash.get(link.hash)
196
+
197
+ def process_project_url(
198
+ self,
199
+ project_url: Link,
200
+ link_evaluator: LinkEvaluator,
201
+ ) -> list[InstallationCandidate]:
202
+ index_response = self._link_collector.fetch_response(project_url)
203
+ if index_response is None:
204
+ return []
205
+
206
+ page_links: list[Link] = []
207
+ if index_response.content_type.lower().startswith('application/vnd.pypi.simple.v1+json'):
208
+ data = json.loads(index_response.content)
209
+ for file in data.get('files', []):
210
+ link = Link.from_json(file, index_response.url)
211
+ if link is None:
212
+ continue
213
+ if link.hash is not None:
214
+ self._link_pypi_dict_by_hash[link.hash] = file
215
+ page_links.append(link)
216
+
217
+ else:
218
+ from pip._internal.index.collector import parse_links # noqa
219
+ page_links = list(parse_links(index_response))
220
+
221
+ from pip._internal.utils.logging import indent_log # noqa
222
+ with indent_log():
223
+ package_links = self.evaluate_links(
224
+ link_evaluator,
225
+ links=page_links,
226
+ )
227
+
228
+ return package_links
229
+
230
+
231
+ def build_package_finder(
232
+ session: PipSession,
233
+ *,
234
+ index_opts: IndexOptions = IndexOptions(),
235
+
236
+ find_links: ta.Sequence[str] | None = None,
237
+ pre: bool = False,
238
+ ) -> MyPackageFinder:
239
+ from pip._internal.models.search_scope import SearchScope # noqa
240
+ search_scope = SearchScope.create(
241
+ find_links=list(find_links or []),
242
+ index_urls=list(index_opts.get_index_urls() or []),
243
+ no_index=index_opts.no_index,
244
+ )
245
+
246
+ from pip._internal.index.collector import LinkCollector # noqa
247
+ link_collector = LinkCollector(
248
+ session=session,
249
+ search_scope=search_scope,
250
+ )
251
+
252
+ # Pass allow_yanked=False to ignore yanked versions.
253
+ from pip._internal.models.selection_prefs import SelectionPreferences # noqa
254
+ selection_prefs = SelectionPreferences(
255
+ allow_yanked=False,
256
+ allow_all_prereleases=pre,
257
+ )
258
+
259
+ return check.isinstance(MyPackageFinder.create(
260
+ link_collector=link_collector,
261
+ selection_prefs=selection_prefs,
262
+ ), MyPackageFinder)
263
+
264
+
265
+ ##
266
+
267
+
268
+ def get_dists(
269
+ *,
270
+ path: list[str] | None = None,
271
+ local: bool = False,
272
+ user: bool = False,
273
+ editable: bool = False,
274
+ include_editable: bool = True,
275
+ skip: ta.Container | None = None,
276
+ ) -> list[BaseDistribution]:
277
+ from pip._internal.metadata import get_environment # noqa
278
+ return list(get_environment(path).iter_installed_distributions(
279
+ local_only=local,
280
+ user_only=user,
281
+ editables_only=editable,
282
+ include_editables=include_editable,
283
+ skip=skip or set(),
284
+ ))
285
+
286
+
287
+ ##
288
+
289
+
290
+ @dc.dataclass()
291
+ class Package:
292
+ dist: BaseDistribution
293
+
294
+ @cached.function
295
+ def version(self) -> str:
296
+ from pip._vendor.packaging.version import InvalidVersion # noqa
297
+ try:
298
+ return str(self.dist.version)
299
+ except InvalidVersion:
300
+ pass
301
+ return self.dist.raw_version
302
+
303
+ @dc.dataclass(frozen=True)
304
+ class Candidate:
305
+ install: InstallationCandidate
306
+
307
+ pypi_dict: dict[str, ta.Any] | None = None
308
+
309
+ @cached.function
310
+ def upload_time(self) -> datetime.datetime | None:
311
+ if (ut := (self.pypi_dict or {}).get('upload-time')) is None:
312
+ return None
313
+
314
+ return datetime.datetime.fromisoformat(check.isinstance(ut, str)) # noqa
315
+
316
+ @cached.property
317
+ def version(self) -> Version:
318
+ return self.install.version
319
+
320
+ @cached.property
321
+ def filetype(self) -> ta.Literal['wheel', 'sdist']:
322
+ if self.install.link.is_wheel:
323
+ return 'wheel'
324
+ else:
325
+ return 'sdist'
326
+
327
+ unfiltered_candidates: ta.Sequence[Candidate] | None = None
328
+ candidates: ta.Sequence[Candidate] | None = None
329
+
330
+ latest_candidate: Candidate | None = None
331
+ suggested_candidate: Candidate | None = None
332
+
333
+
334
+ def get_best_candidate(
335
+ pkg: Package,
336
+ finder: MyPackageFinder,
337
+ candidates: ta.Sequence[Package.Candidate],
338
+ ) -> Package.Candidate | None:
339
+ candidates_by_install: ta.Mapping[InstallationCandidate, Package.Candidate] = col.make_map((
340
+ (c.install, c)
341
+ for c in candidates
342
+ ), strict=True, identity=True)
343
+
344
+ evaluator = finder.make_candidate_evaluator(
345
+ project_name=pkg.dist.canonical_name,
346
+ )
347
+ best_install = evaluator.sort_best_candidate([c.install for c in candidates])
348
+ if best_install is None:
349
+ return None
350
+
351
+ return candidates_by_install[best_install]
352
+
353
+
354
+ def set_package_finder_info(
355
+ pkg: Package,
356
+ finder: MyPackageFinder,
357
+ *,
358
+ pre: bool = False,
359
+ max_uploaded_at: datetime.datetime | None = None,
360
+ min_time_since_prev_version: datetime.timedelta | None = None,
361
+ ) -> None:
362
+ pkg.unfiltered_candidates = [
363
+ Package.Candidate(
364
+ c,
365
+ finder.get_link_pypi_dict(c.link),
366
+ )
367
+ for c in finder.find_all_candidates(pkg.dist.canonical_name)
368
+ ]
369
+
370
+ #
371
+
372
+ candidates = pkg.unfiltered_candidates
373
+
374
+ if not pre:
375
+ # Remove prereleases
376
+ candidates = [
377
+ c
378
+ for c in candidates
379
+ if not c.install.version.is_prerelease
380
+ ]
381
+
382
+ pkg.candidates = candidates
383
+
384
+ #
385
+
386
+ pkg.latest_candidate = get_best_candidate(pkg, finder, pkg.candidates)
387
+
388
+ #
389
+
390
+ suggested_candidates = candidates
391
+
392
+ if min_time_since_prev_version is not None:
393
+ # candidates_by_version = col.multi_map((c.install.version, c) for c in candidates)
394
+ # uploaded_at_by_version = {
395
+ # v: min([c_ut for c in cs if (c_ut := c.upload_time()) is not None], default=None)
396
+ # for v, cs in candidates_by_version.items()
397
+ # }
398
+ raise NotImplementedError
399
+
400
+ if max_uploaded_at is not None:
401
+ suggested_candidates = [
402
+ c
403
+ for c in suggested_candidates
404
+ if not (
405
+ (c_dt := c.upload_time()) is not None and
406
+ c_dt > max_uploaded_at
407
+ )
408
+ ]
409
+
410
+ pkg.suggested_candidate = get_best_candidate(pkg, finder, suggested_candidates)
411
+
412
+
413
+ ##
414
+
415
+
416
+ class Context:
417
+ def __init__(self) -> None:
418
+ super().__init__()
419
+
420
+ #
421
+
422
+ _session: PipSession | None = None
423
+
424
+ def session(self) -> PipSession:
425
+ if self._session is None:
426
+ self._session = build_session()
427
+ return self._session
428
+
429
+ #
430
+
431
+ _finder: MyPackageFinder | None = None
432
+
433
+ def finder(self) -> MyPackageFinder:
434
+ if self._finder is None:
435
+ self._finder = build_package_finder(self.session())
436
+ return self._finder
437
+
438
+ #
439
+
440
+ def close(self) -> None:
441
+ if self._session is not None:
442
+ self._session.close()
443
+ self._session = None
444
+
445
+
446
+ ##
447
+
448
+
449
+ def human_round_td(td: datetime.timedelta) -> str:
450
+ """Round a timedelta to its largest sensible unit."""
451
+
452
+ seconds = td.total_seconds()
453
+
454
+ # Define unit sizes in seconds
455
+ units = [
456
+ ('y', 365 * 24 * 3600),
457
+ ('mo', 30 * 24 * 3600),
458
+ ('w', 7 * 24 * 3600),
459
+ ('d', 24 * 3600),
460
+ ('h', 3600),
461
+ ('m', 60),
462
+ ('s', 1),
463
+ ]
464
+
465
+ for suffix, unit_seconds in units:
466
+ value = seconds / unit_seconds
467
+ if abs(value) >= 1: # first unit where magnitude is >= 1
468
+ return f'{round(value)}{suffix}'
469
+
470
+ return '0s'
471
+
472
+
473
+ #
474
+
475
+
476
+ def format_for_json(
477
+ pkgs: ta.Sequence[Package],
478
+ *,
479
+ now: datetime.datetime | None = None,
480
+ ) -> list[dict[str, ta.Any]]:
481
+ infos: list[dict[str, ta.Any]] = []
482
+
483
+ for pkg in pkgs:
484
+ latest = check.not_none(pkg.latest_candidate)
485
+ suggested = check.not_none(pkg.suggested_candidate)
486
+
487
+ info = {
488
+ 'name': pkg.dist.raw_name,
489
+ 'version': pkg.version(),
490
+ 'location': pkg.dist.location or '',
491
+ 'installer': pkg.dist.installer,
492
+ 'latest_version': str(latest.install.version),
493
+ 'latest_upload_time': lut.isoformat() if (lut := latest.upload_time()) is not None else None,
494
+ 'suggested_version': str(suggested.install.version),
495
+ 'suggested_upload_time': sut.isoformat() if (sut := suggested.upload_time()) is not None else None,
496
+ }
497
+
498
+ if editable_project_location := pkg.dist.editable_project_location:
499
+ info['editable_project_location'] = editable_project_location
500
+
501
+ infos.append(info)
502
+
503
+ return infos
504
+
505
+
506
+ #
507
+
508
+
509
+ def format_for_columns(pkgs: ta.Sequence[Package]) -> tuple[list[list[str]], list[str]]:
510
+ """Convert the package data into something usable by output_package_listing_columns."""
511
+
512
+ header = [
513
+ 'Package',
514
+ 'Current',
515
+
516
+ 'Suggested',
517
+ 'Age',
518
+
519
+ 'Latest',
520
+ 'Age',
521
+ ]
522
+
523
+ # def wheel_build_tag(dist: BaseDistribution) -> str | None:
524
+ # try:
525
+ # wheel_file = dist.read_text('WHEEL')
526
+ # except FileNotFoundError:
527
+ # return None
528
+ # return email.parser.Parser().parsestr(wheel_file).get('Build')
529
+
530
+ # build_tags = [wheel_build_tag(p.dist) for p in pkgs]
531
+ # has_build_tags = any(build_tags)
532
+ # if has_build_tags:
533
+ # header.append('Build')
534
+
535
+ # has_editables = any(x.dist.editable for x in pkgs)
536
+ # if has_editables:
537
+ # header.append('Editable project location')
538
+
539
+ rows = []
540
+ for pkg in pkgs:
541
+ sc = check.not_none(pkg.suggested_candidate)
542
+ lc = check.not_none(pkg.latest_candidate)
543
+
544
+ row = [
545
+ pkg.dist.raw_name,
546
+ pkg.dist.raw_version,
547
+ ]
548
+
549
+ def add_c(c):
550
+ if c is None:
551
+ row.extend(['', ''])
552
+ return
553
+
554
+ row.append(str(c.version))
555
+
556
+ if (l_ut := c.upload_time()) is not None:
557
+ row.append(human_round_td(now_utc() - l_ut))
558
+ else:
559
+ row.append('')
560
+
561
+ add_c(sc if sc.version != pkg.dist.version else None)
562
+ add_c(lc if sc is not lc else None)
563
+
564
+ # if has_build_tags:
565
+ # row.append(build_tags[i] or '')
566
+
567
+ # if has_editables:
568
+ # row.append(pkg.dist.editable_project_location or '')
569
+
570
+ rows.append(row)
571
+
572
+ return rows, header
573
+
574
+
575
+ def _tabulate(
576
+ rows: ta.Iterable[ta.Iterable[ta.Any]],
577
+ *,
578
+ sep: str = ' ',
579
+ ) -> tuple[list[str], list[int]]:
580
+ """
581
+ Return a list of formatted rows and a list of column sizes.
582
+
583
+ For example::
584
+
585
+ >>> tabulate([['foobar', 2000], [0xdeadbeef]])
586
+ (['foobar 2000', '3735928559'], [10, 4])
587
+ """
588
+
589
+ rows = [tuple(map(str, row)) for row in rows]
590
+ sizes = [max(map(len, col)) for col in itertools.zip_longest(*rows, fillvalue='')]
591
+ table = [sep.join(map(str.ljust, row, sizes)).rstrip() for row in rows]
592
+ return table, sizes
593
+
594
+
595
+ def render_package_listing_columns(data: list[list[str]], header: list[str]) -> list[str]:
596
+ # insert the header first: we need to know the size of column names
597
+ if len(data) > 0:
598
+ data.insert(0, header)
599
+
600
+ pkg_strings, sizes = _tabulate(data, sep=' ')
601
+
602
+ # Create and add a separator.
603
+ if len(data) > 0:
604
+ pkg_strings.insert(1, ' '.join('-' * x for x in sizes))
605
+
606
+ return pkg_strings
607
+
608
+
609
+ ##
610
+
611
+
612
+ def _main() -> None:
613
+ import argparse
614
+
615
+ parser = argparse.ArgumentParser()
616
+ parser.add_argument('--exclude', action='append', dest='excludes')
617
+ parser.add_argument('--min-age-h', type=float, default=24)
618
+ parser.add_argument('-P', '--parallelism', type=int, default=3)
619
+ parser.add_argument('--json', action='store_true')
620
+ args = parser.parse_args()
621
+
622
+ max_uploaded_at: datetime.datetime | None = None
623
+ if args.min_age_h is not None:
624
+ max_uploaded_at = now_utc() - datetime.timedelta(hours=args.min_age_h)
625
+ min_time_since_prev_version: datetime.timedelta | None = None # datetime.timedelta(days=1)
626
+
627
+ #
628
+
629
+ from pip._internal.utils.compat import stdlib_pkgs # noqa
630
+ skip = set(stdlib_pkgs)
631
+ if args.excludes:
632
+ from pip._vendor.packaging.utils import canonicalize_name # noqa
633
+ skip.update(canonicalize_name(n) for n in args.excludes)
634
+
635
+ pkgs = [
636
+ Package(dist)
637
+ for dist in get_dists(
638
+ skip=skip,
639
+ )
640
+ ]
641
+
642
+ #
643
+
644
+ with ObjectPool[Context](Context).manage(lambda ctx: ctx.close()) as ctx_pool:
645
+ with conc.new_executor(args.parallelism) as exe:
646
+ def set_pkg_latest_info(pkg: Package) -> None:
647
+ with ctx_pool.acquire() as ctx: # noqa
648
+ set_package_finder_info(
649
+ pkg,
650
+ ctx.finder(),
651
+ max_uploaded_at=max_uploaded_at,
652
+ min_time_since_prev_version=min_time_since_prev_version,
653
+ )
654
+
655
+ conc.wait_all_futures_or_raise([
656
+ exe.submit(set_pkg_latest_info, pkg)
657
+ for pkg in pkgs
658
+ ])
659
+
660
+ #
661
+
662
+ outdated_pkgs = [
663
+ pkg
664
+ for pkg in pkgs
665
+ if (li := pkg.latest_candidate) is not None
666
+ and li.version > pkg.dist.version
667
+ ]
668
+
669
+ outdated_pkgs.sort(key=lambda x: x.dist.raw_name)
670
+
671
+ #
672
+
673
+ if args.json:
674
+ print(json.dumps_pretty(format_for_json(outdated_pkgs)))
675
+
676
+ else:
677
+ # stable_pkgs, unstable_pkgs = col.partition(
678
+ # outdated_pkgs,
679
+ # lambda pkg: pkg.latest_candidate is pkg.suggested_candidate,
680
+ # )
681
+
682
+ print('\n'.join(render_package_listing_columns(*format_for_columns(outdated_pkgs))))
683
+
684
+
685
+ if __name__ == '__main__':
686
+ _main()
omdev/pyproject/cli.py CHANGED
@@ -40,7 +40,7 @@ from omlish.formats.toml.parser import toml_loads
40
40
  from omlish.lite.cached import cached_nullary
41
41
  from omlish.lite.check import check
42
42
  from omlish.lite.runtime import check_lite_runtime_version
43
- from omlish.logs.standard import configure_standard_logging
43
+ from omlish.logs.std.standard import configure_standard_logging
44
44
 
45
45
  from .configs import PyprojectConfig
46
46
  from .configs import PyprojectConfigPreparer