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.
Files changed (57) hide show
  1. knobly/__init__.py +82 -0
  2. knobly/_anatomy.py +65 -0
  3. knobly/_api/.gitignore +2 -0
  4. knobly/_api/__init__.py +1 -0
  5. knobly/_api/codes.py +294 -0
  6. knobly/_api/docs.py +29 -0
  7. knobly/_api/errors.py +88 -0
  8. knobly/_api/io.py +134 -0
  9. knobly/_api/kinds.py +171 -0
  10. knobly/_api/limits.py +37 -0
  11. knobly/_api/record.py +17 -0
  12. knobly/_api/rendered.py +276 -0
  13. knobly/_api/shared.py +349 -0
  14. knobly/_api/syntax.py +117 -0
  15. knobly/_api/written.py +2189 -0
  16. knobly/_block.py +79 -0
  17. knobly/_diagnostics.py +46 -0
  18. knobly/_docs.py +70 -0
  19. knobly/_gen/Knobly.interp +75 -0
  20. knobly/_gen/Knobly.tokens +30 -0
  21. knobly/_gen/KnoblyLexer.interp +85 -0
  22. knobly/_gen/KnoblyLexer.py +165 -0
  23. knobly/_gen/KnoblyLexer.tokens +30 -0
  24. knobly/_gen/KnoblyParser.py +1531 -0
  25. knobly/_gen/__init__.py +0 -0
  26. knobly/_glob.py +18 -0
  27. knobly/_origin.py +59 -0
  28. knobly/_report.py +104 -0
  29. knobly/_source.py +88 -0
  30. knobly/_text.py +49 -0
  31. knobly/_values.py +87 -0
  32. knobly/files.py +98 -0
  33. knobly/include.py +42 -0
  34. knobly/py.typed +0 -0
  35. knobly/rendered/__init__.py +32 -0
  36. knobly/rendered/_render.py +138 -0
  37. knobly/rendered/_tree.py +145 -0
  38. knobly/syntax/__init__.py +27 -0
  39. knobly/syntax/_cst.py +42 -0
  40. knobly/syntax/_parse.py +137 -0
  41. knobly/syntax/_recovery.py +307 -0
  42. knobly/syntax/_walk.py +138 -0
  43. knobly/written/__init__.py +126 -0
  44. knobly/written/_bind.py +985 -0
  45. knobly/written/_document.py +996 -0
  46. knobly/written/_expand.py +68 -0
  47. knobly/written/_format.py +32 -0
  48. knobly/written/_graph.py +143 -0
  49. knobly/written/_model.py +971 -0
  50. knobly/written/_names.py +29 -0
  51. knobly/written/_processor.py +39 -0
  52. knobly/written/_read.py +86 -0
  53. knobly/written/_shape.py +122 -0
  54. knobly/written/_value.py +324 -0
  55. knobly-1.1.0.dist-info/METADATA +8 -0
  56. knobly-1.1.0.dist-info/RECORD +57 -0
  57. 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
@@ -0,0 +1,2 @@
1
+ # Generated by tools/gen-api.py from spec/schema/; do not edit, and never commit.
2
+ *
@@ -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
+ )