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.
- dp_wizard_templates/VERSION +1 -1
- dp_wizard_templates/code_template.py +142 -64
- dp_wizard_templates/converters.py +4 -4
- dp_wizard_templates/py.typed +0 -0
- {dp_wizard_templates-0.2.0.dist-info → dp_wizard_templates-0.4.0.dist-info}/METADATA +1 -1
- dp_wizard_templates-0.4.0.dist-info/RECORD +9 -0
- dp_wizard_templates-0.2.0.dist-info/RECORD +0 -8
- {dp_wizard_templates-0.2.0.dist-info → dp_wizard_templates-0.4.0.dist-info}/WHEEL +0 -0
- {dp_wizard_templates-0.2.0.dist-info → dp_wizard_templates-0.4.0.dist-info}/licenses/LICENSE +0 -0
dp_wizard_templates/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
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
|
|
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
|
-
|
|
20
|
-
r"\s
|
|
21
|
-
"\
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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__(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
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(
|
|
117
|
+
self._template, count = re.subn(
|
|
118
|
+
rf"\b{k_re}\b", stringifier(v), self._template
|
|
119
|
+
)
|
|
79
120
|
if count == 0:
|
|
80
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
|
120
|
-
|
|
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
|
-
|
|
123
|
-
raise
|
|
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
|
|
@@ -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,,
|
|
File without changes
|
{dp_wizard_templates-0.2.0.dist-info → dp_wizard_templates-0.4.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|