omlish 0.0.0.dev447__py3-none-any.whl → 0.0.0.dev484__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 omlish might be problematic. Click here for more details.

Files changed (226) hide show
  1. omlish/.omlish-manifests.json +12 -0
  2. omlish/__about__.py +15 -15
  3. omlish/argparse/all.py +17 -9
  4. omlish/argparse/cli.py +16 -3
  5. omlish/argparse/utils.py +21 -0
  6. omlish/asyncs/asyncio/rlock.py +110 -0
  7. omlish/asyncs/asyncio/sync.py +43 -0
  8. omlish/asyncs/asyncio/utils.py +2 -0
  9. omlish/asyncs/sync.py +25 -0
  10. omlish/bootstrap/_marshal.py +1 -1
  11. omlish/bootstrap/diag.py +12 -21
  12. omlish/bootstrap/main.py +2 -5
  13. omlish/bootstrap/sys.py +27 -28
  14. omlish/cexts/__init__.py +0 -0
  15. omlish/cexts/include/omlish/omlish.hh +1 -0
  16. omlish/collections/__init__.py +13 -1
  17. omlish/collections/attrregistry.py +210 -0
  18. omlish/collections/cache/impl.py +1 -0
  19. omlish/collections/identity.py +1 -0
  20. omlish/collections/mappings.py +28 -0
  21. omlish/collections/trie.py +5 -1
  22. omlish/collections/utils.py +77 -0
  23. omlish/concurrent/all.py +2 -1
  24. omlish/concurrent/futures.py +25 -0
  25. omlish/concurrent/threadlets.py +1 -1
  26. omlish/daemons/reparent.py +2 -3
  27. omlish/daemons/spawning.py +2 -3
  28. omlish/dataclasses/__init__.py +2 -0
  29. omlish/dataclasses/impl/api/classes/decorator.py +3 -0
  30. omlish/dataclasses/impl/api/classes/make.py +3 -0
  31. omlish/dataclasses/impl/concerns/repr.py +15 -2
  32. omlish/dataclasses/impl/configs.py +97 -37
  33. omlish/dataclasses/impl/generation/compilation.py +21 -19
  34. omlish/dataclasses/impl/generation/globals.py +1 -0
  35. omlish/dataclasses/impl/generation/ops.py +1 -0
  36. omlish/dataclasses/impl/generation/processor.py +105 -24
  37. omlish/dataclasses/impl/processing/base.py +8 -0
  38. omlish/dataclasses/impl/processing/driving.py +8 -8
  39. omlish/dataclasses/specs.py +1 -0
  40. omlish/dataclasses/tools/modifiers.py +5 -0
  41. omlish/diag/cmds/__init__.py +0 -0
  42. omlish/diag/{lslocks.py → cmds/lslocks.py} +6 -6
  43. omlish/diag/{lsof.py → cmds/lsof.py} +6 -6
  44. omlish/diag/{ps.py → cmds/ps.py} +6 -6
  45. omlish/diag/pycharm.py +16 -2
  46. omlish/diag/pydevd.py +58 -40
  47. omlish/diag/replserver/console.py +1 -1
  48. omlish/dispatch/__init__.py +18 -12
  49. omlish/dispatch/methods.py +50 -140
  50. omlish/dom/rendering.py +1 -1
  51. omlish/formats/dotenv.py +1 -1
  52. omlish/formats/json/stream/__init__.py +13 -0
  53. omlish/funcs/guard.py +225 -0
  54. omlish/graphs/dot/rendering.py +1 -1
  55. omlish/http/all.py +44 -4
  56. omlish/http/clients/asyncs.py +33 -27
  57. omlish/http/clients/base.py +17 -1
  58. omlish/http/clients/coro/__init__.py +0 -0
  59. omlish/http/clients/coro/sync.py +171 -0
  60. omlish/http/clients/default.py +208 -29
  61. omlish/http/clients/executor.py +56 -0
  62. omlish/http/clients/httpx.py +72 -11
  63. omlish/http/clients/middleware.py +181 -0
  64. omlish/http/clients/sync.py +33 -27
  65. omlish/http/clients/syncasync.py +49 -0
  66. omlish/http/clients/urllib.py +6 -3
  67. omlish/http/coro/client/connection.py +15 -6
  68. omlish/http/coro/io.py +2 -0
  69. omlish/http/flasky/__init__.py +40 -0
  70. omlish/http/flasky/_compat.py +2 -0
  71. omlish/http/flasky/api.py +82 -0
  72. omlish/http/flasky/app.py +203 -0
  73. omlish/http/flasky/cvs.py +59 -0
  74. omlish/http/flasky/requests.py +20 -0
  75. omlish/http/flasky/responses.py +23 -0
  76. omlish/http/flasky/routes.py +23 -0
  77. omlish/http/flasky/types.py +15 -0
  78. omlish/http/urls.py +67 -0
  79. omlish/inject/__init__.py +38 -18
  80. omlish/inject/_dataclasses.py +4986 -0
  81. omlish/inject/binder.py +4 -48
  82. omlish/inject/elements.py +27 -0
  83. omlish/inject/helpers/__init__.py +0 -0
  84. omlish/inject/{utils.py → helpers/constfn.py} +3 -3
  85. omlish/inject/{tags.py → helpers/id.py} +2 -2
  86. omlish/inject/helpers/multis.py +143 -0
  87. omlish/inject/helpers/wrappers.py +54 -0
  88. omlish/inject/impl/elements.py +47 -17
  89. omlish/inject/impl/injector.py +20 -19
  90. omlish/inject/impl/inspect.py +10 -1
  91. omlish/inject/impl/maysync.py +3 -4
  92. omlish/inject/impl/multis.py +3 -0
  93. omlish/inject/impl/sync.py +3 -4
  94. omlish/inject/injector.py +31 -2
  95. omlish/inject/inspect.py +35 -0
  96. omlish/inject/maysync.py +2 -4
  97. omlish/inject/multis.py +8 -0
  98. omlish/inject/overrides.py +3 -3
  99. omlish/inject/privates.py +6 -0
  100. omlish/inject/providers.py +3 -2
  101. omlish/inject/sync.py +5 -4
  102. omlish/io/buffers.py +118 -0
  103. omlish/io/readers.py +29 -0
  104. omlish/iterators/transforms.py +2 -2
  105. omlish/lang/__init__.py +178 -97
  106. omlish/lang/_asyncs.cc +186 -0
  107. omlish/lang/asyncs.py +17 -0
  108. omlish/lang/casing.py +11 -0
  109. omlish/lang/contextmanagers.py +28 -4
  110. omlish/lang/functions.py +31 -22
  111. omlish/lang/imports/_capture.cc +11 -9
  112. omlish/lang/imports/capture.py +363 -170
  113. omlish/lang/imports/proxy.py +455 -152
  114. omlish/lang/lazyglobals.py +22 -9
  115. omlish/lang/params.py +17 -0
  116. omlish/lang/recursion.py +0 -1
  117. omlish/lang/sequences.py +124 -0
  118. omlish/lite/abstract.py +54 -24
  119. omlish/lite/asyncs.py +2 -2
  120. omlish/lite/attrops.py +2 -0
  121. omlish/lite/contextmanagers.py +4 -4
  122. omlish/lite/dataclasses.py +44 -0
  123. omlish/lite/maybes.py +8 -0
  124. omlish/lite/maysync.py +1 -5
  125. omlish/lite/pycharm.py +1 -1
  126. omlish/lite/typing.py +6 -0
  127. omlish/logs/all.py +1 -1
  128. omlish/logs/utils.py +1 -1
  129. omlish/manifests/loading.py +2 -1
  130. omlish/marshal/__init__.py +33 -13
  131. omlish/marshal/_dataclasses.py +2774 -0
  132. omlish/marshal/base/configs.py +12 -0
  133. omlish/marshal/base/contexts.py +36 -21
  134. omlish/marshal/base/funcs.py +8 -11
  135. omlish/marshal/base/options.py +8 -0
  136. omlish/marshal/base/registries.py +146 -44
  137. omlish/marshal/base/types.py +40 -16
  138. omlish/marshal/composite/iterables.py +33 -20
  139. omlish/marshal/composite/literals.py +20 -18
  140. omlish/marshal/composite/mappings.py +36 -23
  141. omlish/marshal/composite/maybes.py +29 -19
  142. omlish/marshal/composite/newtypes.py +16 -16
  143. omlish/marshal/composite/optionals.py +14 -14
  144. omlish/marshal/composite/special.py +15 -15
  145. omlish/marshal/composite/unions/__init__.py +0 -0
  146. omlish/marshal/composite/unions/literals.py +93 -0
  147. omlish/marshal/composite/unions/primitives.py +103 -0
  148. omlish/marshal/factories/invalidate.py +18 -68
  149. omlish/marshal/factories/method.py +26 -0
  150. omlish/marshal/factories/moduleimport/factories.py +22 -65
  151. omlish/marshal/factories/multi.py +13 -25
  152. omlish/marshal/factories/recursive.py +42 -56
  153. omlish/marshal/factories/typecache.py +29 -74
  154. omlish/marshal/factories/typemap.py +42 -43
  155. omlish/marshal/objects/dataclasses.py +129 -106
  156. omlish/marshal/objects/marshal.py +18 -14
  157. omlish/marshal/objects/namedtuples.py +48 -42
  158. omlish/marshal/objects/unmarshal.py +19 -15
  159. omlish/marshal/polymorphism/marshal.py +9 -11
  160. omlish/marshal/polymorphism/metadata.py +16 -5
  161. omlish/marshal/polymorphism/standard.py +13 -1
  162. omlish/marshal/polymorphism/unions.py +15 -105
  163. omlish/marshal/polymorphism/unmarshal.py +9 -10
  164. omlish/marshal/singular/enums.py +14 -18
  165. omlish/marshal/standard.py +10 -6
  166. omlish/marshal/trivial/any.py +1 -1
  167. omlish/marshal/trivial/forbidden.py +21 -26
  168. omlish/metadata.py +23 -1
  169. omlish/os/forkhooks.py +4 -0
  170. omlish/os/pidfiles/pinning.py +2 -2
  171. omlish/reflect/types.py +1 -0
  172. omlish/secrets/marshal.py +1 -1
  173. omlish/specs/jmespath/__init__.py +12 -3
  174. omlish/specs/jmespath/_dataclasses.py +2893 -0
  175. omlish/specs/jmespath/ast.py +1 -1
  176. omlish/specs/jsonrpc/__init__.py +13 -0
  177. omlish/specs/jsonrpc/_marshal.py +32 -23
  178. omlish/specs/jsonrpc/conns.py +10 -7
  179. omlish/specs/jsonschema/_marshal.py +1 -1
  180. omlish/specs/jsonschema/keywords/__init__.py +7 -0
  181. omlish/specs/jsonschema/keywords/_dataclasses.py +1644 -0
  182. omlish/specs/openapi/_marshal.py +31 -22
  183. omlish/sql/{tabledefs/alchemy.py → alchemy/tabledefs.py} +2 -2
  184. omlish/sql/queries/_marshal.py +2 -2
  185. omlish/sql/queries/rendering.py +1 -1
  186. omlish/sql/tabledefs/_marshal.py +1 -1
  187. omlish/subprocesses/base.py +4 -0
  188. omlish/subprocesses/editor.py +1 -1
  189. omlish/sync.py +155 -21
  190. omlish/term/alt.py +60 -0
  191. omlish/term/confirm.py +8 -8
  192. omlish/term/pager.py +235 -0
  193. omlish/term/terminfo.py +935 -0
  194. omlish/term/termstate.py +67 -0
  195. omlish/term/vt100/terminal.py +0 -3
  196. omlish/testing/pytest/plugins/asyncs/fixtures.py +4 -1
  197. omlish/testing/pytest/plugins/skips.py +2 -1
  198. omlish/testing/unittest/main.py +3 -3
  199. omlish/text/docwrap/__init__.py +3 -0
  200. omlish/text/docwrap/__main__.py +11 -0
  201. omlish/text/docwrap/api.py +83 -0
  202. omlish/text/docwrap/cli.py +86 -0
  203. omlish/text/docwrap/groups.py +84 -0
  204. omlish/text/docwrap/lists.py +167 -0
  205. omlish/text/docwrap/parts.py +146 -0
  206. omlish/text/docwrap/reflowing.py +106 -0
  207. omlish/text/docwrap/rendering.py +151 -0
  208. omlish/text/docwrap/utils.py +11 -0
  209. omlish/text/docwrap/wrapping.py +59 -0
  210. omlish/text/filecache.py +2 -2
  211. omlish/text/lorem.py +6 -0
  212. omlish/text/parts.py +2 -2
  213. omlish/text/textwrap.py +51 -0
  214. omlish/typedvalues/marshal.py +85 -59
  215. omlish/typedvalues/values.py +2 -1
  216. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/METADATA +29 -28
  217. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/RECORD +222 -171
  218. omlish/dataclasses/impl/generation/mangling.py +0 -18
  219. omlish/funcs/match.py +0 -227
  220. omlish/marshal/factories/match.py +0 -34
  221. omlish/marshal/factories/simple.py +0 -28
  222. /omlish/inject/impl/{providers2.py → providersmap.py} +0 -0
  223. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/WHEEL +0 -0
  224. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/entry_points.txt +0 -0
  225. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/licenses/LICENSE +0 -0
  226. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,67 @@
1
+ import dataclasses as dc
2
+ import termios
3
+
4
+
5
+ ##
6
+
7
+
8
+ @dc.dataclass()
9
+ class TermState:
10
+ iflag: int
11
+ oflag: int
12
+ cflag: int
13
+ lflag: int
14
+ ispeed: int
15
+ ospeed: int
16
+ cc: list[bytes]
17
+
18
+ #
19
+
20
+ def copy(self) -> 'TermState':
21
+ return TermState(*self.as_list()) # type: ignore[arg-type]
22
+
23
+ def as_list(self) -> list[int | list[bytes]]:
24
+ return [
25
+ self.iflag,
26
+ self.oflag,
27
+ self.cflag,
28
+ self.lflag,
29
+ self.ispeed,
30
+ self.ospeed,
31
+ self.cc[:],
32
+ ]
33
+
34
+
35
+ def get_term_state(fd: int) -> TermState:
36
+ return TermState(*termios.tcgetattr(fd))
37
+
38
+
39
+ def set_term_state(fd: int, attrs: TermState, when: int = termios.TCSANOW) -> None:
40
+ termios.tcsetattr(fd, when, attrs.as_list())
41
+
42
+
43
+ ##
44
+
45
+
46
+ class TermStateStack:
47
+ def __init__(self, fd: int = 0) -> None: # noqa
48
+ super().__init__()
49
+
50
+ self._fd = fd
51
+ self._stack: list[TermState] = []
52
+
53
+ #
54
+
55
+ def get(self) -> TermState:
56
+ return get_term_state(self._fd)
57
+
58
+ def set(self, attrs: TermState, when: int = termios.TCSANOW) -> None:
59
+ set_term_state(self._fd, attrs, when)
60
+
61
+ #
62
+
63
+ def save(self) -> None:
64
+ self._stack.append(self.get())
65
+
66
+ def restore(self) -> None:
67
+ self.set(self._stack.pop())
@@ -5,9 +5,6 @@ import typing as ta
5
5
  from ...lite.check import check
6
6
 
7
7
 
8
- T = ta.TypeVar('T')
9
-
10
-
11
8
  ##
12
9
 
13
10
 
@@ -141,7 +141,10 @@ class AsyncsFixture:
141
141
  finally:
142
142
  nursery_fixture.cancel_scope.cancel()
143
143
 
144
- except BaseException as exc: # noqa
144
+ except* (Skipped, XFailed):
145
+ pass
146
+
147
+ except* BaseException as exc: # noqa
145
148
  test_ctx.crash(self, exc)
146
149
 
147
150
  finally:
@@ -11,10 +11,11 @@ from ._registry import register
11
11
  class SkipsPlugin:
12
12
  def pytest_collection_modifyitems(self, session, items):
13
13
  dct: dict[str, set[str]] = {}
14
- for arg in session.config.args:
14
+ for i, arg in enumerate(session.config.args):
15
15
  ca = resolve_collection_argument(
16
16
  session.config.invocation_params.dir,
17
17
  arg,
18
+ i,
18
19
  as_pypath=session.config.option.pyargs,
19
20
  )
20
21
  if ca.path.is_file():
@@ -257,11 +257,11 @@ class UnittestRunCli:
257
257
 
258
258
  if exit:
259
259
  if not result.num_tests_run and not result.skipped:
260
- sys.exit(self.NO_TESTS_EXITCODE)
260
+ raise SystemExit(self.NO_TESTS_EXITCODE)
261
261
  elif result.was_successful:
262
- sys.exit(0)
262
+ raise SystemExit(0)
263
263
  else:
264
- sys.exit(1)
264
+ raise SystemExit(1)
265
265
 
266
266
 
267
267
  ##
@@ -0,0 +1,3 @@
1
+ from .api import ( # noqa
2
+ docwrap,
3
+ )
@@ -0,0 +1,11 @@
1
+ # @omlish-manifest
2
+ _CLI_MODULE = {'!omdev.cli.types.CliModule': {
3
+ 'name': 'docwrap',
4
+ 'module': __name__,
5
+ }}
6
+
7
+
8
+ if __name__ == '__main__':
9
+ from .cli import _main
10
+
11
+ _main()
@@ -0,0 +1,83 @@
1
+ import dataclasses as dc
2
+ import typing as ta
3
+
4
+ from ..textwrap import TextwrapOpts
5
+ from .groups import group_indents
6
+ from .lists import ListBuilder
7
+ from .parts import Part
8
+ from .parts import build_root
9
+ from .reflowing import TextwrapReflower
10
+ from .reflowing import reflow_block_text
11
+
12
+
13
+ ##
14
+
15
+
16
+ DEFAULT_TAB_WIDTH: int = 4
17
+
18
+
19
+ def replace_tabs(s: str, tab_width: int | None = None) -> str:
20
+ if tab_width is None:
21
+ tab_width = DEFAULT_TAB_WIDTH
22
+ return s.replace('\t', ' ' * tab_width)
23
+
24
+
25
+ ##
26
+
27
+
28
+ def parse(
29
+ s: str,
30
+ *,
31
+ tab_width: int | None = None,
32
+ allow_improper_list_children: bool | ta.Literal['lists_only'] | None = None,
33
+ ) -> Part:
34
+ s = replace_tabs(
35
+ s,
36
+ tab_width=tab_width,
37
+ )
38
+
39
+ root = build_root(s)
40
+
41
+ root = group_indents(root)
42
+
43
+ root = ListBuilder(
44
+ allow_improper_children=allow_improper_list_children,
45
+ ).build_lists(root)
46
+
47
+ return root
48
+
49
+
50
+ ##
51
+
52
+
53
+ DEFAULT_TEXTWRAP_OPTS = TextwrapOpts(
54
+ width=120,
55
+ break_on_hyphens=False,
56
+ )
57
+
58
+
59
+ def docwrap(
60
+ s: str,
61
+ *,
62
+ width: int | None = None,
63
+ textwrap: TextwrapOpts | ta.Mapping[str, ta.Any] | None = None,
64
+ allow_improper_list_children: bool | ta.Literal['lists_only'] = False,
65
+ ) -> Part:
66
+ if isinstance(textwrap, ta.Mapping):
67
+ textwrap = TextwrapOpts(**textwrap)
68
+ elif textwrap is None:
69
+ textwrap = DEFAULT_TEXTWRAP_OPTS
70
+ if width is not None:
71
+ textwrap = dc.replace(textwrap, width=width)
72
+
73
+ root = parse(
74
+ s,
75
+ allow_improper_list_children=allow_improper_list_children,
76
+ )
77
+
78
+ root = reflow_block_text(
79
+ root,
80
+ TextwrapReflower(opts=textwrap),
81
+ )
82
+
83
+ return root
@@ -0,0 +1,86 @@
1
+ """
2
+ Usable as a jetbrains external tool:
3
+ `om docwrap -i "$FilePath$" -s "$SelectionStartLine$" -e "$SelectionEndLine$"`
4
+ """
5
+ import argparse
6
+ import sys
7
+ import typing as ta
8
+
9
+ from .api import docwrap
10
+ from .rendering import render
11
+
12
+
13
+ ##
14
+
15
+
16
+ def _main(argv: ta.Sequence[str] | None = None) -> None:
17
+ parser = argparse.ArgumentParser()
18
+ parser.add_argument('file', nargs='?')
19
+
20
+ parser.add_argument('-w', '--width', type=int, default=120)
21
+
22
+ parser.add_argument('-s', '--start-line', type=int)
23
+ parser.add_argument('-e', '--end-line', type=int)
24
+ parser.add_argument('-i', '--in-place', action='store_true')
25
+
26
+ args = parser.parse_args(argv)
27
+
28
+ #
29
+
30
+ if args.file:
31
+ with open(args.file) as f:
32
+ in_txt = f.read()
33
+ else:
34
+ if args.in_place:
35
+ raise ValueError('Cannot use --in-place without specifying a file')
36
+ in_txt = sys.stdin.read()
37
+
38
+ in_lines = in_txt.splitlines()
39
+
40
+ #
41
+
42
+ if args.start_line is not None and args.end_line is not None:
43
+ if args.start_line > args.end_line:
44
+ raise ValueError('Start line cannot be greater than end line')
45
+ if args.start_line is not None:
46
+ if args.start_line < 1:
47
+ raise ValueError('Start line cannot be less than 1')
48
+ start_line = args.start_line - 1
49
+ else:
50
+ start_line = 0
51
+ if args.end_line is not None:
52
+ if args.end_line < 1:
53
+ raise ValueError('End line cannot be less than 1')
54
+ end_line = args.end_line - 1
55
+ else:
56
+ end_line = len(in_lines) - 1
57
+
58
+ #
59
+
60
+ in_part = '\n'.join(in_lines[start_line:end_line + 1])
61
+
62
+ root = docwrap(
63
+ in_part,
64
+ width=args.width,
65
+ )
66
+
67
+ out_part = render(root)
68
+
69
+ out_txt = '\n'.join([
70
+ *in_lines[:start_line],
71
+ out_part,
72
+ *in_lines[end_line + 1:],
73
+ '',
74
+ ])
75
+
76
+ #
77
+
78
+ if args.in_place:
79
+ with open(args.file, 'w') as f:
80
+ f.write(out_txt)
81
+ else:
82
+ print(out_txt)
83
+
84
+
85
+ if __name__ == '__main__':
86
+ _main()
@@ -0,0 +1,84 @@
1
+ import typing as ta
2
+
3
+ from ... import check
4
+ from ... import dataclasses as dc
5
+ from .parts import Blank
6
+ from .parts import Block
7
+ from .parts import Indent
8
+ from .parts import Part
9
+ from .parts import Text
10
+ from .parts import blockify
11
+
12
+
13
+ ##
14
+
15
+
16
+ @dc.dataclass()
17
+ @dc.extra_class_params(default_repr_fn=dc.truthy_repr)
18
+ class _IndentGroup:
19
+ n: int
20
+ cs: list[ta.Union[Blank, Text, '_IndentGroup']] = dc.field(default_factory=list)
21
+
22
+
23
+ def group_indents(root: Part) -> Part:
24
+ rg = _IndentGroup(0)
25
+ stk: list[_IndentGroup] = [rg]
26
+
27
+ for p in (root.ps if isinstance(root, Block) else [root]):
28
+ if isinstance(p, Blank):
29
+ stk[-1].cs.append(p)
30
+ continue
31
+
32
+ n: int
33
+ t: Text
34
+ if isinstance(p, Text):
35
+ n, t = 0, p
36
+ elif isinstance(p, Indent):
37
+ n = p.n
38
+ t = check.isinstance(p.p, Text)
39
+ else:
40
+ raise TypeError(p)
41
+
42
+ while n < stk[-1].n:
43
+ stk.pop()
44
+
45
+ if n > stk[-1].n:
46
+ nxt = _IndentGroup(n=n, cs=[t])
47
+ stk[-1].cs.append(nxt)
48
+ stk.append(nxt)
49
+
50
+ else:
51
+ check.state(stk[-1].n == n)
52
+ stk[-1].cs.append(t)
53
+
54
+ #
55
+
56
+ def relativize(g: '_IndentGroup') -> None:
57
+ for c in g.cs:
58
+ if isinstance(c, _IndentGroup):
59
+ check.state(c.n > g.n)
60
+ relativize(c)
61
+ c.n -= g.n
62
+
63
+ relativize(rg)
64
+
65
+ #
66
+
67
+ def convert(g: '_IndentGroup') -> Part:
68
+ if g.n < 1:
69
+ check.state(g is rg)
70
+
71
+ lst: list[Part] = []
72
+ for c in g.cs:
73
+ if isinstance(c, (Blank, Text)):
74
+ lst.append(c)
75
+
76
+ elif isinstance(c, _IndentGroup):
77
+ lst.append(Indent(c.n, convert(c))) # type: ignore[arg-type]
78
+
79
+ else:
80
+ raise TypeError(c)
81
+
82
+ return blockify(*lst)
83
+
84
+ return convert(rg)
@@ -0,0 +1,167 @@
1
+ """
2
+ TODO:
3
+ - numeric lettered lists (even unordered) (with separator - `1)` / `1:` / ...)
4
+ """
5
+ import typing as ta
6
+
7
+ from ... import check
8
+ from .parts import Blank
9
+ from .parts import Block
10
+ from .parts import Indent
11
+ from .parts import List
12
+ from .parts import Part
13
+ from .parts import Text
14
+ from .parts import blockify
15
+ from .utils import all_same
16
+
17
+
18
+ ##
19
+
20
+
21
+ class ListBuilder:
22
+ DEFAULT_ALLOW_IMPROPER_CHILDREN: ta.ClassVar[bool | ta.Literal['lists_only']] = False
23
+ DEFAULT_LIST_PREFIXES: ta.ClassVar[ta.Sequence[str]] = ['*', '-']
24
+
25
+ def __init__(
26
+ self,
27
+ *,
28
+ list_prefixes: ta.Iterable[str] | None = None,
29
+ allow_improper_children: bool | ta.Literal['lists_only'] | None = None,
30
+ ) -> None:
31
+ super().__init__()
32
+
33
+ if list_prefixes is None:
34
+ list_prefixes = self.DEFAULT_LIST_PREFIXES
35
+ self._list_prefixes = set(check.not_isinstance(list_prefixes, str))
36
+ if allow_improper_children is None:
37
+ allow_improper_children = self.DEFAULT_ALLOW_IMPROPER_CHILDREN
38
+ self._allow_improper_children = allow_improper_children
39
+
40
+ self._len_sorted_list_prefixes = sorted(self._list_prefixes, key=len, reverse=True)
41
+
42
+ #
43
+
44
+ def _should_promote_indent_child(self, p: Indent) -> bool:
45
+ ac = self._allow_improper_children
46
+ if isinstance(ac, bool):
47
+ return ac
48
+ elif ac == 'lists_only':
49
+ return isinstance(p.p, List)
50
+ else:
51
+ raise TypeError(ac)
52
+
53
+ #
54
+
55
+ class _DetectedList(ta.NamedTuple):
56
+ pfx: str
57
+ ofs: int
58
+ len: int
59
+
60
+ def _detect_list(self, ps: ta.Sequence[Part], st: int = 0) -> _DetectedList | None:
61
+ if not ps:
62
+ return None
63
+
64
+ for lp in self._len_sorted_list_prefixes:
65
+ sp = lp + ' '
66
+
67
+ mo = -1
68
+ n = st
69
+ while n < len(ps):
70
+ p = ps[n]
71
+
72
+ if isinstance(p, (Blank, Text)):
73
+ if isinstance(p, Text):
74
+ if p.s.startswith(sp):
75
+ if mo < 0:
76
+ mo = n
77
+ elif mo >= 0:
78
+ break
79
+
80
+ elif isinstance(p, Indent):
81
+ if mo >= 0 and p.n < len(sp):
82
+ if not self._should_promote_indent_child(p):
83
+ break
84
+
85
+ elif isinstance(p, List):
86
+ if mo >= 0:
87
+ break
88
+
89
+ else:
90
+ raise TypeError(p)
91
+
92
+ n += 1
93
+
94
+ if mo >= 0:
95
+ return ListBuilder._DetectedList(lp, mo, n - mo)
96
+
97
+ return None
98
+
99
+ def _build_list(self, lp: str, ps: ta.Sequence[Part]) -> List:
100
+ sp = lp + ' '
101
+
102
+ new: list[list[Part]] = []
103
+
104
+ f = check.isinstance(ps[0], Text)
105
+ check.state(f.s.startswith(sp))
106
+ new.append([Text(f.s[len(sp):])])
107
+ del f
108
+
109
+ for i in range(1, len(ps)):
110
+ p = ps[i]
111
+
112
+ if isinstance(p, Blank):
113
+ new[-1].append(p)
114
+
115
+ elif isinstance(p, Text):
116
+ check.state(p.s.startswith(sp))
117
+ new.append([Text(p.s[len(sp):])])
118
+
119
+ elif isinstance(p, Indent):
120
+ if p.n < len(sp):
121
+ check.state(self._should_promote_indent_child(p))
122
+ p = Indent(len(sp), p.p)
123
+
124
+ if p.n == len(sp):
125
+ new[-1].append(p.p)
126
+
127
+ else:
128
+ raise NotImplementedError
129
+
130
+ else:
131
+ raise TypeError(p)
132
+
133
+ #
134
+
135
+ return List(lp, [blockify(*x) for x in new])
136
+
137
+ def build_lists(self, root: Part) -> Part:
138
+ def rec(p: Part) -> Part: # noqa
139
+ if isinstance(p, Block):
140
+ new = [rec(c) for c in p.ps]
141
+ if not all_same(new, p.ps):
142
+ return rec(blockify(*new))
143
+
144
+ st = 0
145
+ diff = False
146
+ while (dl := self._detect_list(new, st)) is not None:
147
+ diff = True
148
+ ln = self._build_list(dl.pfx, new[dl.ofs:dl.ofs + dl.len])
149
+ new[dl.ofs:dl.ofs + dl.len] = [ln]
150
+ st = dl.ofs + 1
151
+
152
+ if diff:
153
+ p = blockify(*new)
154
+ return p
155
+
156
+ elif isinstance(p, Indent):
157
+ if (n := rec(p.p)) is not p.p:
158
+ p = Indent(p.n, n) # type: ignore[arg-type]
159
+ return p
160
+
161
+ elif isinstance(p, (Blank, Text, List)):
162
+ return p
163
+
164
+ else:
165
+ raise TypeError(p)
166
+
167
+ return rec(root)
@@ -0,0 +1,146 @@
1
+ import typing as ta
2
+
3
+ from ... import check
4
+ from ... import dataclasses as dc
5
+ from ... import lang
6
+
7
+
8
+ ##
9
+
10
+
11
+ @dc.dataclass(frozen=True)
12
+ @dc.extra_class_params(terse_repr=True)
13
+ class Part(lang.Abstract, lang.Sealed):
14
+ pass
15
+
16
+
17
+ @dc.dataclass(frozen=True)
18
+ @dc.extra_class_params(terse_repr=True)
19
+ class Text(Part, lang.Final):
20
+ s: str
21
+
22
+ @dc.init
23
+ def _check_s(self) -> None:
24
+ check.non_empty_str(self.s)
25
+ check.state(self.s == self.s.strip())
26
+
27
+
28
+ @dc.dataclass(frozen=True)
29
+ @dc.extra_class_params(terse_repr=True)
30
+ class Blank(Part, lang.Final):
31
+ pass
32
+
33
+
34
+ @dc.dataclass(frozen=True)
35
+ @dc.extra_class_params(terse_repr=True)
36
+ class Indent(Part, lang.Final):
37
+ n: int = dc.xfield(validate=lambda n: n > 0)
38
+ p: ta.Union[Text, 'Block', 'List'] = dc.xfield(coerce=lambda p: check.isinstance(p, (Text, Block, List)))
39
+
40
+
41
+ @dc.dataclass(frozen=True)
42
+ @dc.extra_class_params(terse_repr=True)
43
+ class Block(Part, lang.Final):
44
+ ps: ta.Sequence[Part]
45
+
46
+ @dc.init
47
+ def _check_ps(self) -> None:
48
+ check.state(len(self.ps) > 1)
49
+ for i, p in enumerate(self.ps):
50
+ check.isinstance(p, Part)
51
+ if i and isinstance(p, Block):
52
+ check.not_isinstance(self.ps[i - 1], Block)
53
+
54
+
55
+ @dc.dataclass(frozen=True)
56
+ @dc.extra_class_params(terse_repr=True)
57
+ class List(Part, lang.Final):
58
+ d: str = dc.xfield(coerce=check.non_empty_str)
59
+ es: ta.Sequence[Part] = dc.xfield()
60
+
61
+ @dc.init
62
+ def _check_es(self) -> None:
63
+ check.not_empty(self.es)
64
+ for e in self.es:
65
+ check.isinstance(e, Part)
66
+
67
+
68
+ ##
69
+
70
+
71
+ def _squish(ps: ta.Sequence[Part]) -> ta.Sequence[Part]:
72
+ for p in ps:
73
+ check.isinstance(p, Part)
74
+
75
+ if len(ps) < 2:
76
+ return ps
77
+
78
+ while True:
79
+ if any(isinstance(p, Block) for p in ps):
80
+ ps = list(lang.flatmap(lambda p: p.ps if isinstance(p, Block) else [p], ps))
81
+ continue
82
+
83
+ if any(
84
+ isinstance(ps[i], Indent) and
85
+ isinstance(ps[i + 1], Indent) and
86
+ ps[i].n == ps[i + 1].n # type: ignore[attr-defined]
87
+ for i in range(len(ps) - 1)
88
+ ):
89
+ new: list[Part | tuple[int, list[Part]]] = []
90
+ for p in ps:
91
+ if isinstance(p, Indent):
92
+ if new and isinstance(y := new[-1], tuple) and p.n == y[0]:
93
+ y[1].append(p.p)
94
+ else:
95
+ new.append((p.n, [p.p]))
96
+ else:
97
+ new.append(p)
98
+ ps = [
99
+ Indent(x[0], blockify(*x[1])) if isinstance(x, tuple) else x # type: ignore[arg-type]
100
+ for x in new
101
+ ]
102
+ continue
103
+
104
+ break
105
+
106
+ return ps
107
+
108
+
109
+ def blockify(*ps: Part) -> Part:
110
+ check.not_empty(ps)
111
+ ps = _squish(ps) # type: ignore[assignment]
112
+ if len(ps) == 1:
113
+ return ps[0]
114
+ return Block(ps)
115
+
116
+
117
+ def unblockify(p: Part) -> ta.Sequence[Part]:
118
+ if isinstance(p, Block):
119
+ return p.ps
120
+ else:
121
+ return [p]
122
+
123
+
124
+ ##
125
+
126
+
127
+ def build_root(s: str) -> Part:
128
+ lst: list[Part] = []
129
+
130
+ for l in s.split('\n'):
131
+ if not (sl := l.strip()):
132
+ lst.append(Blank())
133
+ continue
134
+
135
+ p: Part = Text(sl)
136
+
137
+ n = next((i for i, c in enumerate(l) if not c.isspace()), 0)
138
+ if n:
139
+ p = Indent(n, p) # type: ignore[arg-type]
140
+
141
+ lst.append(p)
142
+
143
+ if len(lst) == 1:
144
+ return lst[0]
145
+ else:
146
+ return Block(lst)