dp_wizard_templates 0.3.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.3.0 → dp_wizard_templates-0.4.0}/.coveragerc +2 -2
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/CHANGELOG.md +5 -0
- {dp_wizard_templates-0.3.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.3.0 → dp_wizard_templates-0.4.0}/dp_wizard_templates/code_template.py +52 -22
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/index.html +2 -2
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/tests/test_code_template.py +86 -11
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/tests/test_index_html.py +1 -1
- dp_wizard_templates-0.3.0/dp_wizard_templates/VERSION +0 -1
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/.flake8 +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/.github/workflows/test.yml +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/.gitignore +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/.nojekyll +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/.pre-commit-config.yaml +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/.pytest.ini +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/LICENSE +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/README.md +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/dp_wizard_templates/__init__.py +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/dp_wizard_templates/converters.py +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/dp_wizard_templates/py.typed +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/examples/_block_demo.py +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/examples/hello-world.html +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/examples/hello-world.ipynb +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/pyproject.toml +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/requirements-dev.in +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/requirements-dev.txt +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/requirements.in +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/requirements.txt +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/scripts/changelog.py +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/scripts/ci.sh +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/scripts/requirements.py +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/tests/fixtures/fake-executed.ipynb +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/tests/fixtures/fake.ipynb +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/tests/fixtures/fake.py +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/tests/test_converters.py +0 -0
- {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/tests/test_misc.py +0 -0
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
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
|
+
|
|
3
8
|
## 0.3.0
|
|
4
9
|
|
|
5
10
|
- Show all errors, and support comment blocks [#12](https://github.com/opendp/dp-wizard-templates/pull/12)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.4.0
|
{dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/dp_wizard_templates/code_template.py
RENAMED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
from typing import Optional, Callable
|
|
1
|
+
from typing import Optional, Callable, Iterable
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
import inspect
|
|
4
4
|
import re
|
|
5
5
|
import black
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TemplateException(Exception):
|
|
10
|
+
pass
|
|
6
11
|
|
|
7
12
|
|
|
8
13
|
def _get_body(func):
|
|
@@ -12,46 +17,71 @@ def _get_body(func):
|
|
|
12
17
|
if not re.match(r"def \w+\((\w+(, \w+)*)?\):", first_line.strip()):
|
|
13
18
|
# Parsing to AST and unparsing is a more robust option,
|
|
14
19
|
# but more complicated.
|
|
15
|
-
raise
|
|
20
|
+
raise TemplateException(
|
|
21
|
+
f"def and parameters should fit on one line: {first_line}"
|
|
22
|
+
)
|
|
16
23
|
|
|
17
24
|
# The "def" should not be in the output,
|
|
18
25
|
# and cleandoc handles the first line differently.
|
|
19
26
|
source_lines[0] = ""
|
|
20
27
|
body = inspect.cleandoc("\n".join(source_lines))
|
|
21
|
-
|
|
22
|
-
r"\s
|
|
23
|
-
"\
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
|
|
31
40
|
return body
|
|
32
41
|
|
|
33
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
|
+
|
|
34
57
|
class Template:
|
|
35
58
|
def __init__(
|
|
36
59
|
self,
|
|
37
60
|
template: str | Callable,
|
|
38
61
|
root: Optional[Path] = None,
|
|
62
|
+
ignore: Iterable[str] = ("TODO",),
|
|
39
63
|
):
|
|
40
|
-
if root is
|
|
41
|
-
template_name = f"_{template}.py"
|
|
42
|
-
template_path = root / template_name
|
|
43
|
-
self._source = f"'{template_name}'"
|
|
44
|
-
self._template = template_path.read_text()
|
|
45
|
-
else:
|
|
64
|
+
if root is None:
|
|
46
65
|
if callable(template):
|
|
47
66
|
self._source = "function template"
|
|
48
67
|
self._template = _get_body(template)
|
|
49
68
|
else:
|
|
50
69
|
self._source = "string template"
|
|
51
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()
|
|
52
81
|
# We want a list of the initial slots, because substitutions
|
|
53
82
|
# can produce sequences of upper case letters that could be mistaken for slots.
|
|
54
83
|
self._initial_slots = self._find_slots()
|
|
84
|
+
self._ignore = ignore
|
|
55
85
|
|
|
56
86
|
def _find_slots(self) -> set[str]:
|
|
57
87
|
# Slots:
|
|
@@ -75,7 +105,7 @@ class Template:
|
|
|
75
105
|
for k, v in kwargs.items():
|
|
76
106
|
function(k, v, errors)
|
|
77
107
|
if errors:
|
|
78
|
-
raise
|
|
108
|
+
raise TemplateException(self._make_message(errors))
|
|
79
109
|
|
|
80
110
|
def _fill_inline_slots(
|
|
81
111
|
self,
|
|
@@ -141,7 +171,7 @@ class Template:
|
|
|
141
171
|
"""
|
|
142
172
|
Fill in string or numeric values. `repr` is called before filling.
|
|
143
173
|
"""
|
|
144
|
-
self._fill_inline_slots(stringifier=
|
|
174
|
+
self._fill_inline_slots(stringifier=_check_repr, **kwargs)
|
|
145
175
|
return self
|
|
146
176
|
|
|
147
177
|
def fill_code_blocks(self, **kwargs) -> "Template":
|
|
@@ -168,10 +198,10 @@ class Template:
|
|
|
168
198
|
return self
|
|
169
199
|
|
|
170
200
|
def finish(self, reformat: bool = False) -> str:
|
|
171
|
-
unfilled_slots = self._initial_slots & self._find_slots()
|
|
201
|
+
unfilled_slots = (self._initial_slots & self._find_slots()) - set(self._ignore)
|
|
172
202
|
if unfilled_slots:
|
|
173
203
|
errors = [f"'{slot}' slot not filled" for slot in unfilled_slots]
|
|
174
|
-
raise
|
|
204
|
+
raise TemplateException(self._make_message(errors))
|
|
175
205
|
|
|
176
206
|
if reformat:
|
|
177
207
|
self._template = black.format_str(self._template, mode=black.Mode())
|
|
@@ -7752,7 +7752,7 @@ to produce other artifacts without adding clutter.</p>
|
|
|
7752
7752
|
</div>
|
|
7753
7753
|
</div>
|
|
7754
7754
|
</div>
|
|
7755
|
-
<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">
|
|
7756
7756
|
<div class="jp-Cell-inputWrapper" tabindex="0">
|
|
7757
7757
|
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
|
|
7758
7758
|
</div>
|
|
@@ -7772,7 +7772,7 @@ and configure pytest (<code>--ignore-glob '**/templates/</code>) and pyright
|
|
|
7772
7772
|
(<code>ignore = ["**/templates/"]</code>) to ignore them.</li>
|
|
7773
7773
|
<li>For template functions, you might have a consistent naming
|
|
7774
7774
|
convention, and configure coverage (<code>exclude_also = def template_</code>)
|
|
7775
|
-
to exclude them as well
|
|
7775
|
+
to exclude them as well, or else use <code># pragma: no cover</code>.</li>
|
|
7776
7776
|
</ul>
|
|
7777
7777
|
</div>
|
|
7778
7778
|
</div>
|
|
@@ -1,5 +1,59 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
1
3
|
import pytest
|
|
2
|
-
from dp_wizard_templates.code_template import Template
|
|
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"
|
|
3
57
|
|
|
4
58
|
|
|
5
59
|
def test_def_too_long():
|
|
@@ -9,7 +63,9 @@ def test_def_too_long():
|
|
|
9
63
|
):
|
|
10
64
|
print(BEGIN, END)
|
|
11
65
|
|
|
12
|
-
with pytest.raises(
|
|
66
|
+
with pytest.raises(
|
|
67
|
+
TemplateException, match=r"def and parameters should fit on one line"
|
|
68
|
+
):
|
|
13
69
|
Template(template)
|
|
14
70
|
|
|
15
71
|
|
|
@@ -36,7 +92,7 @@ def test_fill_expressions():
|
|
|
36
92
|
def test_fill_expressions_missing_slots_in_template():
|
|
37
93
|
template = Template("No one ... the ... ...!")
|
|
38
94
|
with pytest.raises(
|
|
39
|
-
|
|
95
|
+
TemplateException,
|
|
40
96
|
match=r"no 'ADJ' slot to fill with 'Spanish', "
|
|
41
97
|
r"no 'NOUN' slot to fill with 'Inquisition', "
|
|
42
98
|
r"no 'VERB' slot to fill with 'expects':",
|
|
@@ -51,7 +107,7 @@ def test_fill_expressions_missing_slots_in_template():
|
|
|
51
107
|
def test_fill_expressions_extra_slots_in_template():
|
|
52
108
|
template = Template("No one VERB ARTICLE ADJ NOUN!")
|
|
53
109
|
with pytest.raises(
|
|
54
|
-
|
|
110
|
+
TemplateException, match=r"'ARTICLE' slot not filled, 'VERB' slot not filled"
|
|
55
111
|
):
|
|
56
112
|
template.fill_expressions(
|
|
57
113
|
ADJ="Spanish",
|
|
@@ -71,7 +127,7 @@ def test_fill_values():
|
|
|
71
127
|
|
|
72
128
|
def test_fill_values_missing_slot_in_template():
|
|
73
129
|
template = Template("assert [STRING] * ... == LIST")
|
|
74
|
-
with pytest.raises(
|
|
130
|
+
with pytest.raises(TemplateException, match=r"no 'NUM' slot to fill with '3'"):
|
|
75
131
|
template.fill_values(
|
|
76
132
|
STRING="🙂",
|
|
77
133
|
NUM=3,
|
|
@@ -81,7 +137,7 @@ def test_fill_values_missing_slot_in_template():
|
|
|
81
137
|
|
|
82
138
|
def test_fill_values_extra_slot_in_template():
|
|
83
139
|
template = Template("CMD [STRING] * NUM == LIST")
|
|
84
|
-
with pytest.raises(
|
|
140
|
+
with pytest.raises(TemplateException, match=r"'CMD' slot not filled"):
|
|
85
141
|
template.fill_values(
|
|
86
142
|
STRING="🙂",
|
|
87
143
|
NUM=3,
|
|
@@ -144,10 +200,16 @@ def test_fill_comment_block():
|
|
|
144
200
|
assert filled == "# placeholder"
|
|
145
201
|
|
|
146
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
|
+
|
|
147
209
|
def test_fill_comment_block_without_comment():
|
|
148
210
|
template = Template("SLOT")
|
|
149
211
|
with pytest.raises(
|
|
150
|
-
|
|
212
|
+
TemplateException,
|
|
151
213
|
match=r"In string template, no 'SLOT' slot to fill with 'placeholder' "
|
|
152
214
|
r"\(comment slots must be prefixed with '#'\)",
|
|
153
215
|
):
|
|
@@ -156,14 +218,16 @@ def test_fill_comment_block_without_comment():
|
|
|
156
218
|
|
|
157
219
|
def test_fill_blocks_missing_slot_in_template_alone():
|
|
158
220
|
template = Template("No block slot")
|
|
159
|
-
with pytest.raises(
|
|
221
|
+
with pytest.raises(
|
|
222
|
+
TemplateException, match=r"no 'SLOT' slot to fill with 'placeholder':"
|
|
223
|
+
):
|
|
160
224
|
template.fill_code_blocks(SLOT="placeholder").finish()
|
|
161
225
|
|
|
162
226
|
|
|
163
227
|
def test_fill_blocks_missing_slot_in_template_not_alone():
|
|
164
228
|
template = Template("No block SLOT")
|
|
165
229
|
with pytest.raises(
|
|
166
|
-
|
|
230
|
+
TemplateException,
|
|
167
231
|
match=r"no 'SLOT' slot to fill with 'placeholder' "
|
|
168
232
|
r"\(block slots must be alone on line\)",
|
|
169
233
|
):
|
|
@@ -172,14 +236,25 @@ def test_fill_blocks_missing_slot_in_template_not_alone():
|
|
|
172
236
|
|
|
173
237
|
def test_fill_blocks_extra_slot_in_template():
|
|
174
238
|
template = Template("EXTRA\nSLOT")
|
|
175
|
-
with pytest.raises(
|
|
239
|
+
with pytest.raises(TemplateException, match=r"'EXTRA' slot not filled"):
|
|
176
240
|
template.fill_code_blocks(SLOT="placeholder").finish()
|
|
177
241
|
|
|
178
242
|
|
|
179
243
|
def test_fill_blocks_not_string():
|
|
180
244
|
template = Template("SOMETHING")
|
|
181
245
|
with pytest.raises(
|
|
182
|
-
|
|
246
|
+
TemplateException,
|
|
183
247
|
match=r"for 'SOMETHING' slot, expected string, not '123'",
|
|
184
248
|
):
|
|
185
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"))
|
|
@@ -200,7 +200,7 @@ notebook_html = convert_nb_to_html(notebook_ipynb)
|
|
|
200
200
|
# (`ignore = ["**/templates/"]`) to ignore them.
|
|
201
201
|
# - For template functions, you might have a consistent naming
|
|
202
202
|
# convention, and configure coverage (`exclude_also = def template_`)
|
|
203
|
-
# to exclude them as well
|
|
203
|
+
# to exclude them as well, or else use `# pragma: no cover`.
|
|
204
204
|
|
|
205
205
|
# # Coda
|
|
206
206
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.3.0
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|