nette-neon 1.0.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.
- neon/__init__.py +83 -0
- neon/__main__.py +5 -0
- neon/_journal.py +40 -0
- neon/builder.py +99 -0
- neon/dumper.py +86 -0
- neon/editing.py +1547 -0
- neon/encoder.py +314 -0
- neon/errors.py +15 -0
- neon/facade.py +23 -0
- neon/layout.py +126 -0
- neon/lexer.py +85 -0
- neon/lint.py +196 -0
- neon/nodes.py +856 -0
- neon/parser.py +479 -0
- neon/position.py +38 -0
- neon/printer.py +48 -0
- neon/py.typed +0 -0
- neon/scanner.py +210 -0
- neon/token.py +390 -0
- neon/traverser.py +96 -0
- neon/values.py +363 -0
- nette_neon-1.0.0.dist-info/METADATA +249 -0
- nette_neon-1.0.0.dist-info/RECORD +26 -0
- nette_neon-1.0.0.dist-info/WHEEL +4 -0
- nette_neon-1.0.0.dist-info/entry_points.txt +2 -0
- nette_neon-1.0.0.dist-info/licenses/license.md +21 -0
neon/__init__.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""NEON parser, encoder and lossless editor."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from .builder import Builder, Place
|
|
6
|
+
from .dumper import dump
|
|
7
|
+
from .encoder import BlockFilter, encode
|
|
8
|
+
from .errors import NeonError
|
|
9
|
+
from .facade import decode, decode_file, parse_file
|
|
10
|
+
from .layout import Layout
|
|
11
|
+
from .lexer import decode_utf8, tokenize
|
|
12
|
+
from .nodes import (
|
|
13
|
+
ArrayItemNode,
|
|
14
|
+
ArrayNode,
|
|
15
|
+
BlockArrayNode,
|
|
16
|
+
CommentPolicy,
|
|
17
|
+
DocumentNode,
|
|
18
|
+
EntityChainNode,
|
|
19
|
+
EntityNode,
|
|
20
|
+
InlineArrayNode,
|
|
21
|
+
ItemIndex,
|
|
22
|
+
LiteralNode,
|
|
23
|
+
Node,
|
|
24
|
+
PathSegment,
|
|
25
|
+
StringNode,
|
|
26
|
+
ValueNode,
|
|
27
|
+
)
|
|
28
|
+
from .parser import parse
|
|
29
|
+
from .position import Position
|
|
30
|
+
from .printer import locate_all, print_node, print_text
|
|
31
|
+
from .scanner import TokenKind, TriviaKind
|
|
32
|
+
from .token import Token, Trivia
|
|
33
|
+
from .traverser import TraverseAction, Traverser, Visitor, VisitResult
|
|
34
|
+
from .values import CHAIN, MAX_DEPTH, Date, Entity, Value
|
|
35
|
+
|
|
36
|
+
# arrays nest 500 levels deep, and walking such a tree takes more frames than the default
|
|
37
|
+
sys.setrecursionlimit(max(sys.getrecursionlimit(), MAX_DEPTH * 20))
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"CHAIN",
|
|
41
|
+
"ArrayItemNode",
|
|
42
|
+
"ArrayNode",
|
|
43
|
+
"BlockArrayNode",
|
|
44
|
+
"BlockFilter",
|
|
45
|
+
"Builder",
|
|
46
|
+
"CommentPolicy",
|
|
47
|
+
"Date",
|
|
48
|
+
"DocumentNode",
|
|
49
|
+
"Entity",
|
|
50
|
+
"EntityChainNode",
|
|
51
|
+
"EntityNode",
|
|
52
|
+
"InlineArrayNode",
|
|
53
|
+
"ItemIndex",
|
|
54
|
+
"Layout",
|
|
55
|
+
"LiteralNode",
|
|
56
|
+
"NeonError",
|
|
57
|
+
"Node",
|
|
58
|
+
"PathSegment",
|
|
59
|
+
"Place",
|
|
60
|
+
"Position",
|
|
61
|
+
"StringNode",
|
|
62
|
+
"Token",
|
|
63
|
+
"TokenKind",
|
|
64
|
+
"TraverseAction",
|
|
65
|
+
"Traverser",
|
|
66
|
+
"Trivia",
|
|
67
|
+
"TriviaKind",
|
|
68
|
+
"Value",
|
|
69
|
+
"ValueNode",
|
|
70
|
+
"VisitResult",
|
|
71
|
+
"Visitor",
|
|
72
|
+
"decode",
|
|
73
|
+
"decode_file",
|
|
74
|
+
"decode_utf8",
|
|
75
|
+
"dump",
|
|
76
|
+
"encode",
|
|
77
|
+
"locate_all",
|
|
78
|
+
"parse",
|
|
79
|
+
"parse_file",
|
|
80
|
+
"print_node",
|
|
81
|
+
"print_text",
|
|
82
|
+
"tokenize",
|
|
83
|
+
]
|
neon/__main__.py
ADDED
neon/_journal.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from contextvars import ContextVar, Token
|
|
3
|
+
from types import TracebackType
|
|
4
|
+
|
|
5
|
+
type _Entries = list[Callable[[], None]]
|
|
6
|
+
|
|
7
|
+
# per thread and per asyncio task, so that an edit never records into a journal of another
|
|
8
|
+
_journal: ContextVar[_Entries | None] = ContextVar("journal", default=None)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def record(undo: Callable[[], None]) -> None:
|
|
12
|
+
"""Notes how to undo a write of the tree, while an atomic() operation runs."""
|
|
13
|
+
entries = _journal.get()
|
|
14
|
+
if entries is not None:
|
|
15
|
+
entries.append(undo)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class atomic:
|
|
19
|
+
"""Runs an operation; if it raises, every write it made is undone."""
|
|
20
|
+
|
|
21
|
+
__slots__ = ("_token",)
|
|
22
|
+
|
|
23
|
+
def __enter__(self) -> None:
|
|
24
|
+
self._token: Token[_Entries | None] | None = None
|
|
25
|
+
if _journal.get() is None:
|
|
26
|
+
self._token = _journal.set([])
|
|
27
|
+
|
|
28
|
+
def __exit__(
|
|
29
|
+
self,
|
|
30
|
+
kind: type[BaseException] | None,
|
|
31
|
+
error: BaseException | None,
|
|
32
|
+
traceback: TracebackType | None,
|
|
33
|
+
) -> None:
|
|
34
|
+
if self._token is None:
|
|
35
|
+
return
|
|
36
|
+
entries = _journal.get()
|
|
37
|
+
_journal.reset(self._token)
|
|
38
|
+
if error is not None and entries:
|
|
39
|
+
for undo in reversed(entries):
|
|
40
|
+
undo()
|
neon/builder.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from .encoder import Encoder, encode
|
|
7
|
+
from .errors import NeonError
|
|
8
|
+
from .nodes import ArrayItemNode, ArrayNode, ValueNode
|
|
9
|
+
from .parser import parse
|
|
10
|
+
from .values import is_integer_key
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class Place:
|
|
15
|
+
"""Where a built node will stand."""
|
|
16
|
+
|
|
17
|
+
block: bool
|
|
18
|
+
"""Write arrays as blocks."""
|
|
19
|
+
indent: str
|
|
20
|
+
"""Indentation of the line of the item that will hold the value."""
|
|
21
|
+
unit: str
|
|
22
|
+
"""Indentation unit of nested blocks."""
|
|
23
|
+
eol: str
|
|
24
|
+
"""Line ending of the document."""
|
|
25
|
+
double_quoted: bool = False
|
|
26
|
+
"""Write multiline strings in \"\"\", because ''' would close an earlier string."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Builder:
|
|
30
|
+
"""
|
|
31
|
+
Makes nodes from values of the host. The text is what encode() writes, so new
|
|
32
|
+
values look the same as encoded ones; it is parsed, so the nodes are valid by
|
|
33
|
+
construction.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def value(value: object, place: Place) -> ValueNode:
|
|
38
|
+
"""A value as a node, without edge trivia; a non-empty array is a block if allowed."""
|
|
39
|
+
block = place.block and _has_items(value)
|
|
40
|
+
text = Encoder(block, place.unit, place.double_quoted).encode(value)
|
|
41
|
+
if block:
|
|
42
|
+
text = _indent(text.rstrip("\n"), place.indent + place.unit, first=True)
|
|
43
|
+
else:
|
|
44
|
+
text = _indent(text, place.indent, first=False)
|
|
45
|
+
node = Builder.parse(text.replace("\n", place.eol))
|
|
46
|
+
if not block:
|
|
47
|
+
node.get_first_token()._set_leading(())
|
|
48
|
+
node.get_last_token()._set_trailing(())
|
|
49
|
+
return node
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def block_item(key: str | None, value: object, place: Place) -> ArrayItemNode:
|
|
53
|
+
"""An item of a block, `key: value` or `- value`, with its indentation and line ending."""
|
|
54
|
+
head = "-" if key is None else Builder.key_text(key) + ":"
|
|
55
|
+
if value is None and key is not None:
|
|
56
|
+
text = head
|
|
57
|
+
elif _has_items(value):
|
|
58
|
+
body = Encoder(True, place.unit, place.double_quoted).encode(value)
|
|
59
|
+
text = head + "\n" + _indent(body.rstrip("\n"), place.unit, first=True)
|
|
60
|
+
else:
|
|
61
|
+
encoded = Encoder(False, place.unit, place.double_quoted).encode(value)
|
|
62
|
+
text = head + " " + encoded
|
|
63
|
+
text = _indent(text, place.indent, first=True).replace("\n", place.eol)
|
|
64
|
+
block = Builder.parse(text + place.eol)
|
|
65
|
+
assert isinstance(block, ArrayNode)
|
|
66
|
+
item = block.items[0]
|
|
67
|
+
block._set_items(())
|
|
68
|
+
return item
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def parse(text: str) -> ValueNode:
|
|
72
|
+
"""The value of a document made of the text, detached, its tokens without an origin."""
|
|
73
|
+
document = parse(text)
|
|
74
|
+
node = document.value
|
|
75
|
+
assert node
|
|
76
|
+
document._set_value(None)
|
|
77
|
+
for token in node.get_tokens():
|
|
78
|
+
token._forget_origin()
|
|
79
|
+
return node
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def key_text(key: str) -> str:
|
|
83
|
+
"""The text of a key: an integer key as digits, anything else as a string."""
|
|
84
|
+
if "\n" in key:
|
|
85
|
+
raise NeonError("A key cannot contain a line break.")
|
|
86
|
+
return key if is_integer_key(key) else encode(key)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _has_items(value: object) -> bool:
|
|
90
|
+
"""A non-empty list, tuple or mapping."""
|
|
91
|
+
return isinstance(value, list | tuple | Mapping) and len(value) > 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _indent(text: str, prefix: str, *, first: bool) -> str:
|
|
95
|
+
"""Prefixes non-empty lines; the first one only if asked."""
|
|
96
|
+
return "\n".join(
|
|
97
|
+
prefix + line if line and (i or first) else line
|
|
98
|
+
for i, line in enumerate(text.split("\n"))
|
|
99
|
+
)
|
neon/dumper.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
|
|
5
|
+
from .nodes import Node
|
|
6
|
+
from .token import Token, Trivia
|
|
7
|
+
|
|
8
|
+
_SLOTS = {
|
|
9
|
+
"DocumentNode": (("value", "value"), ("endOfFile", "end_of_file")),
|
|
10
|
+
"BlockArrayNode": (("items", "items"),),
|
|
11
|
+
"InlineArrayNode": (
|
|
12
|
+
("openBracket", "open_bracket"),
|
|
13
|
+
("items", "items"),
|
|
14
|
+
("closeBracket", "close_bracket"),
|
|
15
|
+
),
|
|
16
|
+
"ArrayItemNode": (
|
|
17
|
+
("bullet", "bullet"),
|
|
18
|
+
("key", "key"),
|
|
19
|
+
("colon", "colon"),
|
|
20
|
+
("value", "value"),
|
|
21
|
+
("separator", "separator"),
|
|
22
|
+
),
|
|
23
|
+
"EntityNode": (("value", "value"), ("attributes", "attributes")),
|
|
24
|
+
"EntityChainNode": (("entities", "entities"),),
|
|
25
|
+
"StringNode": (("token", "token"),),
|
|
26
|
+
"LiteralNode": (("token", "token"),),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_SHORT_ESCAPES = {"\b": "\\b", "\f": "\\f", "\n": "\\n", "\r": "\\r", "\t": "\\t"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def dump(node: Node) -> str:
|
|
33
|
+
"""Text form of the tree, as in the conformance corpus."""
|
|
34
|
+
lines: list[str] = []
|
|
35
|
+
|
|
36
|
+
def write(node: Node, prefix: str, depth: int) -> None:
|
|
37
|
+
indent = "\t" * depth
|
|
38
|
+
name = type(node).__name__
|
|
39
|
+
lines.append(indent + prefix + name)
|
|
40
|
+
for slot, attribute in _SLOTS[name]:
|
|
41
|
+
value = getattr(node, attribute)
|
|
42
|
+
if value is None:
|
|
43
|
+
continue
|
|
44
|
+
if isinstance(value, tuple):
|
|
45
|
+
if not value:
|
|
46
|
+
lines.append(f"{indent}\t{slot}: []")
|
|
47
|
+
continue
|
|
48
|
+
lines.append(f"{indent}\t{slot}:")
|
|
49
|
+
for item in value:
|
|
50
|
+
write(item, "- ", depth + 2)
|
|
51
|
+
elif isinstance(value, Node):
|
|
52
|
+
write(value, slot + ": ", depth + 1)
|
|
53
|
+
else:
|
|
54
|
+
lines.append(f"{indent}\t{slot}: {dump_token(value)}")
|
|
55
|
+
|
|
56
|
+
write(node, "", 0)
|
|
57
|
+
return "\n".join(lines) + "\n"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def dump_token(token: Token) -> str:
|
|
61
|
+
res = f"{token.kind} {_dump_text(token.text)}"
|
|
62
|
+
if token.leading_trivia:
|
|
63
|
+
res += " <" + _dump_trivia(token.leading_trivia)
|
|
64
|
+
if token.trailing_trivia:
|
|
65
|
+
res += " >" + _dump_trivia(token.trailing_trivia)
|
|
66
|
+
return res
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _dump_trivia(trivia: Sequence[Trivia]) -> str:
|
|
70
|
+
return "[" + ", ".join(f"{t.kind} {_dump_text(t.text)}" for t in trivia) + "]"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _dump_text(text: str) -> str:
|
|
74
|
+
"""JSON string escaping only quotes, backslashes and control characters."""
|
|
75
|
+
res = ['"']
|
|
76
|
+
for c in text:
|
|
77
|
+
if c in _SHORT_ESCAPES:
|
|
78
|
+
res.append(_SHORT_ESCAPES[c])
|
|
79
|
+
elif c in '"\\':
|
|
80
|
+
res.append("\\" + c)
|
|
81
|
+
elif c < " ":
|
|
82
|
+
res.append(f"\\u{ord(c):04x}")
|
|
83
|
+
else:
|
|
84
|
+
res.append(c)
|
|
85
|
+
res.append('"')
|
|
86
|
+
return "".join(res)
|