dtab 0.1.0__tar.gz

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.
dtab-0.1.0/PKG-INFO ADDED
@@ -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.
dtab-0.1.0/README.md ADDED
@@ -0,0 +1,65 @@
1
+ <p align="center"><img src="https://raw.githubusercontent.com/RyannDaGreat/dtab/main/assets/logo.jpg" alt="dtab" width="640"></p>
2
+
3
+ # dtab
4
+
5
+ Config files made of tab-separated paths. One line is one path into a tree.
6
+
7
+ ```
8
+ objects l1,l2 light
9
+ deltas l1 position x 1 y .5
10
+ z -2
11
+ ```
12
+
13
+ ```python
14
+ import dtab
15
+ dtab.parse(open("scene.dtab").read())
16
+ # {'objects': {'l1': 'light', 'l2': 'light'},
17
+ # 'deltas': {'l1': {'position': {'x': '1', 'y': '.5', 'z': '-2'}}}}
18
+ ```
19
+
20
+ ## Why
21
+
22
+ - 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.
23
+ - Simple. Six rules, one pass, about 70 lines per implementation.
24
+ - 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.
25
+ - 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:
26
+
27
+ ```
28
+ a b c x
29
+ a b d x
30
+ ```
31
+ ```
32
+ a
33
+ b
34
+ c x
35
+ d x
36
+ ```
37
+ ```
38
+ a b c,d x
39
+ ```
40
+
41
+ ## Rules
42
+
43
+ - 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.
44
+ - 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.
45
+ - An indented line continues the path of the line above it.
46
+ - Writing a key again replaces it. Writing into an object merges.
47
+ - `a,b` writes the same value under `a` and under `b`.
48
+ - An entry that starts with a space is a comment.
49
+
50
+ Every value is a string. Cast the ones you need. Keys are identifiers (letters, digits, underscores),
51
+ so attribute access like `config.deltas.l1` works with EasyDict and friends.
52
+
53
+ ## Install
54
+
55
+ | | |
56
+ |---|---|
57
+ | Python | `pip install dtab` then `import dtab` |
58
+ | JavaScript | `npm install dtab` then `const dtab = require('dtab')`, or `<script src="https://cdn.jsdelivr.net/npm/dtab/dtab.js">` for `window.dtab` |
59
+ | 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. |
60
+
61
+ ## API
62
+
63
+ - `parse(text)` returns nested dicts (Python) or plain objects (JavaScript). Raises on an invalid key, with the line number.
64
+ - `stringify(tree)` writes the tree back out, one key per line.
65
+ - Command line: `dtab scene.dtab` prints the tree as JSON.
@@ -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,10 @@
1
+ README.md
2
+ dtab.py
3
+ pyproject.toml
4
+ dtab.egg-info/PKG-INFO
5
+ dtab.egg-info/SOURCES.txt
6
+ dtab.egg-info/dependency_links.txt
7
+ dtab.egg-info/entry_points.txt
8
+ dtab.egg-info/requires.txt
9
+ dtab.egg-info/top_level.txt
10
+ test/test_dtab.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dtab = dtab:_main
@@ -0,0 +1 @@
1
+ fire
@@ -0,0 +1 @@
1
+ dtab
dtab-0.1.0/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()
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "dtab"
7
+ description = "Config files made of tab-separated paths. One line is one path into a tree."
8
+ readme = "README.md"
9
+ requires-python = ">=3.8"
10
+ dependencies = ["fire"]
11
+ dynamic = ["version"]
12
+
13
+ [project.scripts]
14
+ dtab = "dtab:_main"
15
+
16
+ [project.urls]
17
+ Homepage = "https://github.com/RyannDaGreat/dtab"
18
+
19
+ [tool.setuptools]
20
+ py-modules = ["dtab"]
21
+
22
+ [tool.setuptools.dynamic]
23
+ version = {attr = "dtab.__version__"}
dtab-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,141 @@
1
+ """
2
+ Tests for dtab. Run with no arguments from the repo root:
3
+
4
+ python test/test_dtab.py
5
+
6
+ Checks:
7
+ 1. dtab.py doctests.
8
+ 2. dtab.py and dtab.js agree with the ORIGINAL Lab-In-A-Cube djson.js (vendored untouched in
9
+ test/original/, run under node by test/run_original.js with its raw-string leaf option) on every
10
+ sample except deviations.dtab.
11
+ 3. dtab.py and dtab.js agree with each other on every sample; the deviating samples match their goldens
12
+ in test/expected/, which were diffed against the original when written. The differences are exactly:
13
+ blank lines are ignored, an object can overwrite a leaf, comma keys respect line order (in game_config
14
+ that is one leaf, deltas.initial.l1.intensity, where the original ignored the later `l1 intensity 1`).
15
+ 4. parse(stringify(parse(text))) == parse(text) for every sample.
16
+ 5. The key rule: bad keys are rejected with a line number in parse and in stringify, and Python's
17
+ str.isidentifier and the JS Unicode regex agree on a set of probes.
18
+ 6. node test/test_dtab.js.
19
+ 7. Vim: the syntax groups over test/samples/highlight.dtab match test/expected/highlight.txt byte by byte
20
+ (that sample deliberately contains invalid keys, so it is not parsed), and plugin/dtab.vim sets the
21
+ filetype when the repo is on 'runtimepath', which is what Vundle and vim-plug do.
22
+
23
+ Needs: python 3, node, vim.
24
+ """
25
+
26
+ import doctest
27
+ import json
28
+ import subprocess
29
+ import sys
30
+ import tempfile
31
+ from pathlib import Path
32
+
33
+ ROOT = Path(__file__).resolve().parent.parent
34
+ HIGHLIGHT_SAMPLE = ROOT / "test" / "samples" / "highlight.dtab"
35
+ SAMPLES = sorted(path for path in (ROOT / "test" / "samples").glob("*.dtab") if path != HIGHLIGHT_SAMPLE)
36
+ DEVIATING = {"deviations.dtab", "game_config.dtab"}
37
+ IDENTIFIER_PROBES = ["café", "变量", "x²", "_x", "0x", "from", "items", "a-b", "ok_1", "ª", "Ⅻ", "℘", "ℕ", "𝔸", "a.b", "é1", "1é"]
38
+
39
+ sys.path.insert(0, str(ROOT))
40
+ import dtab # noqa: E402
41
+
42
+
43
+ def run(*command):
44
+ """Query (runs a subprocess). The command's stdout."""
45
+ return subprocess.run(command, capture_output=True, text=True, check=True, cwd=ROOT).stdout
46
+
47
+
48
+ def raises_value_error(function, *fragments):
49
+ """Query (calls function). Asserts function() raises ValueError mentioning every fragment."""
50
+ try:
51
+ function()
52
+ except ValueError as error:
53
+ for fragment in fragments:
54
+ assert fragment in str(error), "error %r does not mention %r" % (str(error), fragment)
55
+ return
56
+ raise AssertionError("expected a ValueError mentioning %r" % (fragments,))
57
+
58
+
59
+ def test_doctests():
60
+ failed, _ = doctest.testmod(dtab)
61
+ assert failed == 0, "dtab.py doctests failed"
62
+
63
+
64
+ def test_readers_agree():
65
+ for path in SAMPLES:
66
+ python_tree = dtab.parse(path.read_text())
67
+ js_tree = json.loads(run("node", "dtab.js", str(path)))
68
+ assert python_tree == js_tree, "%s: dtab.py and dtab.js disagree" % path.name
69
+ if path.name in DEVIATING:
70
+ golden = json.loads((ROOT / "test" / "expected" / (path.stem + ".json")).read_text())
71
+ assert python_tree == golden, "%s: does not match golden" % path.name
72
+ else:
73
+ original = json.loads(run("node", "test/run_original.js", str(path)))
74
+ assert python_tree == original, "%s: does not match the original djson.js" % path.name
75
+
76
+
77
+ def test_round_trips():
78
+ for path in SAMPLES:
79
+ once = dtab.parse(path.read_text())
80
+ assert dtab.parse(dtab.stringify(once)) == once, "%s: did not round trip" % path.name
81
+
82
+
83
+ def test_key_rule():
84
+ for text, fragments in [
85
+ ("a\tcheckpoint.initial 1", ["line 1", "'checkpoint.initial'"]),
86
+ ("ok 1\n\t2nd 2", ["line 2", "'2nd'"]),
87
+ ("a-b 1", ["'a-b'"]),
88
+ ("~scope\n\tx 1", ["'~scope'"]),
89
+ ("log\t@ e", ["'@'"]),
90
+ ("a,b.c\tx 1", ["'a,b.c'"]),
91
+ ]:
92
+ raises_value_error(lambda: dtab.parse(text), *fragments)
93
+ assert dtab.parse("items 1\nfrom 2\n_private 3\ncafé 4") == {"items": "1", "from": "2", "_private": "3", "café": "4"}
94
+ for tree in [{"a b": "1"}, {"a,b": "1"}, {0: "1"}, {"": "1"}, {"a": "x\ty"}, {"a": "x\ny"}]:
95
+ raises_value_error(lambda: dtab.stringify(tree), "dtab")
96
+ python_verdicts = [probe.isidentifier() for probe in IDENTIFIER_PROBES]
97
+ js_verdicts = json.loads(run(
98
+ "node", "-e",
99
+ "const d = require('./dtab.js'); console.log(JSON.stringify(%s.map(p => { try { d.parse(p + ' 1'); return true } catch { return false } })))"
100
+ % json.dumps(IDENTIFIER_PROBES),
101
+ ))
102
+ assert python_verdicts == js_verdicts, "identifier rule differs: %s" % [
103
+ (probe, py, js) for probe, py, js in zip(IDENTIFIER_PROBES, python_verdicts, js_verdicts) if py != js]
104
+
105
+
106
+ def test_js_suite():
107
+ subprocess.run(["node", "test/test_dtab.js"], check=True, cwd=ROOT)
108
+
109
+
110
+ def vim(*commands):
111
+ """Query (runs vim headless). Runs the -c commands in order in a clean vim and returns nothing."""
112
+ arguments = ["vim", "-N", "-u", "NONE", "-i", "NONE", "-es"]
113
+ for command in commands:
114
+ arguments += ["-c", command]
115
+ subprocess.run(arguments + ["-c", "qa!"], check=True, cwd=ROOT)
116
+
117
+
118
+ def test_vim_highlighting():
119
+ with tempfile.TemporaryDirectory() as directory:
120
+ out = Path(directory) / "highlights.txt"
121
+ vim("syntax on", "source dtab.vim", "edit " + str(HIGHLIGHT_SAMPLE),
122
+ "source test/dump_highlights.vim", "call DumpHighlights('%s')" % out)
123
+ got = out.read_text()
124
+ expected = (ROOT / "test" / "expected" / "highlight.txt").read_text()
125
+ assert got == expected, "vim highlighting differs:\nexpected:\n%s\ngot:\n%s" % (expected, got)
126
+
127
+
128
+ def test_vim_plugin_shim():
129
+ with tempfile.TemporaryDirectory() as directory:
130
+ out = Path(directory) / "filetype.txt"
131
+ vim("set rtp+=" + str(ROOT), "runtime! plugin/*.vim", "edit " + str(SAMPLES[0]),
132
+ "call writefile([&filetype], '%s')" % out)
133
+ assert out.read_text().strip() == "dtab", "plugin/dtab.vim did not set the filetype"
134
+
135
+
136
+ if __name__ == "__main__":
137
+ for test in [test_doctests, test_readers_agree, test_round_trips, test_key_rule, test_js_suite,
138
+ test_vim_highlighting, test_vim_plugin_shim]:
139
+ test()
140
+ print("ok " + test.__name__)
141
+ print("All dtab tests passed")