dp_wizard_templates 0.2.0__py2.py3-none-any.whl → 0.4.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.2.0
1
+ 0.4.0
@@ -1,6 +1,13 @@
1
+ from typing import Optional, Callable, Iterable
2
+ from pathlib import Path
1
3
  import inspect
2
4
  import re
3
5
  import black
6
+ import json
7
+
8
+
9
+ class TemplateException(Exception):
10
+ pass
4
11
 
5
12
 
6
13
  def _get_body(func):
@@ -10,44 +17,73 @@ def _get_body(func):
10
17
  if not re.match(r"def \w+\((\w+(, \w+)*)?\):", first_line.strip()):
11
18
  # Parsing to AST and unparsing is a more robust option,
12
19
  # but more complicated.
13
- 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
+ )
14
23
 
15
24
  # The "def" should not be in the output,
16
25
  # and cleandoc handles the first line differently.
17
26
  source_lines[0] = ""
18
27
  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
- )
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
+
29
40
  return body
30
41
 
31
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
+
32
57
  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:
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:
40
65
  if callable(template):
41
66
  self._source = "function template"
42
67
  self._template = _get_body(template)
43
68
  else:
44
69
  self._source = "string template"
45
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()
46
81
  # We want a list of the initial slots, because substitutions
47
82
  # can produce sequences of upper case letters that could be mistaken for slots.
48
83
  self._initial_slots = self._find_slots()
84
+ self._ignore = ignore
49
85
 
50
- def _find_slots(self):
86
+ def _find_slots(self) -> set[str]:
51
87
  # Slots:
52
88
  # - are all caps or underscores
53
89
  # - have word boundary on either side
@@ -55,75 +91,117 @@ class Template:
55
91
  slot_re = r"\b[A-Z][A-Z_]{2,}\b"
56
92
  return set(re.findall(slot_re, self._template))
57
93
 
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
94
+ def _make_message(self, errors: list[str]) -> str:
95
+ return (
96
+ f"In {self._source}, " + ", ".join(sorted(errors)) + f":\n{self._template}"
97
+ )
71
98
 
72
- def fill_values(self, **kwargs):
73
- """
74
- Fill in string or numeric values. `repr` is called before filling.
75
- """
99
+ def _loop_kwargs(
100
+ self,
101
+ function: Callable[[str, str, list[str]], None],
102
+ **kwargs,
103
+ ) -> None:
104
+ errors = []
76
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):
77
116
  k_re = re.escape(k)
78
- self._template, count = re.subn(rf"\b{k_re}\b", repr(v), self._template)
117
+ self._template, count = re.subn(
118
+ rf"\b{k_re}\b", stringifier(v), self._template
119
+ )
79
120
  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
121
+ errors.append(f"no '{k}' slot to fill with '{v}'")
85
122
 
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():
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):
91
132
  if not isinstance(v, str):
92
- raise Exception(f"For {k} in {self._source}, expected string, not {v}")
133
+ errors.append(f"for '{k}' slot, expected string, not '{v}'")
134
+ return
93
135
 
94
136
  def match_indent(match):
95
137
  # This does what we want, but binding is confusing.
96
138
  return "\n".join(
97
- match.group(1) + line for line in v.split("\n") # noqa: B023
139
+ match.group(1) + line for line in splitter(v) # noqa: B023
98
140
  )
99
141
 
100
142
  k_re = re.escape(k)
101
143
  self._template, count = re.subn(
102
- rf"^([ \t]*){k_re}$",
144
+ rf"^([ \t]*{prefix_re}){k_re}$",
103
145
  match_indent,
104
146
  self._template,
105
147
  flags=re.MULTILINE,
106
148
  )
107
149
  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
- )
150
+ base_message = f"no '{k}' slot to fill with '{v}'"
112
151
  if k in self._template:
113
- raise Exception(
114
- f"Block slots must be alone on line; {base_message}"
152
+ note = (
153
+ "comment slots must be prefixed with '#'"
154
+ if prefix_re
155
+ else "block slots must be alone on line"
115
156
  )
116
- raise Exception(base_message)
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)
117
175
  return self
118
176
 
119
- def finish(self, reformat=False) -> str:
120
- unfilled_slots = self._initial_slots & self._find_slots()
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)
121
202
  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
- )
203
+ errors = [f"'{slot}' slot not filled" for slot in unfilled_slots]
204
+ raise TemplateException(self._make_message(errors))
127
205
 
128
206
  if reformat:
129
207
  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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dp_wizard_templates
3
- Version: 0.2.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
@@ -0,0 +1,9 @@
1
+ dp_wizard_templates/VERSION,sha256=zXhTTtFa1GkStxM50UF9DQQ9gwnCuUQV8-0bnR_frtA,5
2
+ dp_wizard_templates/__init__.py,sha256=E2xrnvZGY24pU3PCryH4TmvhNYsLmxXCtfvIcYNbTYw,126
3
+ dp_wizard_templates/code_template.py,sha256=mHJKN0pzPHIGSF3K_ffwM1m1VPAjQ-zdwQawzPgKoQk,6606
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.4.0.dist-info/licenses/LICENSE,sha256=FDrIMeZPiT4g_4w0i1Ec4Bc8h9wfNytroheQN4508yU,1063
7
+ dp_wizard_templates-0.4.0.dist-info/WHEEL,sha256=Dyt6SBfaasWElUrURkknVFAZDHSTwxg3PaTza7RSbkY,100
8
+ dp_wizard_templates-0.4.0.dist-info/METADATA,sha256=zXPD9TDjoYP-T3kIO_yJMARkU3L50Qte8zyEVLwbQYs,1881
9
+ dp_wizard_templates-0.4.0.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- dp_wizard_templates/VERSION,sha256=kR_AxIywxwYB21d1qb7xt0DcTMn5tGOJufBWP-frlNc,5
2
- dp_wizard_templates/__init__.py,sha256=E2xrnvZGY24pU3PCryH4TmvhNYsLmxXCtfvIcYNbTYw,126
3
- dp_wizard_templates/code_template.py,sha256=HwnmKmJStkan9jkdyRQ4zVnJcvHWymzzCt8lwdTdtX4,4504
4
- dp_wizard_templates/converters.py,sha256=voLc2KlJ7WZGYMtA8x0pB1xBcLJs974uoX2nCJO7uD0,4331
5
- dp_wizard_templates-0.2.0.dist-info/licenses/LICENSE,sha256=FDrIMeZPiT4g_4w0i1Ec4Bc8h9wfNytroheQN4508yU,1063
6
- dp_wizard_templates-0.2.0.dist-info/WHEEL,sha256=Dyt6SBfaasWElUrURkknVFAZDHSTwxg3PaTza7RSbkY,100
7
- dp_wizard_templates-0.2.0.dist-info/METADATA,sha256=zvMOoJELX1KP5e_3gIcimNjp62lDNyOd7BfkGahvdLg,1881
8
- dp_wizard_templates-0.2.0.dist-info/RECORD,,