omdev 0.0.0.dev486__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 (40) hide show
  1. omdev/.omlish-manifests.json +1 -1
  2. omdev/README.md +51 -0
  3. omdev/__about__.py +4 -2
  4. omdev/ci/cli.py +1 -1
  5. omdev/cli/clicli.py +37 -7
  6. omdev/dataclasses/cli.py +1 -1
  7. omdev/interp/cli.py +1 -1
  8. omdev/interp/types.py +3 -2
  9. omdev/interp/uv/provider.py +36 -0
  10. omdev/manifests/main.py +1 -1
  11. omdev/packaging/revisions.py +1 -1
  12. omdev/py/tools/pipdepup.py +150 -93
  13. omdev/pyproject/cli.py +1 -1
  14. omdev/pyproject/tools/aboutdeps.py +5 -0
  15. omdev/scripts/ci.py +361 -25
  16. omdev/scripts/interp.py +43 -8
  17. omdev/scripts/lib/logs.py +117 -21
  18. omdev/scripts/pyproject.py +404 -29
  19. omdev/tools/json/formats.py +2 -0
  20. omdev/tools/jsonview/cli.py +19 -61
  21. omdev/tools/jsonview/resources/jsonview.html.j2 +43 -0
  22. omdev/tools/pawk/README.md +195 -0
  23. omdev/tui/apps/edit/main.py +5 -1
  24. omdev/tui/apps/irc/app.py +28 -20
  25. omdev/tui/apps/irc/commands.py +1 -1
  26. omdev/tui/rich/__init__.py +12 -0
  27. omdev/tui/textual/__init__.py +41 -2
  28. omdev/tui/textual/app2.py +6 -1
  29. omdev/tui/textual/debug/__init__.py +10 -0
  30. omdev/tui/textual/debug/dominfo.py +151 -0
  31. omdev/tui/textual/debug/screen.py +24 -0
  32. omdev/tui/textual/devtools.py +187 -0
  33. omdev/tui/textual/logging2.py +20 -0
  34. omdev/tui/textual/types.py +45 -0
  35. {omdev-0.0.0.dev486.dist-info → omdev-0.0.0.dev495.dist-info}/METADATA +10 -6
  36. {omdev-0.0.0.dev486.dist-info → omdev-0.0.0.dev495.dist-info}/RECORD +40 -31
  37. {omdev-0.0.0.dev486.dist-info → omdev-0.0.0.dev495.dist-info}/WHEEL +0 -0
  38. {omdev-0.0.0.dev486.dist-info → omdev-0.0.0.dev495.dist-info}/entry_points.txt +0 -0
  39. {omdev-0.0.0.dev486.dist-info → omdev-0.0.0.dev495.dist-info}/licenses/LICENSE +0 -0
  40. {omdev-0.0.0.dev486.dist-info → omdev-0.0.0.dev495.dist-info}/top_level.txt +0 -0
@@ -39,7 +39,7 @@
39
39
  "module": ".cli.clicli",
40
40
  "attr": "_CLI_MODULE",
41
41
  "file": "omdev/cli/clicli.py",
42
- "line": 233,
42
+ "line": 263,
43
43
  "value": {
44
44
  "!.cli.types.CliModule": {
45
45
  "name": "cli",
omdev/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # Overview
2
+
3
+ Development utilities and support code.
4
+
5
+ # Notable packages
6
+
7
+ - **[cli](https://github.com/wrmsr/omlish/blob/master/omdev/cli)** - The codebase's all-in-one CLI. This is not
8
+ installed as an entrypoint / command when this package is itself installed - that is separated into the `omdev-cli`
9
+ installable package so as to not pollute users' bin/ directories when depping this lib for its utility code.
10
+
11
+ - **[amalg](https://github.com/wrmsr/omlish/blob/master/omdev/amalg)** - The [amalgamator](#amalgamation).
12
+
13
+ - **[pyproject](https://github.com/wrmsr/omlish/blob/master/omdev/pyproject)**
14
+ ([amalg](https://github.com/wrmsr/omlish/blob/master/omdev/scripts/pyproject.py)) - python project management tool.
15
+ wrangles but does not replace tools like venv, pip, setuptools, and uv. does things like sets up venvs, generates
16
+ [`.pkg`](https://github.com/wrmsr/omlish/blob/master/.pkg) directories and their `pyproject.toml`'s (from their
17
+ `__about__.py`'s), and packages them. this should grow to eat more and more of the Makefile. as it is amalgamated it
18
+ requires no installation and can just be dropped into other projects / repos.
19
+
20
+ - **[ci](https://github.com/wrmsr/omlish/blob/master/omdev/ci)**
21
+ ([amalg](https://github.com/wrmsr/omlish/blob/master/omdev/scripts/ci.py)) - ci runner. given a
22
+ [`compose.yml`](https://github.com/wrmsr/omlish/blob/master/docker/compose.yml)
23
+ and requirements.txt files, takes care of building and caching of containers and venvs and execution of required ci
24
+ commands. detects and [natively uses](https://github.com/wrmsr/omlish/blob/master/omdev/ci/github/api/v2)
25
+ github-action's caching system. unifies ci execution between local dev and github runners.
26
+
27
+ - **[tools.json](https://github.com/wrmsr/omlish/blob/master/omdev/tools/json)** (cli: `om j`) - a tool for json-like
28
+ data, obviously in the vein of [jq](https://github.com/jqlang/jq) but using the internal
29
+ [jmespath](https://github.com/wrmsr/omlish/blob/master/omlish/specs/jmespath) engine. supports
30
+ [true streaming](https://github.com/wrmsr/omlish/blob/master/omlish/formats/json/stream) json input and output, as
31
+ well as [various other](https://github.com/wrmsr/omlish/blob/master/omdev/tools/json/formats.py) non-streaming input
32
+ formats.
33
+
34
+ - **[tools.git](https://github.com/wrmsr/omlish/blob/master/omdev/tools/git)** (cli: `om git`) - a tool for various lazy
35
+ git operations, including the one that (poorly) writes all of these commit messages.
36
+
37
+ # Amalgamation
38
+
39
+ Amalgamation is the process of stitching together multiple python source files into a single self-contained python
40
+ script. ['lite'](https://github.com/wrmsr/omlish/blob/master/omlish#lite-code) code is written in a style conducive to
41
+ this.
42
+
43
+ # Local storage
44
+
45
+ Some of this code, when asked, will store things on the local filesystem. The only directories used (outside of ones
46
+ explicitly specified as command or function arguments) are managed in
47
+ [home.paths](https://github.com/wrmsr/omlish/blob/master/omdev/home/paths.py), and are the following:
48
+
49
+ - `$OMLISH_HOME`, default of `~/.omlish` - persistent things like config and state.
50
+ - `$OMLISH_CACHE`, default of `~/.cache/omlish` - used for things like the local ci cache and
51
+ [various other](https://github.com/search?q=repo%3Awrmsr%2Fomlish+%22dcache.%22&type=code) cached data.
omdev/__about__.py CHANGED
@@ -13,7 +13,7 @@ class Project(ProjectBase):
13
13
 
14
14
  optional_dependencies = {
15
15
  'black': [
16
- 'black ~= 25.11',
16
+ 'black ~= 25.12',
17
17
  ],
18
18
 
19
19
  'c': [
@@ -44,7 +44,9 @@ class Project(ProjectBase):
44
44
 
45
45
  'tui': [
46
46
  'rich ~= 14.2',
47
- 'textual ~= 6.8',
47
+ 'textual ~= 6.11', # [syntax]
48
+ 'textual-dev ~= 1.8',
49
+ 'textual-speedups ~= 0.2',
48
50
  ],
49
51
  }
50
52
 
omdev/ci/cli.py CHANGED
@@ -22,7 +22,7 @@ from omlish.argparse.cli import argparse_cmd
22
22
  from omlish.lite.check import check
23
23
  from omlish.lite.inject import inj
24
24
  from omlish.logs.modules import get_module_logger
25
- from omlish.logs.standard import configure_standard_logging
25
+ from omlish.logs.std.standard import configure_standard_logging
26
26
 
27
27
  from .cache import DirectoryFileCache
28
28
  from .ci import Ci
omdev/cli/clicli.py CHANGED
@@ -1,9 +1,11 @@
1
+ import dataclasses as dc
1
2
  import inspect
2
3
  import os
3
4
  import re
4
5
  import shlex
5
6
  import subprocess
6
7
  import sys
8
+ import time
7
9
  import typing as ta
8
10
  import urllib.parse
9
11
  import urllib.request
@@ -13,6 +15,7 @@ from omlish import lang
13
15
  from omlish.argparse import all as ap
14
16
  from omlish.os.temp import temp_dir_context
15
17
 
18
+ from ..packaging.versions import Version
16
19
  from ..pip import get_root_dists
17
20
  from ..pip import lookup_latest_package_version
18
21
  from . import install
@@ -87,22 +90,49 @@ class CliCli(ap.Cli):
87
90
 
88
91
  #
89
92
 
93
+ @dc.dataclass()
94
+ class ReinstallWouldNotUpgradeError(Exception):
95
+ current_version: str
96
+ target_version: str
97
+
98
+ def __str__(self) -> str:
99
+ return f'Current version {self.current_version} is not older than target version {self.target_version} '
100
+
90
101
  @ap.cmd(
91
102
  ap.arg('--url', default=DEFAULT_REINSTALL_URL),
92
103
  ap.arg('--local', action='store_true'),
93
104
  ap.arg('--no-deps', action='store_true'),
94
105
  ap.arg('--no-uv', action='store_true'),
95
106
  ap.arg('--dry-run', action='store_true'),
107
+ ap.arg('--must-upgrade', action='store_true'),
108
+ ap.arg('--must-upgrade-loop', action='store_true'),
96
109
  ap.arg('--version'),
97
110
  ap.arg('extra_deps', nargs='*'),
98
111
  )
99
112
  def reinstall(self) -> None:
100
- latest_version = _parse_latest_version_str(lookup_latest_package_version(__package__.split('.')[0]))
101
-
102
- if self.args.version is not None:
103
- target_version: str = self.args.version
104
- else:
105
- target_version = latest_version
113
+ current_version = __about__.__version__
114
+
115
+ while True:
116
+ latest_version = _parse_latest_version_str(lookup_latest_package_version(__package__.split('.')[0]))
117
+
118
+ if self.args.version is not None:
119
+ target_version: str = self.args.version
120
+ else:
121
+ target_version = latest_version
122
+
123
+ if self.args.must_upgrade or self.args.must_upgrade_loop:
124
+ current_vo = Version(current_version)
125
+ target_vo = Version(target_version)
126
+ if not (target_vo > current_vo):
127
+ ex = CliCli.ReinstallWouldNotUpgradeError(current_version, target_version)
128
+ if self.args.must_upgrade_loop:
129
+ print(ex)
130
+ time.sleep(1)
131
+ continue
132
+ else:
133
+ raise ex
134
+
135
+ break
106
136
 
107
137
  #
108
138
 
@@ -186,7 +216,7 @@ class CliCli(ap.Cli):
186
216
 
187
217
  #
188
218
 
189
- print(f'Current version: {__about__.__version__}')
219
+ print(f'Current version: {current_version}')
190
220
  print(f'Latest version: {latest_version}')
191
221
  print(f'Target version: {target_version}')
192
222
  print()
omdev/dataclasses/cli.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from omlish.argparse import all as ap
2
- from omlish.logs.standard import configure_standard_logging
2
+ from omlish.logs.std.standard import configure_standard_logging
3
3
 
4
4
  from .codegen import DataclassCodeGen
5
5
 
omdev/interp/cli.py CHANGED
@@ -17,7 +17,7 @@ from omlish.lite.check import check
17
17
  from omlish.lite.inject import Injector
18
18
  from omlish.lite.inject import inj
19
19
  from omlish.lite.runtime import check_lite_runtime_version
20
- from omlish.logs.standard import configure_standard_logging
20
+ from omlish.logs.std.standard import configure_standard_logging
21
21
 
22
22
  from .inject import bind_interp
23
23
  from .resolvers import InterpResolver
omdev/interp/types.py CHANGED
@@ -85,9 +85,10 @@ class InterpSpecifier:
85
85
  def parse(cls, s: str) -> 'InterpSpecifier':
86
86
  s, o = InterpOpts.parse_suffix(s)
87
87
  if not any(s.startswith(o) for o in Specifier.OPERATORS):
88
- s = '~=' + s
89
88
  if s.count('.') < 2:
90
- s += '.0'
89
+ s = '~=' + s + '.0'
90
+ else:
91
+ s = '==' + s
91
92
  return cls(
92
93
  specifier=Specifier(s),
93
94
  opts=o,
@@ -5,7 +5,9 @@ uv run --python 3.11.6 pip
5
5
  uv venv --python 3.11.6 --seed barf
6
6
  python3 -m venv barf && barf/bin/pip install uv && barf/bin/uv venv --python 3.11.6 --seed barf2
7
7
  uv python find '3.13.10'
8
+ uv python list --output-format=json
8
9
  """
10
+ import dataclasses as dc
9
11
  import typing as ta
10
12
 
11
13
  from omlish.logs.protocols import LoggerLike
@@ -21,6 +23,34 @@ from .uv import Uv
21
23
  ##
22
24
 
23
25
 
26
+ @dc.dataclass(frozen=True)
27
+ class UvPythonListOutput:
28
+ key: str
29
+ version: str
30
+
31
+ @dc.dataclass(frozen=True)
32
+ class VersionParts:
33
+ major: int
34
+ minor: int
35
+ patch: int
36
+
37
+ version_parts: VersionParts
38
+
39
+ path: ta.Optional[str]
40
+ symlink: ta.Optional[str]
41
+
42
+ url: str
43
+
44
+ os: str # emscripten linux macos
45
+ variant: str # default freethreaded
46
+ implementation: str # cpython graalpy pyodide pypy
47
+ arch: str # aarch64 wasm32 x86_64
48
+ libc: str # gnu musl none
49
+
50
+
51
+ ##
52
+
53
+
24
54
  class UvInterpProvider(InterpProvider):
25
55
  def __init__(
26
56
  self,
@@ -40,3 +70,9 @@ class UvInterpProvider(InterpProvider):
40
70
 
41
71
  async def get_installed_version(self, version: InterpVersion) -> Interp:
42
72
  raise NotImplementedError
73
+
74
+ # async def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
75
+ # return []
76
+
77
+ # async def install_version(self, version: InterpVersion) -> Interp:
78
+ # raise TypeError
omdev/manifests/main.py CHANGED
@@ -5,7 +5,7 @@ import multiprocessing as mp
5
5
  import os.path
6
6
 
7
7
  from omlish.lite.json import json_dumps_pretty
8
- from omlish.logs.standard import configure_standard_logging
8
+ from omlish.logs.std.standard import configure_standard_logging
9
9
 
10
10
  from .building import ManifestBuilder
11
11
  from .building import check_package_manifests
@@ -15,7 +15,7 @@ import zipfile
15
15
  from omlish.lite.cached import cached_nullary
16
16
  from omlish.lite.check import check
17
17
  from omlish.logs.modules import get_module_logger
18
- from omlish.logs.standard import configure_standard_logging
18
+ from omlish.logs.std.standard import configure_standard_logging
19
19
 
20
20
  from ..git.revisions import get_git_revision
21
21
  from .wheelfile import WheelFile
@@ -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 email.parser
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
- @dc.dataclass(frozen=True, kw_only=True)
316
- class LatestInfo:
317
- candidate: 'Package.Candidate'
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
- version: Version
320
- filetype: str
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
- latest_info: LatestInfo | None = None
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
- candidates = [
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
- candidates = [
401
+ suggested_candidates = [
366
402
  c
367
- for c in candidates
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
- latest_info = check.not_none(pkg.latest_info)
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(latest_info.version),
483
- 'latest_filetype': latest_info.filetype,
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 = ['Package', 'Version', 'Latest', 'Age', 'Type']
512
+ header = [
513
+ 'Package',
514
+ 'Current',
504
515
 
505
- def wheel_build_tag(dist: BaseDistribution) -> str | None:
506
- try:
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
- if has_build_tags:
535
- row.append(build_tags[i] or '')
519
+ 'Latest',
520
+ 'Age',
521
+ ]
536
522
 
537
- if has_editables:
538
- row.append(proj.dist.editable_project_location or '')
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
- data.append(row)
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
- return data, header
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
- from pip._internal.utils.misc import tabulate # noqa
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, ' '.join('-' * x for x in sizes))
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=4 * 24)
569
- parser.add_argument('-P', '--parallelism', type=int, default=4)
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 = now_utc() - datetime.timedelta(hours=args.min_age_h)
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
- pkgs = [
662
+ outdated_pkgs = [
612
663
  pkg
613
664
  for pkg in pkgs
614
- if (li := pkg.latest_info) is not None
665
+ if (li := pkg.latest_candidate) is not None
615
666
  and li.version > pkg.dist.version
616
667
  ]
617
668
 
618
- pkgs.sort(key=lambda x: x.dist.raw_name)
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(pkgs)))
674
+ print(json.dumps_pretty(format_for_json(outdated_pkgs)))
675
+
624
676
  else:
625
- print('\n'.join(render_package_listing_columns(*format_for_columns(pkgs))))
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
@@ -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
@@ -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