dp_wizard_templates 0.1.0__py2.py3-none-any.whl → 0.3.0__py2.py3-none-any.whl

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.

@@ -1 +1 @@
1
- 0.1.0
1
+ 0.3.0
@@ -1,9 +1,11 @@
1
+ from typing import Optional, Callable
2
+ from pathlib import Path
3
+ import inspect
1
4
  import re
5
+ import black
2
6
 
3
7
 
4
8
  def _get_body(func):
5
- import inspect
6
- import re
7
9
 
8
10
  source_lines = inspect.getsource(func).splitlines()
9
11
  first_line = source_lines[0]
@@ -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,73 +61,119 @@ 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)
145
+ return self
146
+
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)
117
168
  return self
118
169
 
119
- def finish(self) -> str:
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))
175
+
176
+ if reformat:
177
+ self._template = black.format_str(self._template, mode=black.Mode())
178
+
127
179
  return self._template
@@ -7,6 +7,7 @@ import json
7
7
  import nbformat
8
8
  import nbconvert
9
9
  import jupytext
10
+ import black
10
11
 
11
12
 
12
13
  def _is_kernel_installed() -> bool:
@@ -27,7 +28,9 @@ class ConversionException(Exception):
27
28
  return f"Script to notebook conversion failed: {self.command}\n{self.stderr})"
28
29
 
29
30
 
30
- def convert_py_to_nb(python_str: str, title: str, execute: bool = False):
31
+ def convert_py_to_nb(
32
+ python_str: str, title: str, execute: bool = False, reformat: bool = True
33
+ ) -> str:
31
34
  """
32
35
  Given Python code as a string, returns a notebook as a string.
33
36
  Calls jupytext as a subprocess:
@@ -43,6 +46,9 @@ def convert_py_to_nb(python_str: str, title: str, execute: bool = False):
43
46
 
44
47
  temp_dir_path = Path(temp_dir)
45
48
  py_path = temp_dir_path / "input.py"
49
+ if reformat:
50
+ # Line length determined by PDF rendering.
51
+ python_str = black.format_str(python_str, mode=black.Mode(line_length=74))
46
52
  py_path.write_text(python_str)
47
53
 
48
54
  argv = [executable] + "-m jupytext --from .py --to .ipynb --output -".split(" ")
@@ -65,13 +71,13 @@ def convert_py_to_nb(python_str: str, title: str, execute: bool = False):
65
71
  return _clean_nb(json.dumps(nb_dict))
66
72
 
67
73
 
68
- def _stable_hash(lines: list[str]):
74
+ def _stable_hash(lines: list[str]) -> str:
69
75
  import hashlib
70
76
 
71
77
  return hashlib.sha1("\n".join(lines).encode()).hexdigest()[:8]
72
78
 
73
79
 
74
- def _clean_nb(nb_json: str):
80
+ def _clean_nb(nb_json: str) -> str:
75
81
  """
76
82
  Given a notebook as a string of JSON, remove the coda and pip output.
77
83
  (The code may produce reports that we do need,
@@ -82,7 +88,9 @@ def _clean_nb(nb_json: str):
82
88
  for cell in nb["cells"]:
83
89
  if "pip install" in cell["source"][0]:
84
90
  cell["outputs"] = []
85
- if "# Coda\n" in cell["source"]:
91
+ # "Coda" may, or may not be followed by "\n".
92
+ # Be flexible!
93
+ if any(line.startswith("# Coda") for line in cell["source"]):
86
94
  break
87
95
  # Make ID stable:
88
96
  cell["id"] = _stable_hash(cell["source"])
@@ -96,13 +104,9 @@ def _clean_nb(nb_json: str):
96
104
  return json.dumps(nb, indent=1)
97
105
 
98
106
 
99
- def convert_nb_to_html(python_nb: str):
100
- return _convert_nb(python_nb, nbconvert.HTMLExporter)
101
-
102
-
103
- def _convert_nb(python_nb: str, exporter_constructor):
107
+ def convert_nb_to_html(python_nb: str, numbered=True) -> str:
104
108
  notebook = nbformat.reads(python_nb, as_version=4)
105
- exporter = exporter_constructor(
109
+ exporter = nbconvert.HTMLExporter(
106
110
  template_name="lab",
107
111
  # The "classic" template's CSS forces large code cells on to
108
112
  # the next page rather than breaking, so use "lab" instead.
@@ -116,4 +120,13 @@ def _convert_nb(python_nb: str, exporter_constructor):
116
120
  # ],
117
121
  )
118
122
  (body, _resources) = exporter.from_notebook_node(notebook)
123
+ if not numbered:
124
+ body = body.replace(
125
+ "</head>",
126
+ """
127
+ <style>
128
+ .jp-InputPrompt {display: none;}
129
+ </style>
130
+ </head>""",
131
+ )
119
132
  return body
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dp_wizard_templates
3
- Version: 0.1.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
@@ -10,7 +10,8 @@ Requires-Dist: ipykernel
10
10
  Requires-Dist: jupyter-client
11
11
  Requires-Dist: jupytext
12
12
  Requires-Dist: nbconvert
13
- Project-URL: Home, https://github.com/opendp/dp-wizard-templates
13
+ Project-URL: GitHub, https://github.com/opendp/dp-wizard-templates
14
+ Project-URL: Homepage, https://opendp.github.io/dp-wizard-templates
14
15
 
15
16
  # DP Wizard Templates
16
17
 
@@ -19,12 +20,7 @@ Project-URL: Home, https://github.com/opendp/dp-wizard-templates
19
20
  DP Wizard Templates lets you use syntactically valid Python code as a template.
20
21
  Templates can be filled and composed to generate entire notebooks.
21
22
 
22
- [`README_test.py`](https://github.com/opendp/dp-wizard-templates/blob/main/README_test.py) provides an example of use,
23
- and an example [output notebook](https://github.com/opendp/dp-wizard-templates/blob/main/README_examples/hello-world.ipynb)
24
- is also available.
25
-
26
- DP Wizard Templates was developed for [DP Wizard](https://github.com/opendp/dp-wizard),
27
- and that codebase remains a good place to look for further examples.
23
+ See the [documentation](https://opendp.github.io/dp-wizard-templates) for more information.
28
24
 
29
25
 
30
26
  ## Development
@@ -0,0 +1,9 @@
1
+ dp_wizard_templates/VERSION,sha256=VQYTU3_EiPOzcq90pAAYefASyEZbgW8bhcbTRGss-0k,5
2
+ dp_wizard_templates/__init__.py,sha256=E2xrnvZGY24pU3PCryH4TmvhNYsLmxXCtfvIcYNbTYw,126
3
+ dp_wizard_templates/code_template.py,sha256=BAt9vsvrusqR3mETsUE4HfsPgF3D0yPHgL8Q0no01h4,5735
4
+ dp_wizard_templates/converters.py,sha256=0Ml_V71Z6zce2SaxDQMzYXg-5jQw2-NWChrY_9SCGIo,4359
5
+ dp_wizard_templates/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ dp_wizard_templates-0.3.0.dist-info/licenses/LICENSE,sha256=FDrIMeZPiT4g_4w0i1Ec4Bc8h9wfNytroheQN4508yU,1063
7
+ dp_wizard_templates-0.3.0.dist-info/WHEEL,sha256=Dyt6SBfaasWElUrURkknVFAZDHSTwxg3PaTza7RSbkY,100
8
+ dp_wizard_templates-0.3.0.dist-info/METADATA,sha256=uBCyEVM3KWsLChzbFCYT8nxjwdxvQeWMsGAKSKCiEYA,1881
9
+ dp_wizard_templates-0.3.0.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- dp_wizard_templates/VERSION,sha256=atlhOkVXmNbZLl9fOQq0uqcFlryGntaxf1zdKyhjXwY,5
2
- dp_wizard_templates/__init__.py,sha256=E2xrnvZGY24pU3PCryH4TmvhNYsLmxXCtfvIcYNbTYw,126
3
- dp_wizard_templates/code_template.py,sha256=GW4wcAguGRFEE_eKLPedjmsklZR9dtx4wn8CYq-avh8,4389
4
- dp_wizard_templates/converters.py,sha256=l-MYAiTAH_isjggH03DKlgV-j-uxc0CyW1hiCLD39yQ,3957
5
- dp_wizard_templates-0.1.0.dist-info/licenses/LICENSE,sha256=FDrIMeZPiT4g_4w0i1Ec4Bc8h9wfNytroheQN4508yU,1063
6
- dp_wizard_templates-0.1.0.dist-info/WHEEL,sha256=Dyt6SBfaasWElUrURkknVFAZDHSTwxg3PaTza7RSbkY,100
7
- dp_wizard_templates-0.1.0.dist-info/METADATA,sha256=ZMvavNpdRK7GvyB-j4ht2JWSzPjhIC52n7VHVCyX70c,2139
8
- dp_wizard_templates-0.1.0.dist-info/RECORD,,