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
omdev/scripts/interp.py CHANGED
@@ -27,6 +27,7 @@ import inspect
27
27
  import itertools
28
28
  import json
29
29
  import logging
30
+ import operator
30
31
  import os
31
32
  import os.path
32
33
  import re
@@ -49,6 +50,54 @@ if sys.version_info < (3, 8):
49
50
  raise OSError(f'Requires python (3, 8), got {sys.version_info} from {sys.executable}') # noqa
50
51
 
51
52
 
53
+ def __omlish_amalg__(): # noqa
54
+ return dict(
55
+ src_files=[
56
+ dict(path='../packaging/versions.py', sha1='71627ad600b3529b829b0e227b0952f2c63c7271'),
57
+ dict(path='../../omlish/lite/abstract.py', sha1='a2fc3f3697fa8de5247761e9d554e70176f37aac'),
58
+ dict(path='../../omlish/lite/cached.py', sha1='0c33cf961ac8f0727284303c7a30c5ea98f714f2'),
59
+ dict(path='../../omlish/lite/check.py', sha1='bb6b6b63333699b84462951a854d99ae83195b94'),
60
+ dict(path='../../omlish/lite/json.py', sha1='57eeddc4d23a17931e00284ffa5cb6e3ce089486'),
61
+ dict(path='../../omlish/lite/reflect.py', sha1='c4fec44bf144e9d93293c996af06f6c65fc5e63d'),
62
+ dict(path='../../omlish/lite/strings.py', sha1='89831ecbc34ad80e118a865eceb390ed399dc4d6'),
63
+ dict(path='../../omlish/logs/levels.py', sha1='91405563d082a5eba874da82aac89d83ce7b6152'),
64
+ dict(path='../../omlish/logs/std/filters.py', sha1='f36aab646d84d31e295b33aaaaa6f8b67ff38b3d'),
65
+ dict(path='../../omlish/logs/std/proxy.py', sha1='3e7301a2aa351127f9c85f61b2f85dcc3f15aafb'),
66
+ dict(path='../packaging/specifiers.py', sha1='a56ab4e8c9b174adb523921f6280ac41e0fce749'),
67
+ dict(path='../../omlish/argparse/cli.py', sha1='f4dc3cd353d14386b5da0306768700e396afd2b3'),
68
+ dict(path='../../omlish/lite/maybes.py', sha1='bdf5136654ccd14b6a072588cad228925bdfbabd'),
69
+ dict(path='../../omlish/lite/runtime.py', sha1='2e752a27ae2bf89b1bb79b4a2da522a3ec360c70'),
70
+ dict(path='../../omlish/lite/timeouts.py', sha1='a0f673033a6943f242e35848d78a41892b9c62a1'),
71
+ dict(path='../../omlish/logs/protocols.py', sha1='05ca4d1d7feb50c4e3b9f22ee371aa7bf4b3dbd1'),
72
+ dict(path='../../omlish/logs/std/json.py', sha1='2a75553131e4d5331bb0cedde42aa183f403fc3b'),
73
+ dict(path='types.py', sha1='caf068a6e81fb6e221d777b341ac5777d92b8091'),
74
+ dict(path='../../omlish/asyncs/asyncio/timeouts.py', sha1='4d31b02b3c39b8f2fa7e94db36552fde6942e36a'),
75
+ dict(path='../../omlish/lite/inject.py', sha1='6f097e3170019a34ff6834d36fcc9cbeed3a7ab4'),
76
+ dict(path='../../omlish/logs/std/standard.py', sha1='5c97c1b9f7ead58d6127d047b873398f708f288d'),
77
+ dict(path='../../omlish/subprocesses/run.py', sha1='8200e48f0c49d164df3503cd0143038d0c4d30aa'),
78
+ dict(path='../../omlish/subprocesses/wrap.py', sha1='8a9b7d2255481fae15c05f5624b0cdc0766f4b3f'),
79
+ dict(path='providers/base.py', sha1='f5d068c21f230d742e9015b033cd6320f4c68898'),
80
+ dict(path='../../omlish/subprocesses/base.py', sha1='cb9f668be5422fecb27222caabb67daac6c1bab9'),
81
+ dict(path='resolvers.py', sha1='817b8e76401cd7a19eb43ca54d65272e4c8a4b0e'),
82
+ dict(path='../../omlish/subprocesses/asyncs.py', sha1='bba44d524c24c6ac73168aee6343488414e5bf48'),
83
+ dict(path='../../omlish/asyncs/asyncio/subprocesses.py', sha1='b6b5f9ae3fd0b9c83593bad2e04a08f726e5904d'),
84
+ dict(path='inspect.py', sha1='736287b4ec8d14a8c30afa0ba23996fdc0662caa'),
85
+ dict(path='pyenv/pyenv.py', sha1='d1f6e657c671c1b1a5b0e627284df656fe2d10d3'),
86
+ dict(path='uv/uv.py', sha1='8c6515cd6755efab3972da92a285e94ccb255515'),
87
+ dict(path='providers/running.py', sha1='85c9cc69ff6fbd6c8cf78ed6262619a30856c2f1'),
88
+ dict(path='providers/system.py', sha1='9638a154475ca98775159d27739563ac7fb2eb16'),
89
+ dict(path='pyenv/install.py', sha1='4a10a19717364b4ba9f3b8bf1d12621cf21ba8b8'),
90
+ dict(path='uv/provider.py', sha1='3c3980878ad2b9fd2cd02172f9424954759c7f06'),
91
+ dict(path='providers/inject.py', sha1='7cc9ebf58cf2ec09545321456bd9da9f9a3a79fb'),
92
+ dict(path='pyenv/provider.py', sha1='377542ce01a35849e2a5b4a4dbafedc26882f983'),
93
+ dict(path='uv/inject.py', sha1='e95d058c2340baa5a3155ec3440f311d1daa10a8'),
94
+ dict(path='pyenv/inject.py', sha1='b8fb68f5a7cae86c70fe1bad6c29a8b2dfc985c3'),
95
+ dict(path='inject.py', sha1='b039abbadf0b096d2724182af2e0ebda2a230852'),
96
+ dict(path='cli.py', sha1='6b747ba4f91e0ab6290b791c2c274f268d11c33e'),
97
+ ],
98
+ )
99
+
100
+
52
101
  ########################################
53
102
 
54
103
 
@@ -60,8 +109,10 @@ VersionCmpLocalType = ta.Union['NegativeInfinityVersionType', _VersionCmpLocalTy
60
109
  VersionCmpKey = ta.Tuple[int, ta.Tuple[int, ...], VersionCmpPrePostDevType, VersionCmpPrePostDevType, VersionCmpPrePostDevType, VersionCmpLocalType] # ta.TypeAlias # noqa
61
110
  VersionComparisonMethod = ta.Callable[[VersionCmpKey, VersionCmpKey], bool] # ta.TypeAlias
62
111
 
63
- # ../../omlish/lite/cached.py
112
+ # ../../omlish/lite/abstract.py
64
113
  T = ta.TypeVar('T')
114
+
115
+ # ../../omlish/lite/cached.py
65
116
  CallableT = ta.TypeVar('CallableT', bound=ta.Callable)
66
117
 
67
118
  # ../../omlish/lite/check.py
@@ -213,12 +264,12 @@ class _BaseVersion:
213
264
 
214
265
  def __lt__(self, other: '_BaseVersion') -> bool:
215
266
  if not isinstance(other, _BaseVersion):
216
- return NotImplemented # type: ignore
267
+ return NotImplemented
217
268
  return self._key < other._key
218
269
 
219
270
  def __le__(self, other: '_BaseVersion') -> bool:
220
271
  if not isinstance(other, _BaseVersion):
221
- return NotImplemented # type: ignore
272
+ return NotImplemented
222
273
  return self._key <= other._key
223
274
 
224
275
  def __eq__(self, other: object) -> bool:
@@ -228,12 +279,12 @@ class _BaseVersion:
228
279
 
229
280
  def __ge__(self, other: '_BaseVersion') -> bool:
230
281
  if not isinstance(other, _BaseVersion):
231
- return NotImplemented # type: ignore
282
+ return NotImplemented
232
283
  return self._key >= other._key
233
284
 
234
285
  def __gt__(self, other: '_BaseVersion') -> bool:
235
286
  if not isinstance(other, _BaseVersion):
236
- return NotImplemented # type: ignore
287
+ return NotImplemented
237
288
  return self._key > other._key
238
289
 
239
290
  def __ne__(self, other: object) -> bool:
@@ -524,25 +575,49 @@ def is_abstract_method(obj: ta.Any) -> bool:
524
575
  return bool(getattr(obj, _IS_ABSTRACT_METHOD_ATTR, False))
525
576
 
526
577
 
527
- def update_abstracts(cls, *, force=False):
578
+ def compute_abstract_methods(cls: type) -> ta.FrozenSet[str]:
579
+ # ~> https://github.com/python/cpython/blob/f3476c6507381ca860eec0989f53647b13517423/Modules/_abc.c#L358
580
+
581
+ # Stage 1: direct abstract methods
582
+
583
+ abstracts = {
584
+ a
585
+ # Get items as a list to avoid mutation issues during iteration
586
+ for a, v in list(cls.__dict__.items())
587
+ if is_abstract_method(v)
588
+ }
589
+
590
+ # Stage 2: inherited abstract methods
591
+
592
+ for base in cls.__bases__:
593
+ # Get __abstractmethods__ from base if it exists
594
+ if (base_abstracts := getattr(base, _ABSTRACT_METHODS_ATTR, None)) is None:
595
+ continue
596
+
597
+ # Iterate over abstract methods in base
598
+ for key in base_abstracts:
599
+ # Check if this class has an attribute with this name
600
+ try:
601
+ value = getattr(cls, key)
602
+ except AttributeError:
603
+ # Attribute not found in this class, skip
604
+ continue
605
+
606
+ # Check if it's still abstract
607
+ if is_abstract_method(value):
608
+ abstracts.add(key)
609
+
610
+ return frozenset(abstracts)
611
+
612
+
613
+ def update_abstracts(cls: ta.Type[T], *, force: bool = False) -> ta.Type[T]:
528
614
  if not force and not hasattr(cls, _ABSTRACT_METHODS_ATTR):
529
615
  # Per stdlib: We check for __abstractmethods__ here because cls might by a C implementation or a python
530
616
  # implementation (especially during testing), and we want to handle both cases.
531
617
  return cls
532
618
 
533
- abstracts: ta.Set[str] = set()
534
-
535
- for scls in cls.__bases__:
536
- for name in getattr(scls, _ABSTRACT_METHODS_ATTR, ()):
537
- value = getattr(cls, name, None)
538
- if getattr(value, _IS_ABSTRACT_METHOD_ATTR, False):
539
- abstracts.add(name)
540
-
541
- for name, value in cls.__dict__.items():
542
- if getattr(value, _IS_ABSTRACT_METHOD_ATTR, False):
543
- abstracts.add(name)
544
-
545
- setattr(cls, _ABSTRACT_METHODS_ATTR, frozenset(abstracts))
619
+ abstracts = compute_abstract_methods(cls)
620
+ setattr(cls, _ABSTRACT_METHODS_ATTR, abstracts)
546
621
  return cls
547
622
 
548
623
 
@@ -596,23 +671,26 @@ class Abstract:
596
671
  super().__init_subclass__(**kwargs)
597
672
 
598
673
  if not (Abstract in cls.__bases__ or abc.ABC in cls.__bases__):
599
- ams = {a: cls for a, o in cls.__dict__.items() if is_abstract_method(o)}
600
-
601
- seen = set(cls.__dict__)
602
- for b in cls.__bases__:
603
- ams.update({a: b for a in set(getattr(b, _ABSTRACT_METHODS_ATTR, [])) - seen}) # noqa
604
- seen.update(dir(b))
674
+ if ams := compute_abstract_methods(cls):
675
+ amd = {
676
+ a: mcls
677
+ for mcls in cls.__mro__[::-1]
678
+ for a in ams
679
+ if a in mcls.__dict__
680
+ }
605
681
 
606
- if ams:
607
682
  raise AbstractTypeError(
608
683
  f'Cannot subclass abstract class {cls.__name__} with abstract methods: ' +
609
684
  ', '.join(sorted([
610
685
  '.'.join([
611
- *([m] if (m := getattr(c, '__module__')) else []),
612
- getattr(c, '__qualname__', getattr(c, '__name__')),
686
+ *([
687
+ *([m] if (m := getattr(c, '__module__')) else []),
688
+ getattr(c, '__qualname__', getattr(c, '__name__')),
689
+ ] if c is not None else '?'),
613
690
  a,
614
691
  ])
615
- for a, c in ams.items()
692
+ for a in ams
693
+ for c in [amd.get(a)]
616
694
  ])),
617
695
  )
618
696
 
@@ -686,6 +764,62 @@ def async_cached_nullary(fn): # ta.Callable[..., T]) -> ta.Callable[..., T]:
686
764
  return _AsyncCachedNullary(fn)
687
765
 
688
766
 
767
+ ##
768
+
769
+
770
+ cached_property = functools.cached_property
771
+
772
+
773
+ class _cached_property: # noqa
774
+ """Backported to pick up https://github.com/python/cpython/commit/056dfc71dce15f81887f0bd6da09d6099d71f979 ."""
775
+
776
+ def __init__(self, func):
777
+ self.func = func
778
+ self.attrname = None # noqa
779
+ self.__doc__ = func.__doc__
780
+ self.__module__ = func.__module__
781
+
782
+ _NOT_FOUND = object()
783
+
784
+ def __set_name__(self, owner, name):
785
+ if self.attrname is None:
786
+ self.attrname = name # noqa
787
+ elif name != self.attrname:
788
+ raise TypeError(
789
+ f'Cannot assign the same cached_property to two different names ({self.attrname!r} and {name!r}).',
790
+ )
791
+
792
+ def __get__(self, instance, owner=None):
793
+ if instance is None:
794
+ return self
795
+ if self.attrname is None:
796
+ raise TypeError('Cannot use cached_property instance without calling __set_name__ on it.')
797
+
798
+ try:
799
+ cache = instance.__dict__
800
+ except AttributeError: # not all objects have __dict__ (e.g. class defines slots)
801
+ raise TypeError(
802
+ f"No '__dict__' attribute on {type(instance).__name__!r} instance to cache {self.attrname!r} property.",
803
+ ) from None
804
+
805
+ val = cache.get(self.attrname, self._NOT_FOUND)
806
+
807
+ if val is self._NOT_FOUND:
808
+ val = self.func(instance)
809
+ try:
810
+ cache[self.attrname] = val
811
+ except TypeError:
812
+ raise TypeError(
813
+ f"The '__dict__' attribute on {type(instance).__name__!r} instance does not support item "
814
+ f"assignment for caching {self.attrname!r} property.",
815
+ ) from None
816
+
817
+ return val
818
+
819
+
820
+ globals()['cached_property'] = _cached_property
821
+
822
+
689
823
  ########################################
690
824
  # ../../../omlish/lite/check.py
691
825
  """
@@ -2050,7 +2184,7 @@ class SpecifierSet(BaseSpecifier):
2050
2184
  if isinstance(other, str):
2051
2185
  other = SpecifierSet(other)
2052
2186
  elif not isinstance(other, SpecifierSet):
2053
- return NotImplemented # type: ignore
2187
+ return NotImplemented
2054
2188
 
2055
2189
  specifier = SpecifierSet()
2056
2190
  specifier._specs = frozenset(self._specs | other._specs)
@@ -2070,6 +2204,7 @@ class SpecifierSet(BaseSpecifier):
2070
2204
  if isinstance(other, (str, Specifier)):
2071
2205
  other = SpecifierSet(str(other))
2072
2206
  elif not isinstance(other, SpecifierSet):
2207
+
2073
2208
  return NotImplemented
2074
2209
 
2075
2210
  return self._specs == other._specs
@@ -2141,6 +2276,7 @@ TODO:
2141
2276
  - pre-run, post-run hooks
2142
2277
  - exitstack?
2143
2278
  - suggestion - difflib.get_close_matches
2279
+ - add_argument_group - group kw on ArgparseKwarg?
2144
2280
  """
2145
2281
 
2146
2282
 
@@ -2151,6 +2287,7 @@ TODO:
2151
2287
  class ArgparseArg:
2152
2288
  args: ta.Sequence[ta.Any]
2153
2289
  kwargs: ta.Mapping[str, ta.Any]
2290
+ group: ta.Optional[str] = None
2154
2291
  dest: ta.Optional[str] = None
2155
2292
 
2156
2293
  def __get__(self, instance, owner=None):
@@ -2160,7 +2297,11 @@ class ArgparseArg:
2160
2297
 
2161
2298
 
2162
2299
  def argparse_arg(*args, **kwargs) -> ArgparseArg:
2163
- return ArgparseArg(args, kwargs)
2300
+ return ArgparseArg(
2301
+ args=args,
2302
+ group=kwargs.pop('group', None),
2303
+ kwargs=kwargs,
2304
+ )
2164
2305
 
2165
2306
 
2166
2307
  def argparse_arg_(*args, **kwargs) -> ta.Any:
@@ -2330,6 +2471,10 @@ class ArgparseCli:
2330
2471
  subparser.set_defaults(_cmd=obj)
2331
2472
 
2332
2473
  elif isinstance(obj, ArgparseArg):
2474
+ if obj.group is not None:
2475
+ # FIXME: add_argument_group
2476
+ raise NotImplementedError
2477
+
2333
2478
  if att in anns:
2334
2479
  ann_kwargs = _get_argparse_arg_ann_kwargs(anns[att])
2335
2480
  obj.kwargs = {**ann_kwargs, **obj.kwargs}
@@ -2375,7 +2520,7 @@ class ArgparseCli:
2375
2520
 
2376
2521
  if self._unknown_args and not (cmd is not None and cmd.accepts_unknown):
2377
2522
  msg = f'unrecognized arguments: {" ".join(self._unknown_args)}'
2378
- if (parser := self.get_parser()).exit_on_error:
2523
+ if (parser := self.get_parser()).exit_on_error: # noqa
2379
2524
  parser.error(msg)
2380
2525
  else:
2381
2526
  raise argparse.ArgumentError(None, msg)
@@ -2395,7 +2540,10 @@ class ArgparseCli:
2395
2540
  return fn()
2396
2541
 
2397
2542
  def cli_run_and_exit(self) -> ta.NoReturn:
2398
- sys.exit(rc if isinstance(rc := self.cli_run(), int) else 0)
2543
+ rc = self.cli_run()
2544
+ if not isinstance(rc, int):
2545
+ rc = 0
2546
+ raise SystemExit(rc)
2399
2547
 
2400
2548
  def __call__(self, *, exit: bool = False) -> ta.Optional[int]: # noqa
2401
2549
  if exit:
@@ -2574,8 +2722,6 @@ class _JustMaybe(_Maybe[T]):
2574
2722
  __slots__ = ('_v', '_hash')
2575
2723
 
2576
2724
  def __init__(self, v: T) -> None:
2577
- super().__init__()
2578
-
2579
2725
  self._v = v
2580
2726
 
2581
2727
  @property
@@ -2633,6 +2779,13 @@ class _EmptyMaybe(_Maybe[T]):
2633
2779
  Maybe._empty = _EmptyMaybe() # noqa
2634
2780
 
2635
2781
 
2782
+ ##
2783
+
2784
+
2785
+ setattr(Maybe, 'just', _JustMaybe) # noqa
2786
+ setattr(Maybe, 'empty', functools.partial(operator.attrgetter('_empty'), Maybe))
2787
+
2788
+
2636
2789
  ########################################
2637
2790
  # ../../../omlish/lite/runtime.py
2638
2791
 
@@ -3022,9 +3175,10 @@ class InterpSpecifier:
3022
3175
  def parse(cls, s: str) -> 'InterpSpecifier':
3023
3176
  s, o = InterpOpts.parse_suffix(s)
3024
3177
  if not any(s.startswith(o) for o in Specifier.OPERATORS):
3025
- s = '~=' + s
3026
3178
  if s.count('.') < 2:
3027
- s += '.0'
3179
+ s = '~=' + s + '.0'
3180
+ else:
3181
+ s = '==' + s
3028
3182
  return cls(
3029
3183
  specifier=Specifier(s),
3030
3184
  opts=o,
@@ -4147,10 +4301,9 @@ inj = InjectionApi()
4147
4301
 
4148
4302
 
4149
4303
  ########################################
4150
- # ../../../omlish/logs/standard.py
4304
+ # ../../../omlish/logs/std/standard.py
4151
4305
  """
4152
4306
  TODO:
4153
- - !! move to std !!
4154
4307
  - structured
4155
4308
  - prefixed
4156
4309
  - debug
@@ -4538,6 +4691,10 @@ class VerboseCalledProcessError(subprocess.CalledProcessError):
4538
4691
  class BaseSubprocesses(Abstract):
4539
4692
  DEFAULT_LOGGER: ta.ClassVar[ta.Optional[LoggerLike]] = None
4540
4693
 
4694
+ PIPE: ta.ClassVar[int] = subprocess.PIPE
4695
+ STDOUT: ta.ClassVar[int] = subprocess.STDOUT
4696
+ DEVNULL: ta.ClassVar[int] = subprocess.DEVNULL
4697
+
4541
4698
  def __init__(
4542
4699
  self,
4543
4700
  *,
@@ -5757,12 +5914,42 @@ uv run pip
5757
5914
  uv run --python 3.11.6 pip
5758
5915
  uv venv --python 3.11.6 --seed barf
5759
5916
  python3 -m venv barf && barf/bin/pip install uv && barf/bin/uv venv --python 3.11.6 --seed barf2
5917
+ uv python find '3.13.10'
5918
+ uv python list --output-format=json
5760
5919
  """
5761
5920
 
5762
5921
 
5763
5922
  ##
5764
5923
 
5765
5924
 
5925
+ @dc.dataclass(frozen=True)
5926
+ class UvPythonListOutput:
5927
+ key: str
5928
+ version: str
5929
+
5930
+ @dc.dataclass(frozen=True)
5931
+ class VersionParts:
5932
+ major: int
5933
+ minor: int
5934
+ patch: int
5935
+
5936
+ version_parts: VersionParts
5937
+
5938
+ path: ta.Optional[str]
5939
+ symlink: ta.Optional[str]
5940
+
5941
+ url: str
5942
+
5943
+ os: str # emscripten linux macos
5944
+ variant: str # default freethreaded
5945
+ implementation: str # cpython graalpy pyodide pypy
5946
+ arch: str # aarch64 wasm32 x86_64
5947
+ libc: str # gnu musl none
5948
+
5949
+
5950
+ ##
5951
+
5952
+
5766
5953
  class UvInterpProvider(InterpProvider):
5767
5954
  def __init__(
5768
5955
  self,
@@ -5783,6 +5970,12 @@ class UvInterpProvider(InterpProvider):
5783
5970
  async def get_installed_version(self, version: InterpVersion) -> Interp:
5784
5971
  raise NotImplementedError
5785
5972
 
5973
+ # async def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
5974
+ # return []
5975
+
5976
+ # async def install_version(self, version: InterpVersion) -> Interp:
5977
+ # raise TypeError
5978
+
5786
5979
 
5787
5980
  ########################################
5788
5981
  # ../providers/inject.py
@@ -13,6 +13,7 @@ import contextvars
13
13
  import dataclasses as dc
14
14
  import functools
15
15
  import inspect
16
+ import operator
16
17
  import sys
17
18
  import threading
18
19
  import types
@@ -27,11 +28,25 @@ if sys.version_info < (3, 8):
27
28
  raise OSError(f'Requires python (3, 8), got {sys.version_info} from {sys.executable}') # noqa
28
29
 
29
30
 
31
+ def __omlish_amalg__(): # noqa
32
+ return dict(
33
+ src_files=[
34
+ dict(path='abstract.py', sha1='a2fc3f3697fa8de5247761e9d554e70176f37aac'),
35
+ dict(path='check.py', sha1='bb6b6b63333699b84462951a854d99ae83195b94'),
36
+ dict(path='reflect.py', sha1='c4fec44bf144e9d93293c996af06f6c65fc5e63d'),
37
+ dict(path='maybes.py', sha1='bdf5136654ccd14b6a072588cad228925bdfbabd'),
38
+ dict(path='inject.py', sha1='6f097e3170019a34ff6834d36fcc9cbeed3a7ab4'),
39
+ ],
40
+ )
41
+
42
+
30
43
  ########################################
31
44
 
32
45
 
33
- # check.py
46
+ # abstract.py
34
47
  T = ta.TypeVar('T')
48
+
49
+ # check.py
35
50
  SizedT = ta.TypeVar('SizedT', bound=ta.Sized)
36
51
  CheckMessage = ta.Union[str, ta.Callable[..., ta.Optional[str]], None] # ta.TypeAlias
37
52
  CheckLateConfigureFn = ta.Callable[['Checks'], None] # ta.TypeAlias
@@ -64,25 +79,49 @@ def is_abstract_method(obj: ta.Any) -> bool:
64
79
  return bool(getattr(obj, _IS_ABSTRACT_METHOD_ATTR, False))
65
80
 
66
81
 
67
- def update_abstracts(cls, *, force=False):
82
+ def compute_abstract_methods(cls: type) -> ta.FrozenSet[str]:
83
+ # ~> https://github.com/python/cpython/blob/f3476c6507381ca860eec0989f53647b13517423/Modules/_abc.c#L358
84
+
85
+ # Stage 1: direct abstract methods
86
+
87
+ abstracts = {
88
+ a
89
+ # Get items as a list to avoid mutation issues during iteration
90
+ for a, v in list(cls.__dict__.items())
91
+ if is_abstract_method(v)
92
+ }
93
+
94
+ # Stage 2: inherited abstract methods
95
+
96
+ for base in cls.__bases__:
97
+ # Get __abstractmethods__ from base if it exists
98
+ if (base_abstracts := getattr(base, _ABSTRACT_METHODS_ATTR, None)) is None:
99
+ continue
100
+
101
+ # Iterate over abstract methods in base
102
+ for key in base_abstracts:
103
+ # Check if this class has an attribute with this name
104
+ try:
105
+ value = getattr(cls, key)
106
+ except AttributeError:
107
+ # Attribute not found in this class, skip
108
+ continue
109
+
110
+ # Check if it's still abstract
111
+ if is_abstract_method(value):
112
+ abstracts.add(key)
113
+
114
+ return frozenset(abstracts)
115
+
116
+
117
+ def update_abstracts(cls: ta.Type[T], *, force: bool = False) -> ta.Type[T]:
68
118
  if not force and not hasattr(cls, _ABSTRACT_METHODS_ATTR):
69
119
  # Per stdlib: We check for __abstractmethods__ here because cls might by a C implementation or a python
70
120
  # implementation (especially during testing), and we want to handle both cases.
71
121
  return cls
72
122
 
73
- abstracts: ta.Set[str] = set()
74
-
75
- for scls in cls.__bases__:
76
- for name in getattr(scls, _ABSTRACT_METHODS_ATTR, ()):
77
- value = getattr(cls, name, None)
78
- if getattr(value, _IS_ABSTRACT_METHOD_ATTR, False):
79
- abstracts.add(name)
80
-
81
- for name, value in cls.__dict__.items():
82
- if getattr(value, _IS_ABSTRACT_METHOD_ATTR, False):
83
- abstracts.add(name)
84
-
85
- setattr(cls, _ABSTRACT_METHODS_ATTR, frozenset(abstracts))
123
+ abstracts = compute_abstract_methods(cls)
124
+ setattr(cls, _ABSTRACT_METHODS_ATTR, abstracts)
86
125
  return cls
87
126
 
88
127
 
@@ -136,23 +175,26 @@ class Abstract:
136
175
  super().__init_subclass__(**kwargs)
137
176
 
138
177
  if not (Abstract in cls.__bases__ or abc.ABC in cls.__bases__):
139
- ams = {a: cls for a, o in cls.__dict__.items() if is_abstract_method(o)}
140
-
141
- seen = set(cls.__dict__)
142
- for b in cls.__bases__:
143
- ams.update({a: b for a in set(getattr(b, _ABSTRACT_METHODS_ATTR, [])) - seen}) # noqa
144
- seen.update(dir(b))
178
+ if ams := compute_abstract_methods(cls):
179
+ amd = {
180
+ a: mcls
181
+ for mcls in cls.__mro__[::-1]
182
+ for a in ams
183
+ if a in mcls.__dict__
184
+ }
145
185
 
146
- if ams:
147
186
  raise AbstractTypeError(
148
187
  f'Cannot subclass abstract class {cls.__name__} with abstract methods: ' +
149
188
  ', '.join(sorted([
150
189
  '.'.join([
151
- *([m] if (m := getattr(c, '__module__')) else []),
152
- getattr(c, '__qualname__', getattr(c, '__name__')),
190
+ *([
191
+ *([m] if (m := getattr(c, '__module__')) else []),
192
+ getattr(c, '__qualname__', getattr(c, '__name__')),
193
+ ] if c is not None else '?'),
153
194
  a,
154
195
  ])
155
- for a, c in ams.items()
196
+ for a in ams
197
+ for c in [amd.get(a)]
156
198
  ])),
157
199
  )
158
200
 
@@ -896,8 +938,6 @@ class _JustMaybe(_Maybe[T]):
896
938
  __slots__ = ('_v', '_hash')
897
939
 
898
940
  def __init__(self, v: T) -> None:
899
- super().__init__()
900
-
901
941
  self._v = v
902
942
 
903
943
  @property
@@ -955,6 +995,13 @@ class _EmptyMaybe(_Maybe[T]):
955
995
  Maybe._empty = _EmptyMaybe() # noqa
956
996
 
957
997
 
998
+ ##
999
+
1000
+
1001
+ setattr(Maybe, 'just', _JustMaybe) # noqa
1002
+ setattr(Maybe, 'empty', functools.partial(operator.attrgetter('_empty'), Maybe))
1003
+
1004
+
958
1005
  ########################################
959
1006
  # inject.py
960
1007