omlish 0.0.0.dev447__py3-none-any.whl → 0.0.0.dev493__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 (265) hide show
  1. omlish/.omlish-manifests.json +12 -0
  2. omlish/README.md +199 -0
  3. omlish/__about__.py +21 -16
  4. omlish/argparse/all.py +17 -9
  5. omlish/argparse/cli.py +16 -3
  6. omlish/argparse/utils.py +21 -0
  7. omlish/asyncs/asyncio/rlock.py +110 -0
  8. omlish/asyncs/asyncio/sync.py +43 -0
  9. omlish/asyncs/asyncio/utils.py +2 -0
  10. omlish/asyncs/sync.py +25 -0
  11. omlish/bootstrap/_marshal.py +1 -1
  12. omlish/bootstrap/diag.py +12 -21
  13. omlish/bootstrap/main.py +2 -5
  14. omlish/bootstrap/sys.py +27 -28
  15. omlish/cexts/__init__.py +0 -0
  16. omlish/cexts/include/omlish/omlish.hh +1 -0
  17. omlish/collections/__init__.py +13 -1
  18. omlish/collections/attrregistry.py +210 -0
  19. omlish/collections/cache/impl.py +1 -0
  20. omlish/collections/identity.py +1 -0
  21. omlish/collections/mappings.py +28 -0
  22. omlish/collections/trie.py +5 -1
  23. omlish/collections/utils.py +77 -0
  24. omlish/concurrent/all.py +2 -1
  25. omlish/concurrent/futures.py +25 -0
  26. omlish/concurrent/threadlets.py +1 -1
  27. omlish/daemons/reparent.py +2 -3
  28. omlish/daemons/spawning.py +2 -3
  29. omlish/dataclasses/__init__.py +2 -0
  30. omlish/dataclasses/impl/api/classes/decorator.py +3 -0
  31. omlish/dataclasses/impl/api/classes/make.py +3 -0
  32. omlish/dataclasses/impl/concerns/repr.py +15 -2
  33. omlish/dataclasses/impl/configs.py +97 -37
  34. omlish/dataclasses/impl/generation/compilation.py +21 -19
  35. omlish/dataclasses/impl/generation/globals.py +1 -0
  36. omlish/dataclasses/impl/generation/ops.py +1 -0
  37. omlish/dataclasses/impl/generation/plans.py +2 -17
  38. omlish/dataclasses/impl/generation/processor.py +106 -25
  39. omlish/dataclasses/impl/processing/base.py +8 -0
  40. omlish/dataclasses/impl/processing/driving.py +19 -7
  41. omlish/dataclasses/specs.py +1 -0
  42. omlish/dataclasses/tools/modifiers.py +5 -0
  43. omlish/diag/_pycharm/runhack.py +1 -1
  44. omlish/diag/cmds/__init__.py +0 -0
  45. omlish/diag/{lslocks.py → cmds/lslocks.py} +6 -6
  46. omlish/diag/{lsof.py → cmds/lsof.py} +6 -6
  47. omlish/diag/{ps.py → cmds/ps.py} +6 -6
  48. omlish/diag/pycharm.py +16 -2
  49. omlish/diag/pydevd.py +58 -40
  50. omlish/diag/replserver/console.py +1 -1
  51. omlish/dispatch/__init__.py +18 -12
  52. omlish/dispatch/methods.py +50 -140
  53. omlish/dom/rendering.py +1 -1
  54. omlish/formats/dotenv.py +1 -1
  55. omlish/formats/json/stream/__init__.py +13 -0
  56. omlish/funcs/guard.py +225 -0
  57. omlish/graphs/dot/rendering.py +1 -1
  58. omlish/http/all.py +44 -4
  59. omlish/http/clients/asyncs.py +33 -27
  60. omlish/http/clients/base.py +17 -1
  61. omlish/http/clients/coro/__init__.py +0 -0
  62. omlish/http/clients/coro/sync.py +171 -0
  63. omlish/http/clients/default.py +208 -29
  64. omlish/http/clients/executor.py +56 -0
  65. omlish/http/clients/httpx.py +72 -11
  66. omlish/http/clients/middleware.py +181 -0
  67. omlish/http/clients/sync.py +33 -27
  68. omlish/http/clients/syncasync.py +49 -0
  69. omlish/http/clients/urllib.py +6 -3
  70. omlish/http/coro/client/connection.py +15 -6
  71. omlish/http/coro/io.py +2 -0
  72. omlish/http/flasky/__init__.py +40 -0
  73. omlish/http/flasky/_compat.py +2 -0
  74. omlish/http/flasky/api.py +82 -0
  75. omlish/http/flasky/app.py +203 -0
  76. omlish/http/flasky/cvs.py +59 -0
  77. omlish/http/flasky/requests.py +20 -0
  78. omlish/http/flasky/responses.py +23 -0
  79. omlish/http/flasky/routes.py +23 -0
  80. omlish/http/flasky/types.py +15 -0
  81. omlish/http/urls.py +67 -0
  82. omlish/inject/__init__.py +57 -29
  83. omlish/inject/_dataclasses.py +5148 -0
  84. omlish/inject/binder.py +11 -52
  85. omlish/inject/eagers.py +2 -0
  86. omlish/inject/elements.py +27 -0
  87. omlish/inject/helpers/__init__.py +0 -0
  88. omlish/inject/{utils.py → helpers/constfn.py} +3 -3
  89. omlish/inject/{tags.py → helpers/id.py} +2 -2
  90. omlish/inject/helpers/late.py +76 -0
  91. omlish/inject/{managed.py → helpers/managed.py} +10 -10
  92. omlish/inject/helpers/multis.py +143 -0
  93. omlish/inject/helpers/wrappers.py +54 -0
  94. omlish/inject/impl/elements.py +54 -21
  95. omlish/inject/impl/injector.py +29 -25
  96. omlish/inject/impl/inspect.py +10 -1
  97. omlish/inject/impl/maysync.py +3 -4
  98. omlish/inject/impl/multis.py +3 -0
  99. omlish/inject/impl/sync.py +3 -4
  100. omlish/inject/injector.py +31 -2
  101. omlish/inject/inspect.py +35 -0
  102. omlish/inject/maysync.py +2 -4
  103. omlish/inject/multis.py +8 -0
  104. omlish/inject/overrides.py +3 -3
  105. omlish/inject/privates.py +6 -0
  106. omlish/inject/providers.py +3 -2
  107. omlish/inject/sync.py +5 -4
  108. omlish/io/buffers.py +118 -0
  109. omlish/io/readers.py +29 -0
  110. omlish/iterators/transforms.py +2 -2
  111. omlish/lang/__init__.py +180 -97
  112. omlish/lang/_asyncs.cc +186 -0
  113. omlish/lang/asyncs.py +17 -0
  114. omlish/lang/casing.py +11 -0
  115. omlish/lang/contextmanagers.py +28 -4
  116. omlish/lang/functions.py +31 -22
  117. omlish/lang/imports/_capture.cc +11 -9
  118. omlish/lang/imports/capture.py +363 -170
  119. omlish/lang/imports/proxy.py +455 -152
  120. omlish/lang/lazyglobals.py +22 -9
  121. omlish/lang/params.py +17 -0
  122. omlish/lang/recursion.py +0 -1
  123. omlish/lang/sequences.py +124 -0
  124. omlish/lifecycles/README.md +30 -0
  125. omlish/lifecycles/__init__.py +87 -13
  126. omlish/lifecycles/_dataclasses.py +1388 -0
  127. omlish/lifecycles/base.py +178 -64
  128. omlish/lifecycles/contextmanagers.py +113 -4
  129. omlish/lifecycles/controller.py +150 -87
  130. omlish/lifecycles/injection.py +143 -0
  131. omlish/lifecycles/listeners.py +56 -0
  132. omlish/lifecycles/managed.py +142 -0
  133. omlish/lifecycles/manager.py +218 -93
  134. omlish/lifecycles/states.py +2 -0
  135. omlish/lifecycles/transitions.py +3 -0
  136. omlish/lifecycles/unwrap.py +57 -0
  137. omlish/lite/abstract.py +54 -24
  138. omlish/lite/asyncs.py +2 -2
  139. omlish/lite/attrops.py +2 -0
  140. omlish/lite/contextmanagers.py +4 -4
  141. omlish/lite/dataclasses.py +44 -0
  142. omlish/lite/maybes.py +8 -0
  143. omlish/lite/maysync.py +1 -5
  144. omlish/lite/pycharm.py +1 -1
  145. omlish/lite/typing.py +24 -0
  146. omlish/logs/_amalg.py +1 -1
  147. omlish/logs/all.py +25 -11
  148. omlish/logs/asyncs.py +73 -0
  149. omlish/logs/base.py +101 -12
  150. omlish/logs/contexts.py +4 -1
  151. omlish/logs/lists.py +125 -0
  152. omlish/logs/modules.py +19 -1
  153. omlish/logs/std/loggers.py +6 -1
  154. omlish/logs/std/noisy.py +11 -9
  155. omlish/logs/{standard.py → std/standard.py} +3 -4
  156. omlish/logs/utils.py +17 -2
  157. omlish/manifests/loading.py +2 -1
  158. omlish/marshal/__init__.py +33 -13
  159. omlish/marshal/_dataclasses.py +2774 -0
  160. omlish/marshal/base/configs.py +12 -0
  161. omlish/marshal/base/contexts.py +36 -21
  162. omlish/marshal/base/funcs.py +8 -11
  163. omlish/marshal/base/options.py +8 -0
  164. omlish/marshal/base/registries.py +146 -44
  165. omlish/marshal/base/types.py +40 -16
  166. omlish/marshal/composite/iterables.py +33 -20
  167. omlish/marshal/composite/literals.py +20 -18
  168. omlish/marshal/composite/mappings.py +36 -23
  169. omlish/marshal/composite/maybes.py +29 -19
  170. omlish/marshal/composite/newtypes.py +16 -16
  171. omlish/marshal/composite/optionals.py +14 -14
  172. omlish/marshal/composite/special.py +15 -15
  173. omlish/marshal/composite/unions/__init__.py +0 -0
  174. omlish/marshal/composite/unions/literals.py +93 -0
  175. omlish/marshal/composite/unions/primitives.py +103 -0
  176. omlish/marshal/factories/invalidate.py +18 -68
  177. omlish/marshal/factories/method.py +26 -0
  178. omlish/marshal/factories/moduleimport/factories.py +22 -65
  179. omlish/marshal/factories/multi.py +13 -25
  180. omlish/marshal/factories/recursive.py +42 -56
  181. omlish/marshal/factories/typecache.py +29 -74
  182. omlish/marshal/factories/typemap.py +42 -43
  183. omlish/marshal/objects/dataclasses.py +129 -106
  184. omlish/marshal/objects/marshal.py +18 -14
  185. omlish/marshal/objects/namedtuples.py +48 -42
  186. omlish/marshal/objects/unmarshal.py +19 -15
  187. omlish/marshal/polymorphism/marshal.py +9 -11
  188. omlish/marshal/polymorphism/metadata.py +16 -5
  189. omlish/marshal/polymorphism/standard.py +13 -1
  190. omlish/marshal/polymorphism/unions.py +15 -105
  191. omlish/marshal/polymorphism/unmarshal.py +9 -10
  192. omlish/marshal/singular/enums.py +14 -18
  193. omlish/marshal/standard.py +10 -6
  194. omlish/marshal/trivial/any.py +1 -1
  195. omlish/marshal/trivial/forbidden.py +21 -26
  196. omlish/metadata.py +23 -1
  197. omlish/os/forkhooks.py +4 -0
  198. omlish/os/pidfiles/pinning.py +2 -2
  199. omlish/reflect/__init__.py +43 -26
  200. omlish/reflect/ops.py +10 -1
  201. omlish/reflect/types.py +1 -0
  202. omlish/secrets/marshal.py +1 -1
  203. omlish/specs/jmespath/__init__.py +12 -3
  204. omlish/specs/jmespath/_dataclasses.py +2893 -0
  205. omlish/specs/jmespath/ast.py +1 -1
  206. omlish/specs/jsonrpc/__init__.py +13 -0
  207. omlish/specs/jsonrpc/_marshal.py +32 -23
  208. omlish/specs/jsonrpc/conns.py +10 -7
  209. omlish/specs/jsonschema/_marshal.py +1 -1
  210. omlish/specs/jsonschema/keywords/__init__.py +7 -0
  211. omlish/specs/jsonschema/keywords/_dataclasses.py +1644 -0
  212. omlish/specs/openapi/_marshal.py +31 -22
  213. omlish/sql/__init__.py +24 -5
  214. omlish/sql/{tabledefs/alchemy.py → alchemy/tabledefs.py} +2 -2
  215. omlish/sql/api/dbapi.py +1 -1
  216. omlish/sql/dbapi/__init__.py +15 -0
  217. omlish/sql/{dbapi.py → dbapi/drivers.py} +2 -2
  218. omlish/sql/queries/__init__.py +3 -0
  219. omlish/sql/queries/_marshal.py +2 -2
  220. omlish/sql/queries/rendering.py +1 -1
  221. omlish/sql/tabledefs/_marshal.py +1 -1
  222. omlish/subprocesses/base.py +4 -0
  223. omlish/subprocesses/editor.py +1 -1
  224. omlish/sync.py +155 -21
  225. omlish/term/alt.py +60 -0
  226. omlish/term/confirm.py +8 -8
  227. omlish/term/pager.py +235 -0
  228. omlish/term/terminfo.py +935 -0
  229. omlish/term/termstate.py +67 -0
  230. omlish/term/vt100/terminal.py +0 -3
  231. omlish/testing/pytest/plugins/asyncs/fixtures.py +4 -1
  232. omlish/testing/pytest/plugins/asyncs/plugin.py +2 -0
  233. omlish/testing/pytest/plugins/skips.py +2 -1
  234. omlish/testing/unittest/main.py +3 -3
  235. omlish/text/docwrap/__init__.py +3 -0
  236. omlish/text/docwrap/__main__.py +11 -0
  237. omlish/text/docwrap/api.py +83 -0
  238. omlish/text/docwrap/cli.py +91 -0
  239. omlish/text/docwrap/groups.py +84 -0
  240. omlish/text/docwrap/lists.py +167 -0
  241. omlish/text/docwrap/parts.py +146 -0
  242. omlish/text/docwrap/reflowing.py +106 -0
  243. omlish/text/docwrap/rendering.py +151 -0
  244. omlish/text/docwrap/utils.py +11 -0
  245. omlish/text/docwrap/wrapping.py +59 -0
  246. omlish/text/filecache.py +2 -2
  247. omlish/text/lorem.py +6 -0
  248. omlish/text/parts.py +2 -2
  249. omlish/text/textwrap.py +51 -0
  250. omlish/typedvalues/marshal.py +85 -59
  251. omlish/typedvalues/values.py +2 -1
  252. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/METADATA +36 -32
  253. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/RECORD +260 -199
  254. omlish/dataclasses/impl/generation/mangling.py +0 -18
  255. omlish/funcs/match.py +0 -227
  256. omlish/lifecycles/abstract.py +0 -86
  257. omlish/marshal/factories/match.py +0 -34
  258. omlish/marshal/factories/simple.py +0 -28
  259. /omlish/inject/{impl → helpers}/proxy.py +0 -0
  260. /omlish/inject/impl/{providers2.py → providersmap.py} +0 -0
  261. /omlish/sql/{abc.py → dbapi/abc.py} +0 -0
  262. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/WHEEL +0 -0
  263. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/entry_points.txt +0 -0
  264. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/licenses/LICENSE +0 -0
  265. {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev493.dist-info}/top_level.txt +0 -0
@@ -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)
@@ -0,0 +1,106 @@
1
+ """
2
+ FIXME:
3
+ - use wrapping.py with all its todos
4
+ """
5
+ import dataclasses as dc
6
+ import textwrap
7
+ import typing as ta
8
+
9
+ from ... import lang
10
+ from ..textwrap import TextwrapOpts
11
+ from .parts import Blank
12
+ from .parts import Block
13
+ from .parts import Indent
14
+ from .parts import List
15
+ from .parts import Part
16
+ from .parts import Text
17
+ from .parts import blockify
18
+ from .parts import unblockify
19
+ from .utils import all_same
20
+
21
+
22
+ ##
23
+
24
+
25
+ class TextReflower(ta.Protocol):
26
+ def __call__(self, strs: ta.Sequence[str], current_indent: int) -> ta.Sequence[str]: ...
27
+
28
+
29
+ class NopReflower:
30
+ def __call__(self, strs: ta.Sequence[str], current_indent: int = 0) -> ta.Sequence[str]:
31
+ return strs
32
+
33
+
34
+ @dc.dataclass(frozen=True)
35
+ class JoiningReflower:
36
+ sep: str = ' '
37
+
38
+ def __call__(self, strs: ta.Sequence[str], current_indent: int = 0) -> ta.Sequence[str]:
39
+ return [self.sep.join(strs)]
40
+
41
+
42
+ @dc.dataclass(frozen=True)
43
+ class TextwrapReflower:
44
+ sep: str = ' '
45
+ opts: TextwrapOpts = TextwrapOpts()
46
+
47
+ def __call__(self, strs: ta.Sequence[str], current_indent: int = 0) -> ta.Sequence[str]:
48
+ return textwrap.wrap(
49
+ self.sep.join(strs),
50
+ **dc.asdict(dc.replace(
51
+ self.opts,
52
+ width=self.opts.width - current_indent,
53
+ )),
54
+ )
55
+
56
+
57
+ ##
58
+
59
+
60
+ def reflow_block_text(
61
+ root: Part,
62
+ reflower: TextReflower = JoiningReflower(),
63
+ ) -> Part:
64
+ def rec(p: Part, ci: int) -> Part:
65
+ if isinstance(p, Blank):
66
+ return p
67
+
68
+ elif isinstance(p, Text):
69
+ if (rf := reflower([p.s], ci)) != [p.s]:
70
+ p = blockify(*map(Text, rf)) # noqa
71
+ return p
72
+
73
+ elif isinstance(p, Indent):
74
+ if (np := rec(p.p, ci + p.n)) is not p.p:
75
+ p = Indent(p.n, np) # type: ignore[arg-type]
76
+ return p
77
+
78
+ elif isinstance(p, List):
79
+ ne = [rec(e, ci + len(p.d) + 1) for e in p.es]
80
+ if not all_same(ne, p.es):
81
+ p = List(p.d, ne)
82
+ return p
83
+
84
+ elif not isinstance(p, Block):
85
+ raise TypeError(p)
86
+
87
+ ps = [rec(c, ci) for c in p.ps]
88
+
89
+ ps = list(unblockify(blockify(*ps)))
90
+
91
+ new: list[Part | list[str]] = []
92
+ for c in ps:
93
+ if isinstance(c, Text):
94
+ if new and isinstance(x := new[-1], list):
95
+ x.append(c.s)
96
+ else:
97
+ new.append([c.s])
98
+ else:
99
+ new.append(c)
100
+
101
+ return blockify(*lang.flatmap(
102
+ lambda x: map(Text, reflower(x, ci)) if isinstance(x, list) else [x], # noqa
103
+ new,
104
+ ))
105
+
106
+ return rec(root, 0)
@@ -0,0 +1,151 @@
1
+ import functools
2
+ import io
3
+ import typing as ta
4
+
5
+ from ... import check
6
+ from .parts import Blank
7
+ from .parts import Block
8
+ from .parts import Indent
9
+ from .parts import List
10
+ from .parts import Part
11
+ from .parts import Text
12
+
13
+
14
+ ##
15
+
16
+
17
+ @functools.lru_cache
18
+ def _indent_str(n: int) -> str:
19
+ return ' ' * n
20
+
21
+
22
+ ##
23
+
24
+
25
+ def render_to(root: Part, out: ta.Callable[[str], ta.Any]) -> None:
26
+ i_stk: list[int] = [0]
27
+ ci = 0
28
+ need_nl = False
29
+
30
+ def write(
31
+ s: str,
32
+ ti: int | None = None,
33
+ endl: bool = False,
34
+ ) -> None:
35
+ check.not_in('\n', s)
36
+
37
+ nonlocal need_nl
38
+ nonlocal ci
39
+ if need_nl:
40
+ check.state(ci == 0)
41
+ out('\n')
42
+ need_nl = False
43
+
44
+ if ti is None:
45
+ ti = i_stk[-1]
46
+ if ci < ti:
47
+ out(_indent_str(ti - ci))
48
+ ci = ti
49
+
50
+ out(s)
51
+
52
+ if endl:
53
+ need_nl = True
54
+ ci = 0
55
+
56
+ def rec(p: Part) -> None:
57
+ nonlocal ci
58
+
59
+ if isinstance(p, Blank):
60
+ check.state(ci == 0)
61
+ out('\n')
62
+
63
+ elif isinstance(p, Text):
64
+ write(p.s, endl=True)
65
+
66
+ elif isinstance(p, Block):
67
+ for c in p.ps:
68
+ rec(c)
69
+
70
+ elif isinstance(p, Indent):
71
+ i_stk.append(i_stk[-1] + p.n)
72
+ rec(p.p)
73
+ i_stk.pop()
74
+
75
+ elif isinstance(p, List):
76
+ i_stk.append((li := i_stk[-1]) + len(p.d) + 1)
77
+ sd = p.d + ' '
78
+ for e in p.es:
79
+ if isinstance(e, Blank):
80
+ rec(e)
81
+ else:
82
+ check.state(ci == 0)
83
+ write(sd, li)
84
+ ci += len(sd)
85
+ rec(e)
86
+ i_stk.pop()
87
+
88
+ else:
89
+ raise TypeError(p)
90
+
91
+ rec(root)
92
+ check.state(i_stk == [0])
93
+
94
+
95
+ def render(root: Part) -> str:
96
+ buf = io.StringIO()
97
+ render_to(root, buf.write)
98
+ return buf.getvalue()
99
+
100
+
101
+ ##
102
+
103
+
104
+ def dump_to(root: Part, out: ta.Callable[[str], ta.Any]) -> None:
105
+ i = 0
106
+
107
+ def write(s: str) -> None:
108
+ out(_indent_str(i * 2))
109
+ out(s)
110
+ out('\n')
111
+
112
+ def rec(p: Part) -> None:
113
+ nonlocal i
114
+
115
+ if isinstance(p, Blank):
116
+ write('blank')
117
+
118
+ elif isinstance(p, Text):
119
+ write(f'text:{p.s!r}')
120
+
121
+ elif isinstance(p, Block):
122
+ write(f'block/{len(p.ps)}')
123
+ i += 1
124
+ for c in p.ps:
125
+ rec(c)
126
+ i -= 1
127
+
128
+ elif isinstance(p, Indent):
129
+ write(f'indent:{p.n}')
130
+ i += 1
131
+ rec(p.p)
132
+ i -= 1
133
+
134
+ elif isinstance(p, List):
135
+ write(f'list/{len(p.es)}:{p.d!r}')
136
+ i += 1
137
+ for e in p.es:
138
+ rec(e)
139
+ i -= 1
140
+
141
+ else:
142
+ raise TypeError(p)
143
+
144
+ rec(root)
145
+ check.state(i == 0)
146
+
147
+
148
+ def dump(root: Part) -> str:
149
+ buf = io.StringIO()
150
+ dump_to(root, buf.write)
151
+ return buf.getvalue()
@@ -0,0 +1,11 @@
1
+ import typing as ta
2
+
3
+
4
+ T = ta.TypeVar('T')
5
+
6
+
7
+ ##
8
+
9
+
10
+ def all_same(l: ta.Sequence[T], r: ta.Sequence[T]) -> bool:
11
+ return len(l) == len(r) and all(x is y for x, y in zip(l, r, strict=True))
@@ -0,0 +1,59 @@
1
+ """
2
+ TODO:
3
+ - a reflowing.TextReflower
4
+ - import textwrap lol
5
+ - obviously handle hyphens, underscores, etc
6
+ - optionally preserve/normalize inter-sentence spaces - '. ' vs '. '
7
+ - detect if 'intentionally' smaller than current remaining line width, if so do not merge.
8
+ - maybe if only ending with punctuation?
9
+ - detect 'matched pairs' of 'quotes'? to preserve whitespace? `foo bar` ...
10
+ - how to escape it lol - if we see any \\` do we give up?
11
+ """
12
+ import re
13
+ import typing as ta
14
+
15
+
16
+ SpanKind: ta.TypeAlias = ta.Literal[
17
+ 'word',
18
+ 'space',
19
+ 'symbol',
20
+ ]
21
+
22
+
23
+ ##
24
+
25
+
26
+ class Span(ta.NamedTuple):
27
+ k: SpanKind
28
+ s: str
29
+
30
+ def __repr__(self) -> str:
31
+ return f'{self.k}:{self.s!r}'
32
+
33
+
34
+ _SPAN_PAT = re.compile(r'(\w+)|(\s+)')
35
+
36
+
37
+ def split_line_spans(s: str) -> list[Span]:
38
+ spans: list[Span] = []
39
+ p = 0
40
+ for m in _SPAN_PAT.finditer(s):
41
+ l, r = m.span()
42
+ if p < l:
43
+ spans.append(Span('symbol', s[p:l]))
44
+ if m.group(1):
45
+ spans.append(Span('word', m.group(1)))
46
+ else:
47
+ spans.append(Span('space', m.group(2)))
48
+ p = r
49
+ if p < len(s):
50
+ spans.append(Span('symbol', s[p:]))
51
+ return spans
52
+
53
+
54
+ def _main() -> None:
55
+ print(split_line_spans('hi i am a string! this has a hy-phen.'))
56
+
57
+
58
+ if __name__ == '__main__':
59
+ _main()
omlish/text/filecache.py CHANGED
@@ -2,10 +2,10 @@
2
2
  Features:
3
3
  - get (line, offset)
4
4
  - invalidate
5
- - filewatcher
5
+ - TODO: filewatcher
6
6
  - linecache interop
7
7
  - tokenize.open - detect encoding - only for .py
8
- - directory / path_parts tree?
8
+ - TODO: directory / path_parts tree?
9
9
 
10
10
  TODO:
11
11
  - read raw, decode (detect)
omlish/text/lorem.py ADDED
@@ -0,0 +1,6 @@
1
+ LOREM = (
2
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore '
3
+ 'magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo '
4
+ 'consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. '
5
+ 'Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
6
+ )
omlish/text/parts.py CHANGED
@@ -75,7 +75,7 @@ class PartTransform:
75
75
  def __call__(self, part: Part | None) -> Part:
76
76
  return self._transform(part)
77
77
 
78
- @dispatch.method
78
+ @dispatch.method(instance_cache=True)
79
79
  def _transform(self, part: Part | None) -> Part:
80
80
  raise TypeError(part)
81
81
 
@@ -214,7 +214,7 @@ class PartRenderer:
214
214
  def __call__(self, part: Part | None) -> None:
215
215
  return self._render(part)
216
216
 
217
- @dispatch.method
217
+ @dispatch.method()
218
218
  def _render(self, part: Part | None) -> None:
219
219
  raise TypeError(part)
220
220
 
@@ -0,0 +1,51 @@
1
+ import dataclasses as dc
2
+
3
+
4
+ ##
5
+
6
+
7
+ @dc.dataclass(frozen=True)
8
+ class TextwrapOpts:
9
+ # The maximum width of wrapped lines (unless break_long_words is false).
10
+ width: int = 70 # noqa
11
+
12
+ _: dc.KW_ONLY
13
+
14
+ # String that will be prepended to the first line of wrapped output. Counts towards the line's width.
15
+ initial_indent: str = ''
16
+
17
+ # String that will be prepended to all lines save the first of wrapped output; also counts towards each line's
18
+ # width.
19
+ subsequent_indent: str = ''
20
+
21
+ # Expand tabs in input text to spaces before further processing. Each tab will become 0 .. 'tabsize' spaces,
22
+ # depending on its position in its line. If false, each tab is treated as a single character.
23
+ expand_tabs: bool = True
24
+
25
+ # Expand tabs in input text to 0 .. 'tabsize' spaces, unless 'expand_tabs' is false.
26
+ tabsize: int = 8
27
+
28
+ # Replace all whitespace characters in the input text by spaces after tab expansion. Note that if expand_tabs is
29
+ # false and replace_whitespace is true, every tab will be converted to a single space!
30
+ replace_whitespace: bool = True
31
+
32
+ # Ensure that sentence-ending punctuation is always followed by two spaces. Off by default because the algorithm is
33
+ # (unavoidably) imperfect.
34
+ fix_sentence_endings: bool = False
35
+
36
+ # Break words longer than 'width'. If false, those words will not be broken, and some lines might be longer than
37
+ # 'width'.
38
+ break_long_words: bool = True
39
+
40
+ # Allow breaking hyphenated words. If true, wrapping will occur preferably on whitespaces and right after hyphens
41
+ # part of compound words.
42
+ break_on_hyphens: bool = True
43
+
44
+ # Drop leading and trailing whitespace from lines.
45
+ drop_whitespace: bool = True
46
+
47
+ # Truncate wrapped lines.
48
+ max_lines: int | None = None
49
+
50
+ # Append to the last line of truncated text.
51
+ placeholder: str = ' [...]'