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 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
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .lint import main
4
+
5
+ sys.exit(main())
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)