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.
- omlish/__about__.py +7 -0
- omlish/__init__.py +0 -0
- omlish/argparse.py +223 -0
- omlish/asyncs/__init__.py +17 -0
- omlish/asyncs/anyio.py +23 -0
- omlish/asyncs/asyncio.py +19 -0
- omlish/asyncs/asyncs.py +76 -0
- omlish/asyncs/futures.py +179 -0
- omlish/asyncs/trio.py +11 -0
- omlish/c3.py +173 -0
- omlish/cached.py +9 -0
- omlish/check.py +231 -0
- omlish/collections/__init__.py +63 -0
- omlish/collections/_abc.py +156 -0
- omlish/collections/_io_abc.py +78 -0
- omlish/collections/cache/__init__.py +11 -0
- omlish/collections/cache/descriptor.py +188 -0
- omlish/collections/cache/impl.py +485 -0
- omlish/collections/cache/types.py +37 -0
- omlish/collections/coerce.py +337 -0
- omlish/collections/frozen.py +148 -0
- omlish/collections/identity.py +106 -0
- omlish/collections/indexed.py +75 -0
- omlish/collections/mappings.py +127 -0
- omlish/collections/ordered.py +81 -0
- omlish/collections/persistent.py +36 -0
- omlish/collections/skiplist.py +193 -0
- omlish/collections/sorted.py +126 -0
- omlish/collections/treap.py +228 -0
- omlish/collections/treapmap.py +144 -0
- omlish/collections/unmodifiable.py +174 -0
- omlish/collections/utils.py +110 -0
- omlish/configs/__init__.py +0 -0
- omlish/configs/flattening.py +147 -0
- omlish/configs/props.py +64 -0
- omlish/dataclasses/__init__.py +83 -0
- omlish/dataclasses/impl/__init__.py +6 -0
- omlish/dataclasses/impl/api.py +260 -0
- omlish/dataclasses/impl/as_.py +76 -0
- omlish/dataclasses/impl/exceptions.py +2 -0
- omlish/dataclasses/impl/fields.py +148 -0
- omlish/dataclasses/impl/frozen.py +55 -0
- omlish/dataclasses/impl/hashing.py +85 -0
- omlish/dataclasses/impl/init.py +173 -0
- omlish/dataclasses/impl/internals.py +118 -0
- omlish/dataclasses/impl/main.py +150 -0
- omlish/dataclasses/impl/metaclass.py +126 -0
- omlish/dataclasses/impl/metadata.py +74 -0
- omlish/dataclasses/impl/order.py +47 -0
- omlish/dataclasses/impl/params.py +150 -0
- omlish/dataclasses/impl/processing.py +16 -0
- omlish/dataclasses/impl/reflect.py +173 -0
- omlish/dataclasses/impl/replace.py +40 -0
- omlish/dataclasses/impl/repr.py +34 -0
- omlish/dataclasses/impl/simple.py +92 -0
- omlish/dataclasses/impl/slots.py +80 -0
- omlish/dataclasses/impl/utils.py +167 -0
- omlish/defs.py +193 -0
- omlish/dispatch/__init__.py +3 -0
- omlish/dispatch/dispatch.py +137 -0
- omlish/dispatch/functions.py +52 -0
- omlish/dispatch/methods.py +162 -0
- omlish/docker.py +149 -0
- omlish/dynamic.py +220 -0
- omlish/graphs/__init__.py +0 -0
- omlish/graphs/dot/__init__.py +19 -0
- omlish/graphs/dot/items.py +162 -0
- omlish/graphs/dot/rendering.py +147 -0
- omlish/graphs/dot/utils.py +30 -0
- omlish/graphs/trees.py +249 -0
- omlish/http/__init__.py +0 -0
- omlish/http/consts.py +20 -0
- omlish/http/wsgi.py +34 -0
- omlish/inject/__init__.py +85 -0
- omlish/inject/binder.py +12 -0
- omlish/inject/bindings.py +49 -0
- omlish/inject/eagers.py +21 -0
- omlish/inject/elements.py +43 -0
- omlish/inject/exceptions.py +49 -0
- omlish/inject/impl/__init__.py +0 -0
- omlish/inject/impl/bindings.py +19 -0
- omlish/inject/impl/elements.py +154 -0
- omlish/inject/impl/injector.py +182 -0
- omlish/inject/impl/inspect.py +98 -0
- omlish/inject/impl/private.py +109 -0
- omlish/inject/impl/providers.py +132 -0
- omlish/inject/impl/scopes.py +198 -0
- omlish/inject/injector.py +40 -0
- omlish/inject/inspect.py +14 -0
- omlish/inject/keys.py +43 -0
- omlish/inject/managed.py +24 -0
- omlish/inject/overrides.py +18 -0
- omlish/inject/private.py +29 -0
- omlish/inject/providers.py +111 -0
- omlish/inject/proxy.py +48 -0
- omlish/inject/scopes.py +84 -0
- omlish/inject/types.py +21 -0
- omlish/iterators.py +184 -0
- omlish/json.py +194 -0
- omlish/lang/__init__.py +112 -0
- omlish/lang/cached.py +267 -0
- omlish/lang/classes/__init__.py +24 -0
- omlish/lang/classes/abstract.py +74 -0
- omlish/lang/classes/restrict.py +137 -0
- omlish/lang/classes/simple.py +120 -0
- omlish/lang/classes/test/__init__.py +0 -0
- omlish/lang/classes/test/test_abstract.py +89 -0
- omlish/lang/classes/test/test_restrict.py +71 -0
- omlish/lang/classes/test/test_simple.py +58 -0
- omlish/lang/classes/test/test_virtual.py +72 -0
- omlish/lang/classes/virtual.py +130 -0
- omlish/lang/clsdct.py +67 -0
- omlish/lang/cmp.py +63 -0
- omlish/lang/contextmanagers.py +249 -0
- omlish/lang/datetimes.py +67 -0
- omlish/lang/descriptors.py +52 -0
- omlish/lang/functions.py +126 -0
- omlish/lang/imports.py +153 -0
- omlish/lang/iterables.py +54 -0
- omlish/lang/maybes.py +136 -0
- omlish/lang/objects.py +103 -0
- omlish/lang/resolving.py +50 -0
- omlish/lang/strings.py +128 -0
- omlish/lang/typing.py +92 -0
- omlish/libc.py +532 -0
- omlish/logs/__init__.py +9 -0
- omlish/logs/_abc.py +247 -0
- omlish/logs/configs.py +62 -0
- omlish/logs/filters.py +9 -0
- omlish/logs/formatters.py +67 -0
- omlish/logs/utils.py +20 -0
- omlish/marshal/__init__.py +52 -0
- omlish/marshal/any.py +25 -0
- omlish/marshal/base.py +201 -0
- omlish/marshal/base64.py +25 -0
- omlish/marshal/dataclasses.py +115 -0
- omlish/marshal/datetimes.py +90 -0
- omlish/marshal/enums.py +43 -0
- omlish/marshal/exceptions.py +7 -0
- omlish/marshal/factories.py +129 -0
- omlish/marshal/global_.py +33 -0
- omlish/marshal/iterables.py +57 -0
- omlish/marshal/mappings.py +66 -0
- omlish/marshal/naming.py +17 -0
- omlish/marshal/objects.py +106 -0
- omlish/marshal/optionals.py +49 -0
- omlish/marshal/polymorphism.py +147 -0
- omlish/marshal/primitives.py +43 -0
- omlish/marshal/registries.py +57 -0
- omlish/marshal/standard.py +80 -0
- omlish/marshal/utils.py +23 -0
- omlish/marshal/uuids.py +29 -0
- omlish/marshal/values.py +30 -0
- omlish/math.py +184 -0
- omlish/os.py +32 -0
- omlish/reflect.py +359 -0
- omlish/replserver/__init__.py +5 -0
- omlish/replserver/__main__.py +4 -0
- omlish/replserver/console.py +247 -0
- omlish/replserver/server.py +146 -0
- omlish/runmodule.py +28 -0
- omlish/stats.py +342 -0
- omlish/term.py +222 -0
- omlish/testing/__init__.py +7 -0
- omlish/testing/pydevd.py +225 -0
- omlish/testing/pytest/__init__.py +8 -0
- omlish/testing/pytest/helpers.py +35 -0
- omlish/testing/pytest/inject/__init__.py +1 -0
- omlish/testing/pytest/inject/harness.py +159 -0
- omlish/testing/pytest/plugins/__init__.py +20 -0
- omlish/testing/pytest/plugins/_registry.py +6 -0
- omlish/testing/pytest/plugins/logging.py +13 -0
- omlish/testing/pytest/plugins/pycharm.py +54 -0
- omlish/testing/pytest/plugins/repeat.py +19 -0
- omlish/testing/pytest/plugins/skips.py +32 -0
- omlish/testing/pytest/plugins/spacing.py +19 -0
- omlish/testing/pytest/plugins/switches.py +70 -0
- omlish/testing/testing.py +102 -0
- omlish/text/__init__.py +0 -0
- omlish/text/delimit.py +171 -0
- omlish/text/indent.py +50 -0
- omlish/text/parts.py +265 -0
- omlish-0.0.0.dev1.dist-info/LICENSE +21 -0
- omlish-0.0.0.dev1.dist-info/METADATA +17 -0
- omlish-0.0.0.dev1.dist-info/RECORD +187 -0
- omlish-0.0.0.dev1.dist-info/WHEEL +5 -0
- 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
|
omlish/text/__init__.py
ADDED
|
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()
|