omdev 0.0.0.dev486__py3-none-any.whl → 0.0.0.dev506__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 +2 -2
- omdev/README.md +51 -0
- omdev/__about__.py +4 -2
- omdev/ci/cli.py +1 -1
- omdev/cli/clicli.py +37 -7
- omdev/dataclasses/cli.py +1 -1
- omdev/interp/cli.py +1 -1
- omdev/interp/types.py +3 -2
- omdev/interp/uv/provider.py +36 -0
- omdev/manifests/main.py +1 -1
- omdev/markdown/incparse.py +392 -0
- omdev/packaging/revisions.py +1 -1
- omdev/py/tools/pipdepup.py +150 -93
- omdev/pyproject/cli.py +2 -36
- omdev/pyproject/configs.py +1 -1
- omdev/pyproject/pkg.py +1 -1
- omdev/pyproject/reqs.py +8 -7
- omdev/pyproject/tools/aboutdeps.py +5 -0
- omdev/pyproject/tools/pyversions.py +47 -0
- omdev/pyproject/versions.py +40 -0
- omdev/scripts/ci.py +369 -26
- omdev/scripts/interp.py +51 -9
- omdev/scripts/lib/inject.py +8 -1
- omdev/scripts/lib/logs.py +117 -21
- omdev/scripts/pyproject.py +479 -76
- omdev/tools/git/cli.py +43 -13
- omdev/tools/json/formats.py +2 -0
- omdev/tools/jsonview/cli.py +19 -61
- omdev/tools/jsonview/resources/jsonview.html.j2 +43 -0
- omdev/tools/pawk/README.md +195 -0
- omdev/tools/sqlrepl.py +189 -78
- omdev/tui/apps/edit/main.py +5 -1
- omdev/tui/apps/irc/app.py +28 -20
- omdev/tui/apps/irc/commands.py +1 -1
- omdev/tui/rich/__init__.py +12 -0
- omdev/tui/rich/markdown2.py +219 -18
- omdev/tui/textual/__init__.py +41 -2
- omdev/tui/textual/app2.py +6 -1
- 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/logging2.py +20 -0
- omdev/tui/textual/types.py +45 -0
- {omdev-0.0.0.dev486.dist-info → omdev-0.0.0.dev506.dist-info}/METADATA +10 -6
- {omdev-0.0.0.dev486.dist-info → omdev-0.0.0.dev506.dist-info}/RECORD +50 -39
- {omdev-0.0.0.dev486.dist-info → omdev-0.0.0.dev506.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev486.dist-info → omdev-0.0.0.dev506.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev486.dist-info → omdev-0.0.0.dev506.dist-info}/licenses/LICENSE +0 -0
- {omdev-0.0.0.dev486.dist-info → omdev-0.0.0.dev506.dist-info}/top_level.txt +0 -0
omdev/py/tools/pipdepup.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"""
|
|
2
2
|
TODO:
|
|
3
|
+
- output 2 tables lol
|
|
3
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' *
|
|
4
7
|
- how to handle non-linearity? new minor vers come out in parallel for diff major vers
|
|
5
8
|
- trie?
|
|
6
9
|
- find which reqs file + lineno to update
|
|
@@ -28,7 +31,7 @@ https://news.ycombinator.com/item?id=46005111
|
|
|
28
31
|
# ~> https://github.com/pypa/pip/blob/a52069365063ea813fe3a3f8bac90397c9426d35/src/pip/_internal/commands/list.py (25.3)
|
|
29
32
|
import dataclasses as dc
|
|
30
33
|
import datetime
|
|
31
|
-
import
|
|
34
|
+
import itertools
|
|
32
35
|
import os.path
|
|
33
36
|
import ssl
|
|
34
37
|
import typing as ta
|
|
@@ -310,16 +313,42 @@ class Package:
|
|
|
310
313
|
|
|
311
314
|
return datetime.datetime.fromisoformat(check.isinstance(ut, str)) # noqa
|
|
312
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
|
|
313
328
|
candidates: ta.Sequence[Candidate] | None = None
|
|
314
329
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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)
|
|
318
343
|
|
|
319
|
-
|
|
320
|
-
|
|
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
|
|
321
350
|
|
|
322
|
-
|
|
351
|
+
return candidates_by_install[best_install]
|
|
323
352
|
|
|
324
353
|
|
|
325
354
|
def set_package_finder_info(
|
|
@@ -330,17 +359,18 @@ def set_package_finder_info(
|
|
|
330
359
|
max_uploaded_at: datetime.datetime | None = None,
|
|
331
360
|
min_time_since_prev_version: datetime.timedelta | None = None,
|
|
332
361
|
) -> None:
|
|
333
|
-
|
|
362
|
+
pkg.unfiltered_candidates = [
|
|
334
363
|
Package.Candidate(
|
|
335
364
|
c,
|
|
336
365
|
finder.get_link_pypi_dict(c.link),
|
|
337
366
|
)
|
|
338
367
|
for c in finder.find_all_candidates(pkg.dist.canonical_name)
|
|
339
368
|
]
|
|
340
|
-
pkg.candidates = candidates
|
|
341
369
|
|
|
342
370
|
#
|
|
343
371
|
|
|
372
|
+
candidates = pkg.unfiltered_candidates
|
|
373
|
+
|
|
344
374
|
if not pre:
|
|
345
375
|
# Remove prereleases
|
|
346
376
|
candidates = [
|
|
@@ -349,8 +379,16 @@ def set_package_finder_info(
|
|
|
349
379
|
if not c.install.version.is_prerelease
|
|
350
380
|
]
|
|
351
381
|
|
|
382
|
+
pkg.candidates = candidates
|
|
383
|
+
|
|
352
384
|
#
|
|
353
385
|
|
|
386
|
+
pkg.latest_candidate = get_best_candidate(pkg, finder, pkg.candidates)
|
|
387
|
+
|
|
388
|
+
#
|
|
389
|
+
|
|
390
|
+
suggested_candidates = candidates
|
|
391
|
+
|
|
354
392
|
if min_time_since_prev_version is not None:
|
|
355
393
|
# candidates_by_version = col.multi_map((c.install.version, c) for c in candidates)
|
|
356
394
|
# uploaded_at_by_version = {
|
|
@@ -359,46 +397,17 @@ def set_package_finder_info(
|
|
|
359
397
|
# }
|
|
360
398
|
raise NotImplementedError
|
|
361
399
|
|
|
362
|
-
#
|
|
363
|
-
|
|
364
400
|
if max_uploaded_at is not None:
|
|
365
|
-
|
|
401
|
+
suggested_candidates = [
|
|
366
402
|
c
|
|
367
|
-
for c in
|
|
403
|
+
for c in suggested_candidates
|
|
368
404
|
if not (
|
|
369
405
|
(c_dt := c.upload_time()) is not None and
|
|
370
406
|
c_dt > max_uploaded_at
|
|
371
407
|
)
|
|
372
408
|
]
|
|
373
409
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
candidates_by_install: ta.Mapping[InstallationCandidate, Package.Candidate] = col.make_map((
|
|
377
|
-
(c.install, c)
|
|
378
|
-
for c in candidates
|
|
379
|
-
), strict=True, identity=True)
|
|
380
|
-
|
|
381
|
-
evaluator = finder.make_candidate_evaluator(
|
|
382
|
-
project_name=pkg.dist.canonical_name,
|
|
383
|
-
)
|
|
384
|
-
best_install = evaluator.sort_best_candidate([c.install for c in candidates])
|
|
385
|
-
if best_install is None:
|
|
386
|
-
return
|
|
387
|
-
best_candidate = candidates_by_install[best_install]
|
|
388
|
-
|
|
389
|
-
#
|
|
390
|
-
|
|
391
|
-
remote_version = best_candidate.install.version
|
|
392
|
-
if best_candidate.install.link.is_wheel:
|
|
393
|
-
typ = 'wheel'
|
|
394
|
-
else:
|
|
395
|
-
typ = 'sdist'
|
|
396
|
-
|
|
397
|
-
pkg.latest_info = Package.LatestInfo(
|
|
398
|
-
candidate=best_candidate,
|
|
399
|
-
version=remote_version,
|
|
400
|
-
filetype=typ,
|
|
401
|
-
)
|
|
410
|
+
pkg.suggested_candidate = get_best_candidate(pkg, finder, suggested_candidates)
|
|
402
411
|
|
|
403
412
|
|
|
404
413
|
##
|
|
@@ -472,20 +481,20 @@ def format_for_json(
|
|
|
472
481
|
infos: list[dict[str, ta.Any]] = []
|
|
473
482
|
|
|
474
483
|
for pkg in pkgs:
|
|
475
|
-
|
|
484
|
+
latest = check.not_none(pkg.latest_candidate)
|
|
485
|
+
suggested = check.not_none(pkg.suggested_candidate)
|
|
476
486
|
|
|
477
487
|
info = {
|
|
478
488
|
'name': pkg.dist.raw_name,
|
|
479
489
|
'version': pkg.version(),
|
|
480
490
|
'location': pkg.dist.location or '',
|
|
481
491
|
'installer': pkg.dist.installer,
|
|
482
|
-
'latest_version': str(
|
|
483
|
-
'
|
|
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,
|
|
484
496
|
}
|
|
485
497
|
|
|
486
|
-
if (l_ut := latest_info.candidate.upload_time()) is not None:
|
|
487
|
-
info['latest_age'] = human_round_td(now_utc() - l_ut)
|
|
488
|
-
|
|
489
498
|
if editable_project_location := pkg.dist.editable_project_location:
|
|
490
499
|
info['editable_project_location'] = editable_project_location
|
|
491
500
|
|
|
@@ -500,46 +509,87 @@ def format_for_json(
|
|
|
500
509
|
def format_for_columns(pkgs: ta.Sequence[Package]) -> tuple[list[list[str]], list[str]]:
|
|
501
510
|
"""Convert the package data into something usable by output_package_listing_columns."""
|
|
502
511
|
|
|
503
|
-
header = [
|
|
512
|
+
header = [
|
|
513
|
+
'Package',
|
|
514
|
+
'Current',
|
|
504
515
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
wheel_file = dist.read_text('WHEEL')
|
|
508
|
-
except FileNotFoundError:
|
|
509
|
-
return None
|
|
510
|
-
return email.parser.Parser().parsestr(wheel_file).get('Build')
|
|
511
|
-
|
|
512
|
-
build_tags = [wheel_build_tag(p.dist) for p in pkgs]
|
|
513
|
-
has_build_tags = any(build_tags)
|
|
514
|
-
if has_build_tags:
|
|
515
|
-
header.append('Build')
|
|
516
|
-
|
|
517
|
-
has_editables = any(x.dist.editable for x in pkgs)
|
|
518
|
-
if has_editables:
|
|
519
|
-
header.append('Editable project location')
|
|
520
|
-
|
|
521
|
-
data = []
|
|
522
|
-
for i, proj in enumerate(pkgs):
|
|
523
|
-
# if we're working on the 'outdated' list, separate out the latest_version and type
|
|
524
|
-
row = [proj.dist.raw_name, proj.dist.raw_version]
|
|
525
|
-
|
|
526
|
-
latest_info = check.not_none(proj.latest_info)
|
|
527
|
-
row.append(str(latest_info.version))
|
|
528
|
-
if (l_ut := latest_info.candidate.upload_time()) is not None:
|
|
529
|
-
row.append(human_round_td(now_utc() - l_ut))
|
|
530
|
-
else:
|
|
531
|
-
row.append('')
|
|
532
|
-
row.append(latest_info.filetype)
|
|
516
|
+
'Suggested',
|
|
517
|
+
'Age',
|
|
533
518
|
|
|
534
|
-
|
|
535
|
-
|
|
519
|
+
'Latest',
|
|
520
|
+
'Age',
|
|
521
|
+
]
|
|
536
522
|
|
|
537
|
-
|
|
538
|
-
|
|
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')
|
|
539
529
|
|
|
540
|
-
|
|
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')
|
|
541
534
|
|
|
542
|
-
|
|
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
|
|
543
593
|
|
|
544
594
|
|
|
545
595
|
def render_package_listing_columns(data: list[list[str]], header: list[str]) -> list[str]:
|
|
@@ -547,12 +597,11 @@ def render_package_listing_columns(data: list[list[str]], header: list[str]) ->
|
|
|
547
597
|
if len(data) > 0:
|
|
548
598
|
data.insert(0, header)
|
|
549
599
|
|
|
550
|
-
|
|
551
|
-
pkg_strings, sizes = tabulate(data)
|
|
600
|
+
pkg_strings, sizes = _tabulate(data, sep=' ')
|
|
552
601
|
|
|
553
602
|
# Create and add a separator.
|
|
554
603
|
if len(data) > 0:
|
|
555
|
-
pkg_strings.insert(1, '
|
|
604
|
+
pkg_strings.insert(1, ' '.join('-' * x for x in sizes))
|
|
556
605
|
|
|
557
606
|
return pkg_strings
|
|
558
607
|
|
|
@@ -565,12 +614,14 @@ def _main() -> None:
|
|
|
565
614
|
|
|
566
615
|
parser = argparse.ArgumentParser()
|
|
567
616
|
parser.add_argument('--exclude', action='append', dest='excludes')
|
|
568
|
-
parser.add_argument('--min-age-h', type=float, default=
|
|
569
|
-
parser.add_argument('-P', '--parallelism', type=int, default=
|
|
617
|
+
parser.add_argument('--min-age-h', type=float, default=24)
|
|
618
|
+
parser.add_argument('-P', '--parallelism', type=int, default=3)
|
|
570
619
|
parser.add_argument('--json', action='store_true')
|
|
571
620
|
args = parser.parse_args()
|
|
572
621
|
|
|
573
|
-
max_uploaded_at: datetime.datetime | None =
|
|
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)
|
|
574
625
|
min_time_since_prev_version: datetime.timedelta | None = None # datetime.timedelta(days=1)
|
|
575
626
|
|
|
576
627
|
#
|
|
@@ -608,21 +659,27 @@ def _main() -> None:
|
|
|
608
659
|
|
|
609
660
|
#
|
|
610
661
|
|
|
611
|
-
|
|
662
|
+
outdated_pkgs = [
|
|
612
663
|
pkg
|
|
613
664
|
for pkg in pkgs
|
|
614
|
-
if (li := pkg.
|
|
665
|
+
if (li := pkg.latest_candidate) is not None
|
|
615
666
|
and li.version > pkg.dist.version
|
|
616
667
|
]
|
|
617
668
|
|
|
618
|
-
|
|
669
|
+
outdated_pkgs.sort(key=lambda x: x.dist.raw_name)
|
|
619
670
|
|
|
620
671
|
#
|
|
621
672
|
|
|
622
673
|
if args.json:
|
|
623
|
-
print(json.dumps_pretty(format_for_json(
|
|
674
|
+
print(json.dumps_pretty(format_for_json(outdated_pkgs)))
|
|
675
|
+
|
|
624
676
|
else:
|
|
625
|
-
|
|
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))))
|
|
626
683
|
|
|
627
684
|
|
|
628
685
|
if __name__ == '__main__':
|
omdev/pyproject/cli.py
CHANGED
|
@@ -22,7 +22,6 @@ See:
|
|
|
22
22
|
import argparse
|
|
23
23
|
import asyncio
|
|
24
24
|
import concurrent.futures as cf
|
|
25
|
-
import dataclasses as dc
|
|
26
25
|
import functools
|
|
27
26
|
import itertools
|
|
28
27
|
import multiprocessing as mp
|
|
@@ -40,47 +39,14 @@ from omlish.formats.toml.parser import toml_loads
|
|
|
40
39
|
from omlish.lite.cached import cached_nullary
|
|
41
40
|
from omlish.lite.check import check
|
|
42
41
|
from omlish.lite.runtime import check_lite_runtime_version
|
|
43
|
-
from omlish.logs.standard import configure_standard_logging
|
|
42
|
+
from omlish.logs.std.standard import configure_standard_logging
|
|
44
43
|
|
|
45
44
|
from .configs import PyprojectConfig
|
|
46
45
|
from .configs import PyprojectConfigPreparer
|
|
47
46
|
from .pkg import BasePyprojectPackageGenerator
|
|
48
47
|
from .pkg import PyprojectPackageGenerator
|
|
49
48
|
from .venvs import Venv
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
##
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@dc.dataclass(frozen=True)
|
|
56
|
-
class VersionsFile:
|
|
57
|
-
name: ta.Optional[str] = '.versions'
|
|
58
|
-
|
|
59
|
-
@staticmethod
|
|
60
|
-
def parse(s: str) -> ta.Mapping[str, str]:
|
|
61
|
-
return {
|
|
62
|
-
k: v
|
|
63
|
-
for l in s.splitlines()
|
|
64
|
-
if (sl := l.split('#')[0].strip())
|
|
65
|
-
for k, _, v in (sl.partition('='),)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
@cached_nullary
|
|
69
|
-
def contents(self) -> ta.Mapping[str, str]:
|
|
70
|
-
if not self.name or not os.path.exists(self.name):
|
|
71
|
-
return {}
|
|
72
|
-
with open(self.name) as f:
|
|
73
|
-
s = f.read()
|
|
74
|
-
return self.parse(s)
|
|
75
|
-
|
|
76
|
-
@staticmethod
|
|
77
|
-
def get_pythons(d: ta.Mapping[str, str]) -> ta.Mapping[str, str]:
|
|
78
|
-
pfx = 'PYTHON_'
|
|
79
|
-
return {k[len(pfx):].lower(): v for k, v in d.items() if k.startswith(pfx)}
|
|
80
|
-
|
|
81
|
-
@cached_nullary
|
|
82
|
-
def pythons(self) -> ta.Mapping[str, str]:
|
|
83
|
-
return self.get_pythons(self.contents())
|
|
49
|
+
from .versions import VersionsFile
|
|
84
50
|
|
|
85
51
|
|
|
86
52
|
##
|
omdev/pyproject/configs.py
CHANGED
|
@@ -24,7 +24,7 @@ class PyprojectConfig:
|
|
|
24
24
|
venvs: ta.Mapping[str, VenvConfig] = dc.field(default_factory=dict)
|
|
25
25
|
|
|
26
26
|
venvs_dir: str = '.venvs'
|
|
27
|
-
versions_file: ta.Optional[str] = '.versions'
|
|
27
|
+
# versions_file: ta.Optional[str] = '.versions' # FIXME:
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class PyprojectConfigPreparer:
|
omdev/pyproject/pkg.py
CHANGED
|
@@ -587,7 +587,7 @@ class _PyprojectRsPackageGenerator(_PyprojectExtensionPackageGenerator):
|
|
|
587
587
|
# `sdist.add_defaults` as an unbound function, not a bound method:
|
|
588
588
|
# https://github.com/pypa/setuptools/blob/9c4d383631d3951fcae0afd73b5d08ff5a262976/setuptools/command/egg_info.py#L581
|
|
589
589
|
from setuptools.command.sdist import sdist # noqa
|
|
590
|
-
sdist
|
|
590
|
+
setattr(sdist, 'add_defaults', (lambda old: lambda sdist: _sdist_add_defaults(old, sdist))(sdist.add_defaults)) # noqa
|
|
591
591
|
|
|
592
592
|
_patch_sdist()
|
|
593
593
|
|
omdev/pyproject/reqs.py
CHANGED
|
@@ -53,13 +53,14 @@ class RequirementsRewriter:
|
|
|
53
53
|
if self.VENV_MAGIC in l:
|
|
54
54
|
lp, _, rp = l.partition(self.VENV_MAGIC)
|
|
55
55
|
rp = rp.partition('#')[0]
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
vs = set(rp.split())
|
|
57
|
+
nvs = {v[1:] for v in vs if v.startswith('!')}
|
|
58
|
+
pvs = {v for v in vs if not v.startswith('!')}
|
|
59
|
+
if (
|
|
60
|
+
(nvs and self._venv in nvs) or
|
|
61
|
+
(pvs and self._venv not in pvs)
|
|
62
|
+
):
|
|
63
|
+
omit = True
|
|
63
64
|
|
|
64
65
|
if (
|
|
65
66
|
not omit and
|
|
@@ -38,11 +38,16 @@ def _main() -> None:
|
|
|
38
38
|
pkg_opt_deps = {d for ds in pkg_prj.optional_dependencies.values() for d in ds}
|
|
39
39
|
for opt_dep in sorted(pkg_opt_deps):
|
|
40
40
|
opt_req = parse_requirement(opt_dep)
|
|
41
|
+
|
|
41
42
|
opt_cn = canonicalize_name(opt_req.name, validate=True)
|
|
43
|
+
|
|
42
44
|
opt_spec = Specifier(opt_req.specifier)
|
|
43
45
|
if re.fullmatch(r'~=\s*\d+(\.\d+)*', str(opt_spec)):
|
|
44
46
|
opt_spec = Specifier(str(opt_spec) + '.0')
|
|
45
47
|
|
|
48
|
+
if opt_cn in pkgs:
|
|
49
|
+
continue
|
|
50
|
+
|
|
46
51
|
opt_dist = dist_dct[opt_cn]
|
|
47
52
|
opt_ver = opt_dist.version
|
|
48
53
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# ruff: noqa: UP006 UP045
|
|
2
|
+
import dataclasses as dc
|
|
3
|
+
import json
|
|
4
|
+
import typing as ta
|
|
5
|
+
import urllib.request
|
|
6
|
+
|
|
7
|
+
from ..versions import VersionsFile
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
##
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dc.dataclass(frozen=True)
|
|
14
|
+
class PyVersion:
|
|
15
|
+
name: str # "Python 3.13.11",
|
|
16
|
+
slug: str # "python-31311"
|
|
17
|
+
version: int # 3
|
|
18
|
+
is_published: bool
|
|
19
|
+
is_latest: bool
|
|
20
|
+
release_date: str # "2025-12-05T19:24:49Z"
|
|
21
|
+
pre_release: bool
|
|
22
|
+
release_page: ta.Optional[str]
|
|
23
|
+
release_notes_url: str # "https://docs.python.org/release/3.13.11/whatsnew/changelog.html"
|
|
24
|
+
show_on_download_page: bool
|
|
25
|
+
resource_uri: str # "https://www.python.org/api/v2/downloads/release/1083/"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
PY_VERSIONS_URL = 'https://www.python.org/api/v2/downloads/release/?is_published=true'
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_py_versions() -> ta.List[PyVersion]:
|
|
32
|
+
with urllib.request.urlopen(PY_VERSIONS_URL) as r: # noqa
|
|
33
|
+
data = json.load(r)
|
|
34
|
+
|
|
35
|
+
return [PyVersion(**dct) for dct in data]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _main() -> None:
|
|
42
|
+
print(get_py_versions())
|
|
43
|
+
print(VersionsFile().pythons())
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == '__main__':
|
|
47
|
+
_main()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# ruff: noqa: UP045
|
|
2
|
+
import dataclasses as dc
|
|
3
|
+
import os.path
|
|
4
|
+
import typing as ta
|
|
5
|
+
|
|
6
|
+
from omlish.lite.cached import cached_nullary
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
##
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dc.dataclass(frozen=True)
|
|
13
|
+
class VersionsFile:
|
|
14
|
+
name: ta.Optional[str] = '.versions'
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def parse(s: str) -> ta.Mapping[str, str]:
|
|
18
|
+
return {
|
|
19
|
+
k: v
|
|
20
|
+
for l in s.splitlines()
|
|
21
|
+
if (sl := l.split('#')[0].strip())
|
|
22
|
+
for k, _, v in (sl.partition('='),)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@cached_nullary
|
|
26
|
+
def contents(self) -> ta.Mapping[str, str]:
|
|
27
|
+
if not self.name or not os.path.exists(self.name):
|
|
28
|
+
return {}
|
|
29
|
+
with open(self.name) as f:
|
|
30
|
+
s = f.read()
|
|
31
|
+
return self.parse(s)
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def get_pythons(d: ta.Mapping[str, str]) -> ta.Mapping[str, str]:
|
|
35
|
+
pfx = 'PYTHON_'
|
|
36
|
+
return {k[len(pfx):].lower(): v for k, v in d.items() if k.startswith(pfx)}
|
|
37
|
+
|
|
38
|
+
@cached_nullary
|
|
39
|
+
def pythons(self) -> ta.Mapping[str, str]:
|
|
40
|
+
return self.get_pythons(self.contents())
|