sphinxnotes-data 1.0a2__tar.gz → 1.0a4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. {sphinxnotes_data-1.0a2/src/sphinxnotes_data.egg-info → sphinxnotes_data-1.0a4}/PKG-INFO +1 -1
  2. sphinxnotes_data-1.0a4/src/sphinxnotes/data/__init__.py +83 -0
  3. sphinxnotes_data-1.0a4/src/sphinxnotes/data/config.py +37 -0
  4. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/src/sphinxnotes/data/data.py +92 -74
  5. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/src/sphinxnotes/data/preset.py +13 -2
  6. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4/src/sphinxnotes_data.egg-info}/PKG-INFO +1 -1
  7. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/src/sphinxnotes_data.egg-info/SOURCES.txt +1 -0
  8. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/tests/test_data.py +38 -6
  9. sphinxnotes_data-1.0a2/src/sphinxnotes/data/__init__.py +0 -62
  10. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/.cruft.json +0 -0
  11. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/.github/workflows/lint.yml +0 -0
  12. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/.github/workflows/pages.yml +0 -0
  13. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/.github/workflows/pypi.yml +0 -0
  14. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/.github/workflows/release.yml +0 -0
  15. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/.github/workflows/tag.yml +0 -0
  16. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/.github/workflows/test.yml +0 -0
  17. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/.gitignore +0 -0
  18. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/.pre-commit-config.yaml +0 -0
  19. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/LICENSE +0 -0
  20. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/MANIFEST.in +0 -0
  21. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/Makefile +0 -0
  22. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/README.rst +0 -0
  23. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/docs/Makefile +0 -0
  24. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/docs/_images/.gitkeep +0 -0
  25. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/docs/_static/.gitkeep +0 -0
  26. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/docs/_static/sphinx-notes.png +0 -0
  27. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/docs/changelog.rst +0 -0
  28. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/docs/conf.py +0 -0
  29. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/docs/index.rst +0 -0
  30. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/docs/make.bat +0 -0
  31. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/pyproject.toml +0 -0
  32. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/ruff.toml +0 -0
  33. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/setup.cfg +0 -0
  34. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/src/sphinxnotes/data/adhoc.py +0 -0
  35. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/src/sphinxnotes/data/extra_contexts.py +0 -0
  36. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/src/sphinxnotes/data/meta.py +0 -0
  37. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/src/sphinxnotes/data/py.typed +0 -0
  38. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/src/sphinxnotes/data/render.py +0 -0
  39. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/src/sphinxnotes/data/template.py +0 -0
  40. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/src/sphinxnotes/data/utils/__init__.py +0 -0
  41. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/src/sphinxnotes/data/utils/context_proxy.py +0 -0
  42. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/src/sphinxnotes/data/utils/freestyle.py +0 -0
  43. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/src/sphinxnotes_data.egg-info/dependency_links.txt +0 -0
  44. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/src/sphinxnotes_data.egg-info/requires.txt +0 -0
  45. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/src/sphinxnotes_data.egg-info/top_level.txt +0 -0
  46. {sphinxnotes_data-1.0a2 → sphinxnotes_data-1.0a4}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sphinxnotes-data
3
- Version: 1.0a2
3
+ Version: 1.0a4
4
4
  Summary: Use reStructuredText/markdown to define, constrain, render, and analyze data in Sphinx documentations
5
5
  Author: Shengyu Zhang
6
6
  Maintainer: Shengyu Zhang
@@ -0,0 +1,83 @@
1
+ """
2
+ sphinxnotes.data
3
+ ~~~~~~~~~~~~~~~~
4
+
5
+ :copyright: Copyright 2025 by the Shengyu Zhang.
6
+ :license: BSD, see LICENSE for details.
7
+ """
8
+
9
+ from __future__ import annotations
10
+ from typing import TYPE_CHECKING
11
+
12
+ from sphinx.util import logging
13
+
14
+ from . import meta
15
+ from .data import (
16
+ Registry,
17
+ Form,
18
+ BoolFlag,
19
+ OperFlag,
20
+ Value,
21
+ ValueWrapper,
22
+ RawData,
23
+ Data,
24
+ Field,
25
+ Schema,
26
+ )
27
+ from .template import Phase, Template
28
+ from .render import (
29
+ Caller,
30
+ pending_node,
31
+ RenderedNode,
32
+ rendered_node,
33
+ rendered_inline_node,
34
+ BaseDataDefiner,
35
+ BaseDataDefineRole,
36
+ BaseDataDefineDirective,
37
+ StrictDataDefineDirective,
38
+ )
39
+
40
+ if TYPE_CHECKING:
41
+ from sphinx.application import Sphinx
42
+
43
+
44
+ """Python API for other Sphinx extesions."""
45
+ __all__ = [
46
+ 'Registry',
47
+ 'Form',
48
+ 'BoolFlag',
49
+ 'OperFlag',
50
+ 'Value',
51
+ 'ValueWrapper',
52
+ 'RawData',
53
+ 'Data',
54
+ 'Field',
55
+ 'Schema',
56
+ 'Phase',
57
+ 'Template',
58
+ 'Caller',
59
+ 'pending_node',
60
+ 'RenderedNode',
61
+ 'rendered_node',
62
+ 'rendered_inline_node',
63
+ 'BaseDataDefiner',
64
+ 'BaseDataDefineRole',
65
+ 'BaseDataDefineDirective',
66
+ 'BaseDataDefineDirective',
67
+ 'StrictDataDefineDirective',
68
+ ]
69
+
70
+ logger = logging.getLogger(__name__)
71
+
72
+
73
+ def setup(app: Sphinx):
74
+ meta.pre_setup(app)
75
+
76
+ from . import config, template, render, adhoc
77
+
78
+ config.setup(app)
79
+ template.setup(app)
80
+ render.setup(app)
81
+ adhoc.setup(app)
82
+
83
+ return meta.post_setup(app)
@@ -0,0 +1,37 @@
1
+ """
2
+ sphinxnotes.data.config
3
+ ~~~~~~~~~~~~~~~~~~~~~~~
4
+
5
+ :copyright: Copyright 2025~2026 by the Shengyu Zhang.
6
+ :license: BSD, see LICENSE for details.
7
+ """
8
+
9
+ from sphinx.application import Sphinx
10
+ from sphinx.config import Config as SphinxConfig
11
+
12
+
13
+ class Config:
14
+ """Global config of extesion."""
15
+
16
+ template_debug: bool
17
+
18
+ date_fmt: str
19
+ time_fmt: str
20
+ datetime_fmt: str
21
+
22
+
23
+ def _config_inited(app: Sphinx, config: SphinxConfig) -> None:
24
+ Config.template_debug = config.data_template_debug
25
+
26
+ Config.date_fmt = config.data_date_fmt
27
+ Config.time_fmt = config.data_time_fmt
28
+ Config.datetime_fmt = config.data_datetime_fmt
29
+
30
+
31
+ def setup(app: Sphinx):
32
+ app.add_config_value('data_template_debug', False, '', bool)
33
+ app.add_config_value('data_date_fmt', '%Y-%m-%d', '', str)
34
+ app.add_config_value('data_time_fmt', '%H:%M:%S', '', str)
35
+ app.add_config_value('data_datetime_fmt', '%Y-%m-%d %H:%M:%S', '', str)
36
+
37
+ app.connect('config-inited', _config_inited)
@@ -3,16 +3,18 @@ from typing import TYPE_CHECKING
3
3
  import re
4
4
  from dataclasses import dataclass, asdict, field as dataclass_field
5
5
  from ast import literal_eval
6
+ from datetime import date, time, datetime
7
+
8
+ from .config import Config
6
9
 
7
10
  if TYPE_CHECKING:
8
11
  from typing import Any, Callable, Generator, Self, Literal
9
12
 
13
+ # =====================================
14
+ # Basic classes: Value, Form, Flag, ...
15
+ # =====================================
10
16
 
11
- #########################################
12
- # Basic classes: Value, Form, Flag, ... #
13
- #########################################
14
-
15
- type PlainValue = bool | int | float | str
17
+ type PlainValue = bool | int | float | str | date | time | datetime
16
18
  type Value = None | PlainValue | list[PlainValue]
17
19
 
18
20
 
@@ -24,14 +26,9 @@ class ValueWrapper:
24
26
  if self.v is None:
25
27
  return None
26
28
  if isinstance(self.v, list):
27
- if len(self.v) == 0:
28
- return None
29
- return self.v[0]
29
+ return self.v[0] if len(self.v) else None
30
30
  return self.v
31
31
 
32
- def as_str(self) -> str | None:
33
- return str(self.as_plain())
34
-
35
32
  def as_list(self) -> list[PlainValue]:
36
33
  if self.v is None:
37
34
  return []
@@ -40,13 +37,22 @@ class ValueWrapper:
40
37
  else:
41
38
  return [self.v]
42
39
 
40
+ def as_str(self) -> str | None:
41
+ v = self.as_plain()
42
+ return self._strify(v) if v is not None else None
43
+
43
44
  def as_str_list(self) -> list[str]:
44
- if self.v is None:
45
- return []
46
- elif isinstance(self.v, list):
47
- return [str(x) for x in self.v]
48
- else:
49
- return [str(self.v)]
45
+ return [self._strify(x) for x in self.as_list()]
46
+
47
+ @staticmethod
48
+ def _strify(v: PlainValue) -> str:
49
+ if isinstance(v, datetime):
50
+ return v.strftime(Config.datetime_fmt)
51
+ elif isinstance(v, date):
52
+ return v.strftime(Config.date_fmt)
53
+ elif isinstance(v, time):
54
+ return v.strftime(Config.time_fmt)
55
+ return str(v)
50
56
 
51
57
 
52
58
  @dataclass(frozen=True)
@@ -77,9 +83,44 @@ class OperFlag(Flag):
77
83
  store: FlagStore = 'assign'
78
84
 
79
85
 
80
- ############
81
- # Registry #
82
- ############
86
+ # ========
87
+ # Registry
88
+ # ========
89
+
90
+
91
+ def _bool_conv(v: str | None) -> bool:
92
+ v = v.lower().strip() if v is not None else None
93
+ if v in ('true', 'yes', '1', 'on', 'y', ''):
94
+ return True
95
+ if v in ('false', 'no', '0', 'off', 'n', None):
96
+ return False
97
+ # Same to :meth:`directives.flag`.
98
+ raise ValueError(f'no argument is allowed; "{v}" supplied')
99
+
100
+
101
+ def _str_conv(v: str) -> str:
102
+ try:
103
+ vv = literal_eval(v)
104
+ except (ValueError, SyntaxError):
105
+ return v
106
+ return vv if isinstance(vv, str) else v
107
+
108
+
109
+ def _date_conv(v: str) -> date:
110
+ return datetime.strptime(v, Config.date_fmt).date()
111
+
112
+
113
+ def _time_conv(v: str) -> time:
114
+ return datetime.strptime(v, Config.time_fmt).time()
115
+
116
+
117
+ def _datetime_conv(v: str) -> datetime:
118
+ return datetime.strptime(v, Config.datetime_fmt)
119
+
120
+
121
+ _required_flag = BoolFlag('required')
122
+
123
+ _sep_flag = OperFlag('sep', etype=str)
83
124
 
84
125
 
85
126
  class Registry:
@@ -95,33 +136,19 @@ class Registry:
95
136
  'num': float,
96
137
  'str': str,
97
138
  'string': str,
139
+ 'date': date,
140
+ 'time': time,
141
+ 'datetime': datetime,
98
142
  }
99
143
 
100
- """Internal type converters."""
101
-
102
- @staticmethod
103
- def _bool_conv(v: str | None) -> bool:
104
- v = v.lower().strip() if v is not None else None
105
- if v in ('true', 'yes', '1', 'on', 'y', ''):
106
- return True
107
- if v in ('false', 'no', '0', 'off', 'n', None):
108
- return False
109
- # Same to :meth:`directives.flag`.
110
- raise ValueError(f'no argument is allowed; "{v}" supplied')
111
-
112
- @staticmethod
113
- def _str_conv(v: str) -> str:
114
- try:
115
- vv = literal_eval(v)
116
- except (ValueError, SyntaxError):
117
- return v
118
- return vv if isinstance(vv, str) else v
119
-
120
144
  convs: dict[type, Callable[[str], Any]] = {
121
145
  bool: _bool_conv,
122
146
  int: int,
123
147
  float: float,
124
148
  str: _str_conv,
149
+ date: _date_conv,
150
+ time: _time_conv,
151
+ datetime: _datetime_conv,
125
152
  }
126
153
 
127
154
  forms: dict[str, Form] = {
@@ -132,9 +159,6 @@ class Registry:
132
159
 
133
160
  """Builtin flags."""
134
161
 
135
- _required_flag = BoolFlag('required')
136
- _sep_flag = OperFlag('sep', etype=str)
137
-
138
162
  flags: dict[str, BoolFlag] = {
139
163
  'required': _required_flag,
140
164
  'require': _required_flag,
@@ -147,9 +171,9 @@ class Registry:
147
171
  }
148
172
 
149
173
 
150
- ##########################
151
- # Data, Field and Schema #
152
- ##########################
174
+ # ======================
175
+ # Data, Field and Schema
176
+ # ======================
153
177
 
154
178
 
155
179
  @dataclass
@@ -216,7 +240,7 @@ class Field:
216
240
  if self.etype is bool:
217
241
  # Special case: A single bool field is valid even when
218
242
  # value is not supplied.
219
- return Registry._bool_conv(rawval)
243
+ return _bool_conv(rawval)
220
244
  return None
221
245
 
222
246
  # Strip whitespace. TODO: supported unchanged?
@@ -305,7 +329,7 @@ class DSLParser:
305
329
 
306
330
  self.field.etype = Registry.etypes[etype]
307
331
  self.field.ctype = Registry.forms[form].ctype
308
- self.field.flags[Registry._sep_flag.name] = Registry.forms[form].sep
332
+ self.field.flags[_sep_flag.name] = Registry.forms[form].sep
309
333
  return
310
334
 
311
335
  # Match: Type only (e.g., "int")
@@ -341,7 +365,7 @@ class DSLParser:
341
365
  )
342
366
 
343
367
  # Deal with builtin flags.
344
- if flag == Registry._sep_flag:
368
+ if flag == _sep_flag:
345
369
  # ctype default to list if 'sep by' is used without a 'xxx of xxx'.
346
370
  if self.field.ctype is None:
347
371
  self.field.ctype = list
@@ -360,7 +384,7 @@ class DSLParser:
360
384
  @dataclass(frozen=True)
361
385
  class Schema(object):
362
386
  name: Field | None
363
- attrs: dict[str, Field]
387
+ attrs: dict[str, Field] | Field
364
388
  content: Field | None
365
389
 
366
390
  @classmethod
@@ -403,8 +427,8 @@ class Schema(object):
403
427
  else:
404
428
  rawattrs = data.attrs.copy()
405
429
  for key, field in self.attrs.items():
406
- rawval = rawattrs.pop(key)
407
- attrs[key] = self._parse_single(('attrs.' + key, field), rawval)
430
+ if rawval := rawattrs.pop(key, None):
431
+ attrs[key] = self._parse_single(('attrs.' + key, field), rawval)
408
432
  for key, rawval in rawattrs.items():
409
433
  raise ValueError(f'unknown attr: "{key}"')
410
434
 
@@ -415,35 +439,29 @@ class Schema(object):
415
439
 
416
440
  return Data(name, attrs, content)
417
441
 
418
- def fields(
419
- self, pred: Callable[[Field], bool] | None = None
420
- ) -> Generator[tuple[str, Field]]:
421
- def ok(f: Field) -> bool:
422
- return not pred or pred(f)
423
-
424
- if self.name and ok(self.name):
442
+ def fields(self) -> Generator[tuple[str, Field]]:
443
+ if self.name:
425
444
  yield 'name', self.name
426
445
 
427
- for name, field in self.attrs.items():
428
- if ok(field):
446
+ if isinstance(self.attrs, Field):
447
+ yield 'attrs', self.attrs
448
+ else:
449
+ for name, field in self.attrs.items():
429
450
  yield name, field
430
451
 
431
- if self.content and ok(self.content):
452
+ if self.content:
432
453
  yield 'content', self.content
433
454
 
434
- def items(
435
- self, data: Data, pred: Callable[[Field], bool] | None = None
436
- ) -> Generator[tuple[str, Field, Value]]:
437
- def ok(f: Field) -> bool:
438
- return not pred or pred(f)
439
-
440
- if self.name and ok(self.name):
455
+ def items(self, data: Data) -> Generator[tuple[str, Field, Value]]:
456
+ if self.name:
441
457
  yield 'name', self.name, data.name
442
458
 
443
- for name, field in self.attrs.items():
444
- if not ok(field):
445
- continue
446
- yield name, field, data.attrs.get(name)
459
+ if isinstance(self.attrs, Field):
460
+ for name, val in data.attrs:
461
+ yield name, self.attrs, val
462
+ else:
463
+ for name, field in self.attrs.items():
464
+ yield name, field, data.attrs.get(name)
447
465
 
448
- if self.content and ok(self.content):
466
+ if self.content:
449
467
  yield 'content', self.content, data.content
@@ -1,5 +1,16 @@
1
+ """
2
+ sphinxnotes.data.preset
3
+ ~~~~~~~~~~~~~~~~~~~~~~~
4
+
5
+ Preset templates and schemas.
6
+
7
+ :copyright: Copyright 2026 by the Shengyu Zhang.
8
+ :license: BSD, see LICENSE for details.
9
+ """
10
+
1
11
  from .data import Schema, Field
2
12
  from .template import Template, Phase
13
+ from .config import Config
3
14
 
4
15
 
5
16
  class Directive:
@@ -10,7 +21,7 @@ class Directive:
10
21
  @staticmethod
11
22
  def template() -> Template:
12
23
  return Template(
13
- debug=True,
24
+ debug=Config.template_debug,
14
25
  phase=Phase.Parsing,
15
26
  text=""".. note::
16
27
 
@@ -36,7 +47,7 @@ class Role:
36
47
  @staticmethod
37
48
  def template() -> Template:
38
49
  return Template(
39
- debug=True,
50
+ debug=Config.template_debug,
40
51
  phase=Phase.Parsing,
41
52
  text="""``{{ content or 'None' }}``
42
53
  :abbr:`ⁱⁿᶠᵒ (This is a default template for rendering the data your deinfed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sphinxnotes-data
3
- Version: 1.0a2
3
+ Version: 1.0a4
4
4
  Summary: Use reStructuredText/markdown to define, constrain, render, and analyze data in Sphinx documentations
5
5
  Author: Shengyu Zhang
6
6
  Maintainer: Shengyu Zhang
@@ -23,6 +23,7 @@ docs/_static/.gitkeep
23
23
  docs/_static/sphinx-notes.png
24
24
  src/sphinxnotes/data/__init__.py
25
25
  src/sphinxnotes/data/adhoc.py
26
+ src/sphinxnotes/data/config.py
26
27
  src/sphinxnotes/data/data.py
27
28
  src/sphinxnotes/data/extra_contexts.py
28
29
  src/sphinxnotes/data/meta.py
@@ -1,22 +1,23 @@
1
1
  import os
2
2
  import sys
3
3
  import unittest
4
- from textwrap import dedent
4
+ from datetime import date, time, datetime
5
5
 
6
6
  sys.path.insert(0, os.path.abspath('./src/sphinxnotes'))
7
7
 
8
8
  from data.data import Field, Registry, BoolFlag, OperFlag
9
+ from data.config import Config
9
10
 
10
11
  class TestFieldParser(unittest.TestCase):
11
12
 
12
13
  # ==========================
13
14
  # Basic Types
14
15
  # ==========================
15
-
16
+
16
17
  def test_basic_scalar(self):
17
18
  f = Field.from_dsl('int')
18
19
  self.assertEqual(f.parse('123'), 123)
19
-
20
+
20
21
  f = Field.from_dsl('bool')
21
22
  self.assertTrue(f.parse('true'))
22
23
  self.assertTrue(f.parse('y'))
@@ -89,7 +90,7 @@ class TestFieldParser(unittest.TestCase):
89
90
  self.assertTrue(f.uniq)
90
91
  f = Field.from_dsl(r'int')
91
92
  self.assertFalse(f.uniq)
92
-
93
+
93
94
  # Test default value.
94
95
  Registry.flags['ref'] = BoolFlag('ref', default=True)
95
96
  f = Field.from_dsl(r'int, ref')
@@ -103,7 +104,7 @@ class TestFieldParser(unittest.TestCase):
103
104
  self.assertEqual(f.group, 'foo')
104
105
  f = Field.from_dsl(r'int')
105
106
  self.assertEqual(f.group, None)
106
-
107
+
107
108
  # Test append
108
109
  Registry.byflags['index'] = OperFlag('index', etype=str, store='append')
109
110
  f = Field.from_dsl(r'int, index by year')
@@ -111,6 +112,23 @@ class TestFieldParser(unittest.TestCase):
111
112
  f = Field.from_dsl(r'int, index by year, index by month')
112
113
  self.assertEqual(f.index, ['year', 'month'])
113
114
 
115
+ def test_datetime(self):
116
+ # FIXME: side effects
117
+ Config.date_fmt = '%Y-%m-%d'
118
+ Config.time_fmt = '%H:%M:%S'
119
+ Config.datetime_fmt = '%Y-%m-%d %H:%M:%S'
120
+
121
+ val = Field.from_dsl('date').parse('2023-10-01')
122
+ self.assertIsInstance(val, date)
123
+ self.assertEqual(val, date(2023, 10, 1))
124
+
125
+ val = Field.from_dsl('time').parse('14:30:00')
126
+ self.assertIsInstance(val, time)
127
+ self.assertEqual(val, time(14, 30, 0))
128
+
129
+ val = Field.from_dsl('datetime').parse('2023-10-01 14:30:00')
130
+ self.assertIsInstance(val, datetime)
131
+ self.assertEqual(val, datetime(2023, 10, 1, 14, 30, 0))
114
132
 
115
133
  # ==========================
116
134
  # Errors
@@ -119,9 +137,23 @@ class TestFieldParser(unittest.TestCase):
119
137
  def test_unsupported_modifier(self):
120
138
  with self.assertRaisesRegex(ValueError, 'unsupported type'):
121
139
  Field.from_dsl('list of unknown')
122
-
140
+
123
141
  with self.assertRaisesRegex(ValueError, 'unknown modifier'):
124
142
  Field.from_dsl('int, random_mod')
125
143
 
144
+
145
+ def test_invalid_formats(self):
146
+ # FIXME: side effects
147
+ Config.date_fmt = '%Y-%m-%d'
148
+ Config.time_fmt = '%H:%M:%S'
149
+ Config.datetime_fmt = '%Y-%m-%d %H:%M:%S'
150
+
151
+ with self.assertRaisesRegex(ValueError, 'failed to parse'):
152
+ Field.from_dsl('date').parse('not-a-date')
153
+
154
+ with self.assertRaisesRegex(ValueError, 'failed to parse'):
155
+ Field.from_dsl('datetime').parse('2023/13/45')
156
+
157
+
126
158
  if __name__ == '__main__':
127
159
  unittest.main()
@@ -1,62 +0,0 @@
1
- """
2
- sphinxnotes.data
3
- ~~~~~~~~~~~~~~~~
4
-
5
- :copyright: Copyright 2025 by the Shengyu Zhang.
6
- :license: BSD, see LICENSE for details.
7
- """
8
-
9
- from __future__ import annotations
10
- from typing import TYPE_CHECKING
11
-
12
- from sphinx.util import logging
13
-
14
- from . import meta
15
- from .data import (
16
- Registry, Form, BoolFlag, OperFlag,
17
- Value, ValueWrapper,
18
- RawData, Data,
19
- Field, Schema,
20
- )
21
- from .template import Phase, Template
22
- from .render import (
23
- Caller,
24
- pending_node, RenderedNode, rendered_node, rendered_inline_node,
25
- BaseDataDefiner, BaseDataDefineRole, BaseDataDefineDirective,
26
- StrictDataDefineDirective,
27
- )
28
-
29
- if TYPE_CHECKING:
30
- from sphinx.application import Sphinx
31
-
32
-
33
- """Python API for other Sphinx extesions."""
34
- __all__ = [
35
- 'Registry', 'Form', 'BoolFlag', 'OperFlag',
36
- 'Value', 'ValueWrapper',
37
- 'RawData', 'Data',
38
- 'Field', 'Schema',
39
-
40
- 'Phase', 'Template',
41
-
42
- 'Caller',
43
- 'pending_node', 'RenderedNode', 'rendered_node', 'rendered_inline_node',
44
- 'BaseDataDefiner', 'BaseDataDefineRole', 'BaseDataDefineDirective',
45
- 'BaseDataDefineDirective', 'StrictDataDefineDirective',
46
- ]
47
-
48
- logger = logging.getLogger(__name__)
49
-
50
-
51
- def setup(app: Sphinx):
52
- meta.pre_setup(app)
53
-
54
- from . import template
55
- from . import render
56
- from . import adhoc
57
-
58
- template.setup(app)
59
- render.setup(app)
60
- adhoc.setup(app)
61
-
62
- return meta.post_setup(app)