From 1862856549555651272d32bcba4fea5adaac6194 Mon Sep 17 00:00:00 2001 From: Shengyu Zhang Date: Mon, 9 Sep 2024 10:39:31 +0800 Subject: [PATCH] refactor: Provide a sphinxnotes.any.api package (#43) --- docs/_schemas/cat.py | 2 +- docs/_schemas/dog2.py | 2 +- docs/_schemas/tmplvar.py | 2 +- docs/conf.py | 7 +- docs/usage.rst | 12 +- src/sphinxnotes/any/__init__.py | 11 +- src/sphinxnotes/any/api.py | 27 +++ src/sphinxnotes/any/directives.py | 3 +- src/sphinxnotes/any/domain.py | 7 +- src/sphinxnotes/any/indexers.py | 168 ++++++++++++++++++ src/sphinxnotes/any/indices.py | 6 +- src/sphinxnotes/any/{schema.py => objects.py} | 155 +--------------- tests/test_schema.py | 2 +- 13 files changed, 220 insertions(+), 184 deletions(-) create mode 100644 src/sphinxnotes/any/api.py create mode 100644 src/sphinxnotes/any/indexers.py rename src/sphinxnotes/any/{schema.py => objects.py} (78%) diff --git a/docs/_schemas/cat.py b/docs/_schemas/cat.py index 69f31e8..5c6d20f 100644 --- a/docs/_schemas/cat.py +++ b/docs/_schemas/cat.py @@ -1,5 +1,5 @@ from textwrap import dedent -from any import Schema, Field +from any.api import Schema, Field cat = Schema( 'cat', diff --git a/docs/_schemas/dog2.py b/docs/_schemas/dog2.py index 1d0ecd8..dd1d4e5 100644 --- a/docs/_schemas/dog2.py +++ b/docs/_schemas/dog2.py @@ -1,5 +1,5 @@ from textwrap import dedent -from any import Schema, Field +from any.api import Schema, Field dog = Schema( 'dog', diff --git a/docs/_schemas/tmplvar.py b/docs/_schemas/tmplvar.py index dde5649..4adc150 100644 --- a/docs/_schemas/tmplvar.py +++ b/docs/_schemas/tmplvar.py @@ -1,5 +1,5 @@ from textwrap import dedent -from any import Schema, Field +from any.api import Schema, Field tmplvar = Schema( 'tmplvar', diff --git a/docs/conf.py b/docs/conf.py index a57c8a7..3ec77e2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -114,12 +114,7 @@ # # DOG FOOD CONFIGURATION START -from any import Schema, Field as F -from any.schema import YearIndexer, MonthIndexer -sys.path.insert(0, os.path.abspath('.')) - -by_year = YearIndexer() -by_month = MonthIndexer() +from any.api import Schema, Field as F, by_year, by_month version_schema = Schema('version', name=F(uniq=True, ref=True, required=True, form=F.Forms.LINES), diff --git a/docs/usage.rst b/docs/usage.rst index 661fbf7..19642a5 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -15,7 +15,7 @@ Defining Schema The necessary python classes for writing schema are listed here: -.. autoclass:: any.Schema +.. autoclass:: any.api.Schema Class-wide shared special keys used in template rendering context: @@ -26,15 +26,15 @@ The necessary python classes for writing schema are listed here: | -.. autoclass:: any.Field +.. autoclass:: any.api.Field | -.. autoclass:: any.Field.Forms +.. autoclass:: any.api.Field.Forms - .. autoattribute:: any.Field.Forms.PLAIN - .. autoattribute:: any.Field.Forms.WORDS - .. autoattribute:: any.Field.Forms.LINES + .. autoattribute:: any.api.Field.Forms.PLAIN + .. autoattribute:: any.api.Field.Forms.WORDS + .. autoattribute:: any.api.Field.Forms.LINES Documenting Object ================== diff --git a/src/sphinxnotes/any/__init__.py b/src/sphinxnotes/any/__init__.py index b149cb6..db4ee17 100644 --- a/src/sphinxnotes/any/__init__.py +++ b/src/sphinxnotes/any/__init__.py @@ -10,24 +10,21 @@ from __future__ import annotations from typing import TYPE_CHECKING +from importlib.metadata import version + from sphinx.util import logging from .template import Environment as TemplateEnvironment from .domain import AnyDomain, warn_missing_reference -from .schema import Schema, Field +from .objects import Schema if TYPE_CHECKING: from sphinx.application import Sphinx from sphinx.config import Config -__version__ = '2.3.1' logger = logging.getLogger(__name__) -# Re-Export -Field = Field -Schema = Schema - def _config_inited(app: Sphinx, config: Config) -> None: AnyDomain.name = config.any_domain_name @@ -51,4 +48,4 @@ def setup(app: Sphinx): app.connect('config-inited', _config_inited) app.connect('warn-missing-reference', warn_missing_reference) - return {'version': __version__} + return {'version': version('sphinxnotes.any')} diff --git a/src/sphinxnotes/any/api.py b/src/sphinxnotes/any/api.py new file mode 100644 index 0000000..1d8ad90 --- /dev/null +++ b/src/sphinxnotes/any/api.py @@ -0,0 +1,27 @@ +""" +sphinxnotes.any.api +~~~~~~~~~~~~~~~~~~~ + +Public API for building configuration of extension. +(such as object schema, and so on). + +:copyright: Copyright 2024 Shengyu Zhang +:license: BSD, see LICENSE for details. +""" + +from .objects import Schema, Field +from .indexers import LiteralIndexer, PathIndexer, YearIndexer, MonthIndexer + +# Object schema. +Schema = Schema +Field = Field + +# Indexers. +LiteralIndexer = LiteralIndexer +PathIndexer = PathIndexer +YearIndexer = YearIndexer +MonthIndexer = MonthIndexer + +# Indexer wrappers. +by_year = YearIndexer() +by_month = MonthIndexer() diff --git a/src/sphinxnotes/any/directives.py b/src/sphinxnotes/any/directives.py index 50b8cfd..ba99d62 100644 --- a/src/sphinxnotes/any/directives.py +++ b/src/sphinxnotes/any/directives.py @@ -15,13 +15,12 @@ from docutils.nodes import Node, Element, fully_normalize_name from docutils.statemachine import StringList from docutils.parsers.rst import directives - from sphinx import addnodes from sphinx.util.docutils import SphinxDirective from sphinx.util.nodes import make_id, nested_parse_with_titles from sphinx.util import logging -from .schema import Schema, Object +from .objects import Schema, Object logger = logging.getLogger(__name__) diff --git a/src/sphinxnotes/any/domain.py b/src/sphinxnotes/any/domain.py index 5c6409b..4328d78 100644 --- a/src/sphinxnotes/any/domain.py +++ b/src/sphinxnotes/any/domain.py @@ -18,10 +18,11 @@ from sphinx.util import logging from sphinx.util.nodes import make_refnode -from .schema import Schema, Object, RefType, Indexer, LiteralIndexer +from .objects import Schema, Object, RefType, Indexer from .directives import AnyDirective from .roles import AnyRole from .indices import AnyIndex +from .indexers import DEFAULT_INDEXER if TYPE_CHECKING: from sphinx.application import Sphinx @@ -200,7 +201,7 @@ def mkindex(reftype: RefType, indexer: Indexer): # Create all-in-one role and index (do not distinguish reference fields). reftypes = [RefType(schema.objtype)] mkrole(reftypes[0]) - mkindex(reftypes[0], LiteralIndexer()) + mkindex(reftypes[0], DEFAULT_INDEXER) # Create {field,indexer}-specificed role and index. for name, field in schema.fields(): @@ -210,7 +211,7 @@ def mkindex(reftype: RefType, indexer: Indexer): mkrole(reftype) # create a role to reference object(s) # Create a fallback indexer, for possible ambiguous reference # (if field is not unique). - mkindex(reftype, LiteralIndexer()) + mkindex(reftype, DEFAULT_INDEXER) for indexer in field.indexers: reftype = RefType(schema.objtype, field=name, indexer=indexer.name) diff --git a/src/sphinxnotes/any/indexers.py b/src/sphinxnotes/any/indexers.py new file mode 100644 index 0000000..fa0911c --- /dev/null +++ b/src/sphinxnotes/any/indexers.py @@ -0,0 +1,168 @@ +""" +sphinxnotes.any.indexers +~~~~~~~~~~~~~~~~~~~~~~~~ + +:cls:`objects.Indexer` implementations. + +:copyright: Copyright 2024 Shengyu Zhang +:license: BSD, see LICENSE for details. +""" + +from typing import Iterable, Literal, Callable +from time import strptime, strftime + +from .objects import Indexer, Category, Value + + +class LiteralIndexer(Indexer): + name = 'literal' + + def classify(self, objref: Value) -> list[Category]: + entries = [] + for v in objref.as_list(): + entries.append(Category(main=v)) + return entries + + def anchor(self, refval: str) -> str: + return refval + + +DEFAULT_INDEXER = LiteralIndexer() + + +class PathIndexer(Indexer): + name = 'path' + + def __init__(self, sep: str, maxsplit: Literal[1, 2]): + self.sep = sep + self.maxsplit = maxsplit + + def classify(self, objref: Value) -> list[Category]: + entries = [] + for v in objref.as_list(): + comps = v.split(self.sep, maxsplit=self.maxsplit) + category = Category(main=comps[0], extra=v) + if self.maxsplit == 2: + category.sub = v[1] if len(comps) > 1 else None + entries.append(category) + return entries + + def anchor(self, refval: str) -> str: + return refval.split(self.sep, maxsplit=self.maxsplit)[0] + + +# I am Chinese :D +# So the date formats follow Chinese conventions. +# TODO: conf +INPUTFMTS = ['%Y-%m-%d', '%Y-%m', '%Y'] +DISPFMTS_Y = '%Y 年' +DISPFMTS_M = '%m 月' +DISPFMTS_YM = '%Y 年 %m 月' +DISPFMTS_MD = '%m 月 %d 日,%a' + + +class YearIndexer(Indexer): + name = 'year' + + def __init__( + self, + inputfmts: list[str] = INPUTFMTS, + dispfmt_y: str = DISPFMTS_Y, + dispfmt_m: str = DISPFMTS_M, + dispfmt_md: str = DISPFMTS_MD, + ): + """*xxxfmt* are date format used by time.strptime/strftime. + + .. seealso:: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes""" + self.inputfmts = inputfmts + self.dispfmt_y = dispfmt_y + self.dispfmt_m = dispfmt_m + self.dispfmt_md = dispfmt_md + + def classify(self, objref: Value) -> list[Category]: + entries = [] + for v in objref.as_list(): + for datefmt in self.inputfmts: + try: + t = strptime(v, datefmt) + except ValueError: + continue # try next datefmt + entries.append( + Category( + main=strftime(self.dispfmt_y, t), + sub=strftime(self.dispfmt_m, t), + extra=strftime(self.dispfmt_md, t), + ) + ) + return entries + + def sort( + self, data: Iterable[Indexer._T], key: Callable[[Indexer._T], Category] + ) -> list[Indexer._T]: + def sort_by_time(x: Category): + t1 = strptime(x.main, self.dispfmt_y) + t2 = strptime(x.sub, self.dispfmt_m) if x.sub else None + t3 = strptime(x.extra, self.dispfmt_md) if x.extra else None + return (t1, t2, t3) + + return sorted(data, key=lambda x: sort_by_time(key(x)), reverse=True) + + def anchor(self, refval: str) -> str: + for datefmt in self.inputfmts: + try: + t = strptime(refval, datefmt) + except ValueError: + continue # try next datefmt + anchor = strftime(self.dispfmt_y, t) + return f'cap-{anchor}' + return '' + + +class MonthIndexer(Indexer): + name = 'month' + + def __init__( + self, + inputfmts: list[str] = INPUTFMTS, + dispfmt_ym: str = DISPFMTS_YM, + dispfmt_md: str = DISPFMTS_MD, + ): + self.inputfmts = inputfmts + self.dispfmt_ym = dispfmt_ym + self.dispfmt_md = dispfmt_md + + def classify(self, objref: Value) -> list[Category]: + entries = [] + for v in objref.as_list(): + for datefmt in self.inputfmts: + try: + t = strptime(v, datefmt) + except ValueError: + continue # try next datefmt + entries.append( + Category( + main=strftime(self.dispfmt_ym, t), + extra=strftime(self.dispfmt_md, t), + ) + ) + return entries + + def sort( + self, data: Iterable[Indexer._T], key: Callable[[Indexer._T], Category] + ) -> list[Indexer._T]: + def sort_by_time(x: Category): + t1 = strptime(x.main, self.dispfmt_ym) + t2 = strptime(x.sub, self.dispfmt_md) if x.sub else None + return (t1, t2) + + return sorted(data, key=lambda x: sort_by_time(key(x)), reverse=True) + + def anchor(self, refval: str) -> str: + for datefmt in self.inputfmts: + try: + t = strptime(refval, datefmt) + except ValueError: + continue # try next datefmt + anchor = strftime(self.dispfmt_ym, t) + return f'cap-{anchor}' + return '' diff --git a/src/sphinxnotes/any/indices.py b/src/sphinxnotes/any/indices.py index 33bc550..5760327 100644 --- a/src/sphinxnotes/any/indices.py +++ b/src/sphinxnotes/any/indices.py @@ -11,12 +11,12 @@ from typing import Iterable, TypeVar import re -from sphinx.domains import Domain, Index, IndexEntry -from sphinx.util import logging from docutils import core, nodes from docutils.parsers.rst import roles +from sphinx.domains import Domain, Index, IndexEntry +from sphinx.util import logging -from .schema import Schema, Value, Indexer, Category, RefType +from .objects import Schema, Value, Indexer, Category, RefType logger = logging.getLogger(__name__) diff --git a/src/sphinxnotes/any/schema.py b/src/sphinxnotes/any/objects.py similarity index 78% rename from src/sphinxnotes/any/schema.py rename to src/sphinxnotes/any/objects.py index 7700d51..b499921 100644 --- a/src/sphinxnotes/any/schema.py +++ b/src/sphinxnotes/any/objects.py @@ -1,8 +1,8 @@ """ -sphinxnotes.any.schema +sphinxnotes.any.objects ~~~~~~~~~~~~~~~~~~~~~~ -Schema and object implementations. +Object and schema implementations. :copyright: Copyright 2021 Shengyu Zhang :license: BSD, see LICENSE for details. @@ -12,7 +12,6 @@ import dataclasses import pickle import hashlib -from time import strptime, strftime from abc import ABC, abstractmethod from sphinx.util import logging @@ -168,156 +167,6 @@ def anchor(self, refval: str) -> str: raise NotImplementedError -class LiteralIndexer(Indexer): - name = 'literal' - - def classify(self, objref: Value) -> list[Category]: - entries = [] - for v in objref.as_list(): - entries.append(Category(main=v)) - return entries - - def anchor(self, refval: str) -> str: - return refval - - -# I am Chinese :D -# So the date formats follow Chinese conventions. -INPUTFMTS = ['%Y-%m-%d', '%Y-%m', '%Y'] -DISPFMTS_Y = '%Y 年' -DISPFMTS_M = '%m 月' -DISPFMTS_YM = '%Y 年 %m 月' -DISPFMTS_MD = '%m 月 %d 日,%a' - - -class YearIndexer(Indexer): - name = 'year' - - def __init__( - self, - inputfmts: list[str] = INPUTFMTS, - dispfmt_y: str = DISPFMTS_Y, - dispfmt_m: str = DISPFMTS_M, - dispfmt_md: str = DISPFMTS_MD, - ): - """*xxxfmt* are date format used by time.strptime/strftime. - - .. seealso:: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes""" - self.inputfmts = inputfmts - self.dispfmt_y = dispfmt_y - self.dispfmt_m = dispfmt_m - self.dispfmt_md = dispfmt_md - - def classify(self, objref: Value) -> list[Category]: - entries = [] - for v in objref.as_list(): - for datefmt in self.inputfmts: - try: - t = strptime(v, datefmt) - except ValueError: - continue # try next datefmt - entries.append( - Category( - main=strftime(self.dispfmt_y, t), - sub=strftime(self.dispfmt_m, t), - extra=strftime(self.dispfmt_md, t), - ) - ) - return entries - - def sort( - self, data: Iterable[Indexer._T], key: Callable[[Indexer._T], Category] - ) -> list[Indexer._T]: - def sort_by_time(x: Category): - t1 = strptime(x.main, self.dispfmt_y) - t2 = strptime(x.sub, self.dispfmt_m) if x.sub else None - t3 = strptime(x.extra, self.dispfmt_md) if x.extra else None - return (t1, t2, t3) - - return sorted(data, key=lambda x: sort_by_time(key(x)), reverse=True) - - def anchor(self, refval: str) -> str: - for datefmt in self.inputfmts: - try: - t = strptime(refval, datefmt) - except ValueError: - continue # try next datefmt - anchor = strftime(self.dispfmt_y, t) - return f'cap-{anchor}' - return '' - - -class MonthIndexer(Indexer): - name = 'month' - - def __init__( - self, - inputfmts: list[str] = INPUTFMTS, - dispfmt_ym: str = DISPFMTS_YM, - dispfmt_md: str = DISPFMTS_MD, - ): - self.inputfmts = inputfmts - self.dispfmt_ym = dispfmt_ym - self.dispfmt_md = dispfmt_md - - def classify(self, objref: Value) -> list[Category]: - entries = [] - for v in objref.as_list(): - for datefmt in self.inputfmts: - try: - t = strptime(v, datefmt) - except ValueError: - continue # try next datefmt - entries.append( - Category( - main=strftime(self.dispfmt_ym, t), - extra=strftime(self.dispfmt_md, t), - ) - ) - return entries - - def sort( - self, data: Iterable[Indexer._T], key: Callable[[Indexer._T], Category] - ) -> list[Indexer._T]: - def sort_by_time(x: Category): - t1 = strptime(x.main, self.dispfmt_ym) - t2 = strptime(x.sub, self.dispfmt_md) if x.sub else None - return (t1, t2) - - return sorted(data, key=lambda x: sort_by_time(key(x)), reverse=True) - - def anchor(self, refval: str) -> str: - for datefmt in self.inputfmts: - try: - t = strptime(refval, datefmt) - except ValueError: - continue # try next datefmt - anchor = strftime(self.dispfmt_ym, t) - return f'cap-{anchor}' - return '' - - -class PathIndexer(Indexer): - name = 'path' - - def __init__(self, sep: str, maxsplit: Literal[1, 2]): - self.sep = sep - self.maxsplit = maxsplit - - def classify(self, objref: Value) -> list[Category]: - entries = [] - for v in objref.as_list(): - comps = v.split(self.sep, maxsplit=self.maxsplit) - category = Category(main=comps[0], extra=v) - if self.maxsplit == 2: - category.sub = v[1] if len(comps) > 1 else None - entries.append(category) - return entries - - def anchor(self, refval: str) -> str: - return refval.split(self.sep, maxsplit=self.maxsplit)[0] - - @dataclasses.dataclass(frozen=True) class Object(object): objtype: str diff --git a/tests/test_schema.py b/tests/test_schema.py index 30f8e3c..5c6e091 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -4,7 +4,7 @@ from textwrap import dedent sys.path.insert(0, os.path.abspath('./src/sphinxnotes')) -from any import Schema, Field +from any.api import Schema, Field class TestSchema(unittest.TestCase):