knobly 1.1.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.
- knobly/__init__.py +82 -0
- knobly/_anatomy.py +65 -0
- knobly/_api/.gitignore +2 -0
- knobly/_api/__init__.py +1 -0
- knobly/_api/codes.py +294 -0
- knobly/_api/docs.py +29 -0
- knobly/_api/errors.py +88 -0
- knobly/_api/io.py +134 -0
- knobly/_api/kinds.py +171 -0
- knobly/_api/limits.py +37 -0
- knobly/_api/record.py +17 -0
- knobly/_api/rendered.py +276 -0
- knobly/_api/shared.py +349 -0
- knobly/_api/syntax.py +117 -0
- knobly/_api/written.py +2189 -0
- knobly/_block.py +79 -0
- knobly/_diagnostics.py +46 -0
- knobly/_docs.py +70 -0
- knobly/_gen/Knobly.interp +75 -0
- knobly/_gen/Knobly.tokens +30 -0
- knobly/_gen/KnoblyLexer.interp +85 -0
- knobly/_gen/KnoblyLexer.py +165 -0
- knobly/_gen/KnoblyLexer.tokens +30 -0
- knobly/_gen/KnoblyParser.py +1531 -0
- knobly/_gen/__init__.py +0 -0
- knobly/_glob.py +18 -0
- knobly/_origin.py +59 -0
- knobly/_report.py +104 -0
- knobly/_source.py +88 -0
- knobly/_text.py +49 -0
- knobly/_values.py +87 -0
- knobly/files.py +98 -0
- knobly/include.py +42 -0
- knobly/py.typed +0 -0
- knobly/rendered/__init__.py +32 -0
- knobly/rendered/_render.py +138 -0
- knobly/rendered/_tree.py +145 -0
- knobly/syntax/__init__.py +27 -0
- knobly/syntax/_cst.py +42 -0
- knobly/syntax/_parse.py +137 -0
- knobly/syntax/_recovery.py +307 -0
- knobly/syntax/_walk.py +138 -0
- knobly/written/__init__.py +126 -0
- knobly/written/_bind.py +985 -0
- knobly/written/_document.py +996 -0
- knobly/written/_expand.py +68 -0
- knobly/written/_format.py +32 -0
- knobly/written/_graph.py +143 -0
- knobly/written/_model.py +971 -0
- knobly/written/_names.py +29 -0
- knobly/written/_processor.py +39 -0
- knobly/written/_read.py +86 -0
- knobly/written/_shape.py +122 -0
- knobly/written/_value.py +324 -0
- knobly-1.1.0.dist-info/METADATA +8 -0
- knobly-1.1.0.dist-info/RECORD +57 -0
- knobly-1.1.0.dist-info/WHEEL +4 -0
knobly/__init__.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""knobly: a configuration language defined by one grammar (grammar/Knobly.g4). One namespace per layer:
|
|
2
|
+
this package root holds the shared and io names (positions, places, origins, diagnostics,
|
|
3
|
+
the error tree, config text, the document source and writer ports and the in-memory adapters);
|
|
4
|
+
``knobly.syntax`` the lossless syntax tree (``parse``); ``knobly.written`` the config as written (the low
|
|
5
|
+
level: ``Processor.load`` returns a ``DocumentGraph`` of editable documents, read further with ``graph.read()`` and
|
|
6
|
+
saved back with ``graph.save(writer)``); ``knobly.rendered`` the rendered tree (``render(graph)``: variables,
|
|
7
|
+
snippets and included files expanded into directives a program reads). The core does no I/O: ``knobly.files``
|
|
8
|
+
gives a filesystem source and writer, ``knobly.include`` the include extension."""
|
|
9
|
+
|
|
10
|
+
# Every public signature is generated from spec/schema/api.json (knobly._api); each module's __all__ equals the
|
|
11
|
+
# generated PUBLIC_NAMES for it (tests/test_api_names.py).
|
|
12
|
+
|
|
13
|
+
from knobly import _docs
|
|
14
|
+
from knobly._api.codes import DiagnosticCode
|
|
15
|
+
from knobly._api.errors import (
|
|
16
|
+
BadTextError,
|
|
17
|
+
DetachedError,
|
|
18
|
+
DiagnosticError,
|
|
19
|
+
KnoblyError,
|
|
20
|
+
LostUpdateError,
|
|
21
|
+
ReadError,
|
|
22
|
+
SaveError,
|
|
23
|
+
)
|
|
24
|
+
from knobly._api.io import DocumentSource, DocumentWriter, FileWrite, LoadContext
|
|
25
|
+
from knobly._api.kinds import PlaceKind, Severity, StepKind
|
|
26
|
+
from knobly._api.limits import PROGRAM_CODE
|
|
27
|
+
from knobly._api.shared import (
|
|
28
|
+
Block,
|
|
29
|
+
ConfigText,
|
|
30
|
+
DirectiveOrigin,
|
|
31
|
+
Locatable,
|
|
32
|
+
Origin,
|
|
33
|
+
ProgramCode,
|
|
34
|
+
SourcePosition,
|
|
35
|
+
SourceSpan,
|
|
36
|
+
)
|
|
37
|
+
from knobly._diagnostics import Diagnostic
|
|
38
|
+
from knobly._origin import Override, Place, Step
|
|
39
|
+
from knobly._report import Collecting, Diagnostics, LocatedDiagnostic
|
|
40
|
+
from knobly._source import check_lost_updates, memory_source, memory_writer
|
|
41
|
+
from knobly._text import quote, text
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"PROGRAM_CODE",
|
|
45
|
+
"BadTextError",
|
|
46
|
+
"Block",
|
|
47
|
+
"Collecting",
|
|
48
|
+
"ConfigText",
|
|
49
|
+
"DetachedError",
|
|
50
|
+
"Diagnostic",
|
|
51
|
+
"DiagnosticCode",
|
|
52
|
+
"DiagnosticError",
|
|
53
|
+
"Diagnostics",
|
|
54
|
+
"DirectiveOrigin",
|
|
55
|
+
"DocumentSource",
|
|
56
|
+
"DocumentWriter",
|
|
57
|
+
"FileWrite",
|
|
58
|
+
"KnoblyError",
|
|
59
|
+
"LoadContext",
|
|
60
|
+
"Locatable",
|
|
61
|
+
"LocatedDiagnostic",
|
|
62
|
+
"LostUpdateError",
|
|
63
|
+
"Origin",
|
|
64
|
+
"Override",
|
|
65
|
+
"Place",
|
|
66
|
+
"PlaceKind",
|
|
67
|
+
"ProgramCode",
|
|
68
|
+
"ReadError",
|
|
69
|
+
"SaveError",
|
|
70
|
+
"Severity",
|
|
71
|
+
"SourcePosition",
|
|
72
|
+
"SourceSpan",
|
|
73
|
+
"Step",
|
|
74
|
+
"StepKind",
|
|
75
|
+
"check_lost_updates",
|
|
76
|
+
"memory_source",
|
|
77
|
+
"memory_writer",
|
|
78
|
+
"quote",
|
|
79
|
+
"text",
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
_docs.document(__name__)
|
knobly/_anatomy.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Directive anatomy (kno-g7n.1.3): which tokens of a ``directive`` node are its name, its values and its block.
|
|
2
|
+
Private to the load layer; a port of ts/src/ast/anatomy.ts."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from knobly._api.kinds import NodeKind, TokenKind
|
|
9
|
+
from knobly._api.syntax import Node, Token
|
|
10
|
+
from knobly.syntax._cst import ValueRef
|
|
11
|
+
|
|
12
|
+
_VALUE_TOKENS = frozenset(
|
|
13
|
+
{TokenKind.WORD, TokenKind.AT_WORD, TokenKind.INTEGER, TokenKind.DIGIT_WORD, TokenKind.STRING, TokenKind.VARIABLE}
|
|
14
|
+
)
|
|
15
|
+
_VALUE_NODES = frozenset({NodeKind.SLICE, NodeKind.LIST})
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class Anatomy:
|
|
20
|
+
name: int
|
|
21
|
+
"""Index of the head token (``node.start``): a WORD names a directive, an AT_WORD a language or extension
|
|
22
|
+
statement."""
|
|
23
|
+
args: tuple[ValueRef, ...]
|
|
24
|
+
"""In source order: each WORD, AT_WORD, INTEGER, DIGIT_WORD, STRING or VARIABLE token directly in the node's
|
|
25
|
+
range, and each slice or list child."""
|
|
26
|
+
block: Node | None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def anatomy_of(tokens: tuple[Token, ...], node: Node) -> Anatomy:
|
|
30
|
+
"""Split a directive node. Spaces, comments, separators, punctuation and error children are never values."""
|
|
31
|
+
values, block = _split(tokens, node)
|
|
32
|
+
return Anatomy(node.start, values, block)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def items_of(tokens: tuple[Token, ...], node: Node) -> tuple[ValueRef, ...]:
|
|
36
|
+
"""A ``list`` node's items in source order, by the same rule as a directive's values (the bracket comes first)."""
|
|
37
|
+
return _split(tokens, node)[0]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def values_of(tokens: tuple[Token, ...], node: Node) -> tuple[ValueRef, ...]:
|
|
41
|
+
"""The values of an entry rule's text (``parse_entry``'s root), by the same rule, from its first token."""
|
|
42
|
+
return _split(tokens, node, node.start)[0]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _split(tokens: tuple[Token, ...], node: Node, first: int | None = None) -> tuple[tuple[ValueRef, ...], Node | None]:
|
|
46
|
+
"""The values and the block of ``node``, from token ``first`` (default: after its name or opening bracket)."""
|
|
47
|
+
values: list[ValueRef] = []
|
|
48
|
+
block: Node | None = None
|
|
49
|
+
children = node.children
|
|
50
|
+
following = 0
|
|
51
|
+
i = node.start + 1 if first is None else first
|
|
52
|
+
while i < node.end:
|
|
53
|
+
child = children[following] if following < len(children) else None
|
|
54
|
+
if child is not None and child.start == i:
|
|
55
|
+
if child.kind in _VALUE_NODES:
|
|
56
|
+
values.append(child)
|
|
57
|
+
elif child.kind is NodeKind.BLOCK:
|
|
58
|
+
block = child
|
|
59
|
+
following += 1
|
|
60
|
+
i = max(child.end, i + 1)
|
|
61
|
+
continue
|
|
62
|
+
if tokens[i].kind in _VALUE_TOKENS:
|
|
63
|
+
values.append(i)
|
|
64
|
+
i += 1
|
|
65
|
+
return tuple(values), block
|
knobly/_api/.gitignore
ADDED
knobly/_api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Generated by tools/gen-api.py from spec/schema/; do not edit, and never commit."""
|
knobly/_api/codes.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""Generated by tools/gen-api.py from spec/schema/; do not edit, and never commit.
|
|
2
|
+
|
|
3
|
+
Source: spec/schema/diagnostics.json."""
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
from typing import Final
|
|
10
|
+
|
|
11
|
+
from knobly._api.kinds import Severity, UnreadableReason
|
|
12
|
+
from knobly._api.limits import MAX_EXPANDED_NESTING, MAX_NESTING
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DiagnosticCode(StrEnum):
|
|
16
|
+
"""knobly's diagnostic codes, by name."""
|
|
17
|
+
|
|
18
|
+
UNTERMINATED_STRING = "E1001"
|
|
19
|
+
"""A string reaches the end of its line or of the text without its closing quote."""
|
|
20
|
+
UNEXPECTED_CHARACTER = "E1003"
|
|
21
|
+
"""A character no token may start with, or a byte-order mark after the first character."""
|
|
22
|
+
LONE_DOLLAR = "E1004"
|
|
23
|
+
"""A $ not followed by a variable name."""
|
|
24
|
+
BAD_ESCAPE = "E1005"
|
|
25
|
+
"""A closed double-quoted string holding a backslash escape other than the six (the BAD_ESCAPE error token),
|
|
26
|
+
placed on the string. A single-quoted string is raw: it has no escapes.
|
|
27
|
+
"""
|
|
28
|
+
UNEXPECTED_TOKEN = "E2001"
|
|
29
|
+
"""A token the grammar does not allow here, when no more specific code applies: among them the end of the text at
|
|
30
|
+
the top level, or a }, where a directive's ; or block is due (3.2). Also an edit's bad text error, from its
|
|
31
|
+
entry rule (2.8): at the first token the entry rule does not allow, or at the end of the text.
|
|
32
|
+
"""
|
|
33
|
+
EXPECTED_CLOSING_BRACE = "E2003"
|
|
34
|
+
"""The text ends inside a block: reported once, at the end of the text, however many blocks are open; not after a
|
|
35
|
+
statement whose error runs to the end of the text, and not inside an open [ (E2004) (3.2).
|
|
36
|
+
"""
|
|
37
|
+
EXPECTED_CLOSING_BRACKET = "E2004"
|
|
38
|
+
"""A stray ], or a [ list with no matching ]."""
|
|
39
|
+
BAD_SLICE = "E2007"
|
|
40
|
+
"""A malformed slice after a slice name (a word or a $name): a[~], a[1~b c], an unclosed slice, or a literal
|
|
41
|
+
bound that is not an integer (a[i], a[1.5], a[1:3], whose 1:3 is one word).
|
|
42
|
+
"""
|
|
43
|
+
VALUES_TOUCH = "E2009"
|
|
44
|
+
"""Two values with nothing between them, such as a"b"."""
|
|
45
|
+
NESTING_TOO_DEEP = "E2010"
|
|
46
|
+
"""Blocks and lists nested deeper than 100 levels. Also an edit's bad text error when the document after the edit
|
|
47
|
+
would nest past it (2.8).
|
|
48
|
+
"""
|
|
49
|
+
INCLUDE_NOT_FOUND = "E3001"
|
|
50
|
+
"""An extension's spec that is no glob matched no file. A glob that matches none is the warning W3005."""
|
|
51
|
+
INCLUDE_RESOLVER_FAILED = "E3004"
|
|
52
|
+
"""The document source rejected while resolving or reading a file. For the root file, load fails instead (6.2)."""
|
|
53
|
+
INCLUDE_BAD_PATH = "E3005"
|
|
54
|
+
"""An include whose values are not exactly one quoted path."""
|
|
55
|
+
INCLUDE_MATCHED_NOTHING = "W3005"
|
|
56
|
+
"""A warning: an extension's spec that is a glob (the glob rule of 4.1) matched no file. The line splices nothing
|
|
57
|
+
for it and is not stopped. A spec that is no glob and matches no file is E3001.
|
|
58
|
+
"""
|
|
59
|
+
EXTENSION_FAILED = "E3006"
|
|
60
|
+
"""An extension threw something other than a diagnostic error, threw a diagnostic error whose code is a warning
|
|
61
|
+
(a warning is never thrown, 2.3; 5) or is neither knobly's nor a program code, or returned something other
|
|
62
|
+
than a list of texts.
|
|
63
|
+
"""
|
|
64
|
+
UNREAD_COPY = "E3007"
|
|
65
|
+
"""An extension line's copy whose specs are not resolved or whose files are not read yet (it is in unread ());
|
|
66
|
+
read () reads them. Placed on the extension line's name.
|
|
67
|
+
"""
|
|
68
|
+
UNKNOWN_EXTENSION = "E3008"
|
|
69
|
+
"""A statement written @name where name is no language statement (set, snippet, use) and no registered
|
|
70
|
+
extension's name. Placed on the @name; the statement is an unreadable line.
|
|
71
|
+
"""
|
|
72
|
+
NON_INTEGER_BOUND = "E4001"
|
|
73
|
+
"""A slice or index bound reached through a variable that is not one integer word, or a literal integer beyond
|
|
74
|
+
the integer range. A literal bound that is not an integer is E2007, found by parse.
|
|
75
|
+
"""
|
|
76
|
+
WRONG_VALUE_KIND = "E4002"
|
|
77
|
+
"""A reader asked for a kind the value is not."""
|
|
78
|
+
NOT_A_NUMBER = "E4004"
|
|
79
|
+
"""A word read as a number that is not a JSON number, or one beyond the double range (1e400; contract 2)."""
|
|
80
|
+
NOT_AN_INTEGER = "E4005"
|
|
81
|
+
"""A word read as an integer that is not one, or one beyond plus or minus 2^53-1."""
|
|
82
|
+
NOT_A_BOOLEAN = "E4006"
|
|
83
|
+
"""A word other than true or false, read as a boolean."""
|
|
84
|
+
BAD_SLICE_NAME = "E4007"
|
|
85
|
+
"""A slice or index whose name is a $name that does not hold exactly one WORD or AT_WORD, the words the grammar's
|
|
86
|
+
sliceName takes ($src[0~3] with $src a string, a list, two values, or a number-like word such as 5, 1.5, - or
|
|
87
|
+
07:30); placed as E4001 is.
|
|
88
|
+
"""
|
|
89
|
+
UNKNOWN_VARIABLE = "E5001"
|
|
90
|
+
"""A $name with no set visible and no override."""
|
|
91
|
+
VARIABLE_SET_TWICE = "E5002"
|
|
92
|
+
"""Two set lines for one name in one block."""
|
|
93
|
+
UNKNOWN_SNIPPET = "E5003"
|
|
94
|
+
"""A use naming no visible snippet."""
|
|
95
|
+
SNIPPET_ARITY = "E5004"
|
|
96
|
+
"""A use with a different number of values than its snippet's parameters."""
|
|
97
|
+
SNIPPET_DEFINED_TWICE = "E5005"
|
|
98
|
+
"""Two snippet lines for one name in one block."""
|
|
99
|
+
EXPANSION_CYCLE = "E5006"
|
|
100
|
+
"""A use or extension line that reaches itself again."""
|
|
101
|
+
EXPANSION_TOO_DEEP = "E5007"
|
|
102
|
+
"""Use and extension lines nested deeper than 16."""
|
|
103
|
+
TOO_MANY_DIRECTIVES = "E5008"
|
|
104
|
+
"""More than 100,000 directives after expansion."""
|
|
105
|
+
BAD_SET = "E5009"
|
|
106
|
+
"""A set line that is not @set $name value...;."""
|
|
107
|
+
BAD_SNIPPET = "E5010"
|
|
108
|
+
"""A malformed snippet line (@snippet name $p... { ... }), or one naming a parameter twice."""
|
|
109
|
+
BAD_USE = "E5011"
|
|
110
|
+
"""A use line that is not @use name value...;."""
|
|
111
|
+
EXTENSION_BLOCK_NOT_SET = "E5012"
|
|
112
|
+
"""An extension line whose block holds a line other than @set."""
|
|
113
|
+
EXPANDED_NESTING_TOO_DEEP = "E5013"
|
|
114
|
+
"""Blocks and lists nested deeper than 256 levels after expansion (2.5): a directive whose block or values, or a
|
|
115
|
+
set, use or extension line whose values, would sit deeper. The statement is unreadable there and its block is
|
|
116
|
+
not walked.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
MESSAGES: Final[Mapping[DiagnosticCode, str]] = MappingProxyType(
|
|
121
|
+
{
|
|
122
|
+
DiagnosticCode.UNTERMINATED_STRING: "unterminated string",
|
|
123
|
+
DiagnosticCode.UNEXPECTED_CHARACTER: "unexpected character",
|
|
124
|
+
DiagnosticCode.LONE_DOLLAR: "'$' must be followed by a variable name",
|
|
125
|
+
DiagnosticCode.BAD_ESCAPE: "unknown escape",
|
|
126
|
+
DiagnosticCode.UNEXPECTED_TOKEN: "unexpected token",
|
|
127
|
+
DiagnosticCode.EXPECTED_CLOSING_BRACE: "expected '}' to close the block",
|
|
128
|
+
DiagnosticCode.EXPECTED_CLOSING_BRACKET: "unmatched bracket",
|
|
129
|
+
DiagnosticCode.BAD_SLICE: "malformed slice",
|
|
130
|
+
DiagnosticCode.VALUES_TOUCH: "values must be separated by a space or comment",
|
|
131
|
+
DiagnosticCode.NESTING_TOO_DEEP: f"blocks and lists nest deeper than {MAX_NESTING} levels",
|
|
132
|
+
DiagnosticCode.INCLUDE_NOT_FOUND: "include not found",
|
|
133
|
+
DiagnosticCode.INCLUDE_RESOLVER_FAILED: "include resolver failed",
|
|
134
|
+
DiagnosticCode.INCLUDE_BAD_PATH: "include takes one quoted path",
|
|
135
|
+
DiagnosticCode.INCLUDE_MATCHED_NOTHING: "include matched nothing",
|
|
136
|
+
DiagnosticCode.EXTENSION_FAILED: "extension failed",
|
|
137
|
+
DiagnosticCode.UNREAD_COPY: "extension copy not read yet",
|
|
138
|
+
DiagnosticCode.UNKNOWN_EXTENSION: "no extension for this @name",
|
|
139
|
+
DiagnosticCode.NON_INTEGER_BOUND: "bound is not an integer",
|
|
140
|
+
DiagnosticCode.WRONG_VALUE_KIND: "wrong kind of value",
|
|
141
|
+
DiagnosticCode.NOT_A_NUMBER: "not a number",
|
|
142
|
+
DiagnosticCode.NOT_AN_INTEGER: "not an integer",
|
|
143
|
+
DiagnosticCode.NOT_A_BOOLEAN: "not true or false",
|
|
144
|
+
DiagnosticCode.BAD_SLICE_NAME: "slice name is not one word",
|
|
145
|
+
DiagnosticCode.UNKNOWN_VARIABLE: "unknown variable",
|
|
146
|
+
DiagnosticCode.VARIABLE_SET_TWICE: "variable set twice in one block",
|
|
147
|
+
DiagnosticCode.UNKNOWN_SNIPPET: "unknown snippet",
|
|
148
|
+
DiagnosticCode.SNIPPET_ARITY: "wrong number of values for the snippet",
|
|
149
|
+
DiagnosticCode.SNIPPET_DEFINED_TWICE: "snippet defined twice in one block",
|
|
150
|
+
DiagnosticCode.EXPANSION_CYCLE: "expansion reaches itself",
|
|
151
|
+
DiagnosticCode.EXPANSION_TOO_DEEP: "expansion nests too deep",
|
|
152
|
+
DiagnosticCode.TOO_MANY_DIRECTIVES: "too many directives after expansion",
|
|
153
|
+
DiagnosticCode.BAD_SET: "write @set $name value...;",
|
|
154
|
+
DiagnosticCode.BAD_SNIPPET: "write @snippet name $param... { ... }",
|
|
155
|
+
DiagnosticCode.BAD_USE: "write @use name value...;",
|
|
156
|
+
DiagnosticCode.EXTENSION_BLOCK_NOT_SET: "an extension's block holds only @set lines",
|
|
157
|
+
DiagnosticCode.EXPANDED_NESTING_TOO_DEEP: f"expanded blocks and lists nest deeper than {MAX_EXPANDED_NESTING}",
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
"""The base message of each code (for people, not part of the contract)."""
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass(frozen=True, slots=True)
|
|
164
|
+
class CodeFacts:
|
|
165
|
+
"""What the registry says of one code: its spec name, owning layer, who finds it, the reason it gives, and its
|
|
166
|
+
severity (its letter's: spec 2.3)."""
|
|
167
|
+
|
|
168
|
+
name: str
|
|
169
|
+
layer: str
|
|
170
|
+
found_by: tuple[str, ...]
|
|
171
|
+
reason: UnreadableReason | None
|
|
172
|
+
severity: Severity
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
CODE_FACTS: Final[Mapping[DiagnosticCode, CodeFacts]] = MappingProxyType(
|
|
176
|
+
{
|
|
177
|
+
DiagnosticCode.UNTERMINATED_STRING: CodeFacts(
|
|
178
|
+
"unterminated string", "syntax", ("parse",), UnreadableReason.SYNTAX, Severity.ERROR
|
|
179
|
+
),
|
|
180
|
+
DiagnosticCode.UNEXPECTED_CHARACTER: CodeFacts(
|
|
181
|
+
"unexpected character", "syntax", ("parse",), UnreadableReason.SYNTAX, Severity.ERROR
|
|
182
|
+
),
|
|
183
|
+
DiagnosticCode.LONE_DOLLAR: CodeFacts(
|
|
184
|
+
"lone dollar", "syntax", ("parse",), UnreadableReason.SYNTAX, Severity.ERROR
|
|
185
|
+
),
|
|
186
|
+
DiagnosticCode.BAD_ESCAPE: CodeFacts(
|
|
187
|
+
"bad escape", "syntax", ("parse",), UnreadableReason.SYNTAX, Severity.ERROR
|
|
188
|
+
),
|
|
189
|
+
DiagnosticCode.UNEXPECTED_TOKEN: CodeFacts(
|
|
190
|
+
"unexpected token", "syntax", ("parse",), UnreadableReason.SYNTAX, Severity.ERROR
|
|
191
|
+
),
|
|
192
|
+
DiagnosticCode.EXPECTED_CLOSING_BRACE: CodeFacts(
|
|
193
|
+
"expected closing brace", "syntax", ("parse",), UnreadableReason.SYNTAX, Severity.ERROR
|
|
194
|
+
),
|
|
195
|
+
DiagnosticCode.EXPECTED_CLOSING_BRACKET: CodeFacts(
|
|
196
|
+
"expected closing bracket", "syntax", ("parse",), UnreadableReason.SYNTAX, Severity.ERROR
|
|
197
|
+
),
|
|
198
|
+
DiagnosticCode.BAD_SLICE: CodeFacts("bad slice", "syntax", ("parse",), UnreadableReason.SYNTAX, Severity.ERROR),
|
|
199
|
+
DiagnosticCode.VALUES_TOUCH: CodeFacts(
|
|
200
|
+
"values touch", "syntax", ("parse",), UnreadableReason.SYNTAX, Severity.ERROR
|
|
201
|
+
),
|
|
202
|
+
DiagnosticCode.NESTING_TOO_DEEP: CodeFacts(
|
|
203
|
+
"nesting too deep", "syntax", ("parse", "edit"), UnreadableReason.SYNTAX, Severity.ERROR
|
|
204
|
+
),
|
|
205
|
+
DiagnosticCode.INCLUDE_NOT_FOUND: CodeFacts(
|
|
206
|
+
"include not found", "written", ("load and read",), UnreadableReason.EXTENSION, Severity.ERROR
|
|
207
|
+
),
|
|
208
|
+
DiagnosticCode.INCLUDE_RESOLVER_FAILED: CodeFacts(
|
|
209
|
+
"include resolver failed", "written", ("load and read",), UnreadableReason.EXTENSION, Severity.ERROR
|
|
210
|
+
),
|
|
211
|
+
DiagnosticCode.INCLUDE_BAD_PATH: CodeFacts(
|
|
212
|
+
"include bad path", "include", ("include extension",), UnreadableReason.EXTENSION, Severity.ERROR
|
|
213
|
+
),
|
|
214
|
+
DiagnosticCode.INCLUDE_MATCHED_NOTHING: CodeFacts(
|
|
215
|
+
"include matched nothing", "written", ("load and read",), None, Severity.WARNING
|
|
216
|
+
),
|
|
217
|
+
DiagnosticCode.EXTENSION_FAILED: CodeFacts(
|
|
218
|
+
"extension failed", "written", ("load and read",), UnreadableReason.EXTENSION, Severity.ERROR
|
|
219
|
+
),
|
|
220
|
+
DiagnosticCode.UNREAD_COPY: CodeFacts(
|
|
221
|
+
"unread copy", "written", ("low level",), UnreadableReason.UNREAD, Severity.ERROR
|
|
222
|
+
),
|
|
223
|
+
DiagnosticCode.UNKNOWN_EXTENSION: CodeFacts(
|
|
224
|
+
"unknown extension", "written", ("low level",), UnreadableReason.EXTENSION, Severity.ERROR
|
|
225
|
+
),
|
|
226
|
+
DiagnosticCode.NON_INTEGER_BOUND: CodeFacts(
|
|
227
|
+
"non-integer bound", "written", ("low level",), UnreadableReason.VALUE, Severity.ERROR
|
|
228
|
+
),
|
|
229
|
+
DiagnosticCode.WRONG_VALUE_KIND: CodeFacts("wrong value kind", "written", ("readers",), None, Severity.ERROR),
|
|
230
|
+
DiagnosticCode.NOT_A_NUMBER: CodeFacts("not a number", "written", ("readers",), None, Severity.ERROR),
|
|
231
|
+
DiagnosticCode.NOT_AN_INTEGER: CodeFacts("not an integer", "written", ("readers",), None, Severity.ERROR),
|
|
232
|
+
DiagnosticCode.NOT_A_BOOLEAN: CodeFacts("not a boolean", "written", ("readers",), None, Severity.ERROR),
|
|
233
|
+
DiagnosticCode.BAD_SLICE_NAME: CodeFacts(
|
|
234
|
+
"bad slice name", "written", ("low level",), UnreadableReason.VALUE, Severity.ERROR
|
|
235
|
+
),
|
|
236
|
+
DiagnosticCode.UNKNOWN_VARIABLE: CodeFacts(
|
|
237
|
+
"unknown variable", "written", ("low level",), UnreadableReason.EXPANSION, Severity.ERROR
|
|
238
|
+
),
|
|
239
|
+
DiagnosticCode.VARIABLE_SET_TWICE: CodeFacts(
|
|
240
|
+
"variable set twice", "written", ("low level", "edit"), UnreadableReason.EXPANSION, Severity.ERROR
|
|
241
|
+
),
|
|
242
|
+
DiagnosticCode.UNKNOWN_SNIPPET: CodeFacts(
|
|
243
|
+
"unknown snippet", "written", ("low level",), UnreadableReason.EXPANSION, Severity.ERROR
|
|
244
|
+
),
|
|
245
|
+
DiagnosticCode.SNIPPET_ARITY: CodeFacts(
|
|
246
|
+
"snippet arity", "written", ("low level",), UnreadableReason.EXPANSION, Severity.ERROR
|
|
247
|
+
),
|
|
248
|
+
DiagnosticCode.SNIPPET_DEFINED_TWICE: CodeFacts(
|
|
249
|
+
"snippet defined twice", "written", ("low level", "edit"), UnreadableReason.EXPANSION, Severity.ERROR
|
|
250
|
+
),
|
|
251
|
+
DiagnosticCode.EXPANSION_CYCLE: CodeFacts(
|
|
252
|
+
"expansion cycle", "written", ("low level",), UnreadableReason.EXPANSION, Severity.ERROR
|
|
253
|
+
),
|
|
254
|
+
DiagnosticCode.EXPANSION_TOO_DEEP: CodeFacts(
|
|
255
|
+
"expansion too deep", "written", ("low level",), UnreadableReason.EXPANSION, Severity.ERROR
|
|
256
|
+
),
|
|
257
|
+
DiagnosticCode.TOO_MANY_DIRECTIVES: CodeFacts(
|
|
258
|
+
"too many directives", "written", ("low level",), UnreadableReason.EXPANSION, Severity.ERROR
|
|
259
|
+
),
|
|
260
|
+
DiagnosticCode.BAD_SET: CodeFacts(
|
|
261
|
+
"bad set", "written", ("low level", "edit"), UnreadableReason.EXPANSION, Severity.ERROR
|
|
262
|
+
),
|
|
263
|
+
DiagnosticCode.BAD_SNIPPET: CodeFacts(
|
|
264
|
+
"bad snippet", "written", ("low level", "edit"), UnreadableReason.EXPANSION, Severity.ERROR
|
|
265
|
+
),
|
|
266
|
+
DiagnosticCode.BAD_USE: CodeFacts(
|
|
267
|
+
"bad use", "written", ("low level", "edit"), UnreadableReason.EXPANSION, Severity.ERROR
|
|
268
|
+
),
|
|
269
|
+
DiagnosticCode.EXTENSION_BLOCK_NOT_SET: CodeFacts(
|
|
270
|
+
"extension block not set", "written", ("low level",), UnreadableReason.EXTENSION, Severity.ERROR
|
|
271
|
+
),
|
|
272
|
+
DiagnosticCode.EXPANDED_NESTING_TOO_DEEP: CodeFacts(
|
|
273
|
+
"expanded nesting too deep", "written", ("low level",), UnreadableReason.EXPANSION, Severity.ERROR
|
|
274
|
+
),
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
"""Each code's facts."""
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@dataclass(frozen=True, slots=True)
|
|
281
|
+
class Described:
|
|
282
|
+
"""One diagnostic's code, message and reason, built from the registry."""
|
|
283
|
+
|
|
284
|
+
code: DiagnosticCode
|
|
285
|
+
message: str
|
|
286
|
+
reason: UnreadableReason | None
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def describe(code: DiagnosticCode, detail: str | None = None, /) -> Described:
|
|
290
|
+
"""The typed diagnostic constructor (spec/codegen.md section 4): a raise site names the code and its detail, and
|
|
291
|
+
the message and reason come from the registry, so they cannot disagree with it. ``detail`` follows ': '."""
|
|
292
|
+
base = MESSAGES[code]
|
|
293
|
+
message = base if detail is None else f"{base}: {detail}"
|
|
294
|
+
return Described(code, message, CODE_FACTS[code].reason)
|
knobly/_api/docs.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Generated by tools/gen-api.py from spec/schema/; do not edit, and never commit.
|
|
2
|
+
|
|
3
|
+
Source: spec/schema/api.json "free functions": the docs of the public functions and constructors."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
from typing import Final
|
|
10
|
+
|
|
11
|
+
FUNCTIONS: Final[Mapping[str, tuple[str, ...]]] = MappingProxyType(
|
|
12
|
+
{
|
|
13
|
+
"knobly": ("knobly._api.io.IoFunctions", "knobly._api.shared.SharedFunctions"),
|
|
14
|
+
"knobly.files": ("knobly._api.io.FilesFunctions",),
|
|
15
|
+
"knobly.include": ("knobly._api.written.IncludeFunctions",),
|
|
16
|
+
"knobly.rendered": ("knobly._api.rendered.RenderedFunctions",),
|
|
17
|
+
"knobly.syntax": ("knobly._api.syntax.SyntaxFunctions",),
|
|
18
|
+
"knobly.written": ("knobly._api.written.WrittenFunctions",),
|
|
19
|
+
}
|
|
20
|
+
)
|
|
21
|
+
"""Each public module's <Module>Functions Protocols: each of its functions takes its doc from one."""
|
|
22
|
+
|
|
23
|
+
CONSTRUCTORS: Final[Mapping[str, str]] = MappingProxyType(
|
|
24
|
+
{
|
|
25
|
+
"knobly.Diagnostics": "knobly._api.shared.DiagnosticsConstructor",
|
|
26
|
+
"knobly.written.Processor": "knobly._api.written.ProcessorConstructor",
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
"""Each constructed class's <Class>Constructor Protocol: its __init__ takes the doc of __call__."""
|
knobly/_api/errors.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Generated by tools/gen-api.py from spec/schema/; do not edit, and never commit.
|
|
2
|
+
|
|
3
|
+
Source: spec/schema/api.json "error groups"."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
|
|
12
|
+
import knobly
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class KnoblyError(Exception):
|
|
16
|
+
"""Never by itself: the base class of every expected-outcome class, so one except or catch clause catches every
|
|
17
|
+
outcome the input or the outside world decides, and no program bug.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DiagnosticError(KnoblyError):
|
|
22
|
+
"""Raised when a reader is given a value of the wrong kind or spelling, or a program's error(...); carries one
|
|
23
|
+
located diagnostic as 'diagnostic'. An expected outcome.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
diagnostic: knobly.LocatedDiagnostic
|
|
27
|
+
|
|
28
|
+
def __init__(self, diagnostic: knobly.LocatedDiagnostic) -> None:
|
|
29
|
+
super().__init__(f"{diagnostic.message}")
|
|
30
|
+
self.diagnostic = diagnostic
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ReadError(KnoblyError):
|
|
34
|
+
"""Raised when load cannot resolve the root path to exactly one file, or cannot read it. No diagnostic and no
|
|
35
|
+
code. An expected outcome.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
paths: tuple[str, ...]
|
|
39
|
+
|
|
40
|
+
def __init__(self, message: str, paths: Sequence[str]) -> None:
|
|
41
|
+
super().__init__(message)
|
|
42
|
+
self.paths = tuple(paths)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class BadTextError(KnoblyError):
|
|
46
|
+
"""Raised when the text given to an edit, or an override's text, is not what that place takes, or the document
|
|
47
|
+
after the edit would nest deeper than MAX_NESTING; 'diagnostics' are located in that text, in the binding's
|
|
48
|
+
string unit, and its message is built from them. An expected outcome.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
diagnostics: tuple[knobly.Diagnostic, ...]
|
|
52
|
+
|
|
53
|
+
def __init__(self, diagnostics: Sequence[knobly.Diagnostic]) -> None:
|
|
54
|
+
super().__init__(f"bad text: {'; '.join(str(d) for d in diagnostics)}")
|
|
55
|
+
self.diagnostics = tuple(diagnostics)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SaveError(KnoblyError):
|
|
59
|
+
"""Raised when the writer failed. An expected outcome."""
|
|
60
|
+
|
|
61
|
+
paths: tuple[str, ...]
|
|
62
|
+
|
|
63
|
+
def __init__(self, message: str, paths: Sequence[str]) -> None:
|
|
64
|
+
super().__init__(message)
|
|
65
|
+
self.paths = tuple(paths)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class LostUpdateError(SaveError):
|
|
69
|
+
"""Raised when a file the save would write changed since it was read or last saved: its current text differs from
|
|
70
|
+
its saved text. Check lost updates raises it inside every writer, and save passes it on unchanged. An expected
|
|
71
|
+
outcome. Paths are only the conflicting files, in batch order.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, paths: Sequence[str]) -> None:
|
|
75
|
+
super().__init__(f"lost update: changed since read: {', '.join(paths)}", paths)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class DetachedError(ReferenceError):
|
|
79
|
+
"""Raised when any member but kind of a detached handle is used. A program bug."""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def argument_type_message(member: str, parameter: str, got: str, /) -> str:
|
|
83
|
+
"""The argument type error's message: the one helper every member that takes text calls."""
|
|
84
|
+
return (
|
|
85
|
+
f"{member}() argument '{parameter}' must be str, not {got}; knobly edits take config text: "
|
|
86
|
+
"quote() a program string, text() one already written as knobly text, str() a number "
|
|
87
|
+
"first"
|
|
88
|
+
)
|