syml 0.5__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.
syml/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """SYML (Simple YAML-like Markup Language) is a simple markup language with similar structure to YAML, but without all the gewgaws and folderol."""
2
+
3
+ from io import TextIOBase
4
+ from typing import Any
5
+
6
+ from . import parsers
7
+
8
+
9
+ def loads(document: str, **kwargs: Any) -> Any: # noqa: ANN401
10
+ """Load a SYML document from a string."""
11
+ return parsers.parse(document, **kwargs)
12
+
13
+
14
+ def load(file_obj: TextIOBase, **kwargs: Any) -> Any: # noqa: ANN401
15
+ """Load a SYML document from a file-like object."""
16
+ return loads(file_obj.read(), **kwargs)
syml/basetypes.py ADDED
@@ -0,0 +1,117 @@
1
+ """SYML base types"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ from attrs import define
10
+
11
+ from syml import utils
12
+
13
+ if TYPE_CHECKING: # pragma: nocover
14
+ from parsimonious.nodes import Node as PNode
15
+
16
+
17
+ StrPath = str | Path
18
+
19
+ StrBool = str | bool
20
+
21
+
22
+ @define(slots=True, frozen=True)
23
+ class Pos:
24
+ """A position within a source file"""
25
+
26
+ index: int
27
+ line: int
28
+ column: int
29
+
30
+ @classmethod
31
+ def from_str_index(cls, text: str, index: int) -> Pos:
32
+ """Get (line_number, col) of `index` in `string`.
33
+
34
+ Based on http://stackoverflow.com/a/24495900
35
+ """
36
+ lines = utils.split_lines(text, keepends=True)
37
+ curr_pos = 0
38
+ for linenum, line in enumerate(lines):
39
+ if curr_pos + len(line) > index:
40
+ return cls(index, linenum + 1, index - curr_pos)
41
+ curr_pos += len(line)
42
+ return cls(len(text), linenum + 1, 0)
43
+
44
+
45
+ @define(slots=True, repr=False, frozen=True)
46
+ class Source:
47
+ """A line within a source file"""
48
+
49
+ filename: str | Path
50
+ start: Pos
51
+ end: Pos
52
+ text: str
53
+ # value corresponds to a line of text
54
+ value: StrBool
55
+
56
+ @classmethod
57
+ def from_node(cls, pnode: PNode, filename: StrPath = '', value: StrBool | None = None) -> Source:
58
+ """Build a Source from the given PNode, filename, and line value."""
59
+ return cls(
60
+ filename=filename,
61
+ start=Pos.from_str_index(pnode.full_text, pnode.start),
62
+ end=Pos.from_str_index(pnode.full_text, pnode.end),
63
+ text=pnode.text,
64
+ value=value if value is not None else pnode.text,
65
+ )
66
+
67
+ @classmethod
68
+ def from_text(
69
+ cls,
70
+ text: str,
71
+ substring: str | None = None,
72
+ source_text: str | None = None,
73
+ value: bool | str | None = None, # noqa: FBT001
74
+ filename: StrPath = '',
75
+ ) -> Source:
76
+ """Build a Source from the given components."""
77
+ if substring is None:
78
+ substring = text
79
+ match = re.search(substring, text)
80
+ if match is None:
81
+ raise ValueError('No match found', substring)
82
+ source_text = match.group() if source_text is None else source_text
83
+ return Source(
84
+ filename=filename,
85
+ start=Pos.from_str_index(text, match.start()),
86
+ end=Pos.from_str_index(text, match.end()),
87
+ text=source_text,
88
+ value=value if value is not None else source_text,
89
+ )
90
+
91
+ def __repr__(self) -> str:
92
+ filename = f'{self.filename}, ' if self.filename else ''
93
+ return f'{filename}Line {self.start.line}, Column {self.start.column} (index {self.start.index}): {self.text!r} ({self.value!r})'
94
+
95
+ def __str__(self) -> str:
96
+ return self.text
97
+
98
+ def __add__(self, other: SourceStr) -> Source:
99
+ if isinstance(other, str):
100
+ lines = utils.split_lines(other, keepends=True)
101
+ return Source(
102
+ filename=self.filename,
103
+ start=self.start,
104
+ end=Pos(index=self.end.index + len(other), line=self.end.line + len(lines), column=len(lines[-1])),
105
+ text=self.text + other,
106
+ value=str(self.value) + other,
107
+ )
108
+ if isinstance(other, Source):
109
+ return self + other.text
110
+
111
+ raise TypeError('Tried to add invalid type to Source', type(other))
112
+
113
+
114
+ SourceStr = Source | str
115
+
116
+
117
+ SourceStrBool = SourceStr | bool
syml/exceptions.py ADDED
@@ -0,0 +1,9 @@
1
+ """Exceptions for parsing SYML documents"""
2
+
3
+
4
+ class ParseError(ValueError):
5
+ """An error encountered while parsing"""
6
+
7
+
8
+ class OutOfContextNodeError(ParseError):
9
+ """A node encountered in an illegal context"""
syml/grammars.py ADDED
@@ -0,0 +1,47 @@
1
+ """Parsimonious grammars for parsing SYML"""
2
+
3
+ import textwrap
4
+
5
+ syml_base_grammar = textwrap.dedent(
6
+ """
7
+ lines = line*
8
+ line = indent (comment / blank / structure / value) &eol
9
+ structure = list_item / key_value / section
10
+
11
+ indent = ~"\\s*"
12
+
13
+ blank = &eol
14
+ comment = ~"(#|//+)+" text?
15
+
16
+ list_item = "-" ws value
17
+
18
+ key_value = section ws data
19
+ section = key ":"
20
+ key = ~"[^\\s:]+"
21
+
22
+ eol = "\\n" / ~"$"
23
+ ws = ~"[ \\t]+"
24
+ text = ~".+"
25
+
26
+ value = structure / data
27
+
28
+ """
29
+ )
30
+
31
+
32
+ text_only_syml_grammar = syml_base_grammar + textwrap.dedent(
33
+ """
34
+ data = text
35
+ """
36
+ )
37
+
38
+ # If we want mare YAML-like booleans:
39
+ boolean_syml_grammar = syml_base_grammar + textwrap.dedent(
40
+ """
41
+ truthy = "true" / "True" / "TRUE"
42
+ falsey = "false" / "False" / "FALSE"
43
+ bool = truthy / falsey
44
+
45
+ data = bool / text
46
+ """
47
+ )
syml/nodes.py ADDED
@@ -0,0 +1,298 @@
1
+ """Node implementations for parsing SYML documents"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import OrderedDict
6
+ from dataclasses import dataclass, field
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ if TYPE_CHECKING: # pragma: nocover
10
+ from parsimonious.nodes import Node as PNode
11
+
12
+ from .basetypes import Pos, Source, SourceStrBool, StrBool, StrPath
13
+ from .exceptions import OutOfContextNodeError
14
+ from .utils import get_line
15
+
16
+
17
+ @dataclass(kw_only=True)
18
+ class SymlNode:
19
+ """A generic node in a SYML document."""
20
+
21
+ pnode: PNode
22
+ level: int | None = field(default=None)
23
+ parent: SymlNode | None = field(default=None)
24
+ comments: list[Comment] = field(default_factory=list)
25
+ children: list[SymlNode] | tuple[SymlNode] = field(default_factory=list)
26
+ value: Any = field(default=None)
27
+
28
+ def __post_init__(self) -> None:
29
+ if self.level is not None:
30
+ self.set_level(self.level)
31
+
32
+ def set_level(self, level: int) -> None:
33
+ """Set this node's level."""
34
+ self.level = level
35
+
36
+ def as_data(self, filename: StrPath = '', raw: bool = False) -> Any: # noqa: FBT001, FBT002, ANN401 # pragma: nocover
37
+ """Return this node as primitive data types."""
38
+ raise NotImplementedError
39
+
40
+ def get_value(self) -> Any: # noqa: ANN401 # pragma: nocover
41
+ """Return the value of this node."""
42
+ return None
43
+
44
+ def get_tip(self) -> SymlNode:
45
+ """Return the tip of this branch."""
46
+ return self
47
+
48
+ def can_add_node(self, node: SymlNode) -> bool: # noqa: ARG002 # pragma: nocover
49
+ """Check if this node can add a child node."""
50
+ return False
51
+
52
+ def add_node(self, node: SymlNode) -> SymlNode: # pragma: nocover
53
+ """Add a child node."""
54
+ raise NotImplementedError
55
+
56
+ def incorporate_node(self, node: SymlNode) -> SymlNode:
57
+ """Incorporate the given node into the tree somewhere nearby."""
58
+ if self.can_add_node(node):
59
+ return self.add_node(node)
60
+ if self.parent is not None:
61
+ return self.parent.incorporate_node(node)
62
+ else: # pragma: nocover # noqa: RET505
63
+ # Shouldn't ever get here:
64
+ self.fail_to_incorporate_node(node)
65
+ return self # pragma: nocover
66
+
67
+ def fail_to_incorporate_node(self, node: SymlNode) -> None:
68
+ """Report a failure to incorporate a node."""
69
+ pnode = node.pnode
70
+ pos = Pos.from_str_index(pnode.full_text, pnode.start)
71
+ line = get_line(pnode.full_text, pos.line)
72
+ raise OutOfContextNodeError('Failed to incorporate a node', pos, line)
73
+
74
+
75
+ SymlNodes = list[SymlNode]
76
+ OptionalSymlNodes = list[SymlNode | None]
77
+
78
+ NodeOrNodes = SymlNodes | SymlNode | StrBool
79
+
80
+ OptionalNodes = NodeOrNodes | None
81
+
82
+
83
+ @dataclass(kw_only=True)
84
+ class ContainerNode(SymlNode):
85
+ """A container node that may contain a child value."""
86
+
87
+ value: SymlNode | StrBool | None = field(default=None)
88
+
89
+ def set_level(self, level: int) -> None:
90
+ """Set the level on this node and its child value."""
91
+ self.level = level
92
+ if isinstance(self.value, SymlNode):
93
+ self.value.set_level(level)
94
+
95
+ def as_data(self, filename: StrPath = '', raw: bool = False) -> StrBool | None: # noqa: FBT001, FBT002
96
+ """Return the container as primitive data types."""
97
+ if isinstance(self.value, SymlNode):
98
+ return self.value.as_data(filename, raw=raw)
99
+ else: # pragma: nocover # noqa: RET505
100
+ # Shouldn't ever get here.
101
+ return self.value
102
+
103
+ def get_value(self) -> Any: # noqa: ANN401 # pragma: nocover
104
+ """Return the value of this node."""
105
+ return self.value
106
+
107
+ def get_tip(self) -> SymlNode:
108
+ """Get the tip of this branch."""
109
+ if self.value is not None and isinstance(self.value, SymlNode):
110
+ return self.value.get_tip()
111
+ return self
112
+
113
+ def incorporate_node(self, node: SymlNode) -> SymlNode:
114
+ """Incorporate the given node into this branch."""
115
+ if self.can_add_node(node):
116
+ intermediary: SymlNode | None = None
117
+ if isinstance(node, KeyValue):
118
+ intermediary = Mapping(pnode=node.pnode, level=self.level)
119
+ elif isinstance(node, ListItem):
120
+ intermediary = List(pnode=node.pnode, level=self.level)
121
+
122
+ if intermediary is not None:
123
+ intermediary.set_level(node.level) # type: ignore[arg-type]
124
+ intermediary = self.incorporate_node(intermediary)
125
+ return intermediary.incorporate_node(node)
126
+ return super().incorporate_node(node)
127
+ if self.parent is not None:
128
+ return self.parent.incorporate_node(node)
129
+ else: # pragma: nocover # noqa: RET505
130
+ self.fail_to_incorporate_node(node)
131
+ return self
132
+
133
+ def can_add_node(self, node: SymlNode) -> bool:
134
+ """Check if this container can add a child node."""
135
+ return self.value is None and (node.level is None or (self.level is not None and node.level > self.level))
136
+
137
+ def add_node(self, node: SymlNode) -> SymlNode:
138
+ """Add a child node to this container."""
139
+ self.value = node
140
+ node.parent = self
141
+ if node.level is None:
142
+ node.set_level(self.level) # type: ignore[arg-type]
143
+ return node.get_tip()
144
+
145
+
146
+ @dataclass(kw_only=True)
147
+ class ParentNode(SymlNode):
148
+ """A parent node that con have multiple children"""
149
+
150
+ children: list[SymlNode] = field(default_factory=list)
151
+
152
+ def __post_init__(self) -> None:
153
+ super().__post_init__()
154
+ self.children = list(self.children)
155
+
156
+ def set_level(self, level: int) -> None:
157
+ """Set the level of this node and its children."""
158
+ self.level = level
159
+ for child in self.children:
160
+ if isinstance(child, SymlNode): # pragma: nobranch
161
+ child.set_level(level)
162
+
163
+ def get_tip(self) -> SymlNode:
164
+ """Return the tip of this branch."""
165
+ if self.children and isinstance(self.children[-1], SymlNode):
166
+ return self.children[-1].get_tip()
167
+ return self
168
+
169
+ def can_add_node(self, node: SymlNode) -> bool:
170
+ """Check if a child node can be added."""
171
+ return node.level is None or (self.level is not None and node.level >= self.level)
172
+
173
+ def add_node(self, node: SymlNode) -> SymlNode:
174
+ """Add a child node."""
175
+ self.children.append(node)
176
+ node.parent = self
177
+ if node.level is None:
178
+ node.set_level(self.level) # type: ignore[arg-type]
179
+ return node.get_tip()
180
+
181
+
182
+ @dataclass(kw_only=True)
183
+ class Root(ContainerNode):
184
+ """A root container node for a SYML document"""
185
+
186
+ value: SymlNode | None = field(default=None)
187
+ level: int = field(default=0)
188
+
189
+ def can_add_node(self, node: SymlNode) -> bool:
190
+ """Check if a child node may be added."""
191
+ return self.value is None and (node.level is None or (self.level is not None and node.level >= self.level))
192
+
193
+
194
+ class Comment(ContainerNode):
195
+ """A comment node"""
196
+
197
+
198
+ class List(ParentNode):
199
+ """A list node"""
200
+
201
+ def can_add_node(self, node: SymlNode) -> bool:
202
+ """Check if a child node can be added."""
203
+ return super().can_add_node(node) and isinstance(node, ListItem)
204
+
205
+ def as_data(self, filename: StrPath = '', raw: bool = False) -> list[StrBool]: # noqa: FBT001, FBT002
206
+ """Return this node as primitive data types."""
207
+ return [c.as_data(filename, raw=raw) for c in self.children]
208
+
209
+
210
+ class ListItem(ContainerNode):
211
+ """A list item within a list."""
212
+
213
+
214
+ class Mapping(ParentNode):
215
+ """A mapping of keys to values"""
216
+
217
+ def can_add_node(self, node: SymlNode) -> bool:
218
+ """Check if a child node may be added."""
219
+ return super().can_add_node(node) and isinstance(node, KeyValue)
220
+
221
+ def as_data(self, filename: StrPath = '', raw: bool = False) -> OrderedDict[StrBool, StrBool]: # noqa: FBT001, FBT002
222
+ """Return this node as primitive data types."""
223
+ return OrderedDict([(c.key.as_data(filename, raw=raw), c.as_data(filename, raw=raw)) for c in self.children]) # type: ignore[attr-defined]
224
+
225
+
226
+ @dataclass(kw_only=True)
227
+ class KeyValue(ContainerNode):
228
+ """A key-value item within a mapping"""
229
+
230
+ key: SymlNode | StrBool | None
231
+
232
+
233
+ @dataclass(kw_only=True)
234
+ class LeafNode(SymlNode):
235
+ """The tip of a branch"""
236
+
237
+ source_text: Source
238
+ value: StrBool | None = field(default=None)
239
+
240
+ def __post_init__(self) -> None:
241
+ super().__post_init__()
242
+ if self.value is not None:
243
+ self.value = [(self.pnode, self.value)] # type: ignore[assignment]
244
+ else:
245
+ self.value = [(self.pnode, self.source_text.value)] # type: ignore[assignment]
246
+
247
+ def get_value(self) -> SourceStrBool:
248
+ """Return this node as a primitive value."""
249
+ return self.value[0][1] # type: ignore[index] # pragma: nocover
250
+
251
+ def as_data(self, filename: StrPath = '', raw: bool = False) -> SourceStrBool | None: # noqa: FBT001, FBT002
252
+ """Return this node as primitive types."""
253
+ if raw:
254
+ return self.get_value()
255
+ start_pnode = self.value[0][0] # type: ignore[index]
256
+ end_pnode = self.value[-1][0] # type: ignore[index]
257
+ start = Pos.from_str_index(start_pnode.full_text, start_pnode.start) # type: ignore[union-attr]
258
+ end = Pos.from_str_index(end_pnode.full_text, end_pnode.end) # type: ignore[union-attr]
259
+
260
+ value = self.get_value()
261
+
262
+ return Source(
263
+ filename=filename,
264
+ start=start,
265
+ end=end,
266
+ text=str(self.source_text) if isinstance(self.source_text, Source) else self.source_text,
267
+ # Values correspond to lines of text
268
+ value=value.value if isinstance(value, Source) else value,
269
+ )
270
+
271
+ def can_add_node(self, node: SymlNode) -> bool:
272
+ """Check if a child node can be added."""
273
+ return isinstance(node, LeafNode) and (
274
+ node.level is None or (self.level is not None and node.level >= self.level)
275
+ )
276
+
277
+
278
+ class TextLeafNode(LeafNode):
279
+ """A leaf node containing a text value."""
280
+
281
+ def add_node(self, node: SymlNode) -> SymlNode:
282
+ """Add a node's text value to this node's text."""
283
+ self.source_text += '\n' + str(node.get_value())
284
+ self.value.append((node.pnode, node.get_value())) # type: ignore[union-attr]
285
+ node.parent = self
286
+ return self.get_tip()
287
+
288
+ def get_value(self) -> str:
289
+ """Return this node as a primitive value."""
290
+ return '\n'.join([str(v[1]) for v in self.value]) # type: ignore[union-attr]
291
+
292
+
293
+ class RawValueLeafNode(LeafNode):
294
+ """A leaf node containing a raw value"""
295
+
296
+ def get_value(self) -> StrBool:
297
+ """Return this node as a primitive value."""
298
+ return self.value[0][1] # type: ignore[index]
syml/parsers.py ADDED
@@ -0,0 +1,134 @@
1
+ """SYML parsers"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from parsimonious import Grammar, NodeVisitor, VisitationError
8
+
9
+ from . import basetypes as types
10
+ from . import grammars, nodes
11
+ from .exceptions import OutOfContextNodeError
12
+
13
+ if TYPE_CHECKING: # pragma: nocover
14
+ from parsimonious.nodes import Node as PNode
15
+
16
+ from .basetypes import StrPath
17
+ from .nodes import OptionalNodes, OptionalSymlNodes, SymlNode, SymlNodes
18
+
19
+
20
+ class TextOnlySymlParser(NodeVisitor): # type: ignore[type-arg]
21
+ """Parser for the SYML variant that allows only text leaf nodes"""
22
+
23
+ grammar = Grammar(grammars.text_only_syml_grammar)
24
+
25
+ def reduce_children(self, children: OptionalSymlNodes) -> SymlNodes:
26
+ """Return all non-null children."""
27
+ return [c for c in children if c is not None]
28
+
29
+ def visit_blank(self, node: PNode, children: OptionalSymlNodes) -> None: # noqa: ARG002
30
+ """Visit a blank."""
31
+ return
32
+
33
+ def visit_line(self, node: PNode, children: OptionalSymlNodes) -> OptionalNodes: # noqa: ARG002
34
+ """Visit a line."""
35
+ indent, value, _ = children
36
+ if value is not None:
37
+ value.set_level(indent) # type: ignore[arg-type]
38
+ return value
39
+ return None
40
+
41
+ def generic_visit(self, node: PNode, children: OptionalSymlNodes) -> SymlNodes | SymlNode | None: # type: ignore[override] # noqa: ARG002
42
+ """Visit a generic node."""
43
+ nodes = self.reduce_children(children)
44
+ if not nodes:
45
+ return None
46
+ if len(nodes) == 1:
47
+ return nodes[0]
48
+ else: # pragma: nocover # noqa: RET505
49
+ return nodes
50
+
51
+ def get_text(self, node: PNode, children: OptionalSymlNodes) -> nodes.TextLeafNode: # noqa: ARG002
52
+ """Return a text leaf node."""
53
+ return nodes.TextLeafNode(pnode=node, source_text=types.Source.from_node(node))
54
+
55
+ def visit_comment(self, node: PNode, children: OptionalSymlNodes) -> nodes.Comment:
56
+ """Visit a comment node."""
57
+ _, text = children
58
+ return nodes.Comment(pnode=node, value=text)
59
+
60
+ visit_text = get_text
61
+ visit_key = get_text
62
+
63
+ def visit_indent(self, node: PNode, children: OptionalSymlNodes) -> int: # noqa: ARG002
64
+ """Visit an indentation token."""
65
+ return len(node.text.replace('\t', ' ' * 4).strip('\n'))
66
+
67
+ def visit_key_value(self, node: PNode, children: SymlNodes) -> OptionalNodes: # noqa: ARG002
68
+ """Visit a mapping value."""
69
+ section, _, value = children
70
+ section.incorporate_node(value)
71
+ return section
72
+
73
+ def visit_section(self, node: PNode, children: OptionalSymlNodes) -> nodes.KeyValue:
74
+ """Visit a key/value section."""
75
+ key, _ = children
76
+ return nodes.KeyValue(pnode=node, key=key)
77
+
78
+ def visit_list_item(self, node: PNode, children: OptionalSymlNodes) -> nodes.ListItem:
79
+ """Visit a list item."""
80
+ _, _, value = children
81
+ li = nodes.ListItem(pnode=node)
82
+ if value is not None: # pragma: nobranch
83
+ li.incorporate_node(value)
84
+ return li
85
+
86
+ def visit_lines(self, node: PNode, children: OptionalSymlNodes) -> nodes.Root:
87
+ """Visit the lines within a SYML document."""
88
+ root = nodes.Root(pnode=node)
89
+ current: SymlNode = root
90
+
91
+ for child in self.reduce_children(children):
92
+ if isinstance(child, nodes.Comment):
93
+ current.comments.append(child)
94
+ else:
95
+ current = current.incorporate_node(child)
96
+ return root
97
+
98
+ def parse(self, *args: Any, **kwargs: Any) -> SymlNode: # noqa: ANN401
99
+ """Parse a SYML document."""
100
+ try:
101
+ return super().parse(*args, **kwargs)
102
+ except VisitationError as exc:
103
+ # Parsimonious swallows errors inside of `visit_` handlers and
104
+ # wraps them in VisitationError cruft.
105
+ if exc.args[0].startswith('OutOfContextNodeError'):
106
+ # Extract the original error message, ignoring the cruft.
107
+ msg = exc.args[0].split('\n\n\n')[0].split(':', 1)[1]
108
+ raise OutOfContextNodeError(msg) from exc
109
+ raise # pragma: no cover
110
+
111
+
112
+ class BooleanSymlParser(TextOnlySymlParser):
113
+ """Syml with support for YAML-like boolean values."""
114
+
115
+ grammar = Grammar(grammars.boolean_syml_grammar)
116
+
117
+ def visit_truthy(self, node: PNode, children: OptionalSymlNodes) -> nodes.RawValueLeafNode: # noqa: ARG002
118
+ """Visit a truthy value."""
119
+ return nodes.RawValueLeafNode(pnode=node, source_text=types.Source.from_node(node), value=True)
120
+
121
+ def visit_falsey(self, node: PNode, children: OptionalSymlNodes) -> nodes.RawValueLeafNode: # noqa: ARG002
122
+ """Visit a falsey value."""
123
+ return nodes.RawValueLeafNode(pnode=node, source_text=types.Source.from_node(node), value=False)
124
+
125
+
126
+ def parse(
127
+ source_syml: str,
128
+ filename: StrPath = '',
129
+ raw: bool = True, # noqa: FBT001, FBT002
130
+ booleans: bool = False, # noqa: FBT001, FBT002
131
+ ) -> list[Any] | dict[str, Any] | str | bool:
132
+ """Parse a SYML document."""
133
+ parser = BooleanSymlParser if booleans else TextOnlySymlParser
134
+ return parser().parse(source_syml).as_data(filename, raw=raw)
syml/utils.py ADDED
@@ -0,0 +1,17 @@
1
+ """Utility functions for SYML"""
2
+
3
+ from funcy import memoize
4
+
5
+
6
+ @memoize
7
+ def split_lines(text: str, keepends: bool = False) -> list[str]: # noqa: FBT001, FBT002
8
+ """Memoized version of str.splitlines()"""
9
+ return text.splitlines(keepends=keepends)
10
+
11
+
12
+ def get_line(text: str, line_number: int) -> str:
13
+ """Return the contents of the specified line number from the given text."""
14
+ try:
15
+ return split_lines(text, keepends=True)[line_number - 1]
16
+ except IndexError:
17
+ return ''
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2016 David Eyk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.1
2
+ Name: syml
3
+ Version: 0.5
4
+ Summary: SYML (Simple YAML-like Markup Language) is a simple markup language with similar structure to YAML, but without all the gewgaws and folderol.
5
+ Home-page: https://github.com/eykd/syml/
6
+ Author: David Eyk
7
+ Author-email: david@worldsenoughstudios.com
8
+ Requires-Python: >=3.12,<4.0
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Requires-Dist: attrs (>=24.3.0,<25.0.0)
12
+ Requires-Dist: funcy (>=2.0,<3.0)
13
+ Requires-Dist: parsimonious (>=0.10.0,<0.11.0)
14
+ Requires-Dist: regex (>=2024.11.6,<2025.0.0)
15
+ Project-URL: Repository, https://github.com/eykd/syml
16
+ Description-Content-Type: text/x-rst
17
+
18
+ ----
19
+ SYML
20
+ ----
21
+
22
+ SYML (Simple YAML-like Markup Language) is a simple markup language with
23
+ similar structure to YAML, but without all the gewgaws and folderol.
24
+
25
+ .. image:: https://travis-ci.org/eykd/syml.svg?branch=master
26
+ :target: https://travis-ci.org/eykd/syml
27
+
28
+ .. image:: https://coveralls.io/repos/github/eykd/syml/badge.svg?branch=master
29
+ :target: https://coveralls.io/github/eykd/syml?branch=master
30
+
31
+
32
+ Example
33
+ =======
34
+
35
+ Here's a simple SYML document::
36
+
37
+ >>> document = """
38
+ foo:
39
+ - bar
40
+ - baz
41
+ - blah
42
+ boo
43
+ baloon
44
+
45
+ booleans?:
46
+ - True
47
+ - False
48
+ - true
49
+ - false
50
+ - TRUE
51
+ - FALSE
52
+ """
53
+
54
+ And the resulting data structure::
55
+
56
+ >>> import syml
57
+ >>> syml.loads(document)
58
+ OrderedDict([('foo', ['bar', 'baz', 'blah\nboo']),
59
+ ('booleans?', ['True', 'False', 'true', 'false', 'TRUE', 'FALSE'])])
60
+
61
+
62
+ All values in SYML are just plain ol' text. But let's face it, sometimes you
63
+ really do want YAML-like booleans::
64
+
65
+ >>> import syml
66
+ >>> syml.loads(document, booleans=True)
67
+ OrderedDict([('foo', ['bar', 'baz', 'blah\nboo']),
68
+ ('booleans?', [True, False, True, False, True, False])])
69
+
@@ -0,0 +1,11 @@
1
+ syml/__init__.py,sha256=xsy4SRFFMCPxHuwnPEI34sQCWrz7va_d1orDbhaFLQA,550
2
+ syml/basetypes.py,sha256=UxTnG2yrv3srtjvsqK6KFV3uZOQFJFwYoGK3rnbT1ZY,3478
3
+ syml/exceptions.py,sha256=kpuBpSUezugFwC30C3Sei4E_c-ZUa-C8uG4fBhvnwT8,215
4
+ syml/grammars.py,sha256=A-rcGlwQNz3--gOLSXeNOrv_fK2nt7L2IjP86sbh-2c,968
5
+ syml/nodes.py,sha256=sWwZGWRg0U-ekaeAnqSoMYST0th51WK_SHjAccRSJDc,10850
6
+ syml/parsers.py,sha256=yq9vYFeqyrT_-F4oBiIcb091vd5rCDZjagvTn2KiN_4,5256
7
+ syml/utils.py,sha256=A8WnE0mNTW6sBX9v42YOUaGiFLYb91o6QOONiMPO1RU,500
8
+ syml-0.5.dist-info/LICENSE,sha256=XwO9yoKd4N5dwRGc8d2P54n_NS3gPXCtzewkBAjXy18,1066
9
+ syml-0.5.dist-info/METADATA,sha256=fzugjRXOqz1PE8V3f5N2lLabnGUvZJ8mAHZnb5FZ7Uw,1840
10
+ syml-0.5.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
11
+ syml-0.5.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any