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.
Files changed (50) hide show
  1. uniparser2/__init__.py +116 -0
  2. uniparser2/cli/__init__.py +1 -0
  3. uniparser2/cli/conformance.py +140 -0
  4. uniparser2/cli/main.py +867 -0
  5. uniparser2/cli/samples.py +314 -0
  6. uniparser2/core/__init__.py +107 -0
  7. uniparser2/core/ast.py +223 -0
  8. uniparser2/core/compile.py +801 -0
  9. uniparser2/core/conformance.py +100 -0
  10. uniparser2/core/errors.py +158 -0
  11. uniparser2/core/evaluator.py +235 -0
  12. uniparser2/core/export.py +199 -0
  13. uniparser2/core/limits.py +51 -0
  14. uniparser2/core/materialize.py +188 -0
  15. uniparser2/core/refs.py +451 -0
  16. uniparser2/core/registry.py +494 -0
  17. uniparser2/core/story.py +456 -0
  18. uniparser2/core/trace.py +292 -0
  19. uniparser2/core/types.py +153 -0
  20. uniparser2/core/udfs.py +83 -0
  21. uniparser2/core/values.py +226 -0
  22. uniparser2/host.py +58 -0
  23. uniparser2/net/__init__.py +28 -0
  24. uniparser2/net/cache.py +157 -0
  25. uniparser2/net/download.py +200 -0
  26. uniparser2/ops/__init__.py +32 -0
  27. uniparser2/ops/basic.py +97 -0
  28. uniparser2/ops/code.py +474 -0
  29. uniparser2/ops/ctx.py +245 -0
  30. uniparser2/ops/htmldom.py +392 -0
  31. uniparser2/ops/jsonquery.py +167 -0
  32. uniparser2/ops/node.py +65 -0
  33. uniparser2/ops/pyops.py +1457 -0
  34. uniparser2/ops/regex.py +221 -0
  35. uniparser2/ops/typeop.py +94 -0
  36. uniparser2/ops/udf.py +227 -0
  37. uniparser2/ops/xmlxpath.py +277 -0
  38. uniparser2/py.typed +2 -0
  39. uniparser2/store/__init__.py +135 -0
  40. uniparser2/store/base.py +362 -0
  41. uniparser2/store/file.py +248 -0
  42. uniparser2/store/fingerprint.py +39 -0
  43. uniparser2/store/gates.py +45 -0
  44. uniparser2/store/model.py +114 -0
  45. uniparser2/store/sqlite.py +291 -0
  46. uniparser2-0.7.0.dist-info/METADATA +387 -0
  47. uniparser2-0.7.0.dist-info/RECORD +50 -0
  48. uniparser2-0.7.0.dist-info/WHEEL +4 -0
  49. uniparser2-0.7.0.dist-info/entry_points.txt +2 -0
  50. 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