dtab 0.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.
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: dtab
3
+ Version: 0.1.0
4
+ Summary: Config files made of tab-separated paths. One line is one path into a tree.
5
+ Project-URL: Homepage, https://github.com/RyannDaGreat/dtab
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: fire
9
+
10
+ <p align="center"><img src="https://raw.githubusercontent.com/RyannDaGreat/dtab/main/assets/logo.jpg" alt="dtab" width="640"></p>
11
+
12
+ # dtab
13
+
14
+ Config files made of tab-separated paths. One line is one path into a tree.
15
+
16
+ ```
17
+ objects l1,l2 light
18
+ deltas l1 position x 1 y .5
19
+ z -2
20
+ ```
21
+
22
+ ```python
23
+ import dtab
24
+ dtab.parse(open("scene.dtab").read())
25
+ # {'objects': {'l1': 'light', 'l2': 'light'},
26
+ # 'deltas': {'l1': {'position': {'x': '1', 'y': '.5', 'z': '-2'}}}}
27
+ ```
28
+
29
+ ## Why
30
+
31
+ - Less to look at. No braces, quotes, or commas between values. A file with its tabs aligned reads like pseudocode, and is easy to write by hand, even on paper.
32
+ - Simple. Six rules, one pass, about 70 lines per implementation.
33
+ - Everything is addressable. There are no lists, so every value has a dotted path: `config.deltas.l1.position.x` works with EasyDict in Python and plain property access in JavaScript.
34
+ - You choose the shape. Lines stack, and `c,d` writes one value under several keys, so the same tree can be written wide, deep, or on one line, trading horizontal space for vertical. These are the same file:
35
+
36
+ ```
37
+ a b c x
38
+ a b d x
39
+ ```
40
+ ```
41
+ a
42
+ b
43
+ c x
44
+ d x
45
+ ```
46
+ ```
47
+ a b c,d x
48
+ ```
49
+
50
+ ## Rules
51
+
52
+ - Tabs separate the steps of a path. `deltas l1 position` walks three keys down. Several tabs in a row count as one, so you can align columns.
53
+ - An entry with a space is `key value`. It sets the key and stays at the same level, so `x 1 y .5` sets two keys.
54
+ - An indented line continues the path of the line above it.
55
+ - Writing a key again replaces it. Writing into an object merges.
56
+ - `a,b` writes the same value under `a` and under `b`.
57
+ - An entry that starts with a space is a comment.
58
+
59
+ Every value is a string. Cast the ones you need. Keys are identifiers (letters, digits, underscores),
60
+ so attribute access like `config.deltas.l1` works with EasyDict and friends.
61
+
62
+ ## Install
63
+
64
+ | | |
65
+ |---|---|
66
+ | Python | `pip install dtab` then `import dtab` |
67
+ | JavaScript | `npm install dtab` then `const dtab = require('dtab')`, or `<script src="https://cdn.jsdelivr.net/npm/dtab/dtab.js">` for `window.dtab` |
68
+ | Vim | `Plugin 'RyannDaGreat/dtab'` (Vundle) or `Plug 'RyannDaGreat/dtab'` (vim-plug). Or paste `dtab.vim` into your vimrc. Highlights `*.dtab` and flags bad keys and trailing tabs. |
69
+
70
+ ## API
71
+
72
+ - `parse(text)` returns nested dicts (Python) or plain objects (JavaScript). Raises on an invalid key, with the line number.
73
+ - `stringify(tree)` writes the tree back out, one key per line.
74
+ - Command line: `dtab scene.dtab` prints the tree as JSON.
@@ -0,0 +1,6 @@
1
+ dtab.py,sha256=Twk8bFyv1dQXqQiCAbKLOlni5bU0jELKgRO1Tx0Ax6g,6278
2
+ dtab-0.1.0.dist-info/METADATA,sha256=0pXDxY8Pp1UgjXpPipU9yzXnTI2ME5oz8C1fxuTc04U,2811
3
+ dtab-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
4
+ dtab-0.1.0.dist-info/entry_points.txt,sha256=XGniY3jO22lT5c9_2F4yH4RTXY9K7MMHkrYOfSyB4bs,36
5
+ dtab-0.1.0.dist-info/top_level.txt,sha256=T3Nwp7ZP0siDtxMJ8DeNxVkT8NS5CA9iGJQXPTuXkaA,5
6
+ dtab-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dtab = dtab:_main
@@ -0,0 +1 @@
1
+ dtab
dtab.py ADDED
@@ -0,0 +1,163 @@
1
+ """
2
+ dtab: config files made of tab-separated paths. One line is one path into a tree.
3
+
4
+ objects l1,l2 light -> {"objects": {"l1": "light", "l2": "light"}}
5
+ deltas l1 position x 1 y .5 -> {"deltas": {"l1": {"position": {"x": "1", "y": ".5"}}}}
6
+ z -2 -> continues the path of the line above
7
+ this entry starts with a space, so it is a comment
8
+
9
+ Rules:
10
+ - Tabs indent, and separate the steps of a path (several in a row count as one, for alignment).
11
+ An entry without a space is a key to step into.
12
+ - An entry with a space is `key value`, split at the first space. It sets the key and stays put.
13
+ - An indented line continues the path of the line above it.
14
+ - Writing a key again replaces it; writing into an object merges. Last line wins.
15
+ - `a,b` writes the same value under a and under b.
16
+ - An entry starting with a space is a comment. A trailing tab is an empty key that swallows the lines under it.
17
+ - Keys are identifiers (str.isidentifier), so trees are EasyDict-friendly. Every value is a string.
18
+
19
+ Single pass, one stack, O(total characters).
20
+ """
21
+
22
+ import json
23
+ import re
24
+
25
+ __version__ = "0.1.0" # SEMANTIC BINDING: dtab-version (also package.json "version")
26
+
27
+ KEY_SEPARATOR = "," # a,b writes the same value under each key
28
+ KEY_RULE = "keys must be identifiers (letters, digits, underscores, not starting with a digit)"
29
+ _TAB_RUN = re.compile(r"\t+") # Several tabs in a row are one separator, so columns can be aligned
30
+
31
+
32
+ def parse(text):
33
+ """
34
+ Pure function. Parses dtab text into nested dicts of strings. Raises ValueError, with the line
35
+ number, on a key that breaks KEY_RULE.
36
+
37
+ Args:
38
+ text (str): dtab source. Whitespace-only lines are ignored.
39
+
40
+ Returns:
41
+ dict
42
+
43
+ Examples:
44
+ >>> parse('objects\\tl1,l2 light\\ndeltas\\tl1\\tposition\\tx 1\\ty .5\\n\\tz -2')
45
+ {'objects': {'l1': 'light', 'l2': 'light'}, 'deltas': {'l1': {'position': {'x': '1', 'y': '.5', 'z': '-2'}}}}
46
+ >>> parse('a\\tb 1\\n\\t comment\\na\\tb 2')
47
+ {'a': {'b': '2'}}
48
+ >>> parse('a\\tb 1\\nc.d\\te 2')
49
+ Traceback (most recent call last):
50
+ ValueError: dtab line 2: invalid key 'c.d': keys must be identifiers (letters, digits, underscores, not starting with a digit)
51
+ """
52
+ root = {}
53
+ stack = [(-1, [root])] # (indent, nodes that deeper lines nest into)
54
+ for line_number, line in enumerate(text.split("\n"), 1):
55
+ if not line.strip():
56
+ continue
57
+ indent = len(line) - len(line.lstrip("\t"))
58
+ while stack[-1][0] >= indent:
59
+ stack.pop()
60
+ nodes = stack[-1][1]
61
+ for entry in _TAB_RUN.split(line[indent:]):
62
+ key, space, value = entry.partition(" ")
63
+ if not key:
64
+ if not space:
65
+ nodes = [{}] # Empty key (trailing tab): everything under it is discarded
66
+ continue
67
+ names = _key_names(key, line_number, allow_commas=True)
68
+ if space:
69
+ for node in nodes:
70
+ for name in names:
71
+ node[name] = value
72
+ else:
73
+ nodes = [_child(node, name) for node in nodes for name in names]
74
+ stack.append((indent, nodes))
75
+ return root
76
+
77
+
78
+ def stringify(tree):
79
+ """
80
+ Pure function. Writes nested dicts as dtab, one key per line, tab-indented. Leaves are written
81
+ with str(). Raises ValueError on a key that breaks KEY_RULE or a leaf containing a tab or newline,
82
+ which dtab cannot represent. parse(stringify(tree)) == tree when every leaf is a str.
83
+
84
+ Args:
85
+ tree (dict): Nested dicts
86
+
87
+ Returns:
88
+ str
89
+
90
+ Examples:
91
+ >>> stringify({'objects': {'l1': 'light'}, 'deltas': {'l1': {'x': 1, 'name': 'a b'}}}).split('\\n')
92
+ ['objects', '\\tl1 light', 'deltas', '\\tl1', '\\t\\tx 1', '\\t\\tname a b']
93
+ """
94
+ lines = []
95
+ _stringify_into(tree, 0, lines)
96
+ return "\n".join(lines)
97
+
98
+
99
+ def _key_names(key, line_number, allow_commas):
100
+ """
101
+ Pure function (raises ValueError). The identifiers a key stands for: `a,b` is two while parsing,
102
+ and a stringify key must be a single bare identifier.
103
+
104
+ Examples:
105
+ >>> _key_names('l1,l2', 1, True), _key_names('table_bottom', None, False)
106
+ (['l1', 'l2'], ['table_bottom'])
107
+ >>> _key_names('a,b', None, False)
108
+ Traceback (most recent call last):
109
+ ValueError: dtab: invalid key 'a,b': keys must be identifiers (letters, digits, underscores, not starting with a digit)
110
+ """
111
+ names = key.split(KEY_SEPARATOR) if allow_commas else [key]
112
+ for name in names:
113
+ if not name.isidentifier():
114
+ where = " line %d" % line_number if line_number else ""
115
+ raise ValueError("dtab%s: invalid key %r: %s" % (where, key, KEY_RULE))
116
+ return names
117
+
118
+
119
+ def _child(node, name):
120
+ """
121
+ Command (may mutate node). node[name] as a dict to step into, replacing a string value if there is one.
122
+
123
+ Examples:
124
+ >>> n = {'a': 'leaf'}; _child(n, 'a')['x'] = '1'; _child(n, 'b') is n['b']; n
125
+ True
126
+ {'a': {'x': '1'}, 'b': {}}
127
+ """
128
+ child = node.get(name)
129
+ if not isinstance(child, dict):
130
+ child = node[name] = {}
131
+ return child
132
+
133
+
134
+ def _stringify_into(node, depth, lines):
135
+ """Command (appends to lines). One dtab line per key of node, indented by depth tabs."""
136
+ for key, value in node.items():
137
+ [key] = _key_names(str(key), None, allow_commas=False)
138
+ indent = "\t" * depth
139
+ if isinstance(value, dict):
140
+ lines.append(indent + key)
141
+ _stringify_into(value, depth + 1, lines)
142
+ else:
143
+ value = str(value)
144
+ if "\t" in value or "\n" in value:
145
+ raise ValueError("dtab: value of %r contains a tab or newline, which dtab cannot represent: %r" % (key, value))
146
+ lines.append(indent + key + " " + value)
147
+
148
+
149
+ def _cli(path):
150
+ """Command (reads a file). Parses a dtab file and returns it as a JSON string."""
151
+ with open(path) as file:
152
+ return json.dumps(parse(file.read()), indent=4)
153
+
154
+
155
+ def _main():
156
+ """Command. Console entry point: dtab FILE prints the tree as JSON."""
157
+ import fire
158
+
159
+ fire.Fire(_cli)
160
+
161
+
162
+ if __name__ == "__main__":
163
+ _main()