-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathctxnodes.py
More file actions
286 lines (237 loc) · 9.22 KB
/
Copy pathctxnodes.py
File metadata and controls
286 lines (237 loc) · 9.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
from __future__ import annotations
from typing import TYPE_CHECKING, override
import pickle
from pprint import pformat
from docutils import nodes
from docutils.parsers.rst.states import Inliner
from .template import Template, Phase
from .ctx import (
UnresolvedContext,
ResolvedContext,
)
from .extractx import ExtraContextRequest, extra_context_loader, extra_context_names
from .markup import MarkupRenderer
from .jinja import TemplateRenderer
from .utils import (
Report,
Reporter,
find_nearest_block_element,
)
if TYPE_CHECKING:
from typing import Callable, Any
from .markup import Host
from .ctx import ResolvedContext
class pending_node(nodes.Element):
"""A docutils node to be rendered."""
#: The context to be rendered by Jinja template.
ctx: UnresolvedContext | ResolvedContext
#: Jinja template for rendering the context.
template: Template
#: Whether rendering to inline nodes.
inline: bool
#: Whether the rendering pipeline is finished (failed is also finished).
rendered: bool
# Stored pickling error for later-phase unresolved context.
_ctx_pickle_error: Exception | None
# Types for hook functions.
type UnresolvedContextHook = Callable[[pending_node, UnresolvedContext], None]
type ResolvedContextHook = Callable[[pending_node, ResolvedContext], None]
type MarkupTextHook = Callable[[pending_node, str], str]
type RenderedNodesHook = Callable[[pending_node, list[nodes.Node]], None]
# Hooks for processing render intermediate products.
_unresolved_context_hooks: list[UnresolvedContextHook]
_resolved_context_hooks: list[ResolvedContextHook]
_markup_text_hooks: list[MarkupTextHook]
_rendered_nodes_hooks: list[RenderedNodesHook]
def __init__(
self,
ctx: UnresolvedContext | ResolvedContext,
tmpl: Template,
inline: bool = False,
rawsource='',
*children,
**attributes,
) -> None:
super().__init__(rawsource, *children, **attributes)
self.ctx = ctx
self.template = tmpl
self.inline = inline
self.rendered = False
# Test whehter ctx pickle-able.
self._ctx_pickle_error = None
if isinstance(ctx, UnresolvedContext) and tmpl.phase != Phase.Parsing:
try:
pickle.dumps(ctx)
except Exception as exc:
self._ctx_pickle_error = exc
# Init hook lists.
self._unresolved_context_hooks = []
self._resolved_context_hooks = []
self._markup_text_hooks = []
self._rendered_nodes_hooks = []
def render(self, host: Host) -> None:
"""
The core function for rendering context and template to docutils nodes.
1. UnresolvedContext -> ResolvedContext
2. TemplateRenderer.render(ResolvedContext) -> Markup Text (``str``)
3. MarkupRenderer.render(Markup Text) -> doctree Nodes (list[nodes.Node])
"""
# Make sure the function is called once.
assert not self.rendered
self.rendered = True
# Clear previous empty reports.
Reporter(self).clear_empty()
# Create debug report.
report = Report('Render Report', 'DEBUG', source=self.source, line=self.line)
# Constructor for error report.
def err_report() -> Report:
if self.template.debug:
# Reuse the render report as possible.
report['type'] = 'ERROR'
return report
return Report('Render Report', 'ERROR', source=self.source, line=self.line)
if self._ctx_pickle_error is not None:
report = err_report()
report.exception(
self._ctx_pickle_error,
caption=(
f'UnresolvedContext used by {self.template.phase} phase templates '
'must be picklable:'
),
)
self += report
return None
# 1. Prepare context for Jinja template.
if isinstance(self.ctx, UnresolvedContext):
pdata = self.ctx
report.code(pformat(pdata), lang='python', caption='Unresolved context:')
for hook in self._unresolved_context_hooks:
hook(self, pdata)
try:
ctx = self.ctx = pdata.resolve(host.env)
except Exception:
report = err_report()
report.current_exception(
caption='Failed to resolve unresolved context:',
debug=self.template.debug,
)
self += report
return None
else:
ctx = self.ctx
for hook in self._resolved_context_hooks:
hook(self, ctx)
report.code(
pformat(ctx),
lang='python',
caption=f'Resolved context (type: {type(ctx)}):',
)
report.code(
self.template.text,
lang='jinja',
caption=f'Template (phase: {self.template.phase}):',
)
extractx_req = ExtraContextRequest(self.template.phase, self, host.env, host)
report.code(
pformat(sorted(extra_context_names())),
lang='python',
caption='Available extra context names:',
)
# 2. Render the template and context to markup text.
try:
markup = TemplateRenderer(self.template.text).render(
ctx,
globals={'load_extra': extra_context_loader(extractx_req)},
debug=self.template.debug,
)
except Exception:
report = err_report()
report.current_exception(
caption='Failed to render Jinja template:', debug=self.template.debug
)
self += report
return
for hook in self._markup_text_hooks:
markup = hook(self, markup)
report.code(markup, lang='rst', caption='Rendered markup text:')
# 3. Render the markup text to doctree nodes.
try:
ns, msgs = MarkupRenderer(host).render(markup, inline=self.inline)
except Exception:
report = err_report()
report.current_exception(
caption=(
'Failed to render markup text '
f'to {"inline " if self.inline else ""}nodes:'
),
debug=self.template.debug,
)
self += report
return
report.code(
'\n\n'.join([n.pformat() for n in ns]),
lang='xml',
caption=f'Rendered nodes (inline: {self.inline}):',
)
if msgs:
report.text('Systemd messages:')
[report.node(msg) for msg in msgs]
# 4. Add rendered nodes to container.
for hook in self._rendered_nodes_hooks:
hook(self, ns)
# TODO: set_source_info?
self += ns
if self.template.debug:
self += report
return
def unwrap(self) -> list[nodes.Node]:
children = self.children
self.clear()
return children
def unwrap_inline(
self, inliner: Report.Inliner
) -> tuple[list[nodes.Node], list[nodes.system_message]]:
# Report (nodes.system_message subclass) is not inline node,
# should be removed before inserting to doctree.
reports = Reporter(self).clear()
for report in reports:
self.append(report.problematic(inliner))
children = self.children
self.clear()
return children, [x for x in reports]
def unwrap_and_replace_self(self) -> None:
children = self.unwrap()
# Replace self with children.
self.replace_self(children)
def unwrap_and_replace_self_inline(self, inliner: Report.Inliner) -> None:
# Unwrap inline nodes and system_message noeds from node.
ns, msgs = self.unwrap_inline(inliner)
# Insert reports to nearst block elements (usually nodes.paragraph).
doctree = inliner.document if isinstance(inliner, Inliner) else inliner[1]
blkparent = find_nearest_block_element(self.parent) or doctree
blkparent += msgs
# Replace self with inline nodes.
self.replace_self(ns)
def hook_unresolved_context(self, hook: UnresolvedContextHook) -> None:
self._unresolved_context_hooks.append(hook)
def hook_resolved_context(self, hook: ResolvedContextHook) -> None:
self._resolved_context_hooks.append(hook)
def hook_markup_text(self, hook: MarkupTextHook) -> None:
self._markup_text_hooks.append(hook)
def hook_rendered_nodes(self, hook: RenderedNodesHook) -> None:
self._rendered_nodes_hooks.append(hook)
"""Methods override from parent."""
@override
def copy(self) -> Any:
# NOTE: pending_node is no supposed to be copy as it does not make sense.
if self.inline:
return nodes.literal(self.rawsource, self.rawsource)
else:
return nodes.literal_block(self.rawsource, self.rawsource)
@override
def deepcopy(self) -> Any:
# NOTE: Copy children is not allowed, so simply forward to self.copy.
return self.copy()
@override
def astext(self) -> str:
return self.rawsource