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.
- omdev/.omlish-manifests.json +18 -30
- omdev/README.md +51 -0
- omdev/__about__.py +11 -7
- omdev/amalg/gen/gen.py +49 -6
- omdev/amalg/gen/imports.py +1 -1
- omdev/amalg/gen/manifests.py +1 -1
- omdev/amalg/gen/resources.py +1 -1
- omdev/amalg/gen/srcfiles.py +13 -3
- omdev/amalg/gen/strip.py +1 -1
- omdev/amalg/gen/types.py +1 -1
- omdev/amalg/gen/typing.py +1 -1
- omdev/amalg/info.py +32 -0
- omdev/cache/data/actions.py +1 -1
- omdev/cache/data/specs.py +1 -1
- omdev/cexts/_boilerplate.cc +2 -3
- omdev/cexts/cmake.py +4 -1
- omdev/ci/cli.py +2 -3
- omdev/cli/clicli.py +37 -7
- omdev/cmdlog/cli.py +1 -2
- omdev/dataclasses/_dumping.py +1960 -0
- omdev/dataclasses/_template.py +22 -0
- omdev/dataclasses/cli.py +7 -2
- omdev/dataclasses/codegen.py +340 -60
- omdev/dataclasses/dumping.py +200 -0
- omdev/interp/cli.py +1 -1
- omdev/interp/types.py +3 -2
- omdev/interp/uv/provider.py +37 -0
- omdev/interp/venvs.py +1 -0
- omdev/irc/messages/base.py +50 -0
- omdev/irc/messages/formats.py +92 -0
- omdev/irc/messages/messages.py +775 -0
- omdev/irc/messages/parsing.py +99 -0
- omdev/irc/numerics/__init__.py +0 -0
- omdev/irc/numerics/formats.py +97 -0
- omdev/irc/numerics/numerics.py +865 -0
- omdev/irc/numerics/types.py +59 -0
- omdev/irc/protocol/LICENSE +11 -0
- omdev/irc/protocol/__init__.py +61 -0
- omdev/irc/protocol/consts.py +6 -0
- omdev/irc/protocol/errors.py +30 -0
- omdev/irc/protocol/message.py +21 -0
- omdev/irc/protocol/nuh.py +55 -0
- omdev/irc/protocol/parsing.py +158 -0
- omdev/irc/protocol/rendering.py +153 -0
- omdev/irc/protocol/tags.py +102 -0
- omdev/irc/protocol/utils.py +30 -0
- omdev/manifests/_dumping.py +125 -25
- omdev/manifests/main.py +1 -1
- omdev/markdown/__init__.py +0 -0
- omdev/markdown/incparse.py +116 -0
- omdev/markdown/tokens.py +51 -0
- omdev/packaging/marshal.py +8 -8
- omdev/packaging/requires.py +6 -6
- omdev/packaging/revisions.py +1 -1
- omdev/packaging/specifiers.py +2 -1
- omdev/packaging/versions.py +4 -4
- omdev/packaging/wheelfile.py +2 -0
- omdev/precheck/blanklines.py +66 -0
- omdev/precheck/caches.py +1 -1
- omdev/precheck/imports.py +14 -1
- omdev/precheck/main.py +4 -3
- omdev/precheck/unicode.py +39 -15
- omdev/py/asts/__init__.py +0 -0
- omdev/py/asts/parents.py +28 -0
- omdev/py/asts/toplevel.py +123 -0
- omdev/py/asts/visitors.py +18 -0
- omdev/py/attrdocs.py +1 -1
- omdev/py/bracepy.py +12 -4
- omdev/py/reprs.py +32 -0
- omdev/py/srcheaders.py +1 -1
- omdev/py/tokens/__init__.py +0 -0
- omdev/py/tools/mkrelimp.py +1 -1
- omdev/py/tools/pipdepup.py +686 -0
- omdev/pyproject/cli.py +1 -1
- omdev/pyproject/pkg.py +190 -45
- omdev/pyproject/reqs.py +31 -9
- omdev/pyproject/tools/__init__.py +0 -0
- omdev/pyproject/tools/aboutdeps.py +60 -0
- omdev/pyproject/venvs.py +8 -1
- omdev/rs/__init__.py +0 -0
- omdev/scripts/ci.py +752 -98
- omdev/scripts/interp.py +232 -39
- omdev/scripts/lib/inject.py +74 -27
- omdev/scripts/lib/logs.py +187 -43
- omdev/scripts/lib/marshal.py +67 -25
- omdev/scripts/pyproject.py +1369 -143
- omdev/tools/git/cli.py +10 -0
- omdev/tools/json/formats.py +2 -0
- omdev/tools/json/processing.py +5 -2
- omdev/tools/jsonview/cli.py +49 -65
- omdev/tools/jsonview/resources/jsonview.html.j2 +43 -0
- omdev/tools/pawk/README.md +195 -0
- omdev/tools/pawk/pawk.py +2 -2
- omdev/tools/pip.py +8 -0
- omdev/tui/__init__.py +0 -0
- omdev/tui/apps/__init__.py +0 -0
- omdev/tui/apps/edit/__init__.py +0 -0
- omdev/tui/apps/edit/main.py +167 -0
- omdev/tui/apps/irc/__init__.py +0 -0
- omdev/tui/apps/irc/__main__.py +4 -0
- omdev/tui/apps/irc/app.py +286 -0
- omdev/tui/apps/irc/client.py +187 -0
- omdev/tui/apps/irc/commands.py +175 -0
- omdev/tui/apps/irc/main.py +26 -0
- omdev/tui/apps/markdown/__init__.py +0 -0
- omdev/tui/apps/markdown/__main__.py +11 -0
- omdev/{ptk → tui/apps}/markdown/cli.py +5 -7
- omdev/tui/rich/__init__.py +46 -0
- omdev/tui/rich/console2.py +20 -0
- omdev/tui/rich/markdown2.py +186 -0
- omdev/tui/textual/__init__.py +265 -0
- omdev/tui/textual/app2.py +16 -0
- omdev/tui/textual/autocomplete/LICENSE +21 -0
- omdev/tui/textual/autocomplete/__init__.py +33 -0
- omdev/tui/textual/autocomplete/matching.py +226 -0
- omdev/tui/textual/autocomplete/paths.py +202 -0
- omdev/tui/textual/autocomplete/widget.py +612 -0
- omdev/tui/textual/debug/__init__.py +10 -0
- omdev/tui/textual/debug/dominfo.py +151 -0
- omdev/tui/textual/debug/screen.py +24 -0
- omdev/tui/textual/devtools.py +187 -0
- omdev/tui/textual/drivers2.py +55 -0
- omdev/tui/textual/logging2.py +20 -0
- omdev/tui/textual/types.py +45 -0
- {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/METADATA +15 -9
- {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/RECORD +135 -80
- omdev/ptk/__init__.py +0 -103
- omdev/ptk/apps/ncdu.py +0 -167
- omdev/ptk/confirm.py +0 -60
- omdev/ptk/markdown/LICENSE +0 -22
- omdev/ptk/markdown/__init__.py +0 -10
- omdev/ptk/markdown/__main__.py +0 -11
- omdev/ptk/markdown/border.py +0 -94
- omdev/ptk/markdown/markdown.py +0 -390
- omdev/ptk/markdown/parser.py +0 -42
- omdev/ptk/markdown/styles.py +0 -29
- omdev/ptk/markdown/tags.py +0 -299
- omdev/ptk/markdown/utils.py +0 -366
- omdev/pyproject/cexts.py +0 -110
- /omdev/{ptk/apps → irc}/__init__.py +0 -0
- /omdev/{tokens → irc/messages}/__init__.py +0 -0
- /omdev/{tokens → py/tokens}/all.py +0 -0
- /omdev/{tokens → py/tokens}/tokenizert.py +0 -0
- /omdev/{tokens → py/tokens}/utils.py +0 -0
- {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev440.dist-info → omdev-0.0.0.dev495.dist-info}/licenses/LICENSE +0 -0
- {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
|