Skip to content

Commit 1e68392

Browse files
authored
Multiple indices support (#40)
- Refactor, better naming - Add MonthIndexer - Support multiple indexers - Fix anchor support of index page
1 parent 6a3401e commit 1e68392

File tree

7 files changed

+273
-163
lines changed

7 files changed

+273
-163
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: :ref:`📅{{ date }} <any-version.date>`
2+
:Date: :version.date.by-year:`📅{{ date }} <{{ date }}>`
33
:Download: :tag:`{{ title }}`
44

55
{% for line in content %}

docs/conf.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,17 @@
114114

115115
#
116116
# DOG FOOD CONFIGURATION START
117-
from any import Schema, Field as F, DateClassifier
117+
from any import Schema, Field as F
118+
from any.schema import YearIndexer, MonthIndexer
118119
sys.path.insert(0, os.path.abspath('.'))
119120

121+
by_year = YearIndexer()
122+
by_month = MonthIndexer()
123+
120124
version_schema = Schema('version',
121125
name=F(uniq=True, ref=True, required=True, form=F.Forms.LINES),
122126
attrs={
123-
'date': F(ref=True, classifiers=[DateClassifier(['%Y-%m-%d'])]),
127+
'date': F(ref=True, indexers=[by_year, by_month]),
124128
},
125129
content=F(form=F.Forms.LINES),
126130
description_template=open('_templates/version.rst', 'r').read(),

src/sphinxnotes/any/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from .template import Environment as TemplateEnvironment
1616
from .domain import AnyDomain, warn_missing_reference
17-
from .schema import Schema, Field, DateClassifier
17+
from .schema import Schema, Field
1818

1919
if TYPE_CHECKING:
2020
from sphinx.application import Sphinx
@@ -27,7 +27,6 @@
2727
# Re-Export
2828
Field = Field
2929
Schema = Schema
30-
DateClassifier = DateClassifier
3130

3231

3332
def _config_inited(app: Sphinx, config: Config) -> None:

src/sphinxnotes/any/domain.py

Lines changed: 69 additions & 41 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, PlainClassifier
21+
from .schema import Schema, Object
2222
from .directives import AnyDirective
2323
from .roles import AnyRole
2424
from .indices import AnyIndex
@@ -118,8 +118,12 @@ def resolve_xref(
118118
assert isinstance(contnode, literal)
119119

120120
logger.debug('[any] resolveing xref of %s', (typ, target))
121-
objtype, objfield = reftype_to_objtype_and_objfield(typ)
121+
122+
reftype = RefType.parse(typ)
123+
objtype, objfield, objidx = reftype.objtype, reftype.field, reftype.index
122124
objids = set()
125+
if objidx:
126+
pass # no need to lookup objds
123127
if objfield:
124128
# NOTE: To prevent change domain data, dont use ``objids = xxx``
125129
ids = self.references.get((objtype, objfield, target))
@@ -135,16 +139,9 @@ def resolve_xref(
135139
has_explicit_title = node['refexplicit']
136140
newtitle = None
137141

138-
if not objids:
139-
# The pending_xref node may be resolved by intersphinx,
140-
# so not emit warning here, see also warn_missing_reference.
141-
return None
142-
elif len(objids) == 1:
143-
todocname, anchor, obj = self.objects[objtype, objids.pop()]
144-
if not has_explicit_title:
145-
newtitle = schema.render_reference(obj)
146-
else:
147-
# Mulitple objects found, we should create link to indices page.
142+
if len(objids) > 1 or objidx is not None:
143+
# Mulitple objects found or reference index explicitly,
144+
# create link to indices page.
148145
(
149146
todocname,
150147
anchor,
@@ -155,6 +152,14 @@ def resolve_xref(
155152
f'ambiguous {objtype} {target} in {self}, '
156153
+ f'ids: {objids} index: {todocname}#{anchor}'
157154
)
155+
elif len(objids) == 1:
156+
todocname, anchor, obj = self.objects[objtype, objids.pop()]
157+
if not has_explicit_title:
158+
newtitle = schema.render_reference(obj)
159+
else:
160+
# The pending_xref node may be resolved by intersphinx,
161+
# so do not emit warning here, see also warn_missing_reference.
162+
return None
158163

159164
if newtitle:
160165
logger.debug(f'[any] rewrite title from {title} to {newtitle}')
@@ -176,18 +181,32 @@ def add_schema(cls, schema: Schema) -> None:
176181
# Add to schemas dict
177182
cls._schemas[schema.objtype] = schema
178183

179-
reftypes = [schema.objtype]
184+
# Generates reftypes for all referenceable fields
185+
# For later use when generating roles and indices.
186+
reftypes = [str(RefType(schema.objtype, None, None))]
180187
for name, field in schema.fields(all=False):
181188
if not field.ref:
182189
continue
183190

184-
# Generates reftypes for all referenceable fields
185-
# For later use when generating roles and indices.
186-
reftype = objtype_and_objfield_to_reftype(schema.objtype, name)
187-
reftypes.append(reftype)
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
188207

189208
for reftype in reftypes:
190-
_, field = reftype_to_objtype_and_objfield(reftype)
209+
field = RefType.parse(reftype).field
191210
# Create role for referencing object by field
192211
cls.roles[reftype] = AnyRole.derive(schema, field)(
193212
# Emit warning when missing reference (node['refwarn'] = True)
@@ -197,19 +216,6 @@ def add_schema(cls, schema: Schema) -> None:
197216
innernodeclass=literal,
198217
)
199218

200-
# FIXME: name and content can not be index now
201-
if field is not None:
202-
classifiers = schema.attrs[field].classifiers
203-
elif schema.name is not None:
204-
classifiers = schema.name.classifiers
205-
else:
206-
classifiers = [PlainClassifier()]
207-
# Generates index for indexing object by fields
208-
for indexer in classifiers:
209-
index = AnyIndex.derive(schema, field, indexer)
210-
cls.indices.append(index)
211-
cls._indices_for_reftype[reftype] = index # TODO: mulitple catelogers.
212-
213219
# TODO: document
214220
cls.object_types[schema.objtype] = ObjType(schema.objtype, *reftypes)
215221
# Generates directive for creating object.
@@ -222,8 +228,8 @@ def _get_index_anchor(self, reftype: str, refval: str) -> tuple[str, str]:
222228
.. warning:: This is no public API of sphinx and may broken in future version.
223229
"""
224230
domain = self.name
225-
index = self._indices_for_reftype[reftype].name
226-
return f'{domain}-{index}', f'cap-{refval}'
231+
index = self._indices_for_reftype[reftype]
232+
return f'{domain}-{index.name}', index.indexer.anchor(refval)
227233

228234

229235
def warn_missing_reference(
@@ -232,20 +238,42 @@ def warn_missing_reference(
232238
if domain and domain.name != AnyDomain.name:
233239
return None
234240

235-
objtype, _ = reftype_to_objtype_and_objfield(node['reftype'])
241+
objtype = RefType.parse(node['reftype']).objtype
236242
target = node['reftarget']
237243

238244
msg = f'undefined {objtype}: {target}'
239245
logger.warning(msg, location=node, type='ref', subtype=objtype)
240246
return True
241247

242248

243-
def reftype_to_objtype_and_objfield(reftype: str) -> tuple[str, str | None]:
244-
"""Helper function for converting reftype(role name) to object infos."""
245-
v = reftype.split('.', maxsplit=1)
246-
return v[0], v[1] if len(v) == 2 else None
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
247259

260+
def __init__(self, objtype: str, field: str | None, index: str | None):
261+
self.objtype = objtype
262+
self.field = field
263+
self.index = index
248264

249-
def objtype_and_objfield_to_reftype(objtype: str, objfield: str) -> str:
250-
"""Helper function for converting object infos to reftype(role name)."""
251-
return objtype + '.' + objfield
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: 48 additions & 45 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, Classifier, Classif
19+
from .schema import Schema, Value, Indexer, Category
2020

2121
logger = logging.getLogger(__name__)
2222

@@ -29,22 +29,24 @@ class AnyIndex(Index):
2929
domain: Domain # for type hint
3030
schema: Schema
3131
field: str | None = None
32-
classifier: Classifier
32+
indexer: Indexer
3333

3434
@classmethod
3535
def derive(
36-
cls, schema: Schema, field: str | None, classifier: Classifier
36+
cls, schema: Schema, field: str | None, indexer: Indexer
3737
) -> type['AnyIndex']:
3838
"""Generate an AnyIndex child class for indexing object."""
3939
# TODO: add Indexer.name
4040
if field:
41-
typ = f'Any{schema.objtype.title()}{field.title()}Index'
42-
name = schema.objtype + '.' + field # TOOD: objtype_and_objfield_to_reftype
43-
localname = f'{schema.objtype.title()} {field.title()} Reference Index'
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()}'
4444
else:
45-
typ = f'Any{schema.objtype.title()}Index'
46-
name = schema.objtype
47-
localname = f'{schema.objtype.title()} Reference Index'
45+
typ = f'Any{schema.objtype.title()}IndexBy{indexer.name.title()}'
46+
name = schema.objtype + '.by-' + indexer.name # TODO: RefType
47+
localname = (
48+
f'{schema.objtype.title()} Reference Index by {indexer.name.title()}'
49+
)
4850
return type(
4951
typ,
5052
(cls,),
@@ -54,7 +56,7 @@ def derive(
5456
'shortname': 'references',
5557
'schema': schema,
5658
'field': field,
57-
'classifier': classifier,
59+
'indexer': indexer,
5860
},
5961
)
6062

@@ -64,11 +66,11 @@ def generate(
6466
"""Override parent method."""
6567

6668
# Single index for generating normal entries (subtype=0).
67-
# Category (lv1) Category (for ordering objids) → objids
68-
singleidx: dict[Classif, dict[Classif, set[str]]] = {}
69+
# Main Category → Extra (for ordering objids) → objids
70+
singleidx: dict[Category, dict[Category, set[str]]] = {}
6971
# Dual index for generating entrie (subtype=1) and its sub-entries (subtype=2).
70-
# Category (lv1) → Category (lv2) Category (for ordering objids) → objids
71-
dualidx: dict[Classif, dict[Classif, dict[Classif, set[str]]]] = {}
72+
# Main category Sub-Category → Extra (for ordering objids) → objids
73+
dualidx: dict[Category, dict[Category, dict[Category, set[str]]]] = {}
7274

7375
objrefs = sorted(self.domain.data['references'].items())
7476
for (objtype, objfield, objref), objids in objrefs:
@@ -77,57 +79,57 @@ def generate(
7779
if self.field and objfield != self.field:
7880
continue
7981

80-
# TODO: pass a real value
81-
for catelog in self.classifier.classify(Value(objref)):
82-
category = catelog.as_category()
83-
entry = catelog.as_entry()
84-
if entry is None:
85-
singleidx.setdefault(category, {}).setdefault(
86-
catelog, set()
87-
).update(objids)
82+
# TODO: pass a real Value
83+
for category in self.indexer.classify(Value(objref)):
84+
main = category.as_main()
85+
sub = category.as_sub()
86+
if sub is None:
87+
singleidx.setdefault(main, {}).setdefault(category, set()).update(
88+
objids
89+
)
8890
else:
89-
dualidx.setdefault(category, {}).setdefault(entry, {}).setdefault(
90-
catelog, set()
91+
dualidx.setdefault(main, {}).setdefault(sub, {}).setdefault(
92+
category, set()
9193
).update(objids)
9294

93-
content: dict[Classif, list[IndexEntry]] = {} # category → entries
94-
for category, entries in self._sort_by_catelog(singleidx):
95-
index_entries = content.setdefault(category, [])
96-
for category, objids in self._sort_by_catelog(entries):
95+
content: dict[Category, list[IndexEntry]] = {} # category → entries
96+
for main, entries in self._sort_by_category(singleidx):
97+
index_entries = content.setdefault(main, [])
98+
for main, objids in self._sort_by_category(entries):
9799
for objid in objids:
98-
entry = self._generate_index_entry(objid, docnames, category)
100+
entry = self._generate_index_entry(objid, docnames, main)
99101
if entry is None:
100102
continue
101103
index_entries.append(entry)
102104

103-
for category, entries in self._sort_by_catelog(dualidx):
104-
index_entries = content.setdefault(category, [])
105-
for entry, subentries in self._sort_by_catelog(entries):
106-
index_entries.append(self._generate_empty_index_entry(entry))
107-
for subentry, objids in self._sort_by_catelog(subentries):
105+
for main, entries in self._sort_by_category(dualidx):
106+
index_entries = content.setdefault(main, [])
107+
for sub, subentries in self._sort_by_category(entries):
108+
index_entries.append(self._generate_subcategory_index_entry(sub))
109+
for subentry, objids in self._sort_by_category(subentries):
108110
for objid in objids:
109111
entry = self._generate_index_entry(objid, docnames, subentry)
110112
if entry is None:
111113
continue
112114
index_entries.append(entry)
113115

114-
# sort by category, and map classif -> str
116+
# sort by category, and map category -> str
115117
sorted_content = [
116-
(classif.leaf, entries)
117-
for classif, entries in self._sort_by_catelog(content)
118+
(category.main, entries)
119+
for category, entries in self._sort_by_category(content)
118120
]
119121

120122
return sorted_content, False
121123

122124
def _generate_index_entry(
123-
self, objid: str, ignore_docnames: Iterable[str] | None, category: Classif
125+
self, objid: str, ignore_docnames: Iterable[str] | None, category: Category
124126
) -> IndexEntry | None:
125127
docname, anchor, obj = self.domain.data['objects'][self.schema.objtype, objid]
126128
if ignore_docnames and docname not in ignore_docnames:
127129
return None
128130
name = self.schema.title_of(obj) or objid
129-
subtype = category.index_entry_subtype
130-
extra = category.leaf
131+
subtype = category.index_entry_subtype()
132+
extra = category.extra or ''
131133
objcont = self.schema.content_of(obj)
132134
if isinstance(objcont, str):
133135
desc = objcont
@@ -148,16 +150,17 @@ def _generate_index_entry(
148150
desc, # description for the entry
149151
)
150152

151-
def _generate_empty_index_entry(self, category: Classif) -> IndexEntry:
153+
def _generate_subcategory_index_entry(self, category: Category) -> IndexEntry:
154+
assert category.sub is not None
152155
return IndexEntry(
153-
category.leaf, category.index_entry_subtype, '', '', '', '', ''
156+
category.sub, category.index_entry_subtype(), '', '', '', '', ''
154157
)
155158

156159
_T = TypeVar('_T')
157160

158-
def _sort_by_catelog(self, d: dict[Classif, _T]) -> list[tuple[Classif, _T]]:
159-
"""Helper for sorting dict items by Category."""
160-
return self.classifier.sort(d.items(), lambda x: x[0])
161+
def _sort_by_category(self, d: dict[Category, _T]) -> list[tuple[Category, _T]]:
162+
"""Helper for sorting dict items by classif."""
163+
return self.indexer.sort(d.items(), lambda x: x[0])
161164

162165

163166
def strip_rst_markups(rst: str) -> str:

0 commit comments

Comments
 (0)