dtab 0.2.0__tar.gz → 0.3.1__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.2.0 → dtab-0.3.1}/PKG-INFO +13 -7
- {dtab-0.2.0 → dtab-0.3.1}/README.md +12 -6
- {dtab-0.2.0 → dtab-0.3.1}/dtab.egg-info/PKG-INFO +13 -7
- {dtab-0.2.0 → dtab-0.3.1}/dtab.py +16 -11
- dtab-0.3.1/test/test_dtab.py +278 -0
- dtab-0.2.0/test/test_dtab.py +0 -189
- {dtab-0.2.0 → dtab-0.3.1}/dtab.egg-info/SOURCES.txt +0 -0
- {dtab-0.2.0 → dtab-0.3.1}/dtab.egg-info/dependency_links.txt +0 -0
- {dtab-0.2.0 → dtab-0.3.1}/dtab.egg-info/entry_points.txt +0 -0
- {dtab-0.2.0 → dtab-0.3.1}/dtab.egg-info/requires.txt +0 -0
- {dtab-0.2.0 → dtab-0.3.1}/dtab.egg-info/top_level.txt +0 -0
- {dtab-0.2.0 → dtab-0.3.1}/pyproject.toml +0 -0
- {dtab-0.2.0 → dtab-0.3.1}/setup.cfg +0 -0
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dtab
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Delta Tab: config files made of tab-separated paths. One line is one path into a tree.
|
|
5
5
|
Project-URL: Homepage, https://github.com/RyannDaGreat/dtab
|
|
6
6
|
Requires-Python: >=3.8
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Requires-Dist: fire
|
|
9
9
|
|
|
10
|
-
<p align="center"><img src="https://raw.githubusercontent.com/RyannDaGreat/dtab/main/assets/logo.jpg" alt="dtab" width="640"></p>
|
|
10
|
+
<!-- <p align="center"><img src="https://raw.githubusercontent.com/RyannDaGreat/dtab/main/assets/logo.jpg" alt="dtab" width="640"></p> -->
|
|
11
|
+
<!-- <img width="6912" height="4188" alt="image" src="https://github.com/user-attachments/assets/db10af67-b7ba-4e42-a808-946c8cdcc2c7" /> -->
|
|
12
|
+
<img width="6912" height="2940" alt="image" src="https://github.com/user-attachments/assets/24f2187b-9e71-45e6-9a4b-427ad00e48c8" />
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
11
16
|
<p align="center"><sub>Logo made with <a href="https://github.com/RyannDaGreat/SvelteLib">PowerRP</a>. <a href="https://ryanndagreat.github.io/SvelteLib/?repo=RyannDaGreat/dtab">Open it in PowerRP</a> to edit it (source: <code>assets/doc.json</code>).</sub></p>
|
|
12
17
|
|
|
13
18
|
# dtab
|
|
@@ -32,7 +37,7 @@ dtab.parse(open("scene.dtab").read())
|
|
|
32
37
|
|
|
33
38
|
- 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.
|
|
34
39
|
- Simple. Six rules, one pass, about 70 lines per implementation.
|
|
35
|
-
- Everything is addressable. There are no lists, so every value has a
|
|
40
|
+
- Everything is addressable. There are no lists, so every value has a path: `config.deltas.l1.position.x` with EasyDict in Python or plain property access in JavaScript, brackets for keys like `file.json`.
|
|
36
41
|
- 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:
|
|
37
42
|
|
|
38
43
|
```
|
|
@@ -59,8 +64,9 @@ dtab.parse(open("scene.dtab").read())
|
|
|
59
64
|
- An entry that starts with a space is a comment.
|
|
60
65
|
- `$key` starts a multiline string. See below.
|
|
61
66
|
|
|
62
|
-
Every value is a string. Cast the ones you need.
|
|
63
|
-
so
|
|
67
|
+
Every value is a string. Cast the ones you need. A key is any run of letters, digits, `_`, `.`, `-` and `/`,
|
|
68
|
+
so `file.json`, `2026-09-07`, `assets/logo` and `0` are keys. Keys that happen to be identifiers work as attributes
|
|
69
|
+
(`config.deltas.l1` with EasyDict and friends); the rest are reached with brackets (`config["file.json"]`).
|
|
64
70
|
|
|
65
71
|
## Multiline strings
|
|
66
72
|
|
|
@@ -92,8 +98,8 @@ the value. Unknown tags are plain text, never an error.
|
|
|
92
98
|
|
|
93
99
|
- **Python**: `pip install dtab` then `import dtab`
|
|
94
100
|
- **JavaScript**: `npm install deltatab` then `const dtab = require('deltatab')`, or `<script src="https://cdn.jsdelivr.net/npm/deltatab/dtab.js">` for `window.dtab`
|
|
95
|
-
- **Vim**: `Plugin 'RyannDaGreat/dtab'` (Vundle) or `Plug 'RyannDaGreat/dtab'` (vim-plug), or paste `dtab.vim` into your vimrc. Highlights `*.dtab`, flags bad keys and trailing tabs,
|
|
96
|
-
- **VS Code**: search "dtab" in Extensions, or `code --install-extension RyannDaGreat.dtab`. Highlighting
|
|
101
|
+
- **Vim**: `Plugin 'RyannDaGreat/dtab'` (Vundle) or `Plug 'RyannDaGreat/dtab'` (vim-plug), or paste `dtab.vim` into your vimrc. Highlights `*.dtab`, flags bad keys and trailing tabs, gives the file a Δ icon in NERDTree if vim-devicons is installed, `:DtabPreview` opens a split showing the tree as JSON that follows your edits (needs `+python3`), and `J` joins the line below with a tab (deep form to wide form) whenever that keeps the tree the same.
|
|
102
|
+
- **VS Code**: search "dtab" in Extensions, or `code --install-extension RyannDaGreat.dtab`. Highlighting, a Δ file icon, and a live JSON preview of the file beside it (the preview button in the title bar, or Cmd+K V).
|
|
97
103
|
|
|
98
104
|
## API
|
|
99
105
|
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
<p align="center"><img src="https://raw.githubusercontent.com/RyannDaGreat/dtab/main/assets/logo.jpg" alt="dtab" width="640"></p>
|
|
1
|
+
<!-- <p align="center"><img src="https://raw.githubusercontent.com/RyannDaGreat/dtab/main/assets/logo.jpg" alt="dtab" width="640"></p> -->
|
|
2
|
+
<!-- <img width="6912" height="4188" alt="image" src="https://github.com/user-attachments/assets/db10af67-b7ba-4e42-a808-946c8cdcc2c7" /> -->
|
|
3
|
+
<img width="6912" height="2940" alt="image" src="https://github.com/user-attachments/assets/24f2187b-9e71-45e6-9a4b-427ad00e48c8" />
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
2
7
|
<p align="center"><sub>Logo made with <a href="https://github.com/RyannDaGreat/SvelteLib">PowerRP</a>. <a href="https://ryanndagreat.github.io/SvelteLib/?repo=RyannDaGreat/dtab">Open it in PowerRP</a> to edit it (source: <code>assets/doc.json</code>).</sub></p>
|
|
3
8
|
|
|
4
9
|
# dtab
|
|
@@ -23,7 +28,7 @@ dtab.parse(open("scene.dtab").read())
|
|
|
23
28
|
|
|
24
29
|
- 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.
|
|
25
30
|
- Simple. Six rules, one pass, about 70 lines per implementation.
|
|
26
|
-
- Everything is addressable. There are no lists, so every value has a
|
|
31
|
+
- Everything is addressable. There are no lists, so every value has a path: `config.deltas.l1.position.x` with EasyDict in Python or plain property access in JavaScript, brackets for keys like `file.json`.
|
|
27
32
|
- 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:
|
|
28
33
|
|
|
29
34
|
```
|
|
@@ -50,8 +55,9 @@ dtab.parse(open("scene.dtab").read())
|
|
|
50
55
|
- An entry that starts with a space is a comment.
|
|
51
56
|
- `$key` starts a multiline string. See below.
|
|
52
57
|
|
|
53
|
-
Every value is a string. Cast the ones you need.
|
|
54
|
-
so
|
|
58
|
+
Every value is a string. Cast the ones you need. A key is any run of letters, digits, `_`, `.`, `-` and `/`,
|
|
59
|
+
so `file.json`, `2026-09-07`, `assets/logo` and `0` are keys. Keys that happen to be identifiers work as attributes
|
|
60
|
+
(`config.deltas.l1` with EasyDict and friends); the rest are reached with brackets (`config["file.json"]`).
|
|
55
61
|
|
|
56
62
|
## Multiline strings
|
|
57
63
|
|
|
@@ -83,8 +89,8 @@ the value. Unknown tags are plain text, never an error.
|
|
|
83
89
|
|
|
84
90
|
- **Python**: `pip install dtab` then `import dtab`
|
|
85
91
|
- **JavaScript**: `npm install deltatab` then `const dtab = require('deltatab')`, or `<script src="https://cdn.jsdelivr.net/npm/deltatab/dtab.js">` for `window.dtab`
|
|
86
|
-
- **Vim**: `Plugin 'RyannDaGreat/dtab'` (Vundle) or `Plug 'RyannDaGreat/dtab'` (vim-plug), or paste `dtab.vim` into your vimrc. Highlights `*.dtab`, flags bad keys and trailing tabs,
|
|
87
|
-
- **VS Code**: search "dtab" in Extensions, or `code --install-extension RyannDaGreat.dtab`. Highlighting
|
|
92
|
+
- **Vim**: `Plugin 'RyannDaGreat/dtab'` (Vundle) or `Plug 'RyannDaGreat/dtab'` (vim-plug), or paste `dtab.vim` into your vimrc. Highlights `*.dtab`, flags bad keys and trailing tabs, gives the file a Δ icon in NERDTree if vim-devicons is installed, `:DtabPreview` opens a split showing the tree as JSON that follows your edits (needs `+python3`), and `J` joins the line below with a tab (deep form to wide form) whenever that keeps the tree the same.
|
|
93
|
+
- **VS Code**: search "dtab" in Extensions, or `code --install-extension RyannDaGreat.dtab`. Highlighting, a Δ file icon, and a live JSON preview of the file beside it (the preview button in the title bar, or Cmd+K V).
|
|
88
94
|
|
|
89
95
|
## API
|
|
90
96
|
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dtab
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Delta Tab: config files made of tab-separated paths. One line is one path into a tree.
|
|
5
5
|
Project-URL: Homepage, https://github.com/RyannDaGreat/dtab
|
|
6
6
|
Requires-Python: >=3.8
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Requires-Dist: fire
|
|
9
9
|
|
|
10
|
-
<p align="center"><img src="https://raw.githubusercontent.com/RyannDaGreat/dtab/main/assets/logo.jpg" alt="dtab" width="640"></p>
|
|
10
|
+
<!-- <p align="center"><img src="https://raw.githubusercontent.com/RyannDaGreat/dtab/main/assets/logo.jpg" alt="dtab" width="640"></p> -->
|
|
11
|
+
<!-- <img width="6912" height="4188" alt="image" src="https://github.com/user-attachments/assets/db10af67-b7ba-4e42-a808-946c8cdcc2c7" /> -->
|
|
12
|
+
<img width="6912" height="2940" alt="image" src="https://github.com/user-attachments/assets/24f2187b-9e71-45e6-9a4b-427ad00e48c8" />
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
11
16
|
<p align="center"><sub>Logo made with <a href="https://github.com/RyannDaGreat/SvelteLib">PowerRP</a>. <a href="https://ryanndagreat.github.io/SvelteLib/?repo=RyannDaGreat/dtab">Open it in PowerRP</a> to edit it (source: <code>assets/doc.json</code>).</sub></p>
|
|
12
17
|
|
|
13
18
|
# dtab
|
|
@@ -32,7 +37,7 @@ dtab.parse(open("scene.dtab").read())
|
|
|
32
37
|
|
|
33
38
|
- 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.
|
|
34
39
|
- Simple. Six rules, one pass, about 70 lines per implementation.
|
|
35
|
-
- Everything is addressable. There are no lists, so every value has a
|
|
40
|
+
- Everything is addressable. There are no lists, so every value has a path: `config.deltas.l1.position.x` with EasyDict in Python or plain property access in JavaScript, brackets for keys like `file.json`.
|
|
36
41
|
- 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:
|
|
37
42
|
|
|
38
43
|
```
|
|
@@ -59,8 +64,9 @@ dtab.parse(open("scene.dtab").read())
|
|
|
59
64
|
- An entry that starts with a space is a comment.
|
|
60
65
|
- `$key` starts a multiline string. See below.
|
|
61
66
|
|
|
62
|
-
Every value is a string. Cast the ones you need.
|
|
63
|
-
so
|
|
67
|
+
Every value is a string. Cast the ones you need. A key is any run of letters, digits, `_`, `.`, `-` and `/`,
|
|
68
|
+
so `file.json`, `2026-09-07`, `assets/logo` and `0` are keys. Keys that happen to be identifiers work as attributes
|
|
69
|
+
(`config.deltas.l1` with EasyDict and friends); the rest are reached with brackets (`config["file.json"]`).
|
|
64
70
|
|
|
65
71
|
## Multiline strings
|
|
66
72
|
|
|
@@ -92,8 +98,8 @@ the value. Unknown tags are plain text, never an error.
|
|
|
92
98
|
|
|
93
99
|
- **Python**: `pip install dtab` then `import dtab`
|
|
94
100
|
- **JavaScript**: `npm install deltatab` then `const dtab = require('deltatab')`, or `<script src="https://cdn.jsdelivr.net/npm/deltatab/dtab.js">` for `window.dtab`
|
|
95
|
-
- **Vim**: `Plugin 'RyannDaGreat/dtab'` (Vundle) or `Plug 'RyannDaGreat/dtab'` (vim-plug), or paste `dtab.vim` into your vimrc. Highlights `*.dtab`, flags bad keys and trailing tabs,
|
|
96
|
-
- **VS Code**: search "dtab" in Extensions, or `code --install-extension RyannDaGreat.dtab`. Highlighting
|
|
101
|
+
- **Vim**: `Plugin 'RyannDaGreat/dtab'` (Vundle) or `Plug 'RyannDaGreat/dtab'` (vim-plug), or paste `dtab.vim` into your vimrc. Highlights `*.dtab`, flags bad keys and trailing tabs, gives the file a Δ icon in NERDTree if vim-devicons is installed, `:DtabPreview` opens a split showing the tree as JSON that follows your edits (needs `+python3`), and `J` joins the line below with a tab (deep form to wide form) whenever that keeps the tree the same.
|
|
102
|
+
- **VS Code**: search "dtab" in Extensions, or `code --install-extension RyannDaGreat.dtab`. Highlighting, a Δ file icon, and a live JSON preview of the file beside it (the preview button in the title bar, or Cmd+K V).
|
|
97
103
|
|
|
98
104
|
## API
|
|
99
105
|
|
|
@@ -21,7 +21,8 @@ Rules:
|
|
|
21
21
|
- `$key` is a multiline leaf: its value is the entries after it on its line plus every line indented
|
|
22
22
|
under it, one line each, with their common indentation removed. A word after the key on the `$` line
|
|
23
23
|
(`$query sql`) is a language tag for editors and is not part of the value.
|
|
24
|
-
- Keys are
|
|
24
|
+
- Keys are one or more letters, digits, or KEY_PUNCTUATION (`_.-/`), so `file.json`, `a/b` and `123aa` are keys. Keys that
|
|
25
|
+
are also Python identifiers work as attributes (config.deltas.l1). Every value is a string.
|
|
25
26
|
|
|
26
27
|
Single pass, one stack, O(total characters).
|
|
27
28
|
"""
|
|
@@ -29,12 +30,14 @@ Single pass, one stack, O(total characters).
|
|
|
29
30
|
import json
|
|
30
31
|
import re
|
|
31
32
|
|
|
32
|
-
__version__ = "0.
|
|
33
|
+
__version__ = "0.3.1" # SEMANTIC BINDING: dtab-version (also package.json "version")
|
|
33
34
|
|
|
34
35
|
KEY_SEPARATOR = "," # a,b writes the same value under each key
|
|
35
36
|
BLOCK_PREFIX = "$" # $key: a leaf whose value is the lines under it ($ as in string)
|
|
36
|
-
|
|
37
|
+
KEY_PUNCTUATION = "_.-/" # Allowed in keys besides letters and digits. SEMANTIC BINDING: dtab-key-punctuation
|
|
38
|
+
KEY_RULE = "keys may contain only letters, digits and " + " ".join(KEY_PUNCTUATION)
|
|
37
39
|
_TAB_RUN = re.compile(r"\t+") # Several tabs in a row are one separator, so columns can be aligned
|
|
40
|
+
_KEY = re.compile(r"[\w" + re.escape(KEY_PUNCTUATION) + "]+") # \w: letters, digits, _ (Unicode); the rest is reserved for syntax
|
|
38
41
|
|
|
39
42
|
|
|
40
43
|
def parse(text):
|
|
@@ -57,9 +60,11 @@ def parse(text):
|
|
|
57
60
|
{'query': 'SELECT *\\n\\n\\tFROM users', 'next': '1'}
|
|
58
61
|
>>> parse('$cmd\\tpip install rp\\tpython train.py')
|
|
59
62
|
{'cmd': 'pip install rp\\npython train.py'}
|
|
60
|
-
>>> parse('a\\tb 1\\
|
|
63
|
+
>>> parse('a\\tb 1\\nfile.json\\tsize 2\\n123aa 3')
|
|
64
|
+
{'a': {'b': '1'}, 'file.json': {'size': '2'}, '123aa': '3'}
|
|
65
|
+
>>> parse('a\\tb 1\\nc|d\\te 2')
|
|
61
66
|
Traceback (most recent call last):
|
|
62
|
-
ValueError: dtab line 2: invalid key 'c
|
|
67
|
+
ValueError: dtab line 2: invalid key 'c|d': keys may contain only letters, digits and _ . - /
|
|
63
68
|
"""
|
|
64
69
|
root = {}
|
|
65
70
|
stack = [(-1, [root])] # (indent, nodes that deeper lines nest into)
|
|
@@ -126,19 +131,19 @@ def stringify(tree):
|
|
|
126
131
|
|
|
127
132
|
def _key_names(key, line_number, allow_commas):
|
|
128
133
|
"""
|
|
129
|
-
Pure function (raises ValueError). The
|
|
130
|
-
|
|
134
|
+
Pure function (raises ValueError). The names a key stands for: `a,b` is two while parsing, and a
|
|
135
|
+
stringify key must be a single name.
|
|
131
136
|
|
|
132
137
|
Examples:
|
|
133
|
-
>>> _key_names('l1,l2', 1, True), _key_names('
|
|
134
|
-
(['l1', 'l2'], ['
|
|
138
|
+
>>> _key_names('l1,l2', 1, True), _key_names('file-thing.json', None, False)
|
|
139
|
+
(['l1', 'l2'], ['file-thing.json'])
|
|
135
140
|
>>> _key_names('a,b', None, False)
|
|
136
141
|
Traceback (most recent call last):
|
|
137
|
-
ValueError: dtab: invalid key 'a,b': keys
|
|
142
|
+
ValueError: dtab: invalid key 'a,b': keys may contain only letters, digits and _ . - /
|
|
138
143
|
"""
|
|
139
144
|
names = key.split(KEY_SEPARATOR) if allow_commas else [key]
|
|
140
145
|
for name in names:
|
|
141
|
-
if not
|
|
146
|
+
if not _KEY.fullmatch(name):
|
|
142
147
|
where = " line %d" % line_number if line_number else ""
|
|
143
148
|
raise ValueError("dtab%s: invalid key %r: %s" % (where, key, KEY_RULE))
|
|
144
149
|
return names
|
|
@@ -0,0 +1,278 @@
|
|
|
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
|
+
and `$` multiline blocks, which the original did not have.
|
|
16
|
+
4. parse(stringify(parse(text))) == parse(text) for every sample.
|
|
17
|
+
5. The key rule (letters, digits, _ . -): bad keys are rejected with a line number in parse and in
|
|
18
|
+
stringify, and the Python and JS character classes agree on a set of Unicode probes.
|
|
19
|
+
6. node test/test_dtab.js.
|
|
20
|
+
7. Vim: the syntax groups over test/samples/highlight.dtab match test/expected/highlight.txt byte by byte
|
|
21
|
+
(that sample deliberately contains invalid keys, so it is not parsed), embedded languages, the Tab and
|
|
22
|
+
shift keys, J, :DtabPreview, and plugin/dtab.vim sets the filetype when the repo is on 'runtimepath',
|
|
23
|
+
which is what Vundle and vim-plug do.
|
|
24
|
+
8. docs/index.html in headless Chrome (test/test_web.js), skipped with a message if puppeteer is not
|
|
25
|
+
installed (`npm install --no-save puppeteer && npx puppeteer browsers install chrome`).
|
|
26
|
+
9. The VS Code extension's TextMate grammar tokenizes the vim highlight sample the same way vim does,
|
|
27
|
+
and its indentation and preview logic (test/test_vscode.js), skipped if vscode-textmate is not installed
|
|
28
|
+
(`npm install --no-save vscode-textmate vscode-oniguruma`).
|
|
29
|
+
10. The JSON preview inside the installed VS Code, in an isolated profile (test/test_vscode_live.js),
|
|
30
|
+
skipped if @vscode/test-electron is not installed.
|
|
31
|
+
|
|
32
|
+
Needs: python 3, node, vim. The optional packages go in together, since an `npm install --no-save` removes
|
|
33
|
+
the ones it was not given: `npm install --no-save puppeteer vscode-textmate vscode-oniguruma @vscode/test-electron`.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import doctest
|
|
37
|
+
import json
|
|
38
|
+
import subprocess
|
|
39
|
+
import sys
|
|
40
|
+
import tempfile
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
|
|
43
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
44
|
+
HIGHLIGHT_SAMPLE = ROOT / "test" / "samples" / "highlight.dtab"
|
|
45
|
+
SAMPLES = sorted(path for path in (ROOT / "test" / "samples").glob("*.dtab") if path != HIGHLIGHT_SAMPLE)
|
|
46
|
+
DEVIATING = {"deviations.dtab", "game_config.dtab", "multiline.dtab"} # multiline: $ blocks did not exist in the original
|
|
47
|
+
IDENTIFIER_PROBES = ["café", "变量", "x²", "_x", "0x", "a-b", "ok_1", "ª", "Ⅻ", "℘", "ℕ", "𝔸", "a.b", "é1", "1é", "a/b", "a:b", "ä-ö.ü", "١٢٣", "a~b"] # the key rule, py vs js
|
|
48
|
+
|
|
49
|
+
sys.path.insert(0, str(ROOT))
|
|
50
|
+
import dtab # noqa: E402
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run(*command):
|
|
54
|
+
"""Query (runs a subprocess). The command's stdout."""
|
|
55
|
+
return subprocess.run(command, capture_output=True, text=True, check=True, cwd=ROOT).stdout
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def raises_value_error(function, *fragments):
|
|
59
|
+
"""Query (calls function). Asserts function() raises ValueError mentioning every fragment."""
|
|
60
|
+
try:
|
|
61
|
+
function()
|
|
62
|
+
except ValueError as error:
|
|
63
|
+
for fragment in fragments:
|
|
64
|
+
assert fragment in str(error), "error %r does not mention %r" % (str(error), fragment)
|
|
65
|
+
return
|
|
66
|
+
raise AssertionError("expected a ValueError mentioning %r" % (fragments,))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_doctests():
|
|
70
|
+
failed, _ = doctest.testmod(dtab)
|
|
71
|
+
assert failed == 0, "dtab.py doctests failed"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_readers_agree():
|
|
75
|
+
for path in SAMPLES:
|
|
76
|
+
python_tree = dtab.parse(path.read_text())
|
|
77
|
+
js_tree = json.loads(run("node", "dtab.js", str(path)))
|
|
78
|
+
assert python_tree == js_tree, "%s: dtab.py and dtab.js disagree" % path.name
|
|
79
|
+
if path.name in DEVIATING:
|
|
80
|
+
golden = json.loads((ROOT / "test" / "expected" / (path.stem + ".json")).read_text())
|
|
81
|
+
assert python_tree == golden, "%s: does not match golden" % path.name
|
|
82
|
+
else:
|
|
83
|
+
original = json.loads(run("node", "test/run_original.js", str(path)))
|
|
84
|
+
assert python_tree == original, "%s: does not match the original djson.js" % path.name
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_round_trips():
|
|
88
|
+
for path in SAMPLES:
|
|
89
|
+
once = dtab.parse(path.read_text())
|
|
90
|
+
assert dtab.parse(dtab.stringify(once)) == once, "%s: did not round trip" % path.name
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_key_rule():
|
|
94
|
+
for text, fragments in [
|
|
95
|
+
("a\tc|d 1", ["line 1", "'c|d'"]),
|
|
96
|
+
("ok 1\n\tk:v 2", ["line 2", "'k:v'"]),
|
|
97
|
+
("~scope\n\tx 1", ["'~scope'"]),
|
|
98
|
+
("log\t@ e", ["'@'"]),
|
|
99
|
+
("a,b#c\tx 1", ["'a,b#c'"]),
|
|
100
|
+
("\"quoted\" 1", ["'\"quoted\"'"]),
|
|
101
|
+
]:
|
|
102
|
+
raises_value_error(lambda: dtab.parse(text), *fragments)
|
|
103
|
+
assert dtab.parse("123aa 1\nfile.json 2\nfile-thing.json 3\n123.json-yaml 4\nitems 5\n_p 6\ncafé 7\n-x 8\nassets/logo.png 9\n/ 10") == {
|
|
104
|
+
"123aa": "1", "file.json": "2", "file-thing.json": "3", "123.json-yaml": "4", "items": "5", "_p": "6", "café": "7", "-x": "8", "assets/logo.png": "9", "/": "10"}
|
|
105
|
+
for tree in [{"a b": "1"}, {"a,b": "1"}, {"a|b": "1"}, {"": "1"}]:
|
|
106
|
+
raises_value_error(lambda: dtab.stringify(tree), "dtab")
|
|
107
|
+
assert dtab.parse(dtab.stringify({0: "a", "file.json": "b"})) == {"0": "a", "file.json": "b"} # int keys are written as text
|
|
108
|
+
for value in ["x\ny", "x\ty", "a\n\n\tb\n c", "#!/bin/bash\necho hi", ""]:
|
|
109
|
+
assert dtab.parse(dtab.stringify({"a": value})) == {"a": value}, "multiline round trip failed for %r" % value
|
|
110
|
+
def accepted(probe):
|
|
111
|
+
try:
|
|
112
|
+
dtab.parse(probe + " 1")
|
|
113
|
+
return True
|
|
114
|
+
except ValueError:
|
|
115
|
+
return False
|
|
116
|
+
python_verdicts = [accepted(probe) for probe in IDENTIFIER_PROBES]
|
|
117
|
+
js_verdicts = json.loads(run(
|
|
118
|
+
"node", "-e",
|
|
119
|
+
"const d = require('./dtab.js'); console.log(JSON.stringify(%s.map(p => { try { d.parse(p + ' 1'); return true } catch { return false } })))"
|
|
120
|
+
% json.dumps(IDENTIFIER_PROBES),
|
|
121
|
+
))
|
|
122
|
+
assert python_verdicts == js_verdicts, "identifier rule differs: %s" % [
|
|
123
|
+
(probe, py, js) for probe, py, js in zip(IDENTIFIER_PROBES, python_verdicts, js_verdicts) if py != js]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_js_suite():
|
|
127
|
+
subprocess.run(["node", "test/test_dtab.js"], check=True, cwd=ROOT)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def vim(*commands):
|
|
131
|
+
"""Query (runs vim headless). Runs the -c commands in order in a clean vim and returns nothing."""
|
|
132
|
+
arguments = ["vim", "-N", "-u", "NONE", "-i", "NONE", "-n", "-es", "-c", "set shortmess+=A"] # -n, +=A: no swap files, no ATTENTION prompt
|
|
133
|
+
for command in commands:
|
|
134
|
+
arguments += ["-c", command]
|
|
135
|
+
subprocess.run(arguments + ["-c", "qa!"], check=True, cwd=ROOT)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_vim_highlighting():
|
|
139
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
140
|
+
out = Path(directory) / "highlights.txt"
|
|
141
|
+
vim("syntax on", "source dtab.vim", "edit " + str(HIGHLIGHT_SAMPLE),
|
|
142
|
+
"source test/dump_highlights.vim", "call DumpHighlights('%s')" % out)
|
|
143
|
+
got = out.read_text()
|
|
144
|
+
expected = (ROOT / "test" / "expected" / "highlight.txt").read_text()
|
|
145
|
+
assert got == expected, "vim highlighting differs:\nexpected:\n%s\ngot:\n%s" % (expected, got)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_vim_embedded_languages():
|
|
149
|
+
"""A tagged $ block (at line start or after other entries) and a shebang block get the language's own groups."""
|
|
150
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
151
|
+
sample = Path(directory) / "embedded.dtab"
|
|
152
|
+
sample.write_text("$query sql\n\tSELECT name FROM t\nconfig\tdb\t$init sql\n\t\tCREATE TABLE t\n$s\n\t#!/bin/bash\n\techo hi\nafter 1\n")
|
|
153
|
+
out = Path(directory) / "groups.txt"
|
|
154
|
+
vim("syntax on", "source dtab.vim", "edit " + str(sample),
|
|
155
|
+
"call writefile([synIDattr(synID(2,2,1),'name'), synIDattr(synID(3,12,1),'name'), synIDattr(synID(4,3,1),'name'), synIDattr(synID(7,2,1),'name'), synIDattr(synID(8,1,1),'name')], '%s')" % out)
|
|
156
|
+
groups = out.read_text().split()
|
|
157
|
+
assert groups == ["sqlStatement", "dtabBlockKey", "sqlStatement", "shStatement", "dtabLeafKey"], groups
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_vim_tab_key():
|
|
161
|
+
"""In insert mode, Tab inside a $ block (past the line's tabs) inserts spaces; elsewhere a tab."""
|
|
162
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
163
|
+
sample = Path(directory) / "tab.dtab"
|
|
164
|
+
sample.write_text("$code python\n\tdef f():\n\t\nafter 1\nx\n\t$code\n\t\tbody\n\t\n\n")
|
|
165
|
+
out = Path(directory) / "lines.txt"
|
|
166
|
+
vim("syntax on", "source dtab.vim", "edit " + str(sample),
|
|
167
|
+
"call cursor(3, 2) | execute \"normal a\\<Tab>return 1\" | call cursor(3, 1) | execute \"normal i\\<Tab>\" | call cursor(4, 6) | execute \"normal i\\<Tab>\"",
|
|
168
|
+
"call cursor(8, 1) | execute \"normal A\\<Tab>\" | call cursor(9, 1) | execute \"normal A\\<Tab>\\<Esc>A\\<Tab>\\<Esc>A\\<Tab>\"",
|
|
169
|
+
"call writefile([getline(3), getline(4), getline(8), getline(9)], '%s')" % out)
|
|
170
|
+
lines = out.read_text().split("\n")
|
|
171
|
+
assert lines[0] == "\t\t return 1", repr(lines[0]) # spaces past the block's tab; a tab at the line start
|
|
172
|
+
assert lines[1] == "after\t 1", repr(lines[1]) # a tab outside the block
|
|
173
|
+
assert lines[2] == "\t\t", repr(lines[2]) # a whitespace line at the $ line's depth is structure: a tab
|
|
174
|
+
assert lines[3] == "\t\t ", repr(lines[3]) # an empty line: tabs until the line is deeper than the $ line, then spaces
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_vim_shift_keys():
|
|
178
|
+
""">> and << (and visual > <) shift block lines by four spaces, other lines by a tab."""
|
|
179
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
180
|
+
sample = Path(directory) / "shift.dtab"
|
|
181
|
+
sample.write_text("before 1\n$code python\n\tdef f():\n\t return 1\n")
|
|
182
|
+
out = Path(directory) / "lines.txt"
|
|
183
|
+
vim("syntax on", "source dtab.vim", "edit " + str(sample),
|
|
184
|
+
"execute '1normal >>' | execute '1normal <<' | execute '3normal >>' | execute '4normal <<' | execute '2normal Vjj>'",
|
|
185
|
+
"execute '3normal 2>j' | execute '3normal <j' | execute '3normal >2j'",
|
|
186
|
+
"call writefile(getline(1, '$'), '%s')" % out)
|
|
187
|
+
lines = out.read_text().split("\n")
|
|
188
|
+
assert lines[0] == "before 1", repr(lines[0]) # >> gave it a tab, << took it away
|
|
189
|
+
assert lines[1] == "\t$code python", repr(lines[1]) # visual > over the $ line and its block: tabs for all
|
|
190
|
+
assert lines[2] == "\t\t def f():", repr(lines[2]) # >> inside the block: spaces; the block's tab; then 2>j, <j, >2j: net one more level
|
|
191
|
+
assert lines[3] == "\t\t return 1", repr(lines[3]) # << removed the spaces; 2>j <j >2j: net one level
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_vim_join():
|
|
195
|
+
"""J joins structure lines with a tab when the tree stays the same, refuses when a line would change parent, joins block text like vim, drops blanks, takes counts and visual ranges."""
|
|
196
|
+
text = ("deltas\n\tl1\n\t\tposition\n\t\t\tx 1\n\t\t\ty .5\nafter 1\nl1\nl2\n$code python\n\tdef f():\n\t return 1\nz 1\n\nend 1\n"
|
|
197
|
+
"u 1\n\tobj\n\tsib 2\nlast 1\nt 1\t\n\tswallowed 2\nk\tleaf 1\n note\nv 1\n note\n\tw 2\n")
|
|
198
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
199
|
+
sample = Path(directory) / "join.dtab"
|
|
200
|
+
sample.write_text(text)
|
|
201
|
+
out = Path(directory) / "lines.txt"
|
|
202
|
+
vim("syntax on", "source dtab.vim", "edit " + str(sample),
|
|
203
|
+
# 3J: deep to wide; J J: the two leaves; J: refused (after 1 is a sibling of deltas); after 1 + l1: allowed, the upper
|
|
204
|
+
# steps into nothing; + l2: refused, l1 steps in; block text joins like vim's J; a $ line keeps its block below it: refused
|
|
205
|
+
"execute '1normal 3J' | execute '1normal J' | execute '1normal J' | execute '1normal J' | execute '2normal J' | execute '2normal J' | execute '5normal J' | execute '4normal J'",
|
|
206
|
+
# a blank line is dropped; visual J; u 1 + obj: refused, obj would capture sib; obj + sib: refused, obj steps in;
|
|
207
|
+
# t 1<Tab> + deeper: refused, the empty key would vanish; k<Tab>leaf 1 + a comment: allowed; v 1 + a comment that a
|
|
208
|
+
# deeper line follows: allowed, since v 1 steps into nothing and w 2 keeps its parent
|
|
209
|
+
"execute '6normal J' | execute '6normal VjJ' | execute '7normal J' | execute '8normal J' | execute '11normal J' | execute '13normal J' | execute '14normal J'",
|
|
210
|
+
"call writefile(getline(1, '$'), '%s')" % out)
|
|
211
|
+
lines = out.read_text().split("\n")[:-1]
|
|
212
|
+
assert lines == ["deltas\tl1\tposition\tx 1\ty .5", "after 1\tl1", "l2", "$code python", "\tdef f(): return 1", "z 1\tend 1",
|
|
213
|
+
"u 1", "\tobj", "\tsib 2", "last 1", "t 1\t", "\tswallowed 2", "k\tleaf 1\t note", "v 1\t note", "\tw 2"], lines
|
|
214
|
+
joined = dtab.parse("\n".join(lines))
|
|
215
|
+
expected = dtab.parse(text)
|
|
216
|
+
expected["code"] = "def f(): return 1" # the one join that changes a value: two lines of block text, joined like vim does
|
|
217
|
+
assert joined == expected, joined
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_vim_preview():
|
|
221
|
+
""":DtabPreview opens a JSON split of the buffer's tree that follows edits (showing the parser's message when the text is broken), and closes it when repeated."""
|
|
222
|
+
sample = SAMPLES[0]
|
|
223
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
224
|
+
out = Path(directory) / "preview.txt"
|
|
225
|
+
vim("syntax on", "source dtab.vim", "edit " + str(sample),
|
|
226
|
+
"DtabPreview | call writefile(getbufline(b:dtab_preview, 1, '$') + ['---'], '%s')" % out,
|
|
227
|
+
"call setline(1, 'bad|key 1') | doautocmd TextChanged",
|
|
228
|
+
"call writefile(getbufline(b:dtab_preview, 1, '$') + ['---', getbufvar(b:dtab_preview, '&filetype')], '%s', 'a')" % out,
|
|
229
|
+
"DtabPreview | call writefile([bufwinnr(b:dtab_preview)], '%s', 'a')" % out)
|
|
230
|
+
tree, message, closed = out.read_text().split("---\n")
|
|
231
|
+
assert json.loads(tree) == dtab.parse(sample.read_text()), "the preview is not the parsed tree"
|
|
232
|
+
assert message.startswith("dtab line 1: invalid key 'bad|key'"), "the preview did not follow the edit: %r" % message
|
|
233
|
+
assert closed == "json\n-1\n", "expected a json split that the second :DtabPreview closes: %r" % closed
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_vim_plugin_shim():
|
|
237
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
238
|
+
out = Path(directory) / "filetype.txt"
|
|
239
|
+
vim("set rtp+=" + str(ROOT), "runtime! plugin/*.vim", "edit " + str(SAMPLES[0]),
|
|
240
|
+
"call writefile([&filetype], '%s')" % out)
|
|
241
|
+
assert out.read_text().strip() == "dtab", "plugin/dtab.vim did not set the filetype"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_web_demo():
|
|
245
|
+
if not (ROOT / "node_modules" / "puppeteer").is_dir():
|
|
246
|
+
print(" (skipped: puppeteer not installed)")
|
|
247
|
+
return
|
|
248
|
+
subprocess.run(["node", "test/test_web.js"], check=True, cwd=ROOT)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_vscode_grammar():
|
|
252
|
+
# The committed grammar and language configuration are what vscode/make_grammar.py generates.
|
|
253
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
254
|
+
sys.path.insert(0, str(ROOT / "vscode"))
|
|
255
|
+
import make_grammar # noqa: E402
|
|
256
|
+
for written in make_grammar.write(directory):
|
|
257
|
+
committed = ROOT / "vscode" / Path(written).relative_to(directory)
|
|
258
|
+
assert Path(written).read_text() == committed.read_text(), "%s is stale: run python vscode/make_grammar.py" % committed
|
|
259
|
+
if not (ROOT / "node_modules" / "vscode-textmate").is_dir():
|
|
260
|
+
print(" (skipped: vscode-textmate not installed)")
|
|
261
|
+
return
|
|
262
|
+
subprocess.run(["node", "test/test_vscode.js"], check=True, cwd=ROOT)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_vscode_live():
|
|
266
|
+
"""The JSON preview inside the installed VS Code (test/test_vscode_live.js), skipped without @vscode/test-electron or the app."""
|
|
267
|
+
if not (ROOT / "node_modules" / "@vscode" / "test-electron").is_dir() or not Path("/Applications/Visual Studio Code.app").is_dir():
|
|
268
|
+
print(" (skipped: @vscode/test-electron or VS Code not installed)")
|
|
269
|
+
return
|
|
270
|
+
subprocess.run(["node", "test/test_vscode_live.js"], check=True, cwd=ROOT)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
if __name__ == "__main__":
|
|
274
|
+
for test in [test_doctests, test_readers_agree, test_round_trips, test_key_rule, test_js_suite,
|
|
275
|
+
test_vim_highlighting, test_vim_embedded_languages, test_vim_tab_key, test_vim_shift_keys, test_vim_join, test_vim_preview, test_vim_plugin_shim, test_web_demo, test_vscode_grammar, test_vscode_live]:
|
|
276
|
+
test()
|
|
277
|
+
print("ok " + test.__name__)
|
|
278
|
+
print("All dtab tests passed")
|
dtab-0.2.0/test/test_dtab.py
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
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
|
-
and `$` multiline blocks, which the original did not have.
|
|
16
|
-
4. parse(stringify(parse(text))) == parse(text) for every sample.
|
|
17
|
-
5. The key rule: bad keys are rejected with a line number in parse and in stringify, and Python's
|
|
18
|
-
str.isidentifier and the JS Unicode regex agree on a set of probes.
|
|
19
|
-
6. node test/test_dtab.js.
|
|
20
|
-
7. Vim: the syntax groups over test/samples/highlight.dtab match test/expected/highlight.txt byte by byte
|
|
21
|
-
(that sample deliberately contains invalid keys, so it is not parsed), and plugin/dtab.vim sets the
|
|
22
|
-
filetype when the repo is on 'runtimepath', which is what Vundle and vim-plug do.
|
|
23
|
-
8. docs/index.html in headless Chrome (test/test_web.js), skipped with a message if puppeteer is not
|
|
24
|
-
installed (`npm install --no-save puppeteer && npx puppeteer browsers install chrome`).
|
|
25
|
-
9. The VS Code extension's TextMate grammar tokenizes the vim highlight sample the same way vim does
|
|
26
|
-
(test/test_vscode.js), skipped if vscode-textmate is not installed
|
|
27
|
-
(`npm install --no-save vscode-textmate vscode-oniguruma`).
|
|
28
|
-
|
|
29
|
-
Needs: python 3, node, vim.
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
import doctest
|
|
33
|
-
import json
|
|
34
|
-
import subprocess
|
|
35
|
-
import sys
|
|
36
|
-
import tempfile
|
|
37
|
-
from pathlib import Path
|
|
38
|
-
|
|
39
|
-
ROOT = Path(__file__).resolve().parent.parent
|
|
40
|
-
HIGHLIGHT_SAMPLE = ROOT / "test" / "samples" / "highlight.dtab"
|
|
41
|
-
SAMPLES = sorted(path for path in (ROOT / "test" / "samples").glob("*.dtab") if path != HIGHLIGHT_SAMPLE)
|
|
42
|
-
DEVIATING = {"deviations.dtab", "game_config.dtab", "multiline.dtab"} # multiline: $ blocks did not exist in the original
|
|
43
|
-
IDENTIFIER_PROBES = ["café", "变量", "x²", "_x", "0x", "from", "items", "a-b", "ok_1", "ª", "Ⅻ", "℘", "ℕ", "𝔸", "a.b", "é1", "1é"]
|
|
44
|
-
|
|
45
|
-
sys.path.insert(0, str(ROOT))
|
|
46
|
-
import dtab # noqa: E402
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def run(*command):
|
|
50
|
-
"""Query (runs a subprocess). The command's stdout."""
|
|
51
|
-
return subprocess.run(command, capture_output=True, text=True, check=True, cwd=ROOT).stdout
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def raises_value_error(function, *fragments):
|
|
55
|
-
"""Query (calls function). Asserts function() raises ValueError mentioning every fragment."""
|
|
56
|
-
try:
|
|
57
|
-
function()
|
|
58
|
-
except ValueError as error:
|
|
59
|
-
for fragment in fragments:
|
|
60
|
-
assert fragment in str(error), "error %r does not mention %r" % (str(error), fragment)
|
|
61
|
-
return
|
|
62
|
-
raise AssertionError("expected a ValueError mentioning %r" % (fragments,))
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def test_doctests():
|
|
66
|
-
failed, _ = doctest.testmod(dtab)
|
|
67
|
-
assert failed == 0, "dtab.py doctests failed"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def test_readers_agree():
|
|
71
|
-
for path in SAMPLES:
|
|
72
|
-
python_tree = dtab.parse(path.read_text())
|
|
73
|
-
js_tree = json.loads(run("node", "dtab.js", str(path)))
|
|
74
|
-
assert python_tree == js_tree, "%s: dtab.py and dtab.js disagree" % path.name
|
|
75
|
-
if path.name in DEVIATING:
|
|
76
|
-
golden = json.loads((ROOT / "test" / "expected" / (path.stem + ".json")).read_text())
|
|
77
|
-
assert python_tree == golden, "%s: does not match golden" % path.name
|
|
78
|
-
else:
|
|
79
|
-
original = json.loads(run("node", "test/run_original.js", str(path)))
|
|
80
|
-
assert python_tree == original, "%s: does not match the original djson.js" % path.name
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def test_round_trips():
|
|
84
|
-
for path in SAMPLES:
|
|
85
|
-
once = dtab.parse(path.read_text())
|
|
86
|
-
assert dtab.parse(dtab.stringify(once)) == once, "%s: did not round trip" % path.name
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def test_key_rule():
|
|
90
|
-
for text, fragments in [
|
|
91
|
-
("a\tcheckpoint.initial 1", ["line 1", "'checkpoint.initial'"]),
|
|
92
|
-
("ok 1\n\t2nd 2", ["line 2", "'2nd'"]),
|
|
93
|
-
("a-b 1", ["'a-b'"]),
|
|
94
|
-
("~scope\n\tx 1", ["'~scope'"]),
|
|
95
|
-
("log\t@ e", ["'@'"]),
|
|
96
|
-
("a,b.c\tx 1", ["'a,b.c'"]),
|
|
97
|
-
]:
|
|
98
|
-
raises_value_error(lambda: dtab.parse(text), *fragments)
|
|
99
|
-
assert dtab.parse("items 1\nfrom 2\n_private 3\ncafé 4") == {"items": "1", "from": "2", "_private": "3", "café": "4"}
|
|
100
|
-
for tree in [{"a b": "1"}, {"a,b": "1"}, {0: "1"}, {"": "1"}]:
|
|
101
|
-
raises_value_error(lambda: dtab.stringify(tree), "dtab")
|
|
102
|
-
for value in ["x\ny", "x\ty", "a\n\n\tb\n c", "#!/bin/bash\necho hi", ""]:
|
|
103
|
-
assert dtab.parse(dtab.stringify({"a": value})) == {"a": value}, "multiline round trip failed for %r" % value
|
|
104
|
-
python_verdicts = [probe.isidentifier() for probe in IDENTIFIER_PROBES]
|
|
105
|
-
js_verdicts = json.loads(run(
|
|
106
|
-
"node", "-e",
|
|
107
|
-
"const d = require('./dtab.js'); console.log(JSON.stringify(%s.map(p => { try { d.parse(p + ' 1'); return true } catch { return false } })))"
|
|
108
|
-
% json.dumps(IDENTIFIER_PROBES),
|
|
109
|
-
))
|
|
110
|
-
assert python_verdicts == js_verdicts, "identifier rule differs: %s" % [
|
|
111
|
-
(probe, py, js) for probe, py, js in zip(IDENTIFIER_PROBES, python_verdicts, js_verdicts) if py != js]
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def test_js_suite():
|
|
115
|
-
subprocess.run(["node", "test/test_dtab.js"], check=True, cwd=ROOT)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def vim(*commands):
|
|
119
|
-
"""Query (runs vim headless). Runs the -c commands in order in a clean vim and returns nothing."""
|
|
120
|
-
arguments = ["vim", "-N", "-u", "NONE", "-i", "NONE", "-n", "-es", "-c", "set shortmess+=A"] # -n, +=A: no swap files, no ATTENTION prompt
|
|
121
|
-
for command in commands:
|
|
122
|
-
arguments += ["-c", command]
|
|
123
|
-
subprocess.run(arguments + ["-c", "qa!"], check=True, cwd=ROOT)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def test_vim_highlighting():
|
|
127
|
-
with tempfile.TemporaryDirectory() as directory:
|
|
128
|
-
out = Path(directory) / "highlights.txt"
|
|
129
|
-
vim("syntax on", "source dtab.vim", "edit " + str(HIGHLIGHT_SAMPLE),
|
|
130
|
-
"source test/dump_highlights.vim", "call DumpHighlights('%s')" % out)
|
|
131
|
-
got = out.read_text()
|
|
132
|
-
expected = (ROOT / "test" / "expected" / "highlight.txt").read_text()
|
|
133
|
-
assert got == expected, "vim highlighting differs:\nexpected:\n%s\ngot:\n%s" % (expected, got)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def test_vim_embedded_languages():
|
|
137
|
-
"""A tagged $ block (at line start or after other entries) and a shebang block get the language's own groups."""
|
|
138
|
-
with tempfile.TemporaryDirectory() as directory:
|
|
139
|
-
sample = Path(directory) / "embedded.dtab"
|
|
140
|
-
sample.write_text("$query sql\n\tSELECT name FROM t\nconfig\tdb\t$init sql\n\t\tCREATE TABLE t\n$s\n\t#!/bin/bash\n\techo hi\nafter 1\n")
|
|
141
|
-
out = Path(directory) / "groups.txt"
|
|
142
|
-
vim("syntax on", "source dtab.vim", "edit " + str(sample),
|
|
143
|
-
"call writefile([synIDattr(synID(2,2,1),'name'), synIDattr(synID(3,12,1),'name'), synIDattr(synID(4,3,1),'name'), synIDattr(synID(7,2,1),'name'), synIDattr(synID(8,1,1),'name')], '%s')" % out)
|
|
144
|
-
groups = out.read_text().split()
|
|
145
|
-
assert groups == ["sqlStatement", "dtabBlockKey", "sqlStatement", "shStatement", "dtabLeafKey"], groups
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def test_vim_tab_key():
|
|
149
|
-
"""In insert mode, Tab inside a $ block (past the line's tabs) inserts spaces; elsewhere a tab."""
|
|
150
|
-
with tempfile.TemporaryDirectory() as directory:
|
|
151
|
-
sample = Path(directory) / "tab.dtab"
|
|
152
|
-
sample.write_text("$code python\n\tdef f():\n\t\nafter 1\n")
|
|
153
|
-
out = Path(directory) / "lines.txt"
|
|
154
|
-
vim("syntax on", "source dtab.vim", "edit " + str(sample),
|
|
155
|
-
"call cursor(3, 2) | execute \"normal a\\<Tab>return 1\" | call cursor(3, 1) | execute \"normal i\\<Tab>\" | call cursor(4, 6) | execute \"normal i\\<Tab>\"",
|
|
156
|
-
"call writefile([getline(3), getline(4)], '%s')" % out)
|
|
157
|
-
lines = out.read_text().split("\n")
|
|
158
|
-
assert lines[0] == "\t\t return 1", repr(lines[0]) # spaces past the block's tab; a tab at the line start
|
|
159
|
-
assert lines[1] == "after\t 1", repr(lines[1]) # a tab outside the block
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def test_vim_plugin_shim():
|
|
163
|
-
with tempfile.TemporaryDirectory() as directory:
|
|
164
|
-
out = Path(directory) / "filetype.txt"
|
|
165
|
-
vim("set rtp+=" + str(ROOT), "runtime! plugin/*.vim", "edit " + str(SAMPLES[0]),
|
|
166
|
-
"call writefile([&filetype], '%s')" % out)
|
|
167
|
-
assert out.read_text().strip() == "dtab", "plugin/dtab.vim did not set the filetype"
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def test_web_demo():
|
|
171
|
-
if not (ROOT / "node_modules" / "puppeteer").is_dir():
|
|
172
|
-
print(" (skipped: puppeteer not installed)")
|
|
173
|
-
return
|
|
174
|
-
subprocess.run(["node", "test/test_web.js"], check=True, cwd=ROOT)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def test_vscode_grammar():
|
|
178
|
-
if not (ROOT / "node_modules" / "vscode-textmate").is_dir():
|
|
179
|
-
print(" (skipped: vscode-textmate not installed)")
|
|
180
|
-
return
|
|
181
|
-
subprocess.run(["node", "test/test_vscode.js"], check=True, cwd=ROOT)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if __name__ == "__main__":
|
|
185
|
-
for test in [test_doctests, test_readers_agree, test_round_trips, test_key_rule, test_js_suite,
|
|
186
|
-
test_vim_highlighting, test_vim_embedded_languages, test_vim_tab_key, test_vim_plugin_shim, test_web_demo, test_vscode_grammar]:
|
|
187
|
-
test()
|
|
188
|
-
print("ok " + test.__name__)
|
|
189
|
-
print("All dtab tests passed")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|