Skip to content

Commit c5355b4

Browse files
authored
refactor: Rework role and index constructing (#41)
Completed all the functions lost compared to v2.
1 parent 1e68392 commit c5355b4

File tree

5 files changed

+130
-108
lines changed

5 files changed

+130
-108
lines changed

docs/_templates/version.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
:Date: :version.date.by-year:`📅{{ date }} <{{ date }}>`
2+
:Date: :version.date+by-year:`📅{{ date }} <{{ date }}>`
33
:Download: :tag:`{{ title }}`
44

55
{% for line in content %}

src/sphinxnotes/any/domain.py

Lines changed: 45 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from sphinx.util import logging
1919
from sphinx.util.nodes import make_refnode
2020

21-
from .schema import Schema, Object
21+
from .schema import Schema, Object, RefType, Indexer, LiteralIndexer
2222
from .directives import AnyDirective
2323
from .roles import AnyRole
2424
from .indices import AnyIndex
@@ -49,9 +49,9 @@ class AnyDomain(Domain):
4949
roles: dict[str, RoleFunction] = {}
5050
#: A list of Index subclasses
5151
indices: list[type[AnyIndex]] = []
52-
#: AnyDomain specific: type -> index class
52+
#: AnyDomain specific: reftype -> index class
5353
_indices_for_reftype: dict[str, type[AnyIndex]] = {}
54-
#: AnyDomain specific: type -> Schema instance
54+
#: AnyDomain specific: objtype -> Schema instance
5555
_schemas: dict[str, Schema] = {}
5656

5757
initial_data: dict[str, Any] = {
@@ -120,7 +120,7 @@ def resolve_xref(
120120
logger.debug('[any] resolveing xref of %s', (typ, target))
121121

122122
reftype = RefType.parse(typ)
123-
objtype, objfield, objidx = reftype.objtype, reftype.field, reftype.index
123+
objtype, objfield, objidx = reftype.objtype, reftype.field, reftype.indexer
124124
objids = set()
125125
if objidx:
126126
pass # no need to lookup objds
@@ -142,10 +142,7 @@ def resolve_xref(
142142
if len(objids) > 1 or objidx is not None:
143143
# Mulitple objects found or reference index explicitly,
144144
# create link to indices page.
145-
(
146-
todocname,
147-
anchor,
148-
) = self._get_index_anchor(typ, target)
145+
(todocname, anchor) = self._get_index_anchor(typ, target)
149146
if not has_explicit_title:
150147
newtitle = schema.render_ambiguous_reference(title)
151148
logger.debug(
@@ -181,43 +178,51 @@ def add_schema(cls, schema: Schema) -> None:
181178
# Add to schemas dict
182179
cls._schemas[schema.objtype] = schema
183180

184-
# Generates reftypes for all referenceable fields
185-
# For later use when generating roles and indices.
186-
reftypes = [str(RefType(schema.objtype, None, None))]
187-
for name, field in schema.fields(all=False):
188-
if not field.ref:
189-
continue
190-
191-
# Field is unique , use ``:objtype.field:`` to reference.
192-
if field.uniq:
193-
reftype = str(RefType(schema.objtype, name, None))
194-
reftypes.append(reftype)
195-
continue
196-
197-
for name, field in schema.fields(all=False):
198-
# Field is not unique, link to index page.
199-
for indexer in field.indexers:
200-
reftype = str(RefType(schema.objtype, name, indexer.name))
201-
reftypes.append(reftype)
202-
203-
# FIXME: name and content can not be index now
204-
index = AnyIndex.derive(schema, name, indexer)
205-
cls.indices.append(index)
206-
cls._indices_for_reftype[reftype] = index
207-
208-
for reftype in reftypes:
209-
field = RefType.parse(reftype).field
210-
# Create role for referencing object by field
211-
cls.roles[reftype] = AnyRole.derive(schema, field)(
181+
def mkrole(reftype: RefType):
182+
"""Create and register role for referencing object."""
183+
role = AnyRole(
212184
# Emit warning when missing reference (node['refwarn'] = True)
213185
warn_dangling=True,
214186
# Inner node (contnode) would be replaced in resolve_xref method,
215187
# so fix its class.
216188
innernodeclass=literal,
217189
)
190+
cls.roles[str(reftype)] = role
191+
logger.debug(f'[any] make role {reftype}{type(role)}')
192+
193+
def mkindex(reftype: RefType, indexer: Indexer):
194+
"""Create and register object index."""
195+
index = AnyIndex.derive(schema, reftype, indexer)
196+
cls.indices.append(index)
197+
cls._indices_for_reftype[str(reftype)] = index
198+
logger.debug(f'[any] make index {reftype}{type(index)}')
199+
200+
# Create all-in-one role and index (do not distinguish reference fields).
201+
reftypes = [RefType(schema.objtype)]
202+
mkrole(reftypes[0])
203+
mkindex(reftypes[0], LiteralIndexer())
204+
205+
# Create {field,indexer}-specificed role and index.
206+
for name, field in schema.fields():
207+
if field.ref:
208+
reftype = RefType(schema.objtype, field=name)
209+
reftypes.append(reftype)
210+
mkrole(reftype) # create a role to reference object(s)
211+
# Create a fallback indexer, for possible ambiguous reference
212+
# (if field is not unique).
213+
mkindex(reftype, LiteralIndexer())
214+
215+
for indexer in field.indexers:
216+
reftype = RefType(schema.objtype, field=name, indexer=indexer.name)
217+
reftypes.append(reftype)
218+
# Create role and index for reference objects by index.
219+
mkrole(reftype)
220+
mkindex(reftype, indexer)
218221

219222
# TODO: document
220-
cls.object_types[schema.objtype] = ObjType(schema.objtype, *reftypes)
223+
cls.object_types[schema.objtype] = ObjType(
224+
schema.objtype, *[str(x) for x in reftypes]
225+
)
221226
# Generates directive for creating object.
222227
cls.directives[schema.objtype] = AnyDirective.derive(schema)
223228

@@ -238,42 +243,9 @@ def warn_missing_reference(
238243
if domain and domain.name != AnyDomain.name:
239244
return None
240245

241-
objtype = RefType.parse(node['reftype']).objtype
246+
reftype = RefType.parse(node['reftype'])
242247
target = node['reftarget']
243248

244-
msg = f'undefined {objtype}: {target}'
245-
logger.warning(msg, location=node, type='ref', subtype=objtype)
249+
msg = f'undefined reftype {reftype}: {target}'
250+
logger.warning(msg, location=node, type='ref', subtype=reftype.objtype)
246251
return True
247-
248-
249-
class RefType(object):
250-
"""Reference type, used as role name and node['reftype'] and
251-
and *typ* argument of :meth:`AnyDomain.resolve_xref` method."""
252-
253-
#: :attr:`ObjType.lname`
254-
objtype: str
255-
#: :attr:`.schema.Field.name`
256-
field: str | None
257-
#: :attr:`.schema.Indexer.name`
258-
index: str | None
259-
260-
def __init__(self, objtype: str, field: str | None, index: str | None):
261-
self.objtype = objtype
262-
self.field = field
263-
self.index = index
264-
265-
@classmethod
266-
def parse(cls, reftype: str):
267-
v = reftype.split('.', maxsplit=2)
268-
objtype = v[0]
269-
field = v[1] if len(v) > 1 else None
270-
index = v[2][3:] if len(v) > 2 else None # skip "by-"
271-
return cls(objtype, field, index)
272-
273-
def __str__(self):
274-
s = self.objtype
275-
if self.field is not None:
276-
s += '.' + self.field
277-
if self.index is not None:
278-
s += '.' + 'by-' + self.index
279-
return s

src/sphinxnotes/any/indices.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from docutils import core, nodes
1717
from docutils.parsers.rst import roles
1818

19-
from .schema import Schema, Value, Indexer, Category
19+
from .schema import Schema, Value, Indexer, Category, RefType
2020

2121
logger = logging.getLogger(__name__)
2222

@@ -28,34 +28,35 @@ class AnyIndex(Index):
2828

2929
domain: Domain # for type hint
3030
schema: Schema
31-
field: str | None = None
31+
reftype: RefType
3232
indexer: Indexer
3333

3434
@classmethod
3535
def derive(
36-
cls, schema: Schema, field: str | None, indexer: Indexer
36+
cls, schema: Schema, reftype: RefType, indexer: Indexer
3737
) -> type['AnyIndex']:
3838
"""Generate an AnyIndex child class for indexing object."""
39-
# TODO: add Indexer.name
40-
if field:
41-
typ = f'Any{schema.objtype.title()}{field.title()}IndexBy{indexer.name.title()}'
42-
name = schema.objtype + '.' + field + '.by-' + indexer.name # TODO: RefType
43-
localname = f'{schema.objtype.title()} {field.title()} Reference Index by {indexer.name.title()}'
44-
else:
45-
typ = f'Any{schema.objtype.title()}IndexBy{indexer.name.title()}'
46-
name = schema.objtype + '.by-' + indexer.name # TODO: RefType
39+
if reftype.field:
40+
clsname = f'Any{reftype.objtype.title()}{reftype.field.title()}Index'
4741
localname = (
48-
f'{schema.objtype.title()} Reference Index by {indexer.name.title()}'
42+
f'{reftype.objtype.title()} {reftype.field.title()} Reference Index'
4943
)
44+
else:
45+
clsname = f'Any{reftype.objtype.title()}Index'
46+
localname = f'{reftype.objtype.title()} Reference Index'
47+
if reftype.indexer:
48+
clsname += 'By' + reftype.indexer.title()
49+
localname += ' by ' + reftype.indexer.title()
5050
return type(
51-
typ,
51+
clsname,
5252
(cls,),
5353
{
54-
'name': name,
54+
# HTML builder will generate /<domain>-<name>.html index page.
55+
'name': str(reftype),
5556
'localname': localname,
5657
'shortname': 'references',
5758
'schema': schema,
58-
'field': field,
59+
'reftype': reftype,
5960
'indexer': indexer,
6061
},
6162
)
@@ -74,9 +75,9 @@ def generate(
7475

7576
objrefs = sorted(self.domain.data['references'].items())
7677
for (objtype, objfield, objref), objids in objrefs:
77-
if objtype != self.schema.objtype:
78+
if objtype != self.reftype.objtype:
7879
continue
79-
if self.field and objfield != self.field:
80+
if self.reftype.field and objfield != self.reftype.field:
8081
continue
8182

8283
# TODO: pass a real Value
@@ -124,7 +125,7 @@ def generate(
124125
def _generate_index_entry(
125126
self, objid: str, ignore_docnames: Iterable[str] | None, category: Category
126127
) -> IndexEntry | None:
127-
docname, anchor, obj = self.domain.data['objects'][self.schema.objtype, objid]
128+
docname, anchor, obj = self.domain.data['objects'][self.reftype.objtype, objid]
128129
if ignore_docnames and docname not in ignore_docnames:
129130
return None
130131
name = self.schema.title_of(obj) or objid

src/sphinxnotes/any/roles.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
from sphinx.util import logging
1414
from sphinx.roles import XRefRole
1515

16-
from .schema import Schema
17-
1816
logger = logging.getLogger(__name__)
1917

2018

@@ -25,11 +23,7 @@ class AnyRole(XRefRole):
2523
objtype.
2624
"""
2725

28-
@classmethod
29-
def derive(cls, schema: Schema, field: str | None = None) -> type['AnyRole']:
30-
"""Generate an AnyRole child class for referencing object."""
31-
return type(
32-
'Any%s%sRole' % (schema.objtype.title(), field.title() if field else ''),
33-
(cls,),
34-
{},
35-
)
26+
# NOTE: derive is not necessary for now.
27+
# @classmethod
28+
# def derive(cls) -> type['AnyRole']:
29+
# pass

src/sphinxnotes/any/schema.py

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -353,9 +353,7 @@ class Forms:
353353
ref: bool = False
354354
required: bool = False
355355
form: Form = Forms.PLAIN
356-
indexers: list[Indexer] = dataclasses.field(
357-
default_factory=lambda: [LiteralIndexer()]
358-
)
356+
indexers: list[Indexer] = dataclasses.field(default_factory=lambda: [])
359357

360358
def value_of(self, rawval: str | None) -> Value:
361359
if rawval is None:
@@ -438,7 +436,9 @@ def __init__(
438436
else:
439437
has_unique = field.uniq
440438

441-
def fields(self, all=True) -> list[tuple[str, Field]]:
439+
def fields(
440+
self, exclude_name: bool = False, exclude_content: bool = False
441+
) -> list[tuple[str, Field]]:
442442
"""Return all fields of schema, including name and content.
443443
444444
.. note::
@@ -448,9 +448,9 @@ def fields(self, all=True) -> list[tuple[str, Field]]:
448448
"""
449449

450450
fields = list(self.attrs.items())
451-
if all and self.content is not None:
451+
if not exclude_content and self.content is not None:
452452
fields.insert(0, (self.CONTENT_KEY, self.content))
453-
if all and self.name is not None:
453+
if not exclude_name and self.name is not None:
454454
fields.insert(0, (self.NAME_KEY, self.name))
455455
return fields
456456

@@ -631,3 +631,58 @@ def __eq__(self, other: Any) -> bool:
631631
if not isinstance(other, Schema):
632632
return False
633633
return pickle.dumps(self) == pickle.dumps(other)
634+
635+
636+
class RefType(object):
637+
"""Reference type, object can be referenced:
638+
639+
- by type *objtype*
640+
- by field *objtype*.*field*
641+
- by field index *objtype*.*field*+by-*index*
642+
"""
643+
644+
#: Object type, same to :attr:`Schema.objtype`.
645+
objtype: str
646+
#: Name of field, see :attr:`.schema.Field.name`.
647+
field: str | None
648+
#: Name of indexer, see :attr:`.schema.Indexer.name`.
649+
indexer: str | None
650+
651+
def __init__(
652+
self, objtype: str, field: str | None = None, indexer: str | None = None
653+
):
654+
self.objtype = objtype
655+
self.field = field
656+
self.indexer = indexer
657+
658+
@classmethod
659+
def parse(cls, reftype: str):
660+
"""Format: <objtype>[.<field>[+<action>]]. Possible action:
661+
662+
- by-<indexer>
663+
"""
664+
665+
if '+' in reftype:
666+
reftype, action = reftype.split('+', 1)
667+
if action.startswith('by-'):
668+
index = action[3:]
669+
else:
670+
raise ValueError(f'unknown action {action} in RefType {reftype}')
671+
else:
672+
index = None
673+
674+
if '.' in reftype:
675+
objtype, field = reftype.split('.', 1)
676+
else:
677+
objtype, field = reftype, None
678+
679+
return cls(objtype, field, index)
680+
681+
def __str__(self):
682+
"""Used as role name and index name."""
683+
s = self.objtype
684+
if self.field:
685+
s += '.' + self.field
686+
if self.indexer:
687+
s += '+' + 'by-' + self.indexer
688+
return s

0 commit comments

Comments
 (0)