dp_wizard_templates 0.2.0__tar.gz → 0.4.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.
Potentially problematic release.
This version of dp_wizard_templates might be problematic. Click here for more details.
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/.coveragerc +2 -2
- dp_wizard_templates-0.4.0/CHANGELOG.md +19 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/PKG-INFO +1 -1
- dp_wizard_templates-0.4.0/dp_wizard_templates/VERSION +1 -0
- dp_wizard_templates-0.4.0/dp_wizard_templates/code_template.py +209 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/dp_wizard_templates/converters.py +4 -4
- dp_wizard_templates-0.4.0/dp_wizard_templates/py.typed +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/examples/_block_demo.py +1 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/examples/hello-world.html +4 -1
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/examples/hello-world.ipynb +4 -1
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/index.html +25 -9
- dp_wizard_templates-0.4.0/tests/test_code_template.py +260 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/tests/test_index_html.py +23 -5
- dp_wizard_templates-0.2.0/CHANGELOG.md +0 -10
- dp_wizard_templates-0.2.0/dp_wizard_templates/VERSION +0 -1
- dp_wizard_templates-0.2.0/dp_wizard_templates/code_template.py +0 -131
- dp_wizard_templates-0.2.0/tests/test_code_template.py +0 -150
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/.flake8 +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/.github/workflows/test.yml +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/.gitignore +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/.nojekyll +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/.pre-commit-config.yaml +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/.pytest.ini +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/LICENSE +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/README.md +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/dp_wizard_templates/__init__.py +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/pyproject.toml +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/requirements-dev.in +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/requirements-dev.txt +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/requirements.in +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/requirements.txt +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/scripts/changelog.py +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/scripts/ci.sh +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/scripts/requirements.py +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/tests/fixtures/fake-executed.ipynb +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/tests/fixtures/fake.ipynb +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/tests/fixtures/fake.py +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/tests/test_converters.py +0 -0
- {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.4.0}/tests/test_misc.py +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
- Safer template filling, and option to ignore slots [#20](https://github.com/opendp/dp-wizard-templates/pull/20)
|
|
6
|
+
- Check for `root` kwarg, and fix test coverage [#19](https://github.com/opendp/dp-wizard-templates/pull/19)
|
|
7
|
+
|
|
8
|
+
## 0.3.0
|
|
9
|
+
|
|
10
|
+
- Show all errors, and support comment blocks [#12](https://github.com/opendp/dp-wizard-templates/pull/12)
|
|
11
|
+
|
|
12
|
+
## 0.2.0
|
|
13
|
+
|
|
14
|
+
- Link to ghpages [#7](https://github.com/opendp/dp-wizard-templates/pull/7)
|
|
15
|
+
- Add black, and a generated index.html [#6](https://github.com/opendp/dp-wizard-templates/pull/6)
|
|
16
|
+
|
|
17
|
+
## 0.1.0
|
|
18
|
+
|
|
19
|
+
Initial release
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.4.0
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
from typing import Optional, Callable, Iterable
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import inspect
|
|
4
|
+
import re
|
|
5
|
+
import black
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TemplateException(Exception):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_body(func):
|
|
14
|
+
|
|
15
|
+
source_lines = inspect.getsource(func).splitlines()
|
|
16
|
+
first_line = source_lines[0]
|
|
17
|
+
if not re.match(r"def \w+\((\w+(, \w+)*)?\):", first_line.strip()):
|
|
18
|
+
# Parsing to AST and unparsing is a more robust option,
|
|
19
|
+
# but more complicated.
|
|
20
|
+
raise TemplateException(
|
|
21
|
+
f"def and parameters should fit on one line: {first_line}"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# The "def" should not be in the output,
|
|
25
|
+
# and cleandoc handles the first line differently.
|
|
26
|
+
source_lines[0] = ""
|
|
27
|
+
body = inspect.cleandoc("\n".join(source_lines))
|
|
28
|
+
comments_to_strip = [
|
|
29
|
+
r"\s+#\s+type:\s+ignore\s*",
|
|
30
|
+
r"\s+#\s+noqa:.+\s*",
|
|
31
|
+
r"\s+#\s+pragma:\s+no cover\s*",
|
|
32
|
+
]
|
|
33
|
+
for comment_re in comments_to_strip:
|
|
34
|
+
body = re.sub(
|
|
35
|
+
comment_re,
|
|
36
|
+
"\n",
|
|
37
|
+
body,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return body
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _check_repr(value):
|
|
44
|
+
"""
|
|
45
|
+
Confirms that the string returned by repr()
|
|
46
|
+
can be evaluated to recreate the original value.
|
|
47
|
+
Takes a conservative approach by checking
|
|
48
|
+
if the value can be serialized to JSON.
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
json.dumps(value)
|
|
52
|
+
except TypeError as e:
|
|
53
|
+
raise TemplateException(e)
|
|
54
|
+
return repr(value)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Template:
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
template: str | Callable,
|
|
61
|
+
root: Optional[Path] = None,
|
|
62
|
+
ignore: Iterable[str] = ("TODO",),
|
|
63
|
+
):
|
|
64
|
+
if root is None:
|
|
65
|
+
if callable(template):
|
|
66
|
+
self._source = "function template"
|
|
67
|
+
self._template = _get_body(template)
|
|
68
|
+
else:
|
|
69
|
+
self._source = "string template"
|
|
70
|
+
self._template = template
|
|
71
|
+
else:
|
|
72
|
+
if callable(template):
|
|
73
|
+
raise TemplateException(
|
|
74
|
+
"If template is function, root kwarg not allowed"
|
|
75
|
+
)
|
|
76
|
+
else:
|
|
77
|
+
template_name = f"_{template}.py"
|
|
78
|
+
template_path = root / template_name
|
|
79
|
+
self._source = f"'{template_name}'"
|
|
80
|
+
self._template = template_path.read_text()
|
|
81
|
+
# We want a list of the initial slots, because substitutions
|
|
82
|
+
# can produce sequences of upper case letters that could be mistaken for slots.
|
|
83
|
+
self._initial_slots = self._find_slots()
|
|
84
|
+
self._ignore = ignore
|
|
85
|
+
|
|
86
|
+
def _find_slots(self) -> set[str]:
|
|
87
|
+
# Slots:
|
|
88
|
+
# - are all caps or underscores
|
|
89
|
+
# - have word boundary on either side
|
|
90
|
+
# - are at least three characters
|
|
91
|
+
slot_re = r"\b[A-Z][A-Z_]{2,}\b"
|
|
92
|
+
return set(re.findall(slot_re, self._template))
|
|
93
|
+
|
|
94
|
+
def _make_message(self, errors: list[str]) -> str:
|
|
95
|
+
return (
|
|
96
|
+
f"In {self._source}, " + ", ".join(sorted(errors)) + f":\n{self._template}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _loop_kwargs(
|
|
100
|
+
self,
|
|
101
|
+
function: Callable[[str, str, list[str]], None],
|
|
102
|
+
**kwargs,
|
|
103
|
+
) -> None:
|
|
104
|
+
errors = []
|
|
105
|
+
for k, v in kwargs.items():
|
|
106
|
+
function(k, v, errors)
|
|
107
|
+
if errors:
|
|
108
|
+
raise TemplateException(self._make_message(errors))
|
|
109
|
+
|
|
110
|
+
def _fill_inline_slots(
|
|
111
|
+
self,
|
|
112
|
+
stringifier: Callable[[str], str],
|
|
113
|
+
**kwargs,
|
|
114
|
+
) -> None:
|
|
115
|
+
def function(k, v, errors):
|
|
116
|
+
k_re = re.escape(k)
|
|
117
|
+
self._template, count = re.subn(
|
|
118
|
+
rf"\b{k_re}\b", stringifier(v), self._template
|
|
119
|
+
)
|
|
120
|
+
if count == 0:
|
|
121
|
+
errors.append(f"no '{k}' slot to fill with '{v}'")
|
|
122
|
+
|
|
123
|
+
self._loop_kwargs(function, **kwargs)
|
|
124
|
+
|
|
125
|
+
def _fill_block_slots(
|
|
126
|
+
self,
|
|
127
|
+
prefix_re: str,
|
|
128
|
+
splitter: Callable[[str], list[str]],
|
|
129
|
+
**kwargs,
|
|
130
|
+
) -> None:
|
|
131
|
+
def function(k, v, errors):
|
|
132
|
+
if not isinstance(v, str):
|
|
133
|
+
errors.append(f"for '{k}' slot, expected string, not '{v}'")
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
def match_indent(match):
|
|
137
|
+
# This does what we want, but binding is confusing.
|
|
138
|
+
return "\n".join(
|
|
139
|
+
match.group(1) + line for line in splitter(v) # noqa: B023
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
k_re = re.escape(k)
|
|
143
|
+
self._template, count = re.subn(
|
|
144
|
+
rf"^([ \t]*{prefix_re}){k_re}$",
|
|
145
|
+
match_indent,
|
|
146
|
+
self._template,
|
|
147
|
+
flags=re.MULTILINE,
|
|
148
|
+
)
|
|
149
|
+
if count == 0:
|
|
150
|
+
base_message = f"no '{k}' slot to fill with '{v}'"
|
|
151
|
+
if k in self._template:
|
|
152
|
+
note = (
|
|
153
|
+
"comment slots must be prefixed with '#'"
|
|
154
|
+
if prefix_re
|
|
155
|
+
else "block slots must be alone on line"
|
|
156
|
+
)
|
|
157
|
+
errors.append(f"{base_message} ({note})")
|
|
158
|
+
else:
|
|
159
|
+
errors.append(base_message)
|
|
160
|
+
|
|
161
|
+
self._loop_kwargs(function, **kwargs)
|
|
162
|
+
|
|
163
|
+
def fill_expressions(self, **kwargs) -> "Template":
|
|
164
|
+
"""
|
|
165
|
+
Fill in variable names, or dicts or lists represented as strings.
|
|
166
|
+
"""
|
|
167
|
+
self._fill_inline_slots(stringifier=str, **kwargs)
|
|
168
|
+
return self
|
|
169
|
+
|
|
170
|
+
def fill_values(self, **kwargs) -> "Template":
|
|
171
|
+
"""
|
|
172
|
+
Fill in string or numeric values. `repr` is called before filling.
|
|
173
|
+
"""
|
|
174
|
+
self._fill_inline_slots(stringifier=_check_repr, **kwargs)
|
|
175
|
+
return self
|
|
176
|
+
|
|
177
|
+
def fill_code_blocks(self, **kwargs) -> "Template":
|
|
178
|
+
"""
|
|
179
|
+
Fill in code blocks. Slot must be alone on line.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def splitter(s):
|
|
183
|
+
return s.split("\n")
|
|
184
|
+
|
|
185
|
+
self._fill_block_slots(prefix_re=r"", splitter=splitter, **kwargs)
|
|
186
|
+
return self
|
|
187
|
+
|
|
188
|
+
def fill_comment_blocks(self, **kwargs) -> "Template":
|
|
189
|
+
"""
|
|
190
|
+
Fill in comment blocks. Slot must be commented.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
def splitter(s):
|
|
194
|
+
stripped = [line.strip() for line in s.split("\n")]
|
|
195
|
+
return [line for line in stripped if line]
|
|
196
|
+
|
|
197
|
+
self._fill_block_slots(prefix_re=r"#\s+", splitter=splitter, **kwargs)
|
|
198
|
+
return self
|
|
199
|
+
|
|
200
|
+
def finish(self, reformat: bool = False) -> str:
|
|
201
|
+
unfilled_slots = (self._initial_slots & self._find_slots()) - set(self._ignore)
|
|
202
|
+
if unfilled_slots:
|
|
203
|
+
errors = [f"'{slot}' slot not filled" for slot in unfilled_slots]
|
|
204
|
+
raise TemplateException(self._make_message(errors))
|
|
205
|
+
|
|
206
|
+
if reformat:
|
|
207
|
+
self._template = black.format_str(self._template, mode=black.Mode())
|
|
208
|
+
|
|
209
|
+
return self._template
|
|
@@ -30,7 +30,7 @@ class ConversionException(Exception):
|
|
|
30
30
|
|
|
31
31
|
def convert_py_to_nb(
|
|
32
32
|
python_str: str, title: str, execute: bool = False, reformat: bool = True
|
|
33
|
-
):
|
|
33
|
+
) -> str:
|
|
34
34
|
"""
|
|
35
35
|
Given Python code as a string, returns a notebook as a string.
|
|
36
36
|
Calls jupytext as a subprocess:
|
|
@@ -71,13 +71,13 @@ def convert_py_to_nb(
|
|
|
71
71
|
return _clean_nb(json.dumps(nb_dict))
|
|
72
72
|
|
|
73
73
|
|
|
74
|
-
def _stable_hash(lines: list[str]):
|
|
74
|
+
def _stable_hash(lines: list[str]) -> str:
|
|
75
75
|
import hashlib
|
|
76
76
|
|
|
77
77
|
return hashlib.sha1("\n".join(lines).encode()).hexdigest()[:8]
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
def _clean_nb(nb_json: str):
|
|
80
|
+
def _clean_nb(nb_json: str) -> str:
|
|
81
81
|
"""
|
|
82
82
|
Given a notebook as a string of JSON, remove the coda and pip output.
|
|
83
83
|
(The code may produce reports that we do need,
|
|
@@ -104,7 +104,7 @@ def _clean_nb(nb_json: str):
|
|
|
104
104
|
return json.dumps(nb, indent=1)
|
|
105
105
|
|
|
106
106
|
|
|
107
|
-
def convert_nb_to_html(python_nb: str, numbered=True):
|
|
107
|
+
def convert_nb_to_html(python_nb: str, numbered=True) -> str:
|
|
108
108
|
notebook = nbformat.reads(python_nb, as_version=4)
|
|
109
109
|
exporter = nbconvert.HTMLExporter(
|
|
110
110
|
template_name="lab",
|
|
File without changes
|
|
@@ -7523,7 +7523,7 @@ even though the lines are not contiguous</p>
|
|
|
7523
7523
|
</div>
|
|
7524
7524
|
</div>
|
|
7525
7525
|
</div>
|
|
7526
|
-
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell" id="cell-id=
|
|
7526
|
+
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell" id="cell-id=d75dbbc1">
|
|
7527
7527
|
<div class="jp-Cell-inputWrapper" tabindex="0">
|
|
7528
7528
|
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
|
|
7529
7529
|
</div>
|
|
@@ -7535,6 +7535,9 @@ even though the lines are not contiguous</p>
|
|
|
7535
7535
|
<span class="w"> </span><span class="sd">"""</span>
|
|
7536
7536
|
<span class="sd"> This demonstrates how larger blocks of code can be built compositionally.</span>
|
|
7537
7537
|
<span class="sd"> """</span>
|
|
7538
|
+
<span class="c1"># Water freezes at:</span>
|
|
7539
|
+
<span class="c1"># 32 Fahrenheit</span>
|
|
7540
|
+
<span class="c1"># 0 Celsius</span>
|
|
7538
7541
|
<span class="k">if</span> <span class="n">temp_c</span> <span class="o"><</span> <span class="mi">0</span><span class="p">:</span>
|
|
7539
7542
|
<span class="nb">print</span><span class="p">(</span><span class="s2">"It is freezing!"</span><span class="p">)</span>
|
|
7540
7543
|
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
{
|
|
18
18
|
"cell_type": "code",
|
|
19
19
|
"execution_count": 1,
|
|
20
|
-
"id": "
|
|
20
|
+
"id": "d75dbbc1",
|
|
21
21
|
"metadata": {},
|
|
22
22
|
"outputs": [
|
|
23
23
|
{
|
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
" \"\"\"\n",
|
|
34
34
|
" This demonstrates how larger blocks of code can be built compositionally.\n",
|
|
35
35
|
" \"\"\"\n",
|
|
36
|
+
" # Water freezes at:\n",
|
|
37
|
+
" # 32 Fahrenheit\n",
|
|
38
|
+
" # 0 Celsius\n",
|
|
36
39
|
" if temp_c < 0:\n",
|
|
37
40
|
" print(\"It is freezing!\")\n",
|
|
38
41
|
"\n",
|
|
@@ -7574,13 +7574,18 @@ itself can be parsed as Python code, so syntax highlighting and linting still wo
|
|
|
7574
7574
|
</div>
|
|
7575
7575
|
</div>
|
|
7576
7576
|
</div>
|
|
7577
|
-
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell" id="cell-id=
|
|
7577
|
+
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell" id="cell-id=39e34438">
|
|
7578
7578
|
<div class="jp-Cell-inputWrapper" tabindex="0">
|
|
7579
7579
|
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
|
|
7580
7580
|
</div>
|
|
7581
7581
|
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
|
|
7582
7582
|
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput" data-mime-type="text/markdown">
|
|
7583
|
-
<p>Note
|
|
7583
|
+
<p>Note that <code>conditional_print_template</code> is not called: Instead,
|
|
7584
|
+
the <code>inspect</code> package is used to load its source, and the slots
|
|
7585
|
+
in all-caps are filled. Including a parameter list is optional,
|
|
7586
|
+
but providing args which match the names of your slots can prevent
|
|
7587
|
+
warnings from your IDE.</p>
|
|
7588
|
+
<p>Different methods are available on the <code>Template</code> object:</p>
|
|
7584
7589
|
<ul>
|
|
7585
7590
|
<li><code>fill_expressions()</code> fills the slot with verbatim text.
|
|
7586
7591
|
It can be used for an expression like this, or for variable names.</li>
|
|
@@ -7590,14 +7595,15 @@ data structure, as long as it has a usable repr.</li>
|
|
|
7590
7595
|
<li><code>finish()</code> converts the template to a string, and will error
|
|
7591
7596
|
if not all slots have been filled.</li>
|
|
7592
7597
|
</ul>
|
|
7593
|
-
<p>
|
|
7598
|
+
<p>(The next section will introduce <code>fill_code_block()</code> and <code>fill_comment_block()</code>.)</p>
|
|
7599
|
+
<p>Templates can also be standalone files. If a <code>root</code> parameter is provided,
|
|
7594
7600
|
the system will prepend <code>_</code> and append <code>.py</code> and look for a corresponding file.
|
|
7595
7601
|
(The convention of prepending <code>_</code> reminds us that although these files
|
|
7596
7602
|
can be parsed, they should not be imported or executed as-is.)</p>
|
|
7597
7603
|
</div>
|
|
7598
7604
|
</div>
|
|
7599
7605
|
</div>
|
|
7600
|
-
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell jp-mod-noOutputs" id="cell-id=
|
|
7606
|
+
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell jp-mod-noOutputs" id="cell-id=c3374f03">
|
|
7601
7607
|
<div class="jp-Cell-inputWrapper" tabindex="0">
|
|
7602
7608
|
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
|
|
7603
7609
|
</div>
|
|
@@ -7612,7 +7618,14 @@ can be parsed, they should not be imported or executed as-is.)</p>
|
|
|
7612
7618
|
<span class="n">block_demo</span> <span class="o">=</span> <span class="p">(</span>
|
|
7613
7619
|
<span class="n">Template</span><span class="p">(</span><span class="s2">"block_demo"</span><span class="p">,</span> <span class="n">root</span><span class="o">=</span><span class="n">root</span> <span class="o">/</span> <span class="s2">"examples"</span><span class="p">)</span>
|
|
7614
7620
|
<span class="o">.</span><span class="n">fill_expressions</span><span class="p">(</span><span class="n">FUNCTION_NAME</span><span class="o">=</span><span class="s2">"freeze_warning"</span><span class="p">,</span> <span class="n">PARAMS</span><span class="o">=</span><span class="s2">"temp_c"</span><span class="p">)</span>
|
|
7615
|
-
<span class="o">.</span><span class="n">
|
|
7621
|
+
<span class="o">.</span><span class="n">fill_code_blocks</span><span class="p">(</span><span class="n">INNER_BLOCK</span><span class="o">=</span><span class="n">conditional_print</span><span class="p">)</span>
|
|
7622
|
+
<span class="o">.</span><span class="n">fill_comment_blocks</span><span class="p">(</span>
|
|
7623
|
+
<span class="n">COMMENT</span><span class="o">=</span><span class="s2">"""</span>
|
|
7624
|
+
<span class="s2"> Water freezes at:</span>
|
|
7625
|
+
<span class="s2"> 32 Fahrenheit</span>
|
|
7626
|
+
<span class="s2"> 0 Celsius</span>
|
|
7627
|
+
<span class="s2"> """</span>
|
|
7628
|
+
<span class="p">)</span>
|
|
7616
7629
|
<span class="o">.</span><span class="n">finish</span><span class="p">()</span>
|
|
7617
7630
|
<span class="p">)</span>
|
|
7618
7631
|
|
|
@@ -7622,6 +7635,9 @@ can be parsed, they should not be imported or executed as-is.)</p>
|
|
|
7622
7635
|
<span class="s1"> """</span>
|
|
7623
7636
|
<span class="s1"> This demonstrates how larger blocks of code can be built compositionally.</span>
|
|
7624
7637
|
<span class="s1"> """</span>
|
|
7638
|
+
<span class="s1"> # Water freezes at:</span>
|
|
7639
|
+
<span class="s1"> # 32 Fahrenheit</span>
|
|
7640
|
+
<span class="s1"> # 0 Celsius</span>
|
|
7625
7641
|
<span class="s1"> if temp_c < 0:</span>
|
|
7626
7642
|
<span class="s1"> print('It is freezing!')</span>
|
|
7627
7643
|
<span class="s1">'''</span>
|
|
@@ -7683,7 +7699,7 @@ to produce other artifacts without adding clutter.</p>
|
|
|
7683
7699
|
</div>
|
|
7684
7700
|
</div>
|
|
7685
7701
|
</div>
|
|
7686
|
-
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell jp-mod-noOutputs" id="cell-id=
|
|
7702
|
+
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell jp-mod-noOutputs" id="cell-id=ccf10f98">
|
|
7687
7703
|
<div class="jp-Cell-inputWrapper" tabindex="0">
|
|
7688
7704
|
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
|
|
7689
7705
|
</div>
|
|
@@ -7720,7 +7736,7 @@ to produce other artifacts without adding clutter.</p>
|
|
|
7720
7736
|
<span class="n">title</span> <span class="o">=</span> <span class="s2">"Hello World!"</span>
|
|
7721
7737
|
<span class="n">notebook_py</span> <span class="o">=</span> <span class="p">(</span>
|
|
7722
7738
|
<span class="n">Template</span><span class="p">(</span><span class="n">notebook_template</span><span class="p">)</span>
|
|
7723
|
-
<span class="o">.</span><span class="n">
|
|
7739
|
+
<span class="o">.</span><span class="n">fill_code_blocks</span><span class="p">(</span><span class="n">BLOCK</span><span class="o">=</span><span class="n">block_demo</span><span class="p">)</span>
|
|
7724
7740
|
<span class="o">.</span><span class="n">fill_expressions</span><span class="p">(</span><span class="n">FUNCTION_NAME</span><span class="o">=</span><span class="s2">"freeze_warning"</span><span class="p">,</span> <span class="n">TITLE</span><span class="o">=</span><span class="n">title</span><span class="p">)</span>
|
|
7725
7741
|
<span class="o">.</span><span class="n">finish</span><span class="p">()</span>
|
|
7726
7742
|
<span class="p">)</span>
|
|
@@ -7736,7 +7752,7 @@ to produce other artifacts without adding clutter.</p>
|
|
|
7736
7752
|
</div>
|
|
7737
7753
|
</div>
|
|
7738
7754
|
</div>
|
|
7739
|
-
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell" id="cell-id=
|
|
7755
|
+
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell" id="cell-id=50f5c776">
|
|
7740
7756
|
<div class="jp-Cell-inputWrapper" tabindex="0">
|
|
7741
7757
|
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
|
|
7742
7758
|
</div>
|
|
@@ -7756,7 +7772,7 @@ and configure pytest (<code>--ignore-glob '**/templates/</code>) and pyright
|
|
|
7756
7772
|
(<code>ignore = ["**/templates/"]</code>) to ignore them.</li>
|
|
7757
7773
|
<li>For template functions, you might have a consistent naming
|
|
7758
7774
|
convention, and configure coverage (<code>exclude_also = def template_</code>)
|
|
7759
|
-
to exclude them as well
|
|
7775
|
+
to exclude them as well, or else use <code># pragma: no cover</code>.</li>
|
|
7760
7776
|
</ul>
|
|
7761
7777
|
</div>
|
|
7762
7778
|
</div>
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from dp_wizard_templates.code_template import Template, TemplateException
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_non_repr_value():
|
|
8
|
+
def template(VALUE):
|
|
9
|
+
print(VALUE)
|
|
10
|
+
|
|
11
|
+
with pytest.raises(
|
|
12
|
+
TemplateException,
|
|
13
|
+
match=r"Object of type set is not JSON serializable",
|
|
14
|
+
):
|
|
15
|
+
Template(template).fill_values(VALUE={1, 2, 3})
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_ignore_todo_by_default():
|
|
19
|
+
def template():
|
|
20
|
+
print("TODO")
|
|
21
|
+
|
|
22
|
+
assert Template(template).finish() == 'print("TODO")'
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_ignore_kwarg():
|
|
26
|
+
def template():
|
|
27
|
+
print("IGNORE_ME")
|
|
28
|
+
|
|
29
|
+
with pytest.raises(
|
|
30
|
+
TemplateException,
|
|
31
|
+
match=r"'IGNORE_ME' slot not filled",
|
|
32
|
+
):
|
|
33
|
+
Template(template).finish()
|
|
34
|
+
|
|
35
|
+
assert Template(template, ignore={"IGNORE_ME"}).finish() == 'print("IGNORE_ME")'
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_strip_pragma():
|
|
39
|
+
def template():
|
|
40
|
+
pass # pragma: no cover
|
|
41
|
+
|
|
42
|
+
assert Template(template).finish() == "pass\n"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_strip_noqa():
|
|
46
|
+
def template():
|
|
47
|
+
pass # noqa: B950 (explanation here!)
|
|
48
|
+
|
|
49
|
+
assert Template(template).finish() == "pass\n"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_strip_type_ignore():
|
|
53
|
+
def template():
|
|
54
|
+
pass # type: ignore
|
|
55
|
+
|
|
56
|
+
assert Template(template).finish() == "pass\n"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_def_too_long():
|
|
60
|
+
def template(
|
|
61
|
+
BEGIN,
|
|
62
|
+
END,
|
|
63
|
+
):
|
|
64
|
+
print(BEGIN, END)
|
|
65
|
+
|
|
66
|
+
with pytest.raises(
|
|
67
|
+
TemplateException, match=r"def and parameters should fit on one line"
|
|
68
|
+
):
|
|
69
|
+
Template(template)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_def_template():
|
|
73
|
+
def template(BEGIN, END):
|
|
74
|
+
print(BEGIN, END)
|
|
75
|
+
|
|
76
|
+
assert (
|
|
77
|
+
Template(template).fill_values(BEGIN="abc", END="xyz").finish()
|
|
78
|
+
== "print('abc', 'xyz')"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_fill_expressions():
|
|
83
|
+
template = Template("No one VERB the ADJ NOUN!")
|
|
84
|
+
filled = template.fill_expressions(
|
|
85
|
+
VERB="expects",
|
|
86
|
+
ADJ="Spanish",
|
|
87
|
+
NOUN="Inquisition",
|
|
88
|
+
).finish()
|
|
89
|
+
assert filled == "No one expects the Spanish Inquisition!"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_fill_expressions_missing_slots_in_template():
|
|
93
|
+
template = Template("No one ... the ... ...!")
|
|
94
|
+
with pytest.raises(
|
|
95
|
+
TemplateException,
|
|
96
|
+
match=r"no 'ADJ' slot to fill with 'Spanish', "
|
|
97
|
+
r"no 'NOUN' slot to fill with 'Inquisition', "
|
|
98
|
+
r"no 'VERB' slot to fill with 'expects':",
|
|
99
|
+
):
|
|
100
|
+
template.fill_expressions(
|
|
101
|
+
VERB="expects",
|
|
102
|
+
ADJ="Spanish",
|
|
103
|
+
NOUN="Inquisition",
|
|
104
|
+
).finish()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_fill_expressions_extra_slots_in_template():
|
|
108
|
+
template = Template("No one VERB ARTICLE ADJ NOUN!")
|
|
109
|
+
with pytest.raises(
|
|
110
|
+
TemplateException, match=r"'ARTICLE' slot not filled, 'VERB' slot not filled"
|
|
111
|
+
):
|
|
112
|
+
template.fill_expressions(
|
|
113
|
+
ADJ="Spanish",
|
|
114
|
+
NOUN="Inquisition",
|
|
115
|
+
).finish()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_fill_values():
|
|
119
|
+
template = Template("assert [STRING] * NUM == LIST")
|
|
120
|
+
filled = template.fill_values(
|
|
121
|
+
STRING="🙂",
|
|
122
|
+
NUM=3,
|
|
123
|
+
LIST=["🙂", "🙂", "🙂"],
|
|
124
|
+
).finish()
|
|
125
|
+
assert filled == "assert ['🙂'] * 3 == ['🙂', '🙂', '🙂']"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_fill_values_missing_slot_in_template():
|
|
129
|
+
template = Template("assert [STRING] * ... == LIST")
|
|
130
|
+
with pytest.raises(TemplateException, match=r"no 'NUM' slot to fill with '3'"):
|
|
131
|
+
template.fill_values(
|
|
132
|
+
STRING="🙂",
|
|
133
|
+
NUM=3,
|
|
134
|
+
LIST=["🙂", "🙂", "🙂"],
|
|
135
|
+
).finish()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_fill_values_extra_slot_in_template():
|
|
139
|
+
template = Template("CMD [STRING] * NUM == LIST")
|
|
140
|
+
with pytest.raises(TemplateException, match=r"'CMD' slot not filled"):
|
|
141
|
+
template.fill_values(
|
|
142
|
+
STRING="🙂",
|
|
143
|
+
NUM=3,
|
|
144
|
+
LIST=["🙂", "🙂", "🙂"],
|
|
145
|
+
).finish()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_fill_blocks():
|
|
149
|
+
# "OK" is less than three characters, so it is not a slot.
|
|
150
|
+
template = Template(
|
|
151
|
+
"""# MixedCase is OK
|
|
152
|
+
|
|
153
|
+
FIRST
|
|
154
|
+
|
|
155
|
+
with fake:
|
|
156
|
+
my_tuple = (
|
|
157
|
+
# SECOND
|
|
158
|
+
VALUE,
|
|
159
|
+
)
|
|
160
|
+
if True:
|
|
161
|
+
THIRD
|
|
162
|
+
""",
|
|
163
|
+
)
|
|
164
|
+
filled = (
|
|
165
|
+
template.fill_code_blocks(
|
|
166
|
+
FIRST="\n".join(f"import {i}" for i in "abc"),
|
|
167
|
+
THIRD="\n".join(f"{i}()" for i in "xyz"),
|
|
168
|
+
)
|
|
169
|
+
.fill_comment_blocks(
|
|
170
|
+
SECOND="This is a\nmulti-line comment",
|
|
171
|
+
)
|
|
172
|
+
.fill_values(VALUE=42)
|
|
173
|
+
.finish()
|
|
174
|
+
)
|
|
175
|
+
assert (
|
|
176
|
+
filled
|
|
177
|
+
== """# MixedCase is OK
|
|
178
|
+
|
|
179
|
+
import a
|
|
180
|
+
import b
|
|
181
|
+
import c
|
|
182
|
+
|
|
183
|
+
with fake:
|
|
184
|
+
my_tuple = (
|
|
185
|
+
# This is a
|
|
186
|
+
# multi-line comment
|
|
187
|
+
42,
|
|
188
|
+
)
|
|
189
|
+
if True:
|
|
190
|
+
x()
|
|
191
|
+
y()
|
|
192
|
+
z()
|
|
193
|
+
"""
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_fill_comment_block():
|
|
198
|
+
template = Template("# SLOT")
|
|
199
|
+
filled = template.fill_comment_blocks(SLOT="placeholder").finish()
|
|
200
|
+
assert filled == "# placeholder"
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_finish_reformat():
|
|
204
|
+
template = Template("print( 'messy','code!' )#comment")
|
|
205
|
+
filled = template.finish(reformat=True)
|
|
206
|
+
assert filled == 'print("messy", "code!") # comment\n'
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_fill_comment_block_without_comment():
|
|
210
|
+
template = Template("SLOT")
|
|
211
|
+
with pytest.raises(
|
|
212
|
+
TemplateException,
|
|
213
|
+
match=r"In string template, no 'SLOT' slot to fill with 'placeholder' "
|
|
214
|
+
r"\(comment slots must be prefixed with '#'\)",
|
|
215
|
+
):
|
|
216
|
+
template.fill_comment_blocks(SLOT="placeholder").finish()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_fill_blocks_missing_slot_in_template_alone():
|
|
220
|
+
template = Template("No block slot")
|
|
221
|
+
with pytest.raises(
|
|
222
|
+
TemplateException, match=r"no 'SLOT' slot to fill with 'placeholder':"
|
|
223
|
+
):
|
|
224
|
+
template.fill_code_blocks(SLOT="placeholder").finish()
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_fill_blocks_missing_slot_in_template_not_alone():
|
|
228
|
+
template = Template("No block SLOT")
|
|
229
|
+
with pytest.raises(
|
|
230
|
+
TemplateException,
|
|
231
|
+
match=r"no 'SLOT' slot to fill with 'placeholder' "
|
|
232
|
+
r"\(block slots must be alone on line\)",
|
|
233
|
+
):
|
|
234
|
+
template.fill_code_blocks(SLOT="placeholder").finish()
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_fill_blocks_extra_slot_in_template():
|
|
238
|
+
template = Template("EXTRA\nSLOT")
|
|
239
|
+
with pytest.raises(TemplateException, match=r"'EXTRA' slot not filled"):
|
|
240
|
+
template.fill_code_blocks(SLOT="placeholder").finish()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_fill_blocks_not_string():
|
|
244
|
+
template = Template("SOMETHING")
|
|
245
|
+
with pytest.raises(
|
|
246
|
+
TemplateException,
|
|
247
|
+
match=r"for 'SOMETHING' slot, expected string, not '123'",
|
|
248
|
+
):
|
|
249
|
+
template.fill_code_blocks(SOMETHING=123).finish()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_no_root_kwarg_with_function_template():
|
|
253
|
+
def template():
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
with pytest.raises(
|
|
257
|
+
TemplateException,
|
|
258
|
+
match=r"If template is function, root kwarg not allowed",
|
|
259
|
+
):
|
|
260
|
+
Template(template, root=Path("not-allowed"))
|
|
@@ -54,7 +54,13 @@ assert conditional_print == "if temp_c < 0:\n print('It is freezing!')"
|
|
|
54
54
|
|
|
55
55
|
# -
|
|
56
56
|
|
|
57
|
-
# Note
|
|
57
|
+
# Note that `conditional_print_template` is not called: Instead,
|
|
58
|
+
# the `inspect` package is used to load its source, and the slots
|
|
59
|
+
# in all-caps are filled. Including a parameter list is optional,
|
|
60
|
+
# but providing args which match the names of your slots can prevent
|
|
61
|
+
# warnings from your IDE.
|
|
62
|
+
#
|
|
63
|
+
# Different methods are available on the `Template` object:
|
|
58
64
|
# - `fill_expressions()` fills the slot with verbatim text.
|
|
59
65
|
# It can be used for an expression like this, or for variable names.
|
|
60
66
|
# - `fill_values()` fills the slot with the repr of the provided value.
|
|
@@ -63,7 +69,9 @@ assert conditional_print == "if temp_c < 0:\n print('It is freezing!')"
|
|
|
63
69
|
# - `finish()` converts the template to a string, and will error
|
|
64
70
|
# if not all slots have been filled.
|
|
65
71
|
#
|
|
66
|
-
#
|
|
72
|
+
# (The next section will introduce `fill_code_block()` and `fill_comment_block()`.)
|
|
73
|
+
#
|
|
74
|
+
# Templates can also be standalone files. If a `root` parameter is provided,
|
|
67
75
|
# the system will prepend `_` and append `.py` and look for a corresponding file.
|
|
68
76
|
# (The convention of prepending `_` reminds us that although these files
|
|
69
77
|
# can be parsed, they should not be imported or executed as-is.)
|
|
@@ -77,7 +85,14 @@ root = Path(__file__).parent.parent
|
|
|
77
85
|
block_demo = (
|
|
78
86
|
Template("block_demo", root=root / "examples")
|
|
79
87
|
.fill_expressions(FUNCTION_NAME="freeze_warning", PARAMS="temp_c")
|
|
80
|
-
.
|
|
88
|
+
.fill_code_blocks(INNER_BLOCK=conditional_print)
|
|
89
|
+
.fill_comment_blocks(
|
|
90
|
+
COMMENT="""
|
|
91
|
+
Water freezes at:
|
|
92
|
+
32 Fahrenheit
|
|
93
|
+
0 Celsius
|
|
94
|
+
"""
|
|
95
|
+
)
|
|
81
96
|
.finish()
|
|
82
97
|
)
|
|
83
98
|
|
|
@@ -87,6 +102,9 @@ assert (
|
|
|
87
102
|
"""
|
|
88
103
|
This demonstrates how larger blocks of code can be built compositionally.
|
|
89
104
|
"""
|
|
105
|
+
# Water freezes at:
|
|
106
|
+
# 32 Fahrenheit
|
|
107
|
+
# 0 Celsius
|
|
90
108
|
if temp_c < 0:
|
|
91
109
|
print('It is freezing!')
|
|
92
110
|
'''
|
|
@@ -152,7 +170,7 @@ def notebook_template(TITLE, BLOCK, FUNCTION_NAME):
|
|
|
152
170
|
title = "Hello World!"
|
|
153
171
|
notebook_py = (
|
|
154
172
|
Template(notebook_template)
|
|
155
|
-
.
|
|
173
|
+
.fill_code_blocks(BLOCK=block_demo)
|
|
156
174
|
.fill_expressions(FUNCTION_NAME="freeze_warning", TITLE=title)
|
|
157
175
|
.finish()
|
|
158
176
|
)
|
|
@@ -182,7 +200,7 @@ notebook_html = convert_nb_to_html(notebook_ipynb)
|
|
|
182
200
|
# (`ignore = ["**/templates/"]`) to ignore them.
|
|
183
201
|
# - For template functions, you might have a consistent naming
|
|
184
202
|
# convention, and configure coverage (`exclude_also = def template_`)
|
|
185
|
-
# to exclude them as well
|
|
203
|
+
# to exclude them as well, or else use `# pragma: no cover`.
|
|
186
204
|
|
|
187
205
|
# # Coda
|
|
188
206
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.2.0
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import inspect
|
|
2
|
-
import re
|
|
3
|
-
import black
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def _get_body(func):
|
|
7
|
-
|
|
8
|
-
source_lines = inspect.getsource(func).splitlines()
|
|
9
|
-
first_line = source_lines[0]
|
|
10
|
-
if not re.match(r"def \w+\((\w+(, \w+)*)?\):", first_line.strip()):
|
|
11
|
-
# Parsing to AST and unparsing is a more robust option,
|
|
12
|
-
# but more complicated.
|
|
13
|
-
raise Exception(f"def and parameters should fit on one line: {first_line}")
|
|
14
|
-
|
|
15
|
-
# The "def" should not be in the output,
|
|
16
|
-
# and cleandoc handles the first line differently.
|
|
17
|
-
source_lines[0] = ""
|
|
18
|
-
body = inspect.cleandoc("\n".join(source_lines))
|
|
19
|
-
body = re.sub(
|
|
20
|
-
r"\s*#\s+type:\s+ignore\s*",
|
|
21
|
-
"\n",
|
|
22
|
-
body,
|
|
23
|
-
)
|
|
24
|
-
body = re.sub(
|
|
25
|
-
r"\s*#\s+noqa:.+",
|
|
26
|
-
"",
|
|
27
|
-
body,
|
|
28
|
-
)
|
|
29
|
-
return body
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class Template:
|
|
33
|
-
def __init__(self, template, root=None):
|
|
34
|
-
if root is not None:
|
|
35
|
-
template_name = f"_{template}.py"
|
|
36
|
-
template_path = root / template_name
|
|
37
|
-
self._source = f"'{template_name}'"
|
|
38
|
-
self._template = template_path.read_text()
|
|
39
|
-
else:
|
|
40
|
-
if callable(template):
|
|
41
|
-
self._source = "function template"
|
|
42
|
-
self._template = _get_body(template)
|
|
43
|
-
else:
|
|
44
|
-
self._source = "string template"
|
|
45
|
-
self._template = template
|
|
46
|
-
# We want a list of the initial slots, because substitutions
|
|
47
|
-
# can produce sequences of upper case letters that could be mistaken for slots.
|
|
48
|
-
self._initial_slots = self._find_slots()
|
|
49
|
-
|
|
50
|
-
def _find_slots(self):
|
|
51
|
-
# Slots:
|
|
52
|
-
# - are all caps or underscores
|
|
53
|
-
# - have word boundary on either side
|
|
54
|
-
# - are at least three characters
|
|
55
|
-
slot_re = r"\b[A-Z][A-Z_]{2,}\b"
|
|
56
|
-
return set(re.findall(slot_re, self._template))
|
|
57
|
-
|
|
58
|
-
def fill_expressions(self, **kwargs):
|
|
59
|
-
"""
|
|
60
|
-
Fill in variable names, or dicts or lists represented as strings.
|
|
61
|
-
"""
|
|
62
|
-
for k, v in kwargs.items():
|
|
63
|
-
k_re = re.escape(k)
|
|
64
|
-
self._template, count = re.subn(rf"\b{k_re}\b", str(v), self._template)
|
|
65
|
-
if count == 0:
|
|
66
|
-
raise Exception(
|
|
67
|
-
f"No '{k}' slot to fill with '{v}' in "
|
|
68
|
-
f"{self._source}:\n\n{self._template}"
|
|
69
|
-
)
|
|
70
|
-
return self
|
|
71
|
-
|
|
72
|
-
def fill_values(self, **kwargs):
|
|
73
|
-
"""
|
|
74
|
-
Fill in string or numeric values. `repr` is called before filling.
|
|
75
|
-
"""
|
|
76
|
-
for k, v in kwargs.items():
|
|
77
|
-
k_re = re.escape(k)
|
|
78
|
-
self._template, count = re.subn(rf"\b{k_re}\b", repr(v), self._template)
|
|
79
|
-
if count == 0:
|
|
80
|
-
raise Exception(
|
|
81
|
-
f"No '{k}' slot to fill with '{v}' in "
|
|
82
|
-
f"{self._source}:\n\n{self._template}"
|
|
83
|
-
)
|
|
84
|
-
return self
|
|
85
|
-
|
|
86
|
-
def fill_blocks(self, **kwargs):
|
|
87
|
-
"""
|
|
88
|
-
Fill in code blocks. Slot must be alone on line.
|
|
89
|
-
"""
|
|
90
|
-
for k, v in kwargs.items():
|
|
91
|
-
if not isinstance(v, str):
|
|
92
|
-
raise Exception(f"For {k} in {self._source}, expected string, not {v}")
|
|
93
|
-
|
|
94
|
-
def match_indent(match):
|
|
95
|
-
# This does what we want, but binding is confusing.
|
|
96
|
-
return "\n".join(
|
|
97
|
-
match.group(1) + line for line in v.split("\n") # noqa: B023
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
k_re = re.escape(k)
|
|
101
|
-
self._template, count = re.subn(
|
|
102
|
-
rf"^([ \t]*){k_re}$",
|
|
103
|
-
match_indent,
|
|
104
|
-
self._template,
|
|
105
|
-
flags=re.MULTILINE,
|
|
106
|
-
)
|
|
107
|
-
if count == 0:
|
|
108
|
-
base_message = (
|
|
109
|
-
f"No '{k}' slot to fill with '{v}' in "
|
|
110
|
-
f"{self._source}:\n\n{self._template}"
|
|
111
|
-
)
|
|
112
|
-
if k in self._template:
|
|
113
|
-
raise Exception(
|
|
114
|
-
f"Block slots must be alone on line; {base_message}"
|
|
115
|
-
)
|
|
116
|
-
raise Exception(base_message)
|
|
117
|
-
return self
|
|
118
|
-
|
|
119
|
-
def finish(self, reformat=False) -> str:
|
|
120
|
-
unfilled_slots = self._initial_slots & self._find_slots()
|
|
121
|
-
if unfilled_slots:
|
|
122
|
-
slots_str = ", ".join(sorted(f"'{slot}'" for slot in unfilled_slots))
|
|
123
|
-
raise Exception(
|
|
124
|
-
f"{slots_str} slot not filled "
|
|
125
|
-
f"in {self._source}:\n\n{self._template}"
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
if reformat:
|
|
129
|
-
self._template = black.format_str(self._template, mode=black.Mode())
|
|
130
|
-
|
|
131
|
-
return self._template
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from dp_wizard_templates.code_template import Template
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def test_def_too_long():
|
|
6
|
-
def template(
|
|
7
|
-
BEGIN,
|
|
8
|
-
END,
|
|
9
|
-
):
|
|
10
|
-
print(BEGIN, END)
|
|
11
|
-
|
|
12
|
-
with pytest.raises(Exception, match=r"def and parameters should fit on one line"):
|
|
13
|
-
Template(template)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def test_def_template():
|
|
17
|
-
def template(BEGIN, END):
|
|
18
|
-
print(BEGIN, END)
|
|
19
|
-
|
|
20
|
-
assert (
|
|
21
|
-
Template(template).fill_values(BEGIN="abc", END="xyz").finish()
|
|
22
|
-
== "print('abc', 'xyz')"
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def test_fill_expressions():
|
|
27
|
-
template = Template("No one VERB the ADJ NOUN!")
|
|
28
|
-
filled = template.fill_expressions(
|
|
29
|
-
VERB="expects",
|
|
30
|
-
ADJ="Spanish",
|
|
31
|
-
NOUN="Inquisition",
|
|
32
|
-
).finish()
|
|
33
|
-
assert filled == "No one expects the Spanish Inquisition!"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def test_fill_expressions_missing_slot_in_template():
|
|
37
|
-
template = Template("No one ... the ADJ NOUN!")
|
|
38
|
-
with pytest.raises(Exception, match=r"No 'VERB' slot to fill with 'expects'"):
|
|
39
|
-
template.fill_expressions(
|
|
40
|
-
VERB="expects",
|
|
41
|
-
ADJ="Spanish",
|
|
42
|
-
NOUN="Inquisition",
|
|
43
|
-
).finish()
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def test_fill_expressions_extra_slot_in_template():
|
|
47
|
-
template = Template("No one VERB ARTICLE ADJ NOUN!")
|
|
48
|
-
with pytest.raises(Exception, match=r"'ARTICLE' slot not filled"):
|
|
49
|
-
template.fill_expressions(
|
|
50
|
-
VERB="expects",
|
|
51
|
-
ADJ="Spanish",
|
|
52
|
-
NOUN="Inquisition",
|
|
53
|
-
).finish()
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def test_fill_values():
|
|
57
|
-
template = Template("assert [STRING] * NUM == LIST")
|
|
58
|
-
filled = template.fill_values(
|
|
59
|
-
STRING="🙂",
|
|
60
|
-
NUM=3,
|
|
61
|
-
LIST=["🙂", "🙂", "🙂"],
|
|
62
|
-
).finish()
|
|
63
|
-
assert filled == "assert ['🙂'] * 3 == ['🙂', '🙂', '🙂']"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def test_fill_values_missing_slot_in_template():
|
|
67
|
-
template = Template("assert [STRING] * ... == LIST")
|
|
68
|
-
with pytest.raises(Exception, match=r"No 'NUM' slot to fill with '3'"):
|
|
69
|
-
template.fill_values(
|
|
70
|
-
STRING="🙂",
|
|
71
|
-
NUM=3,
|
|
72
|
-
LIST=["🙂", "🙂", "🙂"],
|
|
73
|
-
).finish()
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def test_fill_values_extra_slot_in_template():
|
|
77
|
-
template = Template("CMD [STRING] * NUM == LIST")
|
|
78
|
-
with pytest.raises(Exception, match=r"'CMD' slot not filled"):
|
|
79
|
-
template.fill_values(
|
|
80
|
-
STRING="🙂",
|
|
81
|
-
NUM=3,
|
|
82
|
-
LIST=["🙂", "🙂", "🙂"],
|
|
83
|
-
).finish()
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def test_fill_blocks():
|
|
87
|
-
# "OK" is less than three characters, so it is not a slot.
|
|
88
|
-
template = Template(
|
|
89
|
-
"""# MixedCase is OK
|
|
90
|
-
|
|
91
|
-
FIRST
|
|
92
|
-
|
|
93
|
-
with fake:
|
|
94
|
-
SECOND
|
|
95
|
-
if True:
|
|
96
|
-
THIRD
|
|
97
|
-
""",
|
|
98
|
-
)
|
|
99
|
-
template.fill_blocks(
|
|
100
|
-
FIRST="\n".join(f"import {i}" for i in "abc"),
|
|
101
|
-
SECOND="\n".join(f"f({i})" for i in "123"),
|
|
102
|
-
THIRD="\n".join(f"{i}()" for i in "xyz"),
|
|
103
|
-
)
|
|
104
|
-
assert (
|
|
105
|
-
template.finish()
|
|
106
|
-
== """# MixedCase is OK
|
|
107
|
-
|
|
108
|
-
import a
|
|
109
|
-
import b
|
|
110
|
-
import c
|
|
111
|
-
|
|
112
|
-
with fake:
|
|
113
|
-
f(1)
|
|
114
|
-
f(2)
|
|
115
|
-
f(3)
|
|
116
|
-
if True:
|
|
117
|
-
x()
|
|
118
|
-
y()
|
|
119
|
-
z()
|
|
120
|
-
"""
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def test_fill_blocks_missing_slot_in_template_alone():
|
|
125
|
-
template = Template("No block slot")
|
|
126
|
-
with pytest.raises(Exception, match=r"No 'SLOT' slot"):
|
|
127
|
-
template.fill_blocks(SLOT="placeholder").finish()
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def test_fill_blocks_missing_slot_in_template_not_alone():
|
|
131
|
-
template = Template("No block SLOT")
|
|
132
|
-
with pytest.raises(
|
|
133
|
-
Exception, match=r"Block slots must be alone on line; No 'SLOT' slot"
|
|
134
|
-
):
|
|
135
|
-
template.fill_blocks(SLOT="placeholder").finish()
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def test_fill_blocks_extra_slot_in_template():
|
|
139
|
-
template = Template("EXTRA\nSLOT")
|
|
140
|
-
with pytest.raises(Exception, match=r"'EXTRA' slot not filled"):
|
|
141
|
-
template.fill_blocks(SLOT="placeholder").finish()
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def test_fill_blocks_not_string():
|
|
145
|
-
template = Template("SOMETHING")
|
|
146
|
-
with pytest.raises(
|
|
147
|
-
Exception,
|
|
148
|
-
match=r"For SOMETHING in string template, expected string, not 123",
|
|
149
|
-
):
|
|
150
|
-
template.fill_blocks(SOMETHING=123).finish()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|