Skip to content

Commit d028490

Browse files
authored
Merge pull request #53 from sphinx-notes/auto
Impl ``AutoObjDefineDirective``
2 parents fdf8615 + bc97d41 commit d028490

5 files changed

Lines changed: 177 additions & 57 deletions

File tree

docs/conf.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,5 +173,3 @@
173173
}
174174

175175
primary_domain = obj_domain_name
176-
177-
data_template_debug = True

docs/conf.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ The extension provides the following configuration:
99
.. autoconfval:: obj_type_defines
1010

1111
.. autoconfval:: obj_domain_dump
12+
13+
.. autoconfval:: obj_auto_obj

src/sphinxnotes/any/__init__.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@
1313

1414
from sphinx.errors import ConfigError
1515
from sphinx.util import logging
16-
from sphinxnotes.data import Schema, Config as DataConfig
16+
from sphinxnotes.data import Schema
1717

1818
from schema import Schema as DictSchema, SchemaError as DictSchemaError, Optional, Or
1919

2020
from . import meta
21-
from .obj import Templates
21+
from .obj import Templates, ObjTypeDef
2222
from .domain import ObjDomain
2323

2424
if TYPE_CHECKING:
@@ -40,11 +40,13 @@
4040
Optional('ref', default='{{ name }}'): str,
4141
Optional('ref_by', default={}): {str: str},
4242
},
43+
Optional('auto', default=False): bool,
44+
Optional('debug', default=False): bool,
4345
}
4446
)
4547

4648

47-
def _validate_objtype_defines_dict(d: dict) -> tuple[Schema, Templates]:
49+
def _validate_objtype_defines_dict(d: dict, config: Config) -> ObjTypeDef:
4850
objdef = OBJTYPE_DEFINE.validate(d)
4951

5052
schemadef = objdef['schema']
@@ -58,10 +60,12 @@ def _validate_objtype_defines_dict(d: dict) -> tuple[Schema, Templates]:
5860
tmplsdef['header'],
5961
tmplsdef['ref'],
6062
tmplsdef['ref_by'],
61-
debug=DataConfig.render_debug,
63+
debug=objdef['debug'],
6264
)
6365

64-
return schema, tmpls
66+
auto = objdef['auto'] or config.obj_auto_obj
67+
68+
return ObjTypeDef(schema=schema, templates=tmpls, auto=auto)
6569

6670

6771
def _config_inited(app: Sphinx, config: Config) -> None:
@@ -70,10 +74,12 @@ def _config_inited(app: Sphinx, config: Config) -> None:
7074
for objtype, objdef in app.config.obj_type_defines.items():
7175
# TODO: check ":" in objtype to support multiple domain
7276
try:
73-
schema, tmpls = _validate_objtype_defines_dict(objdef)
77+
objtypedef = _validate_objtype_defines_dict(objdef, config)
7478
except (DictSchemaError, ValueError) as e:
75-
raise ConfigError(f'Validating obj_type_defines[{repr(objtype)}]: {e}') from e
76-
ObjDomain.add_objtype(objtype, schema, tmpls)
79+
raise ConfigError(
80+
f'Validating obj_type_defines[{repr(objtype)}]: {e}'
81+
) from e
82+
ObjDomain.add_objtype(objtype, objtypedef)
7783

7884
app.add_domain(ObjDomain)
7985

@@ -97,6 +103,7 @@ def setup(app: Sphinx):
97103
'The ``objdef`` vaule is also a ``dict``, '
98104
'please refer to :ref:`writing-objdef` for more details.',
99105
)
106+
app.add_config_value('obj_auto_obj', True, 'env', types=bool)
100107

101108
app.connect('config-inited', _config_inited)
102109

src/sphinxnotes/any/domain.py

Lines changed: 148 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from typing import TYPE_CHECKING, override, cast, TypeVar
1313
from pprint import pformat
1414

15+
from sphinxnotes.data.data import PendingData
16+
1517
from .indexers import LiteralIndexer, PathIndexer, YearIndexer, MonthIndexer
1618

1719
from docutils import nodes
@@ -23,18 +25,25 @@
2325
from sphinx.errors import ExtensionError
2426
from sphinxnotes.data import (
2527
PlainValue,
28+
RawData,
29+
Template,
2630
ValueWrapper,
2731
ParsedData,
2832
Schema,
2933
pending_node,
30-
rendered_node,
3134
StrictDataDefineDirective,
3235
)
33-
from sphinxnotes.data.utils import Report, find_nearest_block_element, find_parent
36+
from sphinxnotes.data.utils import (
37+
Report,
38+
find_nearest_block_element,
39+
find_parent,
40+
find_titular_node_upward,
41+
)
3442

3543
from .obj import (
3644
Object,
3745
RefType,
46+
ObjTypeDef,
3847
Templates,
3948
Category,
4049
Indexer,
@@ -163,14 +172,26 @@ def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]:
163172
"""Publish methods."""
164173

165174
@classmethod
166-
def add_objtype(cls, objtype: str, schema: Schema, tmpls: Templates) -> None:
175+
def add_objtype(cls, objtype: str, typedef: ObjTypeDef) -> None:
176+
schema = typedef.schema
177+
tmpls = typedef.templates
178+
167179
cls.schemas[objtype] = schema
168180
cls.templates[objtype] = tmpls
169181

170-
# Create a directive for defining object.
171-
cls.directives[objtype] = ObjDefineDirective.derive(
172-
objtype, schema, tmpls.content
173-
)
182+
# Create a directive for documenting object.
183+
if typedef.auto:
184+
cls.directives[objtype] = AutoObjDefineDirective.derive(
185+
objtype,
186+
schema,
187+
tmpls.obj,
188+
)
189+
else:
190+
cls.directives[objtype] = ObjDefineDirective.derive(
191+
objtype,
192+
schema,
193+
tmpls.obj,
194+
)
174195

175196
def mkrole(reftype: RefType):
176197
"""Create and register role for referencing object."""
@@ -265,73 +286,83 @@ def _get_index_anchor(self, reftype: str, refval: str) -> tuple[str, str]:
265286

266287

267288
class ObjDefineDirective(StrictDataDefineDirective):
289+
"""Methods that override from parent."""
290+
268291
@override
269292
def process_pending_node(self, n: pending_node) -> bool:
270293
if n.template == self.template:
271-
n.hook_rendered_node(self._setup_objdesc)
272-
294+
n.hook_rendered_nodes(self._build_objdesc)
273295
return super().process_pending_node(n)
274296

275-
def _setup_objdesc(self, pending: pending_node, rendered: rendered_node) -> None:
297+
"""Methods used internal."""
298+
299+
def _build_objdesc(self, pending: pending_node, rendered: list[nodes.Node]) -> None:
276300
"""Wrap rendered.children into ObjectDescription.
277301
278302
TODO: considier inherit from ObjectDescription directive?
279303
280304
Before::
281305
282-
<rendered_node>
283-
<...> # children
306+
<nodes...> # the pass-in argument: ``ns``
284307
285308
After::
286309
287-
<rendered_node>
288-
<desc>
289-
<desc_signature>
290-
<desc_name>
291-
<pending_node> # header, wait for rendering
292-
<desc_content>
293-
<...> # the original children
310+
<desc>
311+
<desc_signature>
312+
<desc_name>
313+
<pending_node> # header, wait for rendering
314+
<desc_content>
315+
<nodes...> # the original ``ns``
294316
"""
295-
296-
domain, objtype = self._get_obj_domain_and_type()
297-
298-
def update_domaon_atts(node: nodes.Element):
299-
"""Attach domain related info to node."""
300-
node['domain'] = domain.name
301-
# 'desctype' is a backwards compatible attribute
302-
node['objtype'] = node['desctype'] = objtype
303-
node['classes'].extend([domain.name, objtype])
317+
domain, objtype = self.get_obj_domain_and_type()
304318

305319
if (hdrtmpl := domain.templates[objtype].header) is None:
306320
# No header template available, no need to generate objdesc.
307-
update_domaon_atts(rendered)
308321
return
309322

310323
# Queue a rendering for header, and setup anchor when rendering done.
311324
hdrnode = pending_node(pending.data, hdrtmpl, inline=True)
312-
hdrnode.hook_rendered_node(self._setup_objdesc_anchor)
325+
hdrnode.hook_rendered_nodes(self._setup_signode_anchor)
313326
self.queue_pending_node(hdrnode)
314327

315328
# Construct ObjectDescription.
316329
signode = addnodes.desc_signature('', '', addnodes.desc_name('', '', hdrnode))
317-
contnode = addnodes.desc_content('', *rendered.children)
330+
contnode = addnodes.desc_content('', *rendered)
318331
descnode = addnodes.desc('', signode, contnode)
319-
update_domaon_atts(descnode)
332+
self.update_domain_atts(descnode)
320333

321-
# Add descnode as child of rendered.
334+
# Replace the pass-in node list.
322335
rendered.clear()
323-
rendered += descnode
336+
rendered.append(descnode)
324337

325-
def _setup_objdesc_anchor(
326-
self, pending: pending_node, rendered: rendered_node
338+
def _setup_signode_anchor(
339+
self, pending: pending_node, rendered: list[nodes.Node]
327340
) -> None:
328-
domain, objtype = self._get_obj_domain_and_type()
329-
330341
ahrnode = find_parent(pending, addnodes.desc_signature)
331342
assert ahrnode
332-
obj = rendered.data
343+
obj = pending.data
333344
assert isinstance(obj, ParsedData)
334345

346+
self.setup_anchor(ahrnode, obj)
347+
348+
"""Helpers methods for self and subclasses."""
349+
350+
def get_obj_domain_and_type(self) -> tuple[ObjDomain, str]:
351+
domainname, _, objtype = self.name.partition(':')
352+
_domain = self.env.get_domain(domainname)
353+
return cast(ObjDomain, _domain), objtype
354+
355+
def update_domain_atts(self, node: nodes.Element):
356+
"""Attach domain related info to node."""
357+
domain, objtype = self.get_obj_domain_and_type()
358+
node['domain'] = domain.name
359+
# 'desctype' is a backwards compatible attribute
360+
node['objtype'] = node['desctype'] = objtype
361+
node['classes'].extend([domain.name, objtype])
362+
363+
def setup_anchor(self, ahrnode: nodes.Element, obj: Object) -> None:
364+
domain, objtype = self.get_obj_domain_and_type()
365+
335366
objids = get_object_uniq_ids(self.schema, obj)
336367
ahrterm = ValueWrapper(objids[0]).as_str() if objids else None
337368
ahrid = make_id(self.env, self.state.document, prefix=objtype, term=ahrterm)
@@ -351,8 +382,8 @@ def _setup_objdesc_anchor(
351382
report = Report(
352383
'Duplicate identifier',
353384
'INFO',
354-
source=pending.source,
355-
line=pending.line,
385+
source=ahrnode.source,
386+
line=ahrnode.line,
356387
)
357388
report.text(
358389
f'Duplicate object identifier at {self.env.docname}#{ahrid}, '
@@ -364,10 +395,82 @@ def _setup_objdesc_anchor(
364395
blkparent = find_nearest_block_element(ahrnode) or self.state.document
365396
blkparent += report
366397

367-
def _get_obj_domain_and_type(self) -> tuple[ObjDomain, str]:
368-
domainname, objtype = self.name.split(':', 1)
369-
_domain = self.env.get_domain(domainname)
370-
return cast(ObjDomain, _domain), objtype
398+
399+
class AutoObjDefineDirective(ObjDefineDirective):
400+
"""Methods that override from parent."""
401+
402+
@override
403+
@classmethod
404+
def derive(
405+
cls, name: str, schema: Schema, tmpl: Template
406+
) -> type[StrictDataDefineDirective]:
407+
subcls = super().derive(name, schema, tmpl)
408+
if schema.name and schema.name.required:
409+
# data.name is resolved from external.
410+
subcls.required_arguments = 0
411+
subcls.optional_arguments = 1
412+
return subcls
413+
414+
@override
415+
def process_pending_node(self, n: pending_node) -> bool:
416+
if (
417+
n.template == self.template
418+
and isinstance(n.data, PendingData)
419+
and self._require_external_header(n.data.schema, n.data.raw)
420+
):
421+
n.hook_raw_data(self._resolve_external_header)
422+
return super(StrictDataDefineDirective, self).process_pending_node(n)
423+
424+
return super().process_pending_node(n)
425+
426+
"""Methods used internal."""
427+
428+
def _require_external_header(self, schema: Schema, data: RawData) -> bool:
429+
# If the data.name is not given (None) or a underscore('_'), we think
430+
# the object requires an external name.
431+
#
432+
# The special underscore is for compatible with sphinxnotes-any<3.
433+
# See also https://sphinx.silverrainz.me/any/tips.html#documenting-section-and-documentation
434+
if not schema.name:
435+
return False
436+
if schema.name.ctype is None:
437+
return data.name in (None, '_')
438+
# HACK: We have to parse the data.name here.
439+
try:
440+
val = schema.name.parse(data.name)
441+
except ValueError:
442+
return False
443+
return ValueWrapper(val).as_str() == '_'
444+
445+
def _resolve_external_header(self, pending: pending_node, raw: RawData) -> None:
446+
domain, objtype = self.get_obj_domain_and_type()
447+
448+
if (hdrtmpl := domain.templates[objtype].header) is None:
449+
# No header template available, no need to generate objdesc.
450+
return
451+
if not (title := find_titular_node_upward(self.state.parent)):
452+
return
453+
454+
if raw.name is None:
455+
raw.name = title.astext()
456+
else:
457+
# HACK: See also _require_external_header.
458+
# TODO: Introduce a new extra context?
459+
raw.name = raw.name.replace('_', title.astext(), count=1)
460+
461+
pending_title = pending_node(pending.data, hdrtmpl, inline=True)
462+
pending_title.hook_rendered_nodes(self._setup_external_anchor)
463+
self.queue_pending_node(pending_title)
464+
465+
# Replace title's children with pending node.
466+
title.clear()
467+
title += pending_title
468+
469+
def _setup_external_anchor(
470+
self, pending: pending_node, rendered: list[nodes.Node]
471+
) -> None:
472+
assert isinstance(pending.data, ParsedData)
473+
self.setup_anchor(pending.parent, pending.data)
371474

372475

373476
# =================

src/sphinxnotes/any/obj.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ class Templates:
9191
cross-references, and etc."""
9292

9393
"""Templates for rendering object"""
94-
content: Template
94+
obj: Template
9595
header: Template | None
9696

9797
# embed: Templates
@@ -113,7 +113,7 @@ def __init__(
113113
ref_by: dict[str, str] = {},
114114
debug: bool = False,
115115
):
116-
self.content = Template(content, Phase.Parsing, debug)
116+
self.obj = Template(content, Phase.Parsing, debug)
117117
self.header = Template(header, Phase.Parsing, debug) if header else None
118118
self.ref = Template(ref, Phase.PostTranform, debug)
119119
self.ref_by = {
@@ -127,6 +127,16 @@ def get_ref_by(self, reftype: RefType) -> Template:
127127
return self.ref
128128

129129

130+
@dataclass
131+
class ObjTypeDef:
132+
schema: Schema
133+
templates: Templates
134+
135+
#: When enabled, resolving external nodes for supplement the missing
136+
# RawData.name and RawData.content.
137+
auto: bool
138+
139+
130140
# ============================
131141
# Basic types for object index
132142
# ============================

0 commit comments

Comments
 (0)