Skip to content

Commit 1862856

Browse files
authored
refactor: Provide a sphinxnotes.any.api package (#43)
1 parent 04d193b commit 1862856

File tree

13 files changed

+220
-184
lines changed

13 files changed

+220
-184
lines changed

docs/_schemas/cat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from textwrap import dedent
2-
from any import Schema, Field
2+
from any.api import Schema, Field
33

44
cat = Schema(
55
'cat',

docs/_schemas/dog2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from textwrap import dedent
2-
from any import Schema, Field
2+
from any.api import Schema, Field
33

44
dog = Schema(
55
'dog',

docs/_schemas/tmplvar.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from textwrap import dedent
2-
from any import Schema, Field
2+
from any.api import Schema, Field
33

44
tmplvar = Schema(
55
'tmplvar',

docs/conf.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,7 @@
114114

115115
#
116116
# DOG FOOD CONFIGURATION START
117-
from any import Schema, Field as F
118-
from any.schema import YearIndexer, MonthIndexer
119-
sys.path.insert(0, os.path.abspath('.'))
120-
121-
by_year = YearIndexer()
122-
by_month = MonthIndexer()
117+
from any.api import Schema, Field as F, by_year, by_month
123118

124119
version_schema = Schema('version',
125120
name=F(uniq=True, ref=True, required=True, form=F.Forms.LINES),

docs/usage.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Defining Schema
1515

1616
The necessary python classes for writing schema are listed here:
1717

18-
.. autoclass:: any.Schema
18+
.. autoclass:: any.api.Schema
1919

2020
Class-wide shared special keys used in template rendering context:
2121

@@ -26,15 +26,15 @@ The necessary python classes for writing schema are listed here:
2626

2727
|
2828
29-
.. autoclass:: any.Field
29+
.. autoclass:: any.api.Field
3030

3131
|
3232
33-
.. autoclass:: any.Field.Forms
33+
.. autoclass:: any.api.Field.Forms
3434

35-
.. autoattribute:: any.Field.Forms.PLAIN
36-
.. autoattribute:: any.Field.Forms.WORDS
37-
.. autoattribute:: any.Field.Forms.LINES
35+
.. autoattribute:: any.api.Field.Forms.PLAIN
36+
.. autoattribute:: any.api.Field.Forms.WORDS
37+
.. autoattribute:: any.api.Field.Forms.LINES
3838

3939
Documenting Object
4040
==================

src/sphinxnotes/any/__init__.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,21 @@
1010

1111
from __future__ import annotations
1212
from typing import TYPE_CHECKING
13+
from importlib.metadata import version
14+
1315
from sphinx.util import logging
1416

1517
from .template import Environment as TemplateEnvironment
1618
from .domain import AnyDomain, warn_missing_reference
17-
from .schema import Schema, Field
19+
from .objects import Schema
1820

1921
if TYPE_CHECKING:
2022
from sphinx.application import Sphinx
2123
from sphinx.config import Config
2224

23-
__version__ = '2.3.1'
2425

2526
logger = logging.getLogger(__name__)
2627

27-
# Re-Export
28-
Field = Field
29-
Schema = Schema
30-
3128

3229
def _config_inited(app: Sphinx, config: Config) -> None:
3330
AnyDomain.name = config.any_domain_name
@@ -51,4 +48,4 @@ def setup(app: Sphinx):
5148
app.connect('config-inited', _config_inited)
5249
app.connect('warn-missing-reference', warn_missing_reference)
5350

54-
return {'version': __version__}
51+
return {'version': version('sphinxnotes.any')}

src/sphinxnotes/any/api.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""
2+
sphinxnotes.any.api
3+
~~~~~~~~~~~~~~~~~~~
4+
5+
Public API for building configuration of extension.
6+
(such as object schema, and so on).
7+
8+
:copyright: Copyright 2024 Shengyu Zhang
9+
:license: BSD, see LICENSE for details.
10+
"""
11+
12+
from .objects import Schema, Field
13+
from .indexers import LiteralIndexer, PathIndexer, YearIndexer, MonthIndexer
14+
15+
# Object schema.
16+
Schema = Schema
17+
Field = Field
18+
19+
# Indexers.
20+
LiteralIndexer = LiteralIndexer
21+
PathIndexer = PathIndexer
22+
YearIndexer = YearIndexer
23+
MonthIndexer = MonthIndexer
24+
25+
# Indexer wrappers.
26+
by_year = YearIndexer()
27+
by_month = MonthIndexer()

src/sphinxnotes/any/directives.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,12 @@
1515
from docutils.nodes import Node, Element, fully_normalize_name
1616
from docutils.statemachine import StringList
1717
from docutils.parsers.rst import directives
18-
1918
from sphinx import addnodes
2019
from sphinx.util.docutils import SphinxDirective
2120
from sphinx.util.nodes import make_id, nested_parse_with_titles
2221
from sphinx.util import logging
2322

24-
from .schema import Schema, Object
23+
from .objects import Schema, Object
2524

2625
logger = logging.getLogger(__name__)
2726

src/sphinxnotes/any/domain.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@
1818
from sphinx.util import logging
1919
from sphinx.util.nodes import make_refnode
2020

21-
from .schema import Schema, Object, RefType, Indexer, LiteralIndexer
21+
from .objects import Schema, Object, RefType, Indexer
2222
from .directives import AnyDirective
2323
from .roles import AnyRole
2424
from .indices import AnyIndex
25+
from .indexers import DEFAULT_INDEXER
2526

2627
if TYPE_CHECKING:
2728
from sphinx.application import Sphinx
@@ -200,7 +201,7 @@ def mkindex(reftype: RefType, indexer: Indexer):
200201
# Create all-in-one role and index (do not distinguish reference fields).
201202
reftypes = [RefType(schema.objtype)]
202203
mkrole(reftypes[0])
203-
mkindex(reftypes[0], LiteralIndexer())
204+
mkindex(reftypes[0], DEFAULT_INDEXER)
204205

205206
# Create {field,indexer}-specificed role and index.
206207
for name, field in schema.fields():
@@ -210,7 +211,7 @@ def mkindex(reftype: RefType, indexer: Indexer):
210211
mkrole(reftype) # create a role to reference object(s)
211212
# Create a fallback indexer, for possible ambiguous reference
212213
# (if field is not unique).
213-
mkindex(reftype, LiteralIndexer())
214+
mkindex(reftype, DEFAULT_INDEXER)
214215

215216
for indexer in field.indexers:
216217
reftype = RefType(schema.objtype, field=name, indexer=indexer.name)

src/sphinxnotes/any/indexers.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
"""
2+
sphinxnotes.any.indexers
3+
~~~~~~~~~~~~~~~~~~~~~~~~
4+
5+
:cls:`objects.Indexer` implementations.
6+
7+
:copyright: Copyright 2024 Shengyu Zhang
8+
:license: BSD, see LICENSE for details.
9+
"""
10+
11+
from typing import Iterable, Literal, Callable
12+
from time import strptime, strftime
13+
14+
from .objects import Indexer, Category, Value
15+
16+
17+
class LiteralIndexer(Indexer):
18+
name = 'literal'
19+
20+
def classify(self, objref: Value) -> list[Category]:
21+
entries = []
22+
for v in objref.as_list():
23+
entries.append(Category(main=v))
24+
return entries
25+
26+
def anchor(self, refval: str) -> str:
27+
return refval
28+
29+
30+
DEFAULT_INDEXER = LiteralIndexer()
31+
32+
33+
class PathIndexer(Indexer):
34+
name = 'path'
35+
36+
def __init__(self, sep: str, maxsplit: Literal[1, 2]):
37+
self.sep = sep
38+
self.maxsplit = maxsplit
39+
40+
def classify(self, objref: Value) -> list[Category]:
41+
entries = []
42+
for v in objref.as_list():
43+
comps = v.split(self.sep, maxsplit=self.maxsplit)
44+
category = Category(main=comps[0], extra=v)
45+
if self.maxsplit == 2:
46+
category.sub = v[1] if len(comps) > 1 else None
47+
entries.append(category)
48+
return entries
49+
50+
def anchor(self, refval: str) -> str:
51+
return refval.split(self.sep, maxsplit=self.maxsplit)[0]
52+
53+
54+
# I am Chinese :D
55+
# So the date formats follow Chinese conventions.
56+
# TODO: conf
57+
INPUTFMTS = ['%Y-%m-%d', '%Y-%m', '%Y']
58+
DISPFMTS_Y = '%Y 年'
59+
DISPFMTS_M = '%m 月'
60+
DISPFMTS_YM = '%Y 年 %m 月'
61+
DISPFMTS_MD = '%m 月 %d 日,%a'
62+
63+
64+
class YearIndexer(Indexer):
65+
name = 'year'
66+
67+
def __init__(
68+
self,
69+
inputfmts: list[str] = INPUTFMTS,
70+
dispfmt_y: str = DISPFMTS_Y,
71+
dispfmt_m: str = DISPFMTS_M,
72+
dispfmt_md: str = DISPFMTS_MD,
73+
):
74+
"""*xxxfmt* are date format used by time.strptime/strftime.
75+
76+
.. seealso:: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes"""
77+
self.inputfmts = inputfmts
78+
self.dispfmt_y = dispfmt_y
79+
self.dispfmt_m = dispfmt_m
80+
self.dispfmt_md = dispfmt_md
81+
82+
def classify(self, objref: Value) -> list[Category]:
83+
entries = []
84+
for v in objref.as_list():
85+
for datefmt in self.inputfmts:
86+
try:
87+
t = strptime(v, datefmt)
88+
except ValueError:
89+
continue # try next datefmt
90+
entries.append(
91+
Category(
92+
main=strftime(self.dispfmt_y, t),
93+
sub=strftime(self.dispfmt_m, t),
94+
extra=strftime(self.dispfmt_md, t),
95+
)
96+
)
97+
return entries
98+
99+
def sort(
100+
self, data: Iterable[Indexer._T], key: Callable[[Indexer._T], Category]
101+
) -> list[Indexer._T]:
102+
def sort_by_time(x: Category):
103+
t1 = strptime(x.main, self.dispfmt_y)
104+
t2 = strptime(x.sub, self.dispfmt_m) if x.sub else None
105+
t3 = strptime(x.extra, self.dispfmt_md) if x.extra else None
106+
return (t1, t2, t3)
107+
108+
return sorted(data, key=lambda x: sort_by_time(key(x)), reverse=True)
109+
110+
def anchor(self, refval: str) -> str:
111+
for datefmt in self.inputfmts:
112+
try:
113+
t = strptime(refval, datefmt)
114+
except ValueError:
115+
continue # try next datefmt
116+
anchor = strftime(self.dispfmt_y, t)
117+
return f'cap-{anchor}'
118+
return ''
119+
120+
121+
class MonthIndexer(Indexer):
122+
name = 'month'
123+
124+
def __init__(
125+
self,
126+
inputfmts: list[str] = INPUTFMTS,
127+
dispfmt_ym: str = DISPFMTS_YM,
128+
dispfmt_md: str = DISPFMTS_MD,
129+
):
130+
self.inputfmts = inputfmts
131+
self.dispfmt_ym = dispfmt_ym
132+
self.dispfmt_md = dispfmt_md
133+
134+
def classify(self, objref: Value) -> list[Category]:
135+
entries = []
136+
for v in objref.as_list():
137+
for datefmt in self.inputfmts:
138+
try:
139+
t = strptime(v, datefmt)
140+
except ValueError:
141+
continue # try next datefmt
142+
entries.append(
143+
Category(
144+
main=strftime(self.dispfmt_ym, t),
145+
extra=strftime(self.dispfmt_md, t),
146+
)
147+
)
148+
return entries
149+
150+
def sort(
151+
self, data: Iterable[Indexer._T], key: Callable[[Indexer._T], Category]
152+
) -> list[Indexer._T]:
153+
def sort_by_time(x: Category):
154+
t1 = strptime(x.main, self.dispfmt_ym)
155+
t2 = strptime(x.sub, self.dispfmt_md) if x.sub else None
156+
return (t1, t2)
157+
158+
return sorted(data, key=lambda x: sort_by_time(key(x)), reverse=True)
159+
160+
def anchor(self, refval: str) -> str:
161+
for datefmt in self.inputfmts:
162+
try:
163+
t = strptime(refval, datefmt)
164+
except ValueError:
165+
continue # try next datefmt
166+
anchor = strftime(self.dispfmt_ym, t)
167+
return f'cap-{anchor}'
168+
return ''

0 commit comments

Comments
 (0)