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.

Files changed (36) hide show
  1. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/.coveragerc +2 -2
  2. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/CHANGELOG.md +5 -0
  3. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/PKG-INFO +1 -1
  4. dp_wizard_templates-0.4.0/dp_wizard_templates/VERSION +1 -0
  5. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/dp_wizard_templates/code_template.py +52 -22
  6. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/index.html +2 -2
  7. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/tests/test_code_template.py +86 -11
  8. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/tests/test_index_html.py +1 -1
  9. dp_wizard_templates-0.3.0/dp_wizard_templates/VERSION +0 -1
  10. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/.flake8 +0 -0
  11. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/.github/workflows/test.yml +0 -0
  12. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/.gitignore +0 -0
  13. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/.nojekyll +0 -0
  14. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/.pre-commit-config.yaml +0 -0
  15. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/.pytest.ini +0 -0
  16. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/LICENSE +0 -0
  17. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/README.md +0 -0
  18. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/dp_wizard_templates/__init__.py +0 -0
  19. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/dp_wizard_templates/converters.py +0 -0
  20. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/dp_wizard_templates/py.typed +0 -0
  21. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/examples/_block_demo.py +0 -0
  22. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/examples/hello-world.html +0 -0
  23. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/examples/hello-world.ipynb +0 -0
  24. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/pyproject.toml +0 -0
  25. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/requirements-dev.in +0 -0
  26. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/requirements-dev.txt +0 -0
  27. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/requirements.in +0 -0
  28. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/requirements.txt +0 -0
  29. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/scripts/changelog.py +0 -0
  30. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/scripts/ci.sh +0 -0
  31. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/scripts/requirements.py +0 -0
  32. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/tests/fixtures/fake-executed.ipynb +0 -0
  33. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/tests/fixtures/fake.ipynb +0 -0
  34. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/tests/fixtures/fake.py +0 -0
  35. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/tests/test_converters.py +0 -0
  36. {dp_wizard_templates-0.3.0 → dp_wizard_templates-0.4.0}/tests/test_misc.py +0 -0
@@ -13,5 +13,5 @@ omit =
13
13
  show_missing = True
14
14
  skip_covered = True
15
15
  fail_under = 100
16
- exclude_also =
17
- template
16
+ # Negative look-ahead: We *do* want to cover test functions.
17
+ exclude_also = def (?!test)\w*template
@@ -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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dp_wizard_templates
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Code templating tools
5
5
  Author-email: The OpenDP Project <info@opendp.org>
6
6
  Description-Content-Type: text/markdown
@@ -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 Exception(f"def and parameters should fit on one line: {first_line}")
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
- body = re.sub(
22
- r"\s*#\s+type:\s+ignore\s*",
23
- "\n",
24
- body,
25
- )
26
- body = re.sub(
27
- r"\s*#\s+noqa:.+",
28
- "",
29
- body,
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 not None:
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 Exception(self._make_message(errors))
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=repr, **kwargs)
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 Exception(self._make_message(errors))
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=746eea6a">
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.</li>
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(Exception, match=r"def and parameters should fit on one line"):
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
- Exception,
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
- Exception, match=r"'ARTICLE' slot not filled, 'VERB' slot not filled"
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(Exception, match=r"no 'NUM' slot to fill with '3'"):
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(Exception, match=r"'CMD' slot not filled"):
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
- Exception,
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(Exception, match=r"no 'SLOT' slot to fill with 'placeholder':"):
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
- Exception,
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(Exception, match=r"'EXTRA' slot not filled"):
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
- Exception,
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