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.
- omlish/.omlish-manifests.json +12 -0
- omlish/__about__.py +15 -15
- omlish/argparse/all.py +17 -9
- omlish/argparse/cli.py +16 -3
- omlish/argparse/utils.py +21 -0
- omlish/asyncs/asyncio/rlock.py +110 -0
- omlish/asyncs/asyncio/sync.py +43 -0
- omlish/asyncs/asyncio/utils.py +2 -0
- omlish/asyncs/sync.py +25 -0
- omlish/bootstrap/_marshal.py +1 -1
- omlish/bootstrap/diag.py +12 -21
- omlish/bootstrap/main.py +2 -5
- omlish/bootstrap/sys.py +27 -28
- omlish/cexts/__init__.py +0 -0
- omlish/cexts/include/omlish/omlish.hh +1 -0
- omlish/collections/__init__.py +13 -1
- omlish/collections/attrregistry.py +210 -0
- omlish/collections/cache/impl.py +1 -0
- omlish/collections/identity.py +1 -0
- omlish/collections/mappings.py +28 -0
- omlish/collections/trie.py +5 -1
- omlish/collections/utils.py +77 -0
- omlish/concurrent/all.py +2 -1
- omlish/concurrent/futures.py +25 -0
- omlish/concurrent/threadlets.py +1 -1
- omlish/daemons/reparent.py +2 -3
- omlish/daemons/spawning.py +2 -3
- omlish/dataclasses/__init__.py +2 -0
- omlish/dataclasses/impl/api/classes/decorator.py +3 -0
- omlish/dataclasses/impl/api/classes/make.py +3 -0
- omlish/dataclasses/impl/concerns/repr.py +15 -2
- omlish/dataclasses/impl/configs.py +97 -37
- omlish/dataclasses/impl/generation/compilation.py +21 -19
- omlish/dataclasses/impl/generation/globals.py +1 -0
- omlish/dataclasses/impl/generation/ops.py +1 -0
- omlish/dataclasses/impl/generation/processor.py +105 -24
- omlish/dataclasses/impl/processing/base.py +8 -0
- omlish/dataclasses/impl/processing/driving.py +8 -8
- omlish/dataclasses/specs.py +1 -0
- omlish/dataclasses/tools/modifiers.py +5 -0
- omlish/diag/cmds/__init__.py +0 -0
- omlish/diag/{lslocks.py → cmds/lslocks.py} +6 -6
- omlish/diag/{lsof.py → cmds/lsof.py} +6 -6
- omlish/diag/{ps.py → cmds/ps.py} +6 -6
- omlish/diag/pycharm.py +16 -2
- omlish/diag/pydevd.py +58 -40
- omlish/diag/replserver/console.py +1 -1
- omlish/dispatch/__init__.py +18 -12
- omlish/dispatch/methods.py +50 -140
- omlish/dom/rendering.py +1 -1
- omlish/formats/dotenv.py +1 -1
- omlish/formats/json/stream/__init__.py +13 -0
- omlish/funcs/guard.py +225 -0
- omlish/graphs/dot/rendering.py +1 -1
- omlish/http/all.py +44 -4
- omlish/http/clients/asyncs.py +33 -27
- omlish/http/clients/base.py +17 -1
- omlish/http/clients/coro/__init__.py +0 -0
- omlish/http/clients/coro/sync.py +171 -0
- omlish/http/clients/default.py +208 -29
- omlish/http/clients/executor.py +56 -0
- omlish/http/clients/httpx.py +72 -11
- omlish/http/clients/middleware.py +181 -0
- omlish/http/clients/sync.py +33 -27
- omlish/http/clients/syncasync.py +49 -0
- omlish/http/clients/urllib.py +6 -3
- omlish/http/coro/client/connection.py +15 -6
- omlish/http/coro/io.py +2 -0
- omlish/http/flasky/__init__.py +40 -0
- omlish/http/flasky/_compat.py +2 -0
- omlish/http/flasky/api.py +82 -0
- omlish/http/flasky/app.py +203 -0
- omlish/http/flasky/cvs.py +59 -0
- omlish/http/flasky/requests.py +20 -0
- omlish/http/flasky/responses.py +23 -0
- omlish/http/flasky/routes.py +23 -0
- omlish/http/flasky/types.py +15 -0
- omlish/http/urls.py +67 -0
- omlish/inject/__init__.py +38 -18
- omlish/inject/_dataclasses.py +4986 -0
- omlish/inject/binder.py +4 -48
- omlish/inject/elements.py +27 -0
- omlish/inject/helpers/__init__.py +0 -0
- omlish/inject/{utils.py → helpers/constfn.py} +3 -3
- omlish/inject/{tags.py → helpers/id.py} +2 -2
- omlish/inject/helpers/multis.py +143 -0
- omlish/inject/helpers/wrappers.py +54 -0
- omlish/inject/impl/elements.py +47 -17
- omlish/inject/impl/injector.py +20 -19
- omlish/inject/impl/inspect.py +10 -1
- omlish/inject/impl/maysync.py +3 -4
- omlish/inject/impl/multis.py +3 -0
- omlish/inject/impl/sync.py +3 -4
- omlish/inject/injector.py +31 -2
- omlish/inject/inspect.py +35 -0
- omlish/inject/maysync.py +2 -4
- omlish/inject/multis.py +8 -0
- omlish/inject/overrides.py +3 -3
- omlish/inject/privates.py +6 -0
- omlish/inject/providers.py +3 -2
- omlish/inject/sync.py +5 -4
- omlish/io/buffers.py +118 -0
- omlish/io/readers.py +29 -0
- omlish/iterators/transforms.py +2 -2
- omlish/lang/__init__.py +178 -97
- omlish/lang/_asyncs.cc +186 -0
- omlish/lang/asyncs.py +17 -0
- omlish/lang/casing.py +11 -0
- omlish/lang/contextmanagers.py +28 -4
- omlish/lang/functions.py +31 -22
- omlish/lang/imports/_capture.cc +11 -9
- omlish/lang/imports/capture.py +363 -170
- omlish/lang/imports/proxy.py +455 -152
- omlish/lang/lazyglobals.py +22 -9
- omlish/lang/params.py +17 -0
- omlish/lang/recursion.py +0 -1
- omlish/lang/sequences.py +124 -0
- omlish/lite/abstract.py +54 -24
- omlish/lite/asyncs.py +2 -2
- omlish/lite/attrops.py +2 -0
- omlish/lite/contextmanagers.py +4 -4
- omlish/lite/dataclasses.py +44 -0
- omlish/lite/maybes.py +8 -0
- omlish/lite/maysync.py +1 -5
- omlish/lite/pycharm.py +1 -1
- omlish/lite/typing.py +6 -0
- omlish/logs/all.py +1 -1
- omlish/logs/utils.py +1 -1
- omlish/manifests/loading.py +2 -1
- omlish/marshal/__init__.py +33 -13
- omlish/marshal/_dataclasses.py +2774 -0
- omlish/marshal/base/configs.py +12 -0
- omlish/marshal/base/contexts.py +36 -21
- omlish/marshal/base/funcs.py +8 -11
- omlish/marshal/base/options.py +8 -0
- omlish/marshal/base/registries.py +146 -44
- omlish/marshal/base/types.py +40 -16
- omlish/marshal/composite/iterables.py +33 -20
- omlish/marshal/composite/literals.py +20 -18
- omlish/marshal/composite/mappings.py +36 -23
- omlish/marshal/composite/maybes.py +29 -19
- omlish/marshal/composite/newtypes.py +16 -16
- omlish/marshal/composite/optionals.py +14 -14
- omlish/marshal/composite/special.py +15 -15
- omlish/marshal/composite/unions/__init__.py +0 -0
- omlish/marshal/composite/unions/literals.py +93 -0
- omlish/marshal/composite/unions/primitives.py +103 -0
- omlish/marshal/factories/invalidate.py +18 -68
- omlish/marshal/factories/method.py +26 -0
- omlish/marshal/factories/moduleimport/factories.py +22 -65
- omlish/marshal/factories/multi.py +13 -25
- omlish/marshal/factories/recursive.py +42 -56
- omlish/marshal/factories/typecache.py +29 -74
- omlish/marshal/factories/typemap.py +42 -43
- omlish/marshal/objects/dataclasses.py +129 -106
- omlish/marshal/objects/marshal.py +18 -14
- omlish/marshal/objects/namedtuples.py +48 -42
- omlish/marshal/objects/unmarshal.py +19 -15
- omlish/marshal/polymorphism/marshal.py +9 -11
- omlish/marshal/polymorphism/metadata.py +16 -5
- omlish/marshal/polymorphism/standard.py +13 -1
- omlish/marshal/polymorphism/unions.py +15 -105
- omlish/marshal/polymorphism/unmarshal.py +9 -10
- omlish/marshal/singular/enums.py +14 -18
- omlish/marshal/standard.py +10 -6
- omlish/marshal/trivial/any.py +1 -1
- omlish/marshal/trivial/forbidden.py +21 -26
- omlish/metadata.py +23 -1
- omlish/os/forkhooks.py +4 -0
- omlish/os/pidfiles/pinning.py +2 -2
- omlish/reflect/types.py +1 -0
- omlish/secrets/marshal.py +1 -1
- omlish/specs/jmespath/__init__.py +12 -3
- omlish/specs/jmespath/_dataclasses.py +2893 -0
- omlish/specs/jmespath/ast.py +1 -1
- omlish/specs/jsonrpc/__init__.py +13 -0
- omlish/specs/jsonrpc/_marshal.py +32 -23
- omlish/specs/jsonrpc/conns.py +10 -7
- omlish/specs/jsonschema/_marshal.py +1 -1
- omlish/specs/jsonschema/keywords/__init__.py +7 -0
- omlish/specs/jsonschema/keywords/_dataclasses.py +1644 -0
- omlish/specs/openapi/_marshal.py +31 -22
- omlish/sql/{tabledefs/alchemy.py → alchemy/tabledefs.py} +2 -2
- omlish/sql/queries/_marshal.py +2 -2
- omlish/sql/queries/rendering.py +1 -1
- omlish/sql/tabledefs/_marshal.py +1 -1
- omlish/subprocesses/base.py +4 -0
- omlish/subprocesses/editor.py +1 -1
- omlish/sync.py +155 -21
- omlish/term/alt.py +60 -0
- omlish/term/confirm.py +8 -8
- omlish/term/pager.py +235 -0
- omlish/term/terminfo.py +935 -0
- omlish/term/termstate.py +67 -0
- omlish/term/vt100/terminal.py +0 -3
- omlish/testing/pytest/plugins/asyncs/fixtures.py +4 -1
- omlish/testing/pytest/plugins/skips.py +2 -1
- omlish/testing/unittest/main.py +3 -3
- omlish/text/docwrap/__init__.py +3 -0
- omlish/text/docwrap/__main__.py +11 -0
- omlish/text/docwrap/api.py +83 -0
- omlish/text/docwrap/cli.py +86 -0
- omlish/text/docwrap/groups.py +84 -0
- omlish/text/docwrap/lists.py +167 -0
- omlish/text/docwrap/parts.py +146 -0
- omlish/text/docwrap/reflowing.py +106 -0
- omlish/text/docwrap/rendering.py +151 -0
- omlish/text/docwrap/utils.py +11 -0
- omlish/text/docwrap/wrapping.py +59 -0
- omlish/text/filecache.py +2 -2
- omlish/text/lorem.py +6 -0
- omlish/text/parts.py +2 -2
- omlish/text/textwrap.py +51 -0
- omlish/typedvalues/marshal.py +85 -59
- omlish/typedvalues/values.py +2 -1
- {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/METADATA +29 -28
- {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/RECORD +222 -171
- omlish/dataclasses/impl/generation/mangling.py +0 -18
- omlish/funcs/match.py +0 -227
- omlish/marshal/factories/match.py +0 -34
- omlish/marshal/factories/simple.py +0 -28
- /omlish/inject/impl/{providers2.py → providersmap.py} +0 -0
- {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/licenses/LICENSE +0 -0
- {omlish-0.0.0.dev447.dist-info → omlish-0.0.0.dev484.dist-info}/top_level.txt +0 -0
omlish/term/termstate.py
ADDED
|
@@ -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())
|
omlish/term/vt100/terminal.py
CHANGED
|
@@ -141,7 +141,10 @@ class AsyncsFixture:
|
|
|
141
141
|
finally:
|
|
142
142
|
nursery_fixture.cancel_scope.cancel()
|
|
143
143
|
|
|
144
|
-
except
|
|
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():
|
omlish/testing/unittest/main.py
CHANGED
|
@@ -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
|
-
|
|
260
|
+
raise SystemExit(self.NO_TESTS_EXITCODE)
|
|
261
261
|
elif result.was_successful:
|
|
262
|
-
|
|
262
|
+
raise SystemExit(0)
|
|
263
263
|
else:
|
|
264
|
-
|
|
264
|
+
raise SystemExit(1)
|
|
265
265
|
|
|
266
266
|
|
|
267
267
|
##
|
|
@@ -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)
|