uniparser2 0.7.0__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.
- uniparser2/__init__.py +116 -0
- uniparser2/cli/__init__.py +1 -0
- uniparser2/cli/conformance.py +140 -0
- uniparser2/cli/main.py +867 -0
- uniparser2/cli/samples.py +314 -0
- uniparser2/core/__init__.py +107 -0
- uniparser2/core/ast.py +223 -0
- uniparser2/core/compile.py +801 -0
- uniparser2/core/conformance.py +100 -0
- uniparser2/core/errors.py +158 -0
- uniparser2/core/evaluator.py +235 -0
- uniparser2/core/export.py +199 -0
- uniparser2/core/limits.py +51 -0
- uniparser2/core/materialize.py +188 -0
- uniparser2/core/refs.py +451 -0
- uniparser2/core/registry.py +494 -0
- uniparser2/core/story.py +456 -0
- uniparser2/core/trace.py +292 -0
- uniparser2/core/types.py +153 -0
- uniparser2/core/udfs.py +83 -0
- uniparser2/core/values.py +226 -0
- uniparser2/host.py +58 -0
- uniparser2/net/__init__.py +28 -0
- uniparser2/net/cache.py +157 -0
- uniparser2/net/download.py +200 -0
- uniparser2/ops/__init__.py +32 -0
- uniparser2/ops/basic.py +97 -0
- uniparser2/ops/code.py +474 -0
- uniparser2/ops/ctx.py +245 -0
- uniparser2/ops/htmldom.py +392 -0
- uniparser2/ops/jsonquery.py +167 -0
- uniparser2/ops/node.py +65 -0
- uniparser2/ops/pyops.py +1457 -0
- uniparser2/ops/regex.py +221 -0
- uniparser2/ops/typeop.py +94 -0
- uniparser2/ops/udf.py +227 -0
- uniparser2/ops/xmlxpath.py +277 -0
- uniparser2/py.typed +2 -0
- uniparser2/store/__init__.py +135 -0
- uniparser2/store/base.py +362 -0
- uniparser2/store/file.py +248 -0
- uniparser2/store/fingerprint.py +39 -0
- uniparser2/store/gates.py +45 -0
- uniparser2/store/model.py +114 -0
- uniparser2/store/sqlite.py +291 -0
- uniparser2-0.7.0.dist-info/METADATA +387 -0
- uniparser2-0.7.0.dist-info/RECORD +50 -0
- uniparser2-0.7.0.dist-info/WHEEL +4 -0
- uniparser2-0.7.0.dist-info/entry_points.txt +2 -0
- uniparser2-0.7.0.dist-info/licenses/LICENSE +21 -0
uniparser2/__init__.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""uniparser2: a declarative, deterministic, serializable text parsing engine.
|
|
2
|
+
|
|
3
|
+
The design notes are in ``docs/specs/`` (``0NN-*.md``; index and conventions in ``docs/README.md``).
|
|
4
|
+
|
|
5
|
+
Importing this package registers the built-in operators automatically (``uniparser2.ops``); the
|
|
6
|
+
core, ``uniparser2.core``, depends on no third-party library and performs no IO (invariant I1 of
|
|
7
|
+
design doc 01 §1.1).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
from . import ops as _ops # noqa: F401 register built-in operators (explicit registry)
|
|
15
|
+
from .core import (
|
|
16
|
+
CODES,
|
|
17
|
+
PREVIEW_LIMIT,
|
|
18
|
+
STATE_FAILED,
|
|
19
|
+
STATE_OK,
|
|
20
|
+
CompiledField,
|
|
21
|
+
CompiledNode,
|
|
22
|
+
CompiledStory,
|
|
23
|
+
Diagnostic,
|
|
24
|
+
Failed,
|
|
25
|
+
JsonValue,
|
|
26
|
+
Materializer,
|
|
27
|
+
Ok,
|
|
28
|
+
Opaque,
|
|
29
|
+
Outcome,
|
|
30
|
+
Ref,
|
|
31
|
+
Scope,
|
|
32
|
+
Trace,
|
|
33
|
+
TraceEvent,
|
|
34
|
+
TypeConstructionError,
|
|
35
|
+
TypeConstructor,
|
|
36
|
+
UnknownTypeError,
|
|
37
|
+
Up2Error,
|
|
38
|
+
Value,
|
|
39
|
+
ValueView,
|
|
40
|
+
compile_field,
|
|
41
|
+
compile_stage,
|
|
42
|
+
compile_story,
|
|
43
|
+
dumps,
|
|
44
|
+
eval_field,
|
|
45
|
+
get_type,
|
|
46
|
+
loads,
|
|
47
|
+
pack,
|
|
48
|
+
parse_ref,
|
|
49
|
+
register_type,
|
|
50
|
+
run,
|
|
51
|
+
type_names,
|
|
52
|
+
unregister_type,
|
|
53
|
+
value_view,
|
|
54
|
+
)
|
|
55
|
+
from .core.udfs import get_udf, register_udf, udf_names, unregister_udf
|
|
56
|
+
from .host import load_entry_point_udfs
|
|
57
|
+
|
|
58
|
+
__version__ = "0.7.0"
|
|
59
|
+
"""The single source of the version: ``[tool.hatch.version]`` in ``pyproject.toml`` reads it."""
|
|
60
|
+
|
|
61
|
+
logger = logging.getLogger("uniparser2")
|
|
62
|
+
# Do not take over the host's logging configuration (essence §C library logging rules).
|
|
63
|
+
logger.addHandler(logging.NullHandler())
|
|
64
|
+
|
|
65
|
+
# Discover the user-defined functions the host declares through entry points (the IO lives in the
|
|
66
|
+
# host layer, so core stays IO-free).
|
|
67
|
+
_ENV_UDFS = load_entry_point_udfs()
|
|
68
|
+
"""The names registered from the ``entry_points`` group ``uniparser2.udfs`` during this import."""
|
|
69
|
+
|
|
70
|
+
__all__ = [
|
|
71
|
+
"CODES",
|
|
72
|
+
"PREVIEW_LIMIT",
|
|
73
|
+
"STATE_FAILED",
|
|
74
|
+
"STATE_OK",
|
|
75
|
+
"CompiledField",
|
|
76
|
+
"CompiledNode",
|
|
77
|
+
"CompiledStory",
|
|
78
|
+
"Diagnostic",
|
|
79
|
+
"Failed",
|
|
80
|
+
"JsonValue",
|
|
81
|
+
"Materializer",
|
|
82
|
+
"Ok",
|
|
83
|
+
"Opaque",
|
|
84
|
+
"Outcome",
|
|
85
|
+
"Ref",
|
|
86
|
+
"Scope",
|
|
87
|
+
"Trace",
|
|
88
|
+
"TraceEvent",
|
|
89
|
+
"TypeConstructionError",
|
|
90
|
+
"TypeConstructor",
|
|
91
|
+
"UnknownTypeError",
|
|
92
|
+
"Up2Error",
|
|
93
|
+
"Value",
|
|
94
|
+
"ValueView",
|
|
95
|
+
"__version__",
|
|
96
|
+
"compile_field",
|
|
97
|
+
"compile_stage",
|
|
98
|
+
"compile_story",
|
|
99
|
+
"dumps",
|
|
100
|
+
"eval_field",
|
|
101
|
+
"get_type",
|
|
102
|
+
"get_udf",
|
|
103
|
+
"load_entry_point_udfs",
|
|
104
|
+
"loads",
|
|
105
|
+
"logger",
|
|
106
|
+
"pack",
|
|
107
|
+
"parse_ref",
|
|
108
|
+
"register_type",
|
|
109
|
+
"register_udf",
|
|
110
|
+
"run",
|
|
111
|
+
"type_names",
|
|
112
|
+
"udf_names",
|
|
113
|
+
"unregister_type",
|
|
114
|
+
"unregister_udf",
|
|
115
|
+
"value_view",
|
|
116
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""``up2`` command-line package (IO is allowed only outside the core; 01 §1.1 invariant I1)."""
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""The **IO side** of the conformance audit: scan test files and extract "what a case declares it
|
|
2
|
+
covers".
|
|
3
|
+
|
|
4
|
+
There are two declaration styles, both of which must be **visible in the source** (the scanner only
|
|
5
|
+
reads source text and never executes tests):
|
|
6
|
+
|
|
7
|
+
1. Decorator (good for a handful of declarations)::
|
|
8
|
+
|
|
9
|
+
@pytest.mark.covers("re", "$match")
|
|
10
|
+
def test_match(...): ...
|
|
11
|
+
|
|
12
|
+
2. Module-level constant (good for batch declarations, e.g. 24 ``py`` functions)::
|
|
13
|
+
|
|
14
|
+
COVERS_PAIRS = (("py", "strip"), ("py", "join"), ...)
|
|
15
|
+
|
|
16
|
+
Error codes need no marker — a ``UP2-EXXXX`` literal appearing in test source counts as coverage of
|
|
17
|
+
that code. The comparison logic is a pure function in ``core.conformance.check`` (01 §1.1 invariant
|
|
18
|
+
I1: core never touches the filesystem).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import ast
|
|
24
|
+
from collections.abc import Iterable
|
|
25
|
+
from functools import cache
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
_CODE_PREFIX = "UP2-E"
|
|
29
|
+
_CODE_DIGITS = 4
|
|
30
|
+
_COVERS_MARK = "covers"
|
|
31
|
+
_COVERS_CONSTANT = "COVERS_PAIRS"
|
|
32
|
+
DEFAULT_TESTS_ROOT = Path("tests")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def scan_tests(root: Path) -> tuple[set[tuple[str, str]], set[str]]:
|
|
36
|
+
"""Scan the test directory and return the covered (operator, action) pairs and error codes.
|
|
37
|
+
|
|
38
|
+
The scan is memoised per root (the tree is stable for the life of a process, and several tests
|
|
39
|
+
and the CLI audit read it), and each caller gets a fresh copy of the sets.
|
|
40
|
+
"""
|
|
41
|
+
pairs, codes = _scan_tests(root)
|
|
42
|
+
return set(pairs), set(codes)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@cache
|
|
46
|
+
def _scan_tests(root: Path) -> tuple[frozenset[tuple[str, str]], frozenset[str]]:
|
|
47
|
+
pairs: set[tuple[str, str]] = set()
|
|
48
|
+
codes: set[str] = set()
|
|
49
|
+
for path in sorted(root.rglob("*.py")):
|
|
50
|
+
text = path.read_text(encoding="utf-8")
|
|
51
|
+
codes.update(_scan_codes(text))
|
|
52
|
+
pairs.update(_scan_covers(text, path=path))
|
|
53
|
+
return frozenset(pairs), frozenset(codes)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _scan_codes(text: str) -> Iterable[str]:
|
|
57
|
+
"""Scan for ``UP2-E`` followed by at least 4 digits.
|
|
58
|
+
|
|
59
|
+
The digit count must be required: the placeholder form ``UP2-E1xxx`` common in docs and
|
|
60
|
+
comments is not a code, and an early implementation only looked for "digits follow", treated it
|
|
61
|
+
as ``UP2-E1`` and reported a pile of orphan codes.
|
|
62
|
+
"""
|
|
63
|
+
start = 0
|
|
64
|
+
while True:
|
|
65
|
+
index = text.find(_CODE_PREFIX, start)
|
|
66
|
+
if index < 0:
|
|
67
|
+
return
|
|
68
|
+
head = index + len(_CODE_PREFIX)
|
|
69
|
+
end = head
|
|
70
|
+
while end < len(text) and text[end].isdigit():
|
|
71
|
+
end += 1
|
|
72
|
+
if end - head >= _CODE_DIGITS:
|
|
73
|
+
yield text[index:end]
|
|
74
|
+
start = max(end, head + 1)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _scan_covers(text: str, *, path: Path) -> set[tuple[str, str]]:
|
|
78
|
+
try:
|
|
79
|
+
tree = ast.parse(text)
|
|
80
|
+
except SyntaxError as exc: # pragma: no cover - pytest reports broken test syntax first
|
|
81
|
+
raise SyntaxError(f"{path} cannot be parsed: {exc}") from exc
|
|
82
|
+
found: set[tuple[str, str]] = set()
|
|
83
|
+
for node in ast.walk(tree):
|
|
84
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
85
|
+
for decorator in node.decorator_list:
|
|
86
|
+
pair = _pair_of(decorator)
|
|
87
|
+
if pair is not None:
|
|
88
|
+
found.add(pair)
|
|
89
|
+
elif isinstance(node, (ast.Assign, ast.AnnAssign)):
|
|
90
|
+
found.update(_pairs_of_constant(node))
|
|
91
|
+
return found
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _pairs_of_constant(node: ast.Assign | ast.AnnAssign) -> set[tuple[str, str]]:
|
|
95
|
+
targets = node.targets if isinstance(node, ast.Assign) else [node.target]
|
|
96
|
+
if not any(
|
|
97
|
+
isinstance(target, ast.Name) and target.id == _COVERS_CONSTANT for target in targets
|
|
98
|
+
):
|
|
99
|
+
return set()
|
|
100
|
+
value = node.value
|
|
101
|
+
if not isinstance(value, (ast.Tuple, ast.List, ast.Set)):
|
|
102
|
+
return set()
|
|
103
|
+
pairs: set[tuple[str, str]] = set()
|
|
104
|
+
for element in value.elts:
|
|
105
|
+
pair = _literal_pair(element)
|
|
106
|
+
if pair is not None:
|
|
107
|
+
pairs.add(pair)
|
|
108
|
+
return pairs
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _pair_of(node: ast.expr) -> tuple[str, str] | None:
|
|
112
|
+
if not isinstance(node, ast.Call) or not _is_covers(node.func):
|
|
113
|
+
return None
|
|
114
|
+
values = [
|
|
115
|
+
arg.value
|
|
116
|
+
for arg in node.args
|
|
117
|
+
if isinstance(arg, ast.Constant) and isinstance(arg.value, str)
|
|
118
|
+
]
|
|
119
|
+
if len(values) < 2:
|
|
120
|
+
return None
|
|
121
|
+
return values[0], values[1]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _literal_pair(node: ast.expr) -> tuple[str, str] | None:
|
|
125
|
+
if not isinstance(node, (ast.Tuple, ast.List)) or len(node.elts) != 2:
|
|
126
|
+
return None
|
|
127
|
+
values = [
|
|
128
|
+
element.value
|
|
129
|
+
for element in node.elts
|
|
130
|
+
if isinstance(element, ast.Constant) and isinstance(element.value, str)
|
|
131
|
+
]
|
|
132
|
+
if len(values) != 2:
|
|
133
|
+
return None
|
|
134
|
+
return values[0], values[1]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _is_covers(node: ast.expr) -> bool:
|
|
138
|
+
if isinstance(node, ast.Attribute):
|
|
139
|
+
return node.attr == _COVERS_MARK
|
|
140
|
+
return isinstance(node, ast.Name) and node.id == _COVERS_MARK
|