dp_wizard_templates 0.2.0__tar.gz → 0.3.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.2.0 → dp_wizard_templates-0.3.0}/CHANGELOG.md +4 -0
  2. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/PKG-INFO +1 -1
  3. dp_wizard_templates-0.3.0/dp_wizard_templates/VERSION +1 -0
  4. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/dp_wizard_templates/code_template.py +94 -46
  5. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/dp_wizard_templates/converters.py +4 -4
  6. dp_wizard_templates-0.3.0/dp_wizard_templates/py.typed +0 -0
  7. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/examples/_block_demo.py +1 -0
  8. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/examples/hello-world.html +4 -1
  9. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/examples/hello-world.ipynb +4 -1
  10. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/index.html +23 -7
  11. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/tests/test_code_template.py +58 -23
  12. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/tests/test_index_html.py +22 -4
  13. dp_wizard_templates-0.2.0/dp_wizard_templates/VERSION +0 -1
  14. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/.coveragerc +0 -0
  15. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/.flake8 +0 -0
  16. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/.github/workflows/test.yml +0 -0
  17. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/.gitignore +0 -0
  18. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/.nojekyll +0 -0
  19. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/.pre-commit-config.yaml +0 -0
  20. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/.pytest.ini +0 -0
  21. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/LICENSE +0 -0
  22. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/README.md +0 -0
  23. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/dp_wizard_templates/__init__.py +0 -0
  24. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/pyproject.toml +0 -0
  25. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/requirements-dev.in +0 -0
  26. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/requirements-dev.txt +0 -0
  27. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/requirements.in +0 -0
  28. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/requirements.txt +0 -0
  29. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/scripts/changelog.py +0 -0
  30. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/scripts/ci.sh +0 -0
  31. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/scripts/requirements.py +0 -0
  32. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/tests/fixtures/fake-executed.ipynb +0 -0
  33. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/tests/fixtures/fake.ipynb +0 -0
  34. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/tests/fixtures/fake.py +0 -0
  35. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/tests/test_converters.py +0 -0
  36. {dp_wizard_templates-0.2.0 → dp_wizard_templates-0.3.0}/tests/test_misc.py +0 -0
@@ -1,5 +1,9 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.3.0
4
+
5
+ - Show all errors, and support comment blocks [#12](https://github.com/opendp/dp-wizard-templates/pull/12)
6
+
3
7
  ## 0.2.0
4
8
 
5
9
  - Link to ghpages [#7](https://github.com/opendp/dp-wizard-templates/pull/7)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dp_wizard_templates
3
- Version: 0.2.0
3
+ Version: 0.3.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,3 +1,5 @@
1
+ from typing import Optional, Callable
2
+ from pathlib import Path
1
3
  import inspect
2
4
  import re
3
5
  import black
@@ -30,7 +32,11 @@ def _get_body(func):
30
32
 
31
33
 
32
34
  class Template:
33
- def __init__(self, template, root=None):
35
+ def __init__(
36
+ self,
37
+ template: str | Callable,
38
+ root: Optional[Path] = None,
39
+ ):
34
40
  if root is not None:
35
41
  template_name = f"_{template}.py"
36
42
  template_path = root / template_name
@@ -47,7 +53,7 @@ class Template:
47
53
  # can produce sequences of upper case letters that could be mistaken for slots.
48
54
  self._initial_slots = self._find_slots()
49
55
 
50
- def _find_slots(self):
56
+ def _find_slots(self) -> set[str]:
51
57
  # Slots:
52
58
  # - are all caps or underscores
53
59
  # - have word boundary on either side
@@ -55,75 +61,117 @@ class Template:
55
61
  slot_re = r"\b[A-Z][A-Z_]{2,}\b"
56
62
  return set(re.findall(slot_re, self._template))
57
63
 
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
64
+ def _make_message(self, errors: list[str]) -> str:
65
+ return (
66
+ f"In {self._source}, " + ", ".join(sorted(errors)) + f":\n{self._template}"
67
+ )
71
68
 
72
- def fill_values(self, **kwargs):
73
- """
74
- Fill in string or numeric values. `repr` is called before filling.
75
- """
69
+ def _loop_kwargs(
70
+ self,
71
+ function: Callable[[str, str, list[str]], None],
72
+ **kwargs,
73
+ ) -> None:
74
+ errors = []
76
75
  for k, v in kwargs.items():
76
+ function(k, v, errors)
77
+ if errors:
78
+ raise Exception(self._make_message(errors))
79
+
80
+ def _fill_inline_slots(
81
+ self,
82
+ stringifier: Callable[[str], str],
83
+ **kwargs,
84
+ ) -> None:
85
+ def function(k, v, errors):
77
86
  k_re = re.escape(k)
78
- self._template, count = re.subn(rf"\b{k_re}\b", repr(v), self._template)
87
+ self._template, count = re.subn(
88
+ rf"\b{k_re}\b", stringifier(v), self._template
89
+ )
79
90
  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
91
+ errors.append(f"no '{k}' slot to fill with '{v}'")
85
92
 
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():
93
+ self._loop_kwargs(function, **kwargs)
94
+
95
+ def _fill_block_slots(
96
+ self,
97
+ prefix_re: str,
98
+ splitter: Callable[[str], list[str]],
99
+ **kwargs,
100
+ ) -> None:
101
+ def function(k, v, errors):
91
102
  if not isinstance(v, str):
92
- raise Exception(f"For {k} in {self._source}, expected string, not {v}")
103
+ errors.append(f"for '{k}' slot, expected string, not '{v}'")
104
+ return
93
105
 
94
106
  def match_indent(match):
95
107
  # This does what we want, but binding is confusing.
96
108
  return "\n".join(
97
- match.group(1) + line for line in v.split("\n") # noqa: B023
109
+ match.group(1) + line for line in splitter(v) # noqa: B023
98
110
  )
99
111
 
100
112
  k_re = re.escape(k)
101
113
  self._template, count = re.subn(
102
- rf"^([ \t]*){k_re}$",
114
+ rf"^([ \t]*{prefix_re}){k_re}$",
103
115
  match_indent,
104
116
  self._template,
105
117
  flags=re.MULTILINE,
106
118
  )
107
119
  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
- )
120
+ base_message = f"no '{k}' slot to fill with '{v}'"
112
121
  if k in self._template:
113
- raise Exception(
114
- f"Block slots must be alone on line; {base_message}"
122
+ note = (
123
+ "comment slots must be prefixed with '#'"
124
+ if prefix_re
125
+ else "block slots must be alone on line"
115
126
  )
116
- raise Exception(base_message)
127
+ errors.append(f"{base_message} ({note})")
128
+ else:
129
+ errors.append(base_message)
130
+
131
+ self._loop_kwargs(function, **kwargs)
132
+
133
+ def fill_expressions(self, **kwargs) -> "Template":
134
+ """
135
+ Fill in variable names, or dicts or lists represented as strings.
136
+ """
137
+ self._fill_inline_slots(stringifier=str, **kwargs)
138
+ return self
139
+
140
+ def fill_values(self, **kwargs) -> "Template":
141
+ """
142
+ Fill in string or numeric values. `repr` is called before filling.
143
+ """
144
+ self._fill_inline_slots(stringifier=repr, **kwargs)
117
145
  return self
118
146
 
119
- def finish(self, reformat=False) -> str:
147
+ def fill_code_blocks(self, **kwargs) -> "Template":
148
+ """
149
+ Fill in code blocks. Slot must be alone on line.
150
+ """
151
+
152
+ def splitter(s):
153
+ return s.split("\n")
154
+
155
+ self._fill_block_slots(prefix_re=r"", splitter=splitter, **kwargs)
156
+ return self
157
+
158
+ def fill_comment_blocks(self, **kwargs) -> "Template":
159
+ """
160
+ Fill in comment blocks. Slot must be commented.
161
+ """
162
+
163
+ def splitter(s):
164
+ stripped = [line.strip() for line in s.split("\n")]
165
+ return [line for line in stripped if line]
166
+
167
+ self._fill_block_slots(prefix_re=r"#\s+", splitter=splitter, **kwargs)
168
+ return self
169
+
170
+ def finish(self, reformat: bool = False) -> str:
120
171
  unfilled_slots = self._initial_slots & self._find_slots()
121
172
  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
- )
173
+ errors = [f"'{slot}' slot not filled" for slot in unfilled_slots]
174
+ raise Exception(self._make_message(errors))
127
175
 
128
176
  if reformat:
129
177
  self._template = black.format_str(self._template, mode=black.Mode())
@@ -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
@@ -2,4 +2,5 @@ def FUNCTION_NAME(PARAMS):
2
2
  """
3
3
  This demonstrates how larger blocks of code can be built compositionally.
4
4
  """
5
+ # COMMENT
5
6
  INNER_BLOCK
@@ -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=bbe7eb3d">
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">&lt;</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": "bbe7eb3d",
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=debe102a">
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 the different methods used:</p>
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>Templates can also be in standalone files. If a <code>root</code> parameter is provided,
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=4fd80ead">
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">fill_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>
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 &lt; 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=b5dc8879">
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">fill_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>
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>
@@ -33,9 +33,14 @@ def test_fill_expressions():
33
33
  assert filled == "No one expects the Spanish Inquisition!"
34
34
 
35
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'"):
36
+ def test_fill_expressions_missing_slots_in_template():
37
+ template = Template("No one ... the ... ...!")
38
+ with pytest.raises(
39
+ Exception,
40
+ match=r"no 'ADJ' slot to fill with 'Spanish', "
41
+ r"no 'NOUN' slot to fill with 'Inquisition', "
42
+ r"no 'VERB' slot to fill with 'expects':",
43
+ ):
39
44
  template.fill_expressions(
40
45
  VERB="expects",
41
46
  ADJ="Spanish",
@@ -43,11 +48,12 @@ def test_fill_expressions_missing_slot_in_template():
43
48
  ).finish()
44
49
 
45
50
 
46
- def test_fill_expressions_extra_slot_in_template():
51
+ def test_fill_expressions_extra_slots_in_template():
47
52
  template = Template("No one VERB ARTICLE ADJ NOUN!")
48
- with pytest.raises(Exception, match=r"'ARTICLE' slot not filled"):
53
+ with pytest.raises(
54
+ Exception, match=r"'ARTICLE' slot not filled, 'VERB' slot not filled"
55
+ ):
49
56
  template.fill_expressions(
50
- VERB="expects",
51
57
  ADJ="Spanish",
52
58
  NOUN="Inquisition",
53
59
  ).finish()
@@ -65,7 +71,7 @@ def test_fill_values():
65
71
 
66
72
  def test_fill_values_missing_slot_in_template():
67
73
  template = Template("assert [STRING] * ... == LIST")
68
- with pytest.raises(Exception, match=r"No 'NUM' slot to fill with '3'"):
74
+ with pytest.raises(Exception, match=r"no 'NUM' slot to fill with '3'"):
69
75
  template.fill_values(
70
76
  STRING="🙂",
71
77
  NUM=3,
@@ -91,18 +97,27 @@ def test_fill_blocks():
91
97
  FIRST
92
98
 
93
99
  with fake:
94
- SECOND
100
+ my_tuple = (
101
+ # SECOND
102
+ VALUE,
103
+ )
95
104
  if True:
96
105
  THIRD
97
106
  """,
98
107
  )
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"),
108
+ filled = (
109
+ template.fill_code_blocks(
110
+ FIRST="\n".join(f"import {i}" for i in "abc"),
111
+ THIRD="\n".join(f"{i}()" for i in "xyz"),
112
+ )
113
+ .fill_comment_blocks(
114
+ SECOND="This is a\nmulti-line comment",
115
+ )
116
+ .fill_values(VALUE=42)
117
+ .finish()
103
118
  )
104
119
  assert (
105
- template.finish()
120
+ filled
106
121
  == """# MixedCase is OK
107
122
 
108
123
  import a
@@ -110,9 +125,11 @@ import b
110
125
  import c
111
126
 
112
127
  with fake:
113
- f(1)
114
- f(2)
115
- f(3)
128
+ my_tuple = (
129
+ # This is a
130
+ # multi-line comment
131
+ 42,
132
+ )
116
133
  if True:
117
134
  x()
118
135
  y()
@@ -121,30 +138,48 @@ with fake:
121
138
  )
122
139
 
123
140
 
141
+ def test_fill_comment_block():
142
+ template = Template("# SLOT")
143
+ filled = template.fill_comment_blocks(SLOT="placeholder").finish()
144
+ assert filled == "# placeholder"
145
+
146
+
147
+ def test_fill_comment_block_without_comment():
148
+ template = Template("SLOT")
149
+ with pytest.raises(
150
+ Exception,
151
+ match=r"In string template, no 'SLOT' slot to fill with 'placeholder' "
152
+ r"\(comment slots must be prefixed with '#'\)",
153
+ ):
154
+ template.fill_comment_blocks(SLOT="placeholder").finish()
155
+
156
+
124
157
  def test_fill_blocks_missing_slot_in_template_alone():
125
158
  template = Template("No block slot")
126
- with pytest.raises(Exception, match=r"No 'SLOT' slot"):
127
- template.fill_blocks(SLOT="placeholder").finish()
159
+ with pytest.raises(Exception, match=r"no 'SLOT' slot to fill with 'placeholder':"):
160
+ template.fill_code_blocks(SLOT="placeholder").finish()
128
161
 
129
162
 
130
163
  def test_fill_blocks_missing_slot_in_template_not_alone():
131
164
  template = Template("No block SLOT")
132
165
  with pytest.raises(
133
- Exception, match=r"Block slots must be alone on line; No 'SLOT' slot"
166
+ Exception,
167
+ match=r"no 'SLOT' slot to fill with 'placeholder' "
168
+ r"\(block slots must be alone on line\)",
134
169
  ):
135
- template.fill_blocks(SLOT="placeholder").finish()
170
+ template.fill_code_blocks(SLOT="placeholder").finish()
136
171
 
137
172
 
138
173
  def test_fill_blocks_extra_slot_in_template():
139
174
  template = Template("EXTRA\nSLOT")
140
175
  with pytest.raises(Exception, match=r"'EXTRA' slot not filled"):
141
- template.fill_blocks(SLOT="placeholder").finish()
176
+ template.fill_code_blocks(SLOT="placeholder").finish()
142
177
 
143
178
 
144
179
  def test_fill_blocks_not_string():
145
180
  template = Template("SOMETHING")
146
181
  with pytest.raises(
147
182
  Exception,
148
- match=r"For SOMETHING in string template, expected string, not 123",
183
+ match=r"for 'SOMETHING' slot, expected string, not '123'",
149
184
  ):
150
- template.fill_blocks(SOMETHING=123).finish()
185
+ template.fill_code_blocks(SOMETHING=123).finish()
@@ -54,7 +54,13 @@ assert conditional_print == "if temp_c < 0:\n print('It is freezing!')"
54
54
 
55
55
  # -
56
56
 
57
- # Note the different methods used:
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
- # Templates can also be in standalone files. If a `root` parameter is provided,
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
- .fill_blocks(INNER_BLOCK=conditional_print)
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
- .fill_blocks(BLOCK=block_demo)
173
+ .fill_code_blocks(BLOCK=block_demo)
156
174
  .fill_expressions(FUNCTION_NAME="freeze_warning", TITLE=title)
157
175
  .finish()
158
176
  )
@@ -1 +0,0 @@
1
- 0.2.0