omlish 0.0.0.dev1__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 (187) hide show
  1. omlish/__about__.py +7 -0
  2. omlish/__init__.py +0 -0
  3. omlish/argparse.py +223 -0
  4. omlish/asyncs/__init__.py +17 -0
  5. omlish/asyncs/anyio.py +23 -0
  6. omlish/asyncs/asyncio.py +19 -0
  7. omlish/asyncs/asyncs.py +76 -0
  8. omlish/asyncs/futures.py +179 -0
  9. omlish/asyncs/trio.py +11 -0
  10. omlish/c3.py +173 -0
  11. omlish/cached.py +9 -0
  12. omlish/check.py +231 -0
  13. omlish/collections/__init__.py +63 -0
  14. omlish/collections/_abc.py +156 -0
  15. omlish/collections/_io_abc.py +78 -0
  16. omlish/collections/cache/__init__.py +11 -0
  17. omlish/collections/cache/descriptor.py +188 -0
  18. omlish/collections/cache/impl.py +485 -0
  19. omlish/collections/cache/types.py +37 -0
  20. omlish/collections/coerce.py +337 -0
  21. omlish/collections/frozen.py +148 -0
  22. omlish/collections/identity.py +106 -0
  23. omlish/collections/indexed.py +75 -0
  24. omlish/collections/mappings.py +127 -0
  25. omlish/collections/ordered.py +81 -0
  26. omlish/collections/persistent.py +36 -0
  27. omlish/collections/skiplist.py +193 -0
  28. omlish/collections/sorted.py +126 -0
  29. omlish/collections/treap.py +228 -0
  30. omlish/collections/treapmap.py +144 -0
  31. omlish/collections/unmodifiable.py +174 -0
  32. omlish/collections/utils.py +110 -0
  33. omlish/configs/__init__.py +0 -0
  34. omlish/configs/flattening.py +147 -0
  35. omlish/configs/props.py +64 -0
  36. omlish/dataclasses/__init__.py +83 -0
  37. omlish/dataclasses/impl/__init__.py +6 -0
  38. omlish/dataclasses/impl/api.py +260 -0
  39. omlish/dataclasses/impl/as_.py +76 -0
  40. omlish/dataclasses/impl/exceptions.py +2 -0
  41. omlish/dataclasses/impl/fields.py +148 -0
  42. omlish/dataclasses/impl/frozen.py +55 -0
  43. omlish/dataclasses/impl/hashing.py +85 -0
  44. omlish/dataclasses/impl/init.py +173 -0
  45. omlish/dataclasses/impl/internals.py +118 -0
  46. omlish/dataclasses/impl/main.py +150 -0
  47. omlish/dataclasses/impl/metaclass.py +126 -0
  48. omlish/dataclasses/impl/metadata.py +74 -0
  49. omlish/dataclasses/impl/order.py +47 -0
  50. omlish/dataclasses/impl/params.py +150 -0
  51. omlish/dataclasses/impl/processing.py +16 -0
  52. omlish/dataclasses/impl/reflect.py +173 -0
  53. omlish/dataclasses/impl/replace.py +40 -0
  54. omlish/dataclasses/impl/repr.py +34 -0
  55. omlish/dataclasses/impl/simple.py +92 -0
  56. omlish/dataclasses/impl/slots.py +80 -0
  57. omlish/dataclasses/impl/utils.py +167 -0
  58. omlish/defs.py +193 -0
  59. omlish/dispatch/__init__.py +3 -0
  60. omlish/dispatch/dispatch.py +137 -0
  61. omlish/dispatch/functions.py +52 -0
  62. omlish/dispatch/methods.py +162 -0
  63. omlish/docker.py +149 -0
  64. omlish/dynamic.py +220 -0
  65. omlish/graphs/__init__.py +0 -0
  66. omlish/graphs/dot/__init__.py +19 -0
  67. omlish/graphs/dot/items.py +162 -0
  68. omlish/graphs/dot/rendering.py +147 -0
  69. omlish/graphs/dot/utils.py +30 -0
  70. omlish/graphs/trees.py +249 -0
  71. omlish/http/__init__.py +0 -0
  72. omlish/http/consts.py +20 -0
  73. omlish/http/wsgi.py +34 -0
  74. omlish/inject/__init__.py +85 -0
  75. omlish/inject/binder.py +12 -0
  76. omlish/inject/bindings.py +49 -0
  77. omlish/inject/eagers.py +21 -0
  78. omlish/inject/elements.py +43 -0
  79. omlish/inject/exceptions.py +49 -0
  80. omlish/inject/impl/__init__.py +0 -0
  81. omlish/inject/impl/bindings.py +19 -0
  82. omlish/inject/impl/elements.py +154 -0
  83. omlish/inject/impl/injector.py +182 -0
  84. omlish/inject/impl/inspect.py +98 -0
  85. omlish/inject/impl/private.py +109 -0
  86. omlish/inject/impl/providers.py +132 -0
  87. omlish/inject/impl/scopes.py +198 -0
  88. omlish/inject/injector.py +40 -0
  89. omlish/inject/inspect.py +14 -0
  90. omlish/inject/keys.py +43 -0
  91. omlish/inject/managed.py +24 -0
  92. omlish/inject/overrides.py +18 -0
  93. omlish/inject/private.py +29 -0
  94. omlish/inject/providers.py +111 -0
  95. omlish/inject/proxy.py +48 -0
  96. omlish/inject/scopes.py +84 -0
  97. omlish/inject/types.py +21 -0
  98. omlish/iterators.py +184 -0
  99. omlish/json.py +194 -0
  100. omlish/lang/__init__.py +112 -0
  101. omlish/lang/cached.py +267 -0
  102. omlish/lang/classes/__init__.py +24 -0
  103. omlish/lang/classes/abstract.py +74 -0
  104. omlish/lang/classes/restrict.py +137 -0
  105. omlish/lang/classes/simple.py +120 -0
  106. omlish/lang/classes/test/__init__.py +0 -0
  107. omlish/lang/classes/test/test_abstract.py +89 -0
  108. omlish/lang/classes/test/test_restrict.py +71 -0
  109. omlish/lang/classes/test/test_simple.py +58 -0
  110. omlish/lang/classes/test/test_virtual.py +72 -0
  111. omlish/lang/classes/virtual.py +130 -0
  112. omlish/lang/clsdct.py +67 -0
  113. omlish/lang/cmp.py +63 -0
  114. omlish/lang/contextmanagers.py +249 -0
  115. omlish/lang/datetimes.py +67 -0
  116. omlish/lang/descriptors.py +52 -0
  117. omlish/lang/functions.py +126 -0
  118. omlish/lang/imports.py +153 -0
  119. omlish/lang/iterables.py +54 -0
  120. omlish/lang/maybes.py +136 -0
  121. omlish/lang/objects.py +103 -0
  122. omlish/lang/resolving.py +50 -0
  123. omlish/lang/strings.py +128 -0
  124. omlish/lang/typing.py +92 -0
  125. omlish/libc.py +532 -0
  126. omlish/logs/__init__.py +9 -0
  127. omlish/logs/_abc.py +247 -0
  128. omlish/logs/configs.py +62 -0
  129. omlish/logs/filters.py +9 -0
  130. omlish/logs/formatters.py +67 -0
  131. omlish/logs/utils.py +20 -0
  132. omlish/marshal/__init__.py +52 -0
  133. omlish/marshal/any.py +25 -0
  134. omlish/marshal/base.py +201 -0
  135. omlish/marshal/base64.py +25 -0
  136. omlish/marshal/dataclasses.py +115 -0
  137. omlish/marshal/datetimes.py +90 -0
  138. omlish/marshal/enums.py +43 -0
  139. omlish/marshal/exceptions.py +7 -0
  140. omlish/marshal/factories.py +129 -0
  141. omlish/marshal/global_.py +33 -0
  142. omlish/marshal/iterables.py +57 -0
  143. omlish/marshal/mappings.py +66 -0
  144. omlish/marshal/naming.py +17 -0
  145. omlish/marshal/objects.py +106 -0
  146. omlish/marshal/optionals.py +49 -0
  147. omlish/marshal/polymorphism.py +147 -0
  148. omlish/marshal/primitives.py +43 -0
  149. omlish/marshal/registries.py +57 -0
  150. omlish/marshal/standard.py +80 -0
  151. omlish/marshal/utils.py +23 -0
  152. omlish/marshal/uuids.py +29 -0
  153. omlish/marshal/values.py +30 -0
  154. omlish/math.py +184 -0
  155. omlish/os.py +32 -0
  156. omlish/reflect.py +359 -0
  157. omlish/replserver/__init__.py +5 -0
  158. omlish/replserver/__main__.py +4 -0
  159. omlish/replserver/console.py +247 -0
  160. omlish/replserver/server.py +146 -0
  161. omlish/runmodule.py +28 -0
  162. omlish/stats.py +342 -0
  163. omlish/term.py +222 -0
  164. omlish/testing/__init__.py +7 -0
  165. omlish/testing/pydevd.py +225 -0
  166. omlish/testing/pytest/__init__.py +8 -0
  167. omlish/testing/pytest/helpers.py +35 -0
  168. omlish/testing/pytest/inject/__init__.py +1 -0
  169. omlish/testing/pytest/inject/harness.py +159 -0
  170. omlish/testing/pytest/plugins/__init__.py +20 -0
  171. omlish/testing/pytest/plugins/_registry.py +6 -0
  172. omlish/testing/pytest/plugins/logging.py +13 -0
  173. omlish/testing/pytest/plugins/pycharm.py +54 -0
  174. omlish/testing/pytest/plugins/repeat.py +19 -0
  175. omlish/testing/pytest/plugins/skips.py +32 -0
  176. omlish/testing/pytest/plugins/spacing.py +19 -0
  177. omlish/testing/pytest/plugins/switches.py +70 -0
  178. omlish/testing/testing.py +102 -0
  179. omlish/text/__init__.py +0 -0
  180. omlish/text/delimit.py +171 -0
  181. omlish/text/indent.py +50 -0
  182. omlish/text/parts.py +265 -0
  183. omlish-0.0.0.dev1.dist-info/LICENSE +21 -0
  184. omlish-0.0.0.dev1.dist-info/METADATA +17 -0
  185. omlish-0.0.0.dev1.dist-info/RECORD +187 -0
  186. omlish-0.0.0.dev1.dist-info/WHEEL +5 -0
  187. omlish-0.0.0.dev1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,54 @@
1
+ import threading
2
+
3
+ from ... import pydevd as opd
4
+ from ._registry import register
5
+
6
+
7
+ @register
8
+ class PycharmPlugin:
9
+
10
+ def pytest_addoption(self, parser):
11
+ parser.addoption('--no-pycharm-debug', action='store_true', default=False, help='Disables pycharm debugging')
12
+
13
+ def pytest_collection(self, session):
14
+ setup = opd.get_setup()
15
+ if setup is not None:
16
+ if hasattr(session.config, '_env_timeout'):
17
+ session.config._env_timeout = None
18
+
19
+ def pytest_exception_interact(self, node, call, report):
20
+ if node.session.config.option.no_pycharm_debug:
21
+ return report
22
+
23
+ try:
24
+ import pydevd
25
+ from pydevd import pydevd_tracing
26
+ except ImportError:
27
+ return report
28
+
29
+ exctype, value, traceback = call.excinfo._excinfo
30
+ frames = []
31
+ while traceback:
32
+ # FIXME: 3.12 DUPLICATE FRAME IDENTITIES!! prob same shit as greenlet/trio, need to 'pin'
33
+ # FIXME: uhh it happens in 3.11 too and that works lol
34
+ frames.append(traceback.tb_frame)
35
+ traceback = traceback.tb_next
36
+
37
+ thread = threading.current_thread()
38
+ frames_by_id = {id(frame): frame for frame in frames}
39
+ frame = frames[-1]
40
+ exception = (exctype, value, traceback)
41
+
42
+ if hasattr(thread, 'additional_info'):
43
+ thread.additional_info.pydev_message = 'test fail'
44
+ try:
45
+ debugger = pydevd.debugger
46
+ except AttributeError:
47
+ debugger = pydevd.get_global_debugger()
48
+
49
+ pydevd_tracing.SetTrace(None)
50
+
51
+ debugger.stop_on_unhandled_exception(thread, frame, frames_by_id, exception)
52
+ # debugger.handle_post_mortem_stop(thread, frame, frames_by_id, exception)
53
+
54
+ return report
@@ -0,0 +1,19 @@
1
+ from ._registry import register
2
+
3
+
4
+ PARAM_NAME = '__repeat'
5
+
6
+
7
+ @register
8
+ class RepeatPlugin:
9
+
10
+ def pytest_addoption(self, parser):
11
+ parser.addoption('--repeat', action='store', type=int, help='Number of times to repeat each test')
12
+
13
+ def pytest_generate_tests(self, metafunc):
14
+ if metafunc.config.option.repeat is None:
15
+ return
16
+
17
+ n = metafunc.config.option.repeat
18
+ metafunc.fixturenames.append(PARAM_NAME)
19
+ metafunc.parametrize(PARAM_NAME, range(n))
@@ -0,0 +1,32 @@
1
+ """
2
+ TODO:
3
+ - go-style tags: +slow,-ci, ...
4
+ """
5
+ from _pytest.main import resolve_collection_argument # noqa
6
+ import pytest
7
+
8
+ from ._registry import register
9
+
10
+
11
+ @register
12
+ class SkipsPlugin:
13
+
14
+ def pytest_collection_modifyitems(self, session, items):
15
+ dct: dict[str, set[str]] = {}
16
+ for arg in session.config.args:
17
+ ca = resolve_collection_argument(
18
+ session.config.invocation_params.dir,
19
+ arg,
20
+ as_pypath=session.config.option.pyargs,
21
+ )
22
+ if ca.path.is_file():
23
+ dct.setdefault(ca.path.as_posix(), set()).update(ca.parts)
24
+
25
+ skip = pytest.mark.skip(reason='skipped (not specified alone)')
26
+ for item in items:
27
+ if 'skip_unless_alone' in item.keywords:
28
+ if item.name not in dct.get(item.fspath.strpath, ()):
29
+ item.add_marker(skip)
30
+
31
+ def pytest_configure(self, config):
32
+ config.addinivalue_line('markers', 'skip_unless_alone: mark test as skipped unless specified alone')
@@ -0,0 +1,19 @@
1
+ from ._registry import register
2
+
3
+
4
+ @register
5
+ class SpacingPlugin:
6
+
7
+ def pytest_addoption(self, parser):
8
+ parser.addoption('--newlines-before', action='store', type=int, help='Adds newlines before tests')
9
+ parser.addoption('--newlines-after', action='store', type=int, help='Adds newlines after tests')
10
+
11
+ def pytest_runtest_setup(self, item):
12
+ if item.session.config.option.newlines_before:
13
+ for _ in range(item.session.config.option.newlines_before):
14
+ print()
15
+
16
+ def pytest_runtest_teardown(self, item):
17
+ if item.session.config.option.newlines_after:
18
+ for _ in range(item.session.config.option.newlines_after):
19
+ print()
@@ -0,0 +1,70 @@
1
+ """
2
+ TODO:
3
+ - inheritance
4
+ - dynamic registration
5
+ - dynamic switching (skip docker if not running, skip online if not online, ...)
6
+ """
7
+ import typing as ta
8
+
9
+ import pytest
10
+
11
+ from .... import check
12
+ from .... import collections as col
13
+ from ._registry import register
14
+
15
+
16
+ Configable = ta.Union[pytest.FixtureRequest, pytest.Config]
17
+
18
+
19
+ SWITCHES = col.OrderedSet([
20
+ 'online',
21
+ 'slow',
22
+ ])
23
+
24
+
25
+ def _get_obj_config(obj: Configable) -> pytest.Config:
26
+ if isinstance(obj, pytest.Config):
27
+ return obj
28
+ elif isinstance(obj, pytest.FixtureRequest):
29
+ return obj.config
30
+ else:
31
+ raise TypeError(obj)
32
+
33
+
34
+ def is_disabled(obj: ta.Optional[Configable], name: str) -> bool:
35
+ check.isinstance(name, str)
36
+ check.in_(name, SWITCHES)
37
+ return obj is not None and _get_obj_config(obj).getoption(f'--no-{name}')
38
+
39
+
40
+ def skip_if_disabled(obj: ta.Optional[Configable], name: str) -> None:
41
+ if is_disabled(obj, name):
42
+ pytest.skip(f'{name} disabled')
43
+
44
+
45
+ def get_switches(obj: Configable) -> ta.Mapping[str, bool]:
46
+ return {
47
+ sw: _get_obj_config(obj).getoption(f'--no-{sw}')
48
+ for sw in SWITCHES
49
+ }
50
+
51
+
52
+ @register
53
+ class SwitchesPlugin:
54
+
55
+ def pytest_addoption(self, parser):
56
+ for sw in SWITCHES:
57
+ parser.addoption(f'--no-{sw}', action='store_true', default=False, help=f'disable {sw} tests')
58
+
59
+ def pytest_collection_modifyitems(self, config, items):
60
+ for sw in SWITCHES:
61
+ if not config.getoption(f'--no-{sw}'):
62
+ continue
63
+ skip = pytest.mark.skip(reason=f'omit --no-{sw} to run')
64
+ for item in items:
65
+ if sw in item.keywords:
66
+ item.add_marker(skip)
67
+
68
+ def pytest_configure(self, config):
69
+ for sw in SWITCHES:
70
+ config.addinivalue_line('markers', f'{sw}: mark test as {sw}')
@@ -0,0 +1,102 @@
1
+ import functools
2
+ import importlib
3
+ import os
4
+ import threading
5
+ import time
6
+ import traceback
7
+ import typing as ta
8
+
9
+
10
+ DEFAULT_TIMEOUT_S = 30
11
+
12
+ T = ta.TypeVar('T')
13
+
14
+
15
+ def call_many_with_timeout(
16
+ fns: ta.Iterable[ta.Callable[[], T]],
17
+ timeout_s: int | float | None = None,
18
+ timeout_exception: Exception = TimeoutError('Thread timeout'),
19
+ ) -> list[T]:
20
+ if timeout_s is None:
21
+ timeout_s = DEFAULT_TIMEOUT_S
22
+
23
+ fns = list(fns)
24
+ missing = object()
25
+ rets: list[ta.Any] = [missing] * len(fns)
26
+ thread_exception: ta.Optional[Exception] = None
27
+
28
+ def inner(fn, idx):
29
+ try:
30
+ nonlocal rets
31
+ rets[idx] = fn()
32
+ except Exception as e:
33
+ nonlocal thread_exception
34
+ thread_exception = e
35
+ raise
36
+
37
+ threads = [threading.Thread(target=inner, args=(fn, idx)) for idx, fn in enumerate(fns)]
38
+ for thread in threads:
39
+ thread.start()
40
+ for thread in threads:
41
+ thread.join(timeout_s)
42
+ for thread in threads:
43
+ if thread.is_alive():
44
+ raise timeout_exception
45
+
46
+ if thread_exception is not None:
47
+ raise thread_exception
48
+ for ret in rets:
49
+ if ret is missing:
50
+ raise ValueError
51
+
52
+ return ta.cast(list[T], rets)
53
+
54
+
55
+ def run_with_timeout(
56
+ *fns: ta.Callable[[], None],
57
+ timeout_s: int | float | None = None,
58
+ timeout_exception: Exception = TimeoutError('Thread timeout'),
59
+ ) -> None:
60
+ call_many_with_timeout(fns, timeout_s, timeout_exception)
61
+
62
+
63
+ def waitpid_with_timeout(
64
+ pid: int,
65
+ timeout_s: int | float | None = None,
66
+ timeout_exception: Exception = TimeoutError('waitpid timeout')
67
+ ) -> int:
68
+ if timeout_s is None:
69
+ timeout_s = DEFAULT_TIMEOUT_S
70
+
71
+ start_time = time.time()
72
+
73
+ while True:
74
+ wait_pid, status = os.waitpid(pid, os.WNOHANG)
75
+ if wait_pid != 0:
76
+ if wait_pid != pid:
77
+ raise ValueError(f'{wait_pid} != {pid}')
78
+ return status
79
+
80
+ elapsed_time = time.time() - start_time
81
+ if elapsed_time >= timeout_s:
82
+ raise timeout_exception
83
+
84
+
85
+ def can_import(*args, **kwargs) -> bool:
86
+ try:
87
+ importlib.import_module(*args, **kwargs)
88
+ except ImportError:
89
+ return False
90
+ else:
91
+ return True
92
+
93
+
94
+ def xfail(fn):
95
+ @functools.wraps(fn)
96
+ def inner(*args, **kwargs):
97
+ try:
98
+ fn(*args, **kwargs)
99
+ except Exception: # noqa
100
+ traceback.print_exc()
101
+
102
+ return inner
File without changes
omlish/text/delimit.py ADDED
@@ -0,0 +1,171 @@
1
+ import io
2
+ import typing as ta
3
+
4
+
5
+ class DelimitedEscaping:
6
+
7
+ def __init__(
8
+ self,
9
+ delimit_char: str,
10
+ quote_char: str,
11
+ escape_char: str,
12
+ escaped_chars: ta.Iterable[str] = (),
13
+ ) -> None:
14
+ super().__init__()
15
+
16
+ self._delimit_char = delimit_char
17
+ self._quote_char = quote_char
18
+ self._escape_char = escape_char
19
+ self._escaped_chars = frozenset(escaped_chars)
20
+
21
+ for c in [delimit_char, quote_char, escape_char]:
22
+ if not isinstance(c, str) or len(c) != 1:
23
+ raise TypeError(c)
24
+ for c in self._escaped_chars:
25
+ if not isinstance(c, str):
26
+ raise TypeError(c)
27
+
28
+ self._all_escaped_chars = frozenset({delimit_char, quote_char, escape_char} | self._escaped_chars)
29
+
30
+ @property
31
+ def delimit_char(self) -> str:
32
+ return self._delimit_char
33
+
34
+ def quote_char(self) -> str:
35
+ return self._quote_char
36
+
37
+ def escape_char(self) -> str:
38
+ return self._escape_char
39
+
40
+ def escaped_chars(self) -> frozenset[str]:
41
+ return self._escaped_chars
42
+
43
+ def all_escaped_chars(self) -> frozenset[str]:
44
+ return self._all_escaped_chars
45
+
46
+ def is_control_char(self, c: str) -> bool:
47
+ if not len(c) == 1:
48
+ raise TypeError(c)
49
+ return c == self._delimit_char or c == self._quote_char or c == self._escape_char
50
+
51
+ def contains_escaped_char(self, s: str) -> bool:
52
+ return any(c in self._all_escaped_chars for c in s)
53
+
54
+ def escape(self, s: str) -> str:
55
+ buf = io.StringIO()
56
+ for c in s:
57
+ if c in self._all_escaped_chars:
58
+ buf.write(self._escape_char)
59
+ buf.write(c)
60
+ return buf.getvalue()
61
+
62
+ def unescape(self, s: str) -> str:
63
+ buf = io.StringIO()
64
+ i = 0
65
+ while i < len(s):
66
+ c = s[i]
67
+ if c == self._escape_char:
68
+ if i > (len(s) - 2):
69
+ raise ValueError(s)
70
+ i += 1
71
+ buf.write(s[i])
72
+ else:
73
+ if c in self._all_escaped_chars:
74
+ raise ValueError(s)
75
+ buf.write(c)
76
+ i += 1
77
+ return buf.getvalue()
78
+
79
+ def quote(self, s: str) -> str:
80
+ if self.contains_escaped_char(s):
81
+ return self._quote_char + self.escape(s) + self._quote_char
82
+ else:
83
+ return s
84
+
85
+ def unquote(self, s: str) -> str:
86
+ if s and s[0] == self._quote_char:
87
+ if len(s) < 2 or s[-1] != self._quote_char:
88
+ raise ValueError(s)
89
+ return self.unescape(s[1:-1])
90
+ else:
91
+ return s
92
+
93
+ def delimit_many(self, strs: ta.Iterable[str]) -> str:
94
+ if isinstance(strs, str):
95
+ raise TypeError(strs)
96
+ buf = io.StringIO()
97
+ count = 0
98
+ for s in strs:
99
+ if count:
100
+ buf.write(self._delimit_char)
101
+ count += 1
102
+ if self.contains_escaped_char(s):
103
+ buf.write(self.quote(s))
104
+ else:
105
+ buf.write(s)
106
+ return buf.getvalue()
107
+
108
+ def delimit(self, s: str) -> str:
109
+ if not isinstance(s, str):
110
+ raise TypeError(s)
111
+ return self.delimit_many([s])
112
+
113
+ def undelimit(self, s: str) -> list[str]:
114
+ ret = []
115
+ buf = io.StringIO()
116
+ count = 0
117
+ i = 0
118
+
119
+ while i < len(s):
120
+ c = s[i]
121
+
122
+ if count:
123
+ if c != self._delimit_char or i >= (len(s) - 1):
124
+ raise ValueError(s)
125
+ i += 1
126
+ c = s[i]
127
+
128
+ quoted = c == self._quote_char
129
+ if quoted:
130
+ if i >= (len(s) - 1):
131
+ raise ValueError(s)
132
+ i += 1
133
+ c = s[i]
134
+ unquoted = False
135
+
136
+ while True:
137
+ if c == self._delimit_char:
138
+ if not quoted:
139
+ break
140
+ else:
141
+ buf.write(c)
142
+ elif c == self._quote_char:
143
+ if not quoted:
144
+ raise ValueError(s)
145
+ unquoted = True
146
+ i += 1
147
+ break
148
+ elif c == self._escape_char:
149
+ if not quoted or i > (len(s) - 2):
150
+ raise ValueError(s)
151
+ i += 1
152
+ buf.write(s[i])
153
+ else:
154
+ if c in self._escaped_chars:
155
+ raise ValueError(s)
156
+ buf.write(c)
157
+
158
+ i += 1
159
+ if i == len(s):
160
+ break
161
+ c = s[i]
162
+
163
+ if quoted and not unquoted:
164
+ raise ValueError(s)
165
+
166
+ ret.append(buf.getvalue())
167
+ buf.seek(0)
168
+ buf.truncate()
169
+ count += 1
170
+
171
+ return ret
omlish/text/indent.py ADDED
@@ -0,0 +1,50 @@
1
+ import contextlib
2
+ import io
3
+ import typing as ta
4
+
5
+ from .. import check
6
+
7
+
8
+ class IndentWriter:
9
+
10
+ DEFAULT_INDENT = ' ' * 4
11
+
12
+ def __init__(
13
+ self,
14
+ *,
15
+ buf: io.StringIO | None = None,
16
+ indent: str | None = None,
17
+ ) -> None:
18
+ super().__init__()
19
+
20
+ self._buf = buf if buf is not None else io.StringIO()
21
+ self._indent = check.isinstance(indent, str) if indent is not None else self.DEFAULT_INDENT
22
+ self._level = 0
23
+ self._has_indented = False
24
+
25
+ @contextlib.contextmanager
26
+ def indent(self, num: int = 1) -> ta.Iterator[None]:
27
+ self._level += num
28
+ try:
29
+ yield
30
+ finally:
31
+ self._level -= num
32
+
33
+ def write(self, s: str) -> None:
34
+ indent = self._indent * self._level
35
+ i = 0
36
+ while i < len(s):
37
+ if not self._has_indented:
38
+ self._buf.write(indent)
39
+ self._has_indented = True
40
+ try:
41
+ n = s.index('\n', i)
42
+ except ValueError:
43
+ self._buf.write(s[i:])
44
+ break
45
+ self._buf.write(s[i:n + 1])
46
+ self._has_indented = False
47
+ i = n + 2
48
+
49
+ def getvalue(self) -> str:
50
+ return self._buf.getvalue()