Skip to content

Commit 30408f6

Browse files
committed
refactor: PendingContext -> ResolvableContext
1 parent 4f83795 commit 30408f6

10 files changed

Lines changed: 138 additions & 108 deletions

File tree

docs/api.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,22 +55,22 @@ Context refers to the dynamic content of a Jinja template. It can be:
5555
Our dedicated data type (:py:class:`sphinxnotes.render.ParsedData`), or any
5656
Python ``dict``.
5757

58-
:py:class:`~sphinxnotes.render.PendingContext`:
58+
:py:class:`~sphinxnotes.render.ResolvableContext`:
5959
Context that is not yet available. For example, it may contain
6060
:py:class:`unparsed data <sphinxnotes.render.RawData>`,
6161
remote data, and more.
6262

63-
:py:class:`PendingContext` can be resolved to
63+
:py:class:`ResolvableContext` can be resolved to
6464
:py:class:`~sphinxnotes.render.ResolvedContext` by calling
65-
:py:meth:`~sphinxnotes.render.PendingContext.resolve`.
65+
:py:meth:`~sphinxnotes.render.ResolvableContext.resolve`.
6666

6767
.. autotype:: sphinxnotes.render.ResolvedContext
6868

69-
.. autoclass:: sphinxnotes.render.PendingContext
69+
.. autoclass:: sphinxnotes.render.ResolvableContext
7070
:members: resolve
7171

72-
``PendingContext`` Implementations
73-
----------------------------------
72+
``ResolvableContext`` Implementations
73+
-------------------------------------
7474

7575
.. autoclass:: sphinxnotes.render.UnparsedData
7676
:show-inheritance:

src/sphinxnotes/render/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
Schema,
2323
)
2424
from .template import Phase, Template
25-
from .ctx import PendingContext, ResolvedContext
25+
from .ctx import ResolvableContext, ResolvedContext
2626
from .ctxnodes import pending_node
2727
from .extractx import (
2828
extra_context,
@@ -56,7 +56,7 @@
5656
'Schema',
5757
'Phase',
5858
'Template',
59-
'PendingContext',
59+
'ResolvableContext',
6060
'ResolvedContext',
6161
'ParsingPhaseExtraContext',
6262
'ParsedPhaseExtraContext',

src/sphinxnotes/render/ctx.py

Lines changed: 2 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,63 +7,17 @@
77
from typing import Any
88
from abc import ABC, abstractmethod
99
from collections.abc import Hashable
10-
from dataclasses import dataclass
11-
1210
from .data import ParsedData
13-
from .utils import Unpicklable
1411

1512
type ResolvedContext = ParsedData | dict[str, Any]
1613
"""Resolved context types used by template rendering."""
1714

1815

19-
@dataclass
20-
class PendingContextRef:
21-
"""An abstract reference to :class:`PendingContext`."""
22-
23-
ref: int
24-
chksum: int
25-
26-
def __hash__(self) -> int:
27-
return hash((self.ref, self.chksum))
28-
29-
30-
class PendingContext(ABC, Unpicklable, Hashable):
31-
"""An abstract representation of context that is not currently available."""
16+
class ResolvableContext(ABC, Hashable):
17+
"""An abstract representation of context that can be resolved later."""
3218

3319
@abstractmethod
3420
def resolve(self) -> ResolvedContext:
3521
"""This method will be called when rendering to get the available
3622
:py:type:`ResolvedContext`."""
3723
...
38-
39-
40-
class PendingContextStorage:
41-
"""Area for temporarily storing :py:class:`PendingContext`.
42-
43-
This class is intended to solve the problem that:
44-
45-
Some :class:`PendingContext` objects are :class:`Unpicklable`, so they cannot be held
46-
by :class:`pending_node` (as ``pending_node`` will be pickled along with
47-
the docutils doctree)
48-
49-
This class maintains a mapping from :class:`PendingContextRef` -> :class:`PendingContext`.
50-
``pending_node`` owns the ``PendingContextRef``, and can retrieve the context
51-
by calling :py:meth:`retrieve`.
52-
"""
53-
54-
_next_id: int
55-
_data: dict[PendingContextRef, PendingContext] = {}
56-
57-
def __init__(self) -> None:
58-
self._next_id = 0
59-
self._data = {}
60-
61-
def stash(self, pending: PendingContext) -> PendingContextRef:
62-
ref = PendingContextRef(self._next_id, hash(pending))
63-
self._next_id += 1
64-
self._data[ref] = pending
65-
return ref
66-
67-
def retrieve(self, ref: PendingContextRef) -> PendingContext | None:
68-
data = self._data.pop(ref, None)
69-
return data

src/sphinxnotes/render/ctxnodes.py

Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
from __future__ import annotations
22
from typing import TYPE_CHECKING, override
3+
import pickle
34
from pprint import pformat
45

56
from docutils import nodes
67
from docutils.parsers.rst.states import Inliner
78

8-
from .template import Template
9+
from .data import ValueWrapper, ParsedData
10+
from .template import Template, Phase
911
from .ctx import (
10-
PendingContextRef,
11-
PendingContext,
12-
PendingContextStorage,
12+
ResolvableContext,
1313
ResolvedContext,
1414
)
1515
from .markup import MarkupRenderer
@@ -21,7 +21,7 @@
2121
)
2222

2323
if TYPE_CHECKING:
24-
from typing import Any, Callable, ClassVar
24+
from typing import Any, Callable
2525
from .markup import Host
2626
from .ctx import ResolvedContext
2727

@@ -30,7 +30,7 @@ class pending_node(nodes.Element):
3030
"""A docutils node to be rendered."""
3131

3232
# The context to be rendered by Jinja template.
33-
ctx: PendingContextRef | ResolvedContext
33+
ctx: ResolvableContext | ResolvedContext
3434
# The extra context as supplement to ctx.
3535
extra: dict[str, Any]
3636
#: Jinja template for rendering the context.
@@ -39,34 +39,33 @@ class pending_node(nodes.Element):
3939
inline: bool
4040
#: Whether the rendering pipeline is finished (failed is also finished).
4141
rendered: bool
42-
43-
#: Mapping of PendingContextRef -> PendingContext.
44-
#:
45-
#: NOTE: ``PendingContextStorage`` holds Unpicklable data (``PendingContext``)
46-
#: but it is doesn't matters :-), cause pickle doesn't deal with ClassVar.
47-
_PENDING_CONTEXTS: ClassVar[PendingContextStorage] = PendingContextStorage()
42+
#: Stored pickling error for later-phase resolvable context.
43+
_ctx_pickle_error: Exception | None
4844

4945
def __init__(
5046
self,
51-
ctx: PendingContext | ResolvedContext,
47+
ctx: ResolvableContext | ResolvedContext,
5248
tmpl: Template,
5349
inline: bool = False,
5450
rawsource='',
5551
*children,
5652
**attributes,
5753
) -> None:
5854
super().__init__(rawsource, *children, **attributes)
59-
if not isinstance(ctx, PendingContext):
60-
self.ctx = ctx
61-
else:
62-
self.ctx = self._PENDING_CONTEXTS.stash(ctx)
55+
self._ctx_pickle_error = None
56+
if isinstance(ctx, ResolvableContext) and tmpl.phase != Phase.Parsing:
57+
try:
58+
pickle.dumps(ctx)
59+
except Exception as exc:
60+
self._ctx_pickle_error = exc
61+
self.ctx = ctx
6362
self.extra = {}
6463
self.template = tmpl
6564
self.inline = inline
6665
self.rendered = False
6766

6867
# Init hook lists.
69-
self._pending_context_hooks = []
68+
self._resolvable_context_hooks = []
7069
self._resolved_data_hooks = []
7170
self._markup_text_hooks = []
7271
self._rendered_nodes_hooks = []
@@ -75,7 +74,7 @@ def render(self, host: Host) -> None:
7574
"""
7675
The core function for rendering context and template to docutils nodes.
7776
78-
1. PendingContextRef -> PendingContext -> ResolvedContext
77+
1. ResolvableContext -> ResolvedContext
7978
2. TemplateRenderer.render(ResolvedContext) -> Markup Text (``str``)
8079
3. MarkupRenderer.render(Markup Text) -> doctree Nodes (list[nodes.Node])
8180
"""
@@ -97,29 +96,30 @@ def err_report() -> Report:
9796
return report
9897
return Report('Render Report', 'ERROR', source=self.source, line=self.line)
9998

100-
# 1. Prepare context for Jinja template.
101-
if isinstance(self.ctx, PendingContextRef):
102-
report.text('Pending context ref:')
103-
report.code(pformat(self.ctx), lang='python')
104-
105-
pdata = self._PENDING_CONTEXTS.retrieve(self.ctx)
106-
if pdata is None:
107-
report = err_report()
108-
report.text(f'Failed to retrieve pending context from ref {self.ctx}')
109-
self += report
110-
return None
99+
if self._ctx_pickle_error is not None:
100+
report = err_report()
101+
report.text(
102+
f'ResolvableContext used by {self.template.phase} phase templates '
103+
'must be picklable:'
104+
)
105+
report.exception(self._ctx_pickle_error)
106+
self += report
107+
return None
111108

112-
report.text('Pending context:')
109+
# 1. Prepare context for Jinja template.
110+
if isinstance(self.ctx, ResolvableContext):
111+
pdata = self.ctx
112+
report.text('Resolvable context:')
113113
report.code(pformat(pdata), lang='python')
114114

115-
for hook in self._pending_context_hooks:
115+
for hook in self._resolvable_context_hooks:
116116
hook(self, pdata)
117117

118118
try:
119119
ctx = self.ctx = pdata.resolve()
120120
except Exception as e:
121121
report = err_report()
122-
report.text('Failed to resolve pending context:')
122+
report.text('Failed to resolve resolvable context:')
123123
report.exception(e)
124124
self += report
125125
return None
@@ -221,18 +221,18 @@ def unwrap_and_replace_self_inline(self, inliner: Report.Inliner) -> None:
221221

222222
"""Hooks for processing render intermediate products."""
223223

224-
type PendingContextHook = Callable[[pending_node, PendingContext], None]
224+
type ResolvableContextHook = Callable[[pending_node, ResolvableContext], None]
225225
type ResolvedContextHook = Callable[[pending_node, ResolvedContext], None]
226226
type MarkupTextHook = Callable[[pending_node, str], str]
227227
type RenderedNodesHook = Callable[[pending_node, list[nodes.Node]], None]
228228

229-
_pending_context_hooks: list[PendingContextHook]
229+
_resolvable_context_hooks: list[ResolvableContextHook]
230230
_resolved_data_hooks: list[ResolvedContextHook]
231231
_markup_text_hooks: list[MarkupTextHook]
232232
_rendered_nodes_hooks: list[RenderedNodesHook]
233233

234-
def hook_pending_context(self, hook: PendingContextHook) -> None:
235-
self._pending_context_hooks.append(hook)
234+
def hook_resolvable_context(self, hook: ResolvableContextHook) -> None:
235+
self._resolvable_context_hooks.append(hook)
236236

237237
def hook_resolved_context(self, hook: ResolvedContextHook) -> None:
238238
self._resolved_data_hooks.append(hook)
@@ -259,3 +259,16 @@ def copy(self) -> Any:
259259
def deepcopy(self) -> Any:
260260
# NOTE: Same to :meth:`copy`.
261261
return self.copy()
262+
263+
@override
264+
def astext(self) -> str:
265+
ctx = self.ctx
266+
if isinstance(ctx, ResolvableContext):
267+
try:
268+
ctx = ctx.resolve()
269+
except Exception:
270+
return ''
271+
if isinstance(ctx, ParsedData):
272+
return ValueWrapper(ctx.content).as_str() or ''
273+
else:
274+
return ''

src/sphinxnotes/render/data.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
from dataclasses import dataclass, asdict, field as dataclass_field
1515
from ast import literal_eval
1616

17-
from .utils import Unpicklable
18-
1917
if TYPE_CHECKING:
2018
from typing import Any, Callable, Generator, Self
2119

@@ -287,7 +285,7 @@ def asdict(self) -> dict[str, Any]:
287285

288286

289287
@dataclass
290-
class Field(Unpicklable):
288+
class Field:
291289
#: Type of element.
292290
etype: type = str
293291
#: Type of container (if the field holds multiple values).
@@ -374,8 +372,9 @@ def parse(self, rawval: str | None) -> Value:
374372
raise ValueError(f"Failed to parse '{rawval}' as {self.etype}: {e}") from e
375373

376374
def __getattr__(self, name: str) -> Value:
377-
if name in self.flags:
378-
return self.flags[name]
375+
flags = self.__dict__.get('flags')
376+
if flags is not None and name in flags:
377+
return flags[name]
379378
raise AttributeError(name)
380379

381380

@@ -491,7 +490,7 @@ def by_option_store_value_error(opt: ByOption) -> ValueError:
491490

492491

493492
@dataclass(frozen=True)
494-
class Schema(Unpicklable):
493+
class Schema:
495494
name: Field | None
496495
attrs: dict[str, Field] | Field
497496
content: Field | None

src/sphinxnotes/render/ext/adhoc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from types import ModuleType
3434
from docutils.utils import Reporter
3535
from sphinx.util.typing import RoleFunction
36-
from .. import PendingContext, ResolvedContext
36+
from .. import ResolvableContext, ResolvedContext
3737

3838

3939
# Keys of env.temp_data.
@@ -140,7 +140,7 @@ class DataRenderDirective(BaseContextDirective):
140140
has_content = True
141141

142142
@override
143-
def current_context(self) -> PendingContext | ResolvedContext:
143+
def current_context(self) -> ResolvableContext | ResolvedContext:
144144
return {}
145145

146146
@override

src/sphinxnotes/render/pipeline.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from sphinx.transforms.post_transforms import SphinxPostTransform, ReferencesResolver
1212

1313
from .template import HostWrapper, Phase, Template, Host
14-
from .ctx import PendingContext, ResolvedContext
14+
from .ctx import ResolvableContext, ResolvedContext
1515
from .ctxnodes import pending_node
1616
from .extractx import ExtraContextGenerator
1717

@@ -83,7 +83,7 @@ def queue_pending_node(self, n: pending_node) -> None:
8383

8484
@final
8585
def queue_context(
86-
self, ctx: PendingContext | ResolvedContext, tmpl: Template
86+
self, ctx: ResolvableContext | ResolvedContext, tmpl: Template
8787
) -> pending_node:
8888
"""A helper method for ``queue_pending_node``."""
8989
pending = pending_node(ctx, tmpl)
@@ -162,7 +162,7 @@ class BaseContextSource(Pipeline):
162162
"""Methods to be implemented."""
163163

164164
@abstractmethod
165-
def current_context(self) -> PendingContext | ResolvedContext:
165+
def current_context(self) -> ResolvableContext | ResolvedContext:
166166
"""Return the context to be rendered."""
167167
...
168168

0 commit comments

Comments
 (0)