rstobj 2.0.0__tar.gz → 2.0.1__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 (55) hide show
  1. {rstobj-2.0.0/rstobj.egg-info → rstobj-2.0.1}/PKG-INFO +42 -23
  2. {rstobj-2.0.0 → rstobj-2.0.1}/README.rst +21 -8
  3. rstobj-2.0.1/pyproject.toml +131 -0
  4. rstobj-2.0.1/rstobj/base.py +139 -0
  5. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj/directives/base.py +4 -1
  6. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj/directives/codeblock.py +4 -1
  7. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj/directives/image.py +6 -2
  8. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj/directives/miscellaneous.py +2 -1
  9. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj/directives/table.py +2 -1
  10. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj/directives/toc.py +6 -1
  11. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj/markup/enum_list.py +2 -1
  12. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj/markup/header.py +27 -7
  13. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj/paths.py +2 -2
  14. rstobj-2.0.1/rstobj/templates/__init__.py +12 -0
  15. rstobj-2.0.1/rstobj/templates/rstobj.directives.base.Directive.rst +8 -0
  16. rstobj-2.0.1/rstobj/templates/rstobj.directives.codeblock.Code.rst +1 -0
  17. rstobj-2.0.1/rstobj/templates/rstobj.directives.codeblock.CodeBlockBase.rst +9 -0
  18. rstobj-2.0.1/rstobj/templates/rstobj.directives.codeblock.CodeBlockEmpty.rst +3 -0
  19. rstobj-2.0.1/rstobj/templates/rstobj.directives.image.Image.rst +18 -0
  20. rstobj-2.0.1/rstobj/templates/rstobj.directives.miscellaneous.Include.rst +30 -0
  21. rstobj-2.0.1/rstobj/templates/rstobj.directives.table.ListTable.rst +29 -0
  22. rstobj-2.0.1/rstobj/templates/rstobj.directives.toc.TableOfContent.rst +12 -0
  23. rstobj-2.0.1/rstobj/templates/rstobj.markup.bullet_list.BulletList.rst +3 -0
  24. rstobj-2.0.1/rstobj/templates/rstobj.markup.enum_list.EnumList.rst +4 -0
  25. rstobj-2.0.1/rstobj/templates/rstobj.markup.header.Header.rst +5 -0
  26. rstobj-2.0.1/rstobj/templates/rstobj.markup.hyperlink.Reference.rst +1 -0
  27. rstobj-2.0.1/rstobj/templates/rstobj.markup.hyperlink.URI.rst +1 -0
  28. {rstobj-2.0.0 → rstobj-2.0.1/rstobj.egg-info}/PKG-INFO +42 -23
  29. rstobj-2.0.1/rstobj.egg-info/SOURCES.txt +47 -0
  30. rstobj-2.0.1/rstobj.egg-info/requires.txt +25 -0
  31. rstobj-2.0.1/tests/test_base.py +85 -0
  32. rstobj-2.0.1/tests/test_option.py +52 -0
  33. rstobj-2.0.0/pyproject.toml +0 -122
  34. rstobj-2.0.0/rstobj/_version.py +0 -4
  35. rstobj-2.0.0/rstobj/base.py +0 -86
  36. rstobj-2.0.0/rstobj/templates/__init__.py +0 -6
  37. rstobj-2.0.0/rstobj.egg-info/SOURCES.txt +0 -33
  38. rstobj-2.0.0/rstobj.egg-info/requires.txt +0 -26
  39. {rstobj-2.0.0 → rstobj-2.0.1}/AUTHORS.rst +0 -0
  40. {rstobj-2.0.0 → rstobj-2.0.1}/LICENSE.txt +0 -0
  41. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj/__init__.py +0 -0
  42. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj/api.py +0 -0
  43. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj/directives/__init__.py +0 -0
  44. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj/directives/api.py +0 -0
  45. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj/markup/__init__.py +0 -0
  46. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj/markup/api.py +0 -0
  47. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj/markup/bullet_list.py +0 -0
  48. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj/markup/hyperlink.py +0 -0
  49. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj/option.py +0 -0
  50. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj/vendor/__init__.py +0 -0
  51. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj/vendor/pytest_cov_helper.py +0 -0
  52. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj.egg-info/dependency_links.txt +0 -0
  53. {rstobj-2.0.0 → rstobj-2.0.1}/rstobj.egg-info/top_level.txt +0 -0
  54. {rstobj-2.0.0 → rstobj-2.0.1}/setup.cfg +0 -0
  55. {rstobj-2.0.0 → rstobj-2.0.1}/tests/test_api.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rstobj
3
- Version: 2.0.0
4
- Summary: An example project generated by cookiecutter-pywf_open_source.
5
- Author-email: Sanhe Hu <husanhe@email.com>
6
- Maintainer-email: Sanhe Hu <husanhe@email.com>
3
+ Version: 2.0.1
4
+ Summary: Construct reStructuredText markup and directives from Python code.
5
+ Author-email: Sanhe Hu <husanhe@gmail.com>
6
+ Maintainer-email: Sanhe Hu <husanhe@gmail.com>
7
7
  License-Expression: MIT
8
8
  Project-URL: Homepage, https://github.com/MacHu-GWU/rstobj-project
9
9
  Project-URL: Documentation, https://rstobj.readthedocs.io/en/latest/
@@ -17,30 +17,36 @@ Classifier: Natural Language :: English
17
17
  Classifier: Operating System :: Microsoft :: Windows
18
18
  Classifier: Operating System :: MacOS
19
19
  Classifier: Operating System :: Unix
20
- Requires-Python: <4.0,>=3.10
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Requires-Python: <4.0,>=3.11
21
26
  Description-Content-Type: text/x-rst
22
27
  License-File: LICENSE.txt
23
28
  License-File: AUTHORS.rst
24
- Requires-Dist: jinja2
29
+ Requires-Dist: jinja2<4.0.0,>=3.1.0
25
30
  Provides-Extra: dev
26
- Requires-Dist: rich<14.0.0,>=13.8.1; extra == "dev"
31
+ Requires-Dist: rich<15.0.0,>=14.3.4; extra == "dev"
27
32
  Provides-Extra: test
28
- Requires-Dist: pytest<9.0.0,>=8.2.2; extra == "test"
29
- Requires-Dist: pytest-cov<7.0.0,>=6.0.0; extra == "test"
33
+ Requires-Dist: pytest<10.0.0,>=9.1.1; extra == "test"
34
+ Requires-Dist: pytest-cov<8.0.0,>=7.1.0; extra == "test"
35
+ Requires-Dist: docutils<1.0.0,>=0.21.2; extra == "test"
30
36
  Provides-Extra: doc
31
- Requires-Dist: Sphinx<8.0.0,>=7.4.7; extra == "doc"
37
+ Requires-Dist: Sphinx<9.0.0,>=8.2.3; extra == "doc"
32
38
  Requires-Dist: sphinx-copybutton<1.0.0,>=0.5.2; extra == "doc"
33
- Requires-Dist: sphinx-design<1.0.0,>=0.6.1; extra == "doc"
39
+ Requires-Dist: sphinx-design<1.0.0,>=0.7.0; extra == "doc"
34
40
  Requires-Dist: sphinx-jinja<3.0.0,>=2.0.2; extra == "doc"
35
- Requires-Dist: furo==2024.8.6; extra == "doc"
41
+ Requires-Dist: furo==2025.12.19; extra == "doc"
36
42
  Requires-Dist: pygments<3.0.0,>=2.18.0; extra == "doc"
37
43
  Requires-Dist: ipython<8.19.0,>=8.18.1; extra == "doc"
38
- Requires-Dist: nbsphinx<1.0.0,>=0.8.12; extra == "doc"
39
- Requires-Dist: docfly==3.0.1; extra == "doc"
44
+ Requires-Dist: ipykernel<8.0.0,>=7.3.0; extra == "doc"
45
+ Requires-Dist: nbsphinx<1.0.0,>=0.9.8; extra == "doc"
46
+ Requires-Dist: docfly<4.0.0,>=3.0.3; extra == "doc"
40
47
  Provides-Extra: mise
41
48
  Requires-Dist: PyGithub<3.0.0,>=2.8.0; extra == "mise"
42
- Requires-Dist: httpx<1.0.0,>=0.28.0; extra == "mise"
43
- Requires-Dist: tomli<3.0.0,>=2.0.0; python_version < "3.11" and extra == "mise"
49
+ Requires-Dist: httpx2<3.0.0,>=2.13.1; extra == "mise"
44
50
  Dynamic: license-file
45
51
 
46
52
 
@@ -116,23 +122,21 @@ I have a `Blog Post <https://github.com/MacHu-GWU/Tech-Blog/issues/6>`_ to share
116
122
 
117
123
  .. code-block:: python
118
124
 
119
- import rstobj # or from rstobj import *
125
+ import rstobj.api as rstobj
120
126
 
121
127
  header = rstobj.markup.Header(title="Section1", header_level=1, auto_label=True)
122
- rst_header = header.render()
123
- print(rst_header)
128
+ print(header.render())
124
129
 
125
130
  ltable = rstobj.directives.ListTable(
126
131
  data=[["id", "name"], [1, "Alice"], [2, "Bob"]],
127
132
  title="Users",
128
133
  header=True,
129
134
  )
130
- rst = ltable.render()
131
- print(rst_ltable)
135
+ print(ltable.render())
132
136
 
133
137
  Output::
134
138
 
135
- .. _section1:
139
+ .. _Section1:
136
140
 
137
141
  Section1
138
142
  ========
@@ -148,7 +152,9 @@ Output::
148
152
  * - 2
149
153
  - Bob
150
154
 
151
- I recommend to use this in your jinja2 template, content of ``outut.rst``::
155
+ See the `Rendering Gallery <https://rstobj.readthedocs.io/en/latest/01-Rendering-Gallery/index.html>`_ for every supported markup and directive, with its Python code, its output, and how it renders.
156
+
157
+ I recommend to use this in your jinja2 template, content of ``output.rst``::
152
158
 
153
159
  {{ header.render() }}
154
160
  {{ ltable.render() }}
@@ -195,3 +201,16 @@ To upgrade to latest version:
195
201
  .. code-block:: console
196
202
 
197
203
  $ pip install --upgrade rstobj
204
+
205
+ .. _use-with-ai-coding-agents:
206
+
207
+ Use with AI Coding Agents
208
+ ------------------------------------------------------------------------------
209
+
210
+ This repo ships an `Agent Skill <https://code.claude.com/docs/en/skills>`_ that documents the public API of ``rstobj``, so an AI coding agent such as Claude Code can use the library without reading its source. It lives in `.claude/skills/pypi-rstobj <https://github.com/MacHu-GWU/rstobj-project/tree/main/.claude/skills/pypi-rstobj>`_. To use it in your project, copy it into your project's ``.claude/skills/`` folder. Run this from your project's root:
211
+
212
+ .. code-block:: console
213
+
214
+ $ git clone --depth 1 https://github.com/MacHu-GWU/rstobj-project.git /tmp/rstobj-project
215
+ $ mkdir -p .claude/skills
216
+ $ cp -R /tmp/rstobj-project/.claude/skills/pypi-rstobj .claude/skills/
@@ -71,23 +71,21 @@ I have a `Blog Post <https://github.com/MacHu-GWU/Tech-Blog/issues/6>`_ to share
71
71
 
72
72
  .. code-block:: python
73
73
 
74
- import rstobj # or from rstobj import *
74
+ import rstobj.api as rstobj
75
75
 
76
76
  header = rstobj.markup.Header(title="Section1", header_level=1, auto_label=True)
77
- rst_header = header.render()
78
- print(rst_header)
77
+ print(header.render())
79
78
 
80
79
  ltable = rstobj.directives.ListTable(
81
80
  data=[["id", "name"], [1, "Alice"], [2, "Bob"]],
82
81
  title="Users",
83
82
  header=True,
84
83
  )
85
- rst = ltable.render()
86
- print(rst_ltable)
84
+ print(ltable.render())
87
85
 
88
86
  Output::
89
87
 
90
- .. _section1:
88
+ .. _Section1:
91
89
 
92
90
  Section1
93
91
  ========
@@ -103,7 +101,9 @@ Output::
103
101
  * - 2
104
102
  - Bob
105
103
 
106
- I recommend to use this in your jinja2 template, content of ``outut.rst``::
104
+ See the `Rendering Gallery <https://rstobj.readthedocs.io/en/latest/01-Rendering-Gallery/index.html>`_ for every supported markup and directive, with its Python code, its output, and how it renders.
105
+
106
+ I recommend to use this in your jinja2 template, content of ``output.rst``::
107
107
 
108
108
  {{ header.render() }}
109
109
  {{ ltable.render() }}
@@ -149,4 +149,17 @@ To upgrade to latest version:
149
149
 
150
150
  .. code-block:: console
151
151
 
152
- $ pip install --upgrade rstobj
152
+ $ pip install --upgrade rstobj
153
+
154
+ .. _use-with-ai-coding-agents:
155
+
156
+ Use with AI Coding Agents
157
+ ------------------------------------------------------------------------------
158
+
159
+ This repo ships an `Agent Skill <https://code.claude.com/docs/en/skills>`_ that documents the public API of ``rstobj``, so an AI coding agent such as Claude Code can use the library without reading its source. It lives in `.claude/skills/pypi-rstobj <https://github.com/MacHu-GWU/rstobj-project/tree/main/.claude/skills/pypi-rstobj>`_. To use it in your project, copy it into your project's ``.claude/skills/`` folder. Run this from your project's root:
160
+
161
+ .. code-block:: console
162
+
163
+ $ git clone --depth 1 https://github.com/MacHu-GWU/rstobj-project.git /tmp/rstobj-project
164
+ $ mkdir -p .claude/skills
165
+ $ cp -R /tmp/rstobj-project/.claude/skills/pypi-rstobj .claude/skills/
@@ -0,0 +1,131 @@
1
+ # [project] table: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/
2
+ [project]
3
+ name = "rstobj"
4
+ # Bump before each release (https://semver.org/), then ``mise run inst`` to update ``uv.lock``.
5
+ version = "2.0.1"
6
+ description = "Construct reStructuredText markup and directives from Python code."
7
+ # An SPDX license expression (PEP 639); keep ``LICENSE.txt`` in sync.
8
+ # Read: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#license-and-license-files
9
+ # Valid identifiers: https://spdx.org/licenses/
10
+ license = "MIT"
11
+ license-files = ["LICENSE.txt", "AUTHORS.rst"]
12
+ authors = [
13
+ { name = "Sanhe Hu", email = "husanhe@gmail.com" },
14
+ ]
15
+ maintainers = [
16
+ { name = "Sanhe Hu", email = "husanhe@gmail.com" },
17
+ ]
18
+ keywords = []
19
+ readme = "README.rst"
20
+ requires-python = ">=3.11,<4.0"
21
+ # Read: https://pypi.org/classifiers/
22
+ classifiers = [
23
+ "Development Status :: 4 - Beta",
24
+ "Intended Audience :: Developers",
25
+ "Natural Language :: English",
26
+ "Operating System :: Microsoft :: Windows",
27
+ "Operating System :: MacOS",
28
+ "Operating System :: Unix",
29
+ "Programming Language :: Python :: 3",
30
+ "Programming Language :: Python :: 3.11",
31
+ "Programming Language :: Python :: 3.12",
32
+ "Programming Language :: Python :: 3.13",
33
+ "Programming Language :: Python :: 3.14",
34
+ ]
35
+
36
+ # --- Dependencies ---
37
+ # Edit these lists, then ``mise run inst`` to update ``uv.lock`` and ``.venv``.
38
+ dependencies = [
39
+ "jinja2>=3.1.0,<4.0.0", # renders the reST templates in rstobj/templates
40
+ ]
41
+
42
+ # --- Optional dependencies (extras) ---
43
+ # Read: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#dependencies-optional-dependencies
44
+ # Each one must support every Python version in ``requires-python``.
45
+ [project.optional-dependencies]
46
+
47
+ # local development
48
+ dev = [
49
+ "rich>=14.3.4,<15.0.0",
50
+ ]
51
+
52
+ # tests and coverage
53
+ test = [
54
+ "pytest>=9.1.1,<10.0.0",
55
+ "pytest-cov>=7.1.0,<8.0.0",
56
+ "docutils>=0.21.2,<1.0.0", # checks that the rendered reST is valid
57
+ ]
58
+
59
+ # docs build
60
+ doc = [
61
+ "Sphinx>=8.2.3,<9.0.0", # held below 9 until its autodoc rewrite is verified
62
+ "sphinx-copybutton>=0.5.2,<1.0.0", # copy button on code blocks
63
+ "sphinx-design>=0.7.0,<1.0.0", # cards, tabs, grids
64
+ "sphinx-jinja>=2.0.2,<3.0.0", # jinja in reST
65
+ "furo==2025.12.19", # theme
66
+ "pygments>=2.18.0,<3.0.0", # syntax highlighting
67
+ "ipython>=8.18.1,<8.19.0", # notebook execution
68
+ "ipykernel>=7.3.0,<8.0.0", # notebook kernel
69
+ "nbsphinx>=0.9.8,<1.0.0", # notebooks as doc pages
70
+ "docfly>=3.0.3,<4.0.0", # toctree and API reference
71
+ ]
72
+
73
+ # mise tasks
74
+ mise = [
75
+ "PyGithub>=2.8.0,<3.0.0", # GitHub API client
76
+ "httpx2>=2.13.1,<3.0.0", # Codecov and Read the Docs APIs
77
+ ]
78
+
79
+ [project.urls]
80
+ Homepage = "https://github.com/MacHu-GWU/rstobj-project"
81
+ Documentation = "https://rstobj.readthedocs.io/en/latest/"
82
+ Repository = "https://github.com/MacHu-GWU/rstobj-project"
83
+ Issues = "https://github.com/MacHu-GWU/rstobj-project/issues"
84
+ Changelog = "https://github.com/MacHu-GWU/rstobj-project/blob/main/release-history.rst"
85
+ Download = "https://pypi.org/pypi/rstobj#files"
86
+
87
+ # Read: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#creating-executable-scripts
88
+ [project.scripts]
89
+
90
+ [tool.setuptools.packages.find]
91
+ where = ["."]
92
+ include = [
93
+ "rstobj*",
94
+ ]
95
+ exclude = [
96
+ "rstobj.tests",
97
+ "rstobj.tests.*",
98
+ "rstobj.docs",
99
+ "rstobj.docs.*",
100
+ ]
101
+
102
+ # Non-Python files shipped in the wheel: the jinja2 templates rendered by ``rstobj.templates``.
103
+ [tool.setuptools.package-data]
104
+ rstobj = [
105
+ "templates/*.rst",
106
+ ]
107
+
108
+ # --- ruff: ``mise run format`` / ``mise run lint`` ---
109
+ [tool.ruff]
110
+ extend-exclude = [
111
+ "rstobj/vendor",
112
+ ".claude/skills/*/assets", # templates, not valid Python
113
+ ]
114
+
115
+ [tool.ruff.lint.per-file-ignores]
116
+ # re-export modules
117
+ "**/__init__.py" = ["F401"]
118
+ "**/api.py" = ["F401"]
119
+
120
+ # --- pytest 9+ native table ---
121
+ # Read: https://docs.pytest.org/en/stable/reference/customize.html#pyproject-toml
122
+ [tool.pytest]
123
+ testpaths = ["tests"]
124
+ # strict = true # only with a pinned pytest
125
+ # filterwarnings = ["error"]
126
+ # markers = ["slow: tests that take more than a few seconds"]
127
+
128
+ # --- Build (``mise run build``); setuptools 77+ for PEP 639 ``license`` ---
129
+ [build-system]
130
+ requires = ["setuptools>=77.0"]
131
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,139 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """
4
+ RestructuredText Object abstraction.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import typing as T
10
+ from dataclasses import dataclass
11
+
12
+ if T.TYPE_CHECKING: # pragma: no cover
13
+ from jinja2 import Template
14
+
15
+ from .templates import env
16
+ from .option import Options
17
+
18
+
19
+ def _indent_line(line: str, prefix: str) -> str:
20
+ """
21
+ Prefix a line and strip its trailing whitespace. A blank line stays empty,
22
+ so indenting never leaves whitespace-only lines.
23
+ """
24
+ line = line.rstrip()
25
+ if line:
26
+ return prefix + line
27
+ else:
28
+ return ""
29
+
30
+
31
+ def _indent_continuation(text: str, width: int, force: bool) -> str:
32
+ """
33
+ Indent every line after the first by ``width`` spaces, so a multi-line
34
+ text stays inside the list item or table cell it starts in.
35
+
36
+ :param force: if False, text whose continuation lines are all indented by
37
+ at least ``width`` already is returned unchanged, so callers that
38
+ indented their strings by hand keep the same output.
39
+ """
40
+ lines = text.split("\n")
41
+ rest = lines[1:]
42
+ if not force:
43
+ is_indented = all(
44
+ (not line.strip()) or (len(line) - len(line.lstrip(" ")) >= width)
45
+ for line in rest
46
+ )
47
+ if is_indented:
48
+ return text
49
+ prefix = " " * width
50
+ return "\n".join([lines[0]] + [_indent_line(line, prefix) for line in rest])
51
+
52
+
53
+ @dataclass(kw_only=True)
54
+ class RstObj:
55
+ """
56
+ The base restructured text object.
57
+ """
58
+
59
+ @property
60
+ def template_name(self) -> str:
61
+ """
62
+ Find template file.
63
+ """
64
+ return "{}.{}.rst".format(self.__module__, self.__class__.__name__)
65
+
66
+ @property
67
+ def template(self) -> Template:
68
+ """
69
+ Return ``jinja2.Template`` instance.
70
+ """
71
+ return env.get_template(self.template_name)
72
+
73
+ def render(
74
+ self,
75
+ indent: int | None = None,
76
+ first_line_indent: int | None = None,
77
+ **kwargs,
78
+ ) -> str:
79
+ """
80
+ Render this object into text.
81
+
82
+ :param indent: global indent. Indent length can be changed in
83
+ :attr:`rstobj.option.Options.tab`.
84
+
85
+ :param first_line_indent: sometimes we only need to indent
86
+ first line, this option will overwrite the ``indent`` argument.
87
+
88
+ :param kwargs: other optional arguments.
89
+ """
90
+ out = self.template.render(obj=self, tab=Options.tab)
91
+ if indent or first_line_indent:
92
+ origin_lines = out.split("\n")
93
+ target_lines = [
94
+ _indent_line(line, Options.tab * (indent or 0)) for line in origin_lines
95
+ ]
96
+ if first_line_indent is not None:
97
+ if first_line_indent >= 0:
98
+ target_lines[0] = _indent_line(
99
+ origin_lines[0],
100
+ Options.tab * first_line_indent,
101
+ )
102
+ else: # pragma: no cover
103
+ raise TypeError
104
+ out = "\n".join(target_lines)
105
+ return out
106
+
107
+ @staticmethod
108
+ def str_or_render(
109
+ value: str | RstObj,
110
+ **kwargs,
111
+ ) -> str:
112
+ """
113
+ If it is a string type, then just return. If it is a RstObj type,
114
+ then return the rendered string.
115
+ """
116
+ if isinstance(value, RstObj):
117
+ return value.render(**kwargs)
118
+ else:
119
+ return str(value)
120
+
121
+ @staticmethod
122
+ def _render_nested(
123
+ value: str | RstObj,
124
+ width: int,
125
+ ) -> str:
126
+ """
127
+ Render a list item or table cell whose text starts after a marker
128
+ ``width`` characters wide, such as ``"- "`` or ``" * - "``.
129
+
130
+ A :class:`RstObj` is rendered without its leading and trailing blank
131
+ lines, and all its continuation lines are indented. A string is taken
132
+ as reStructuredText; its continuation lines are indented unless they
133
+ already are.
134
+ """
135
+ if isinstance(value, RstObj):
136
+ text = value.render().strip("\n")
137
+ return _indent_continuation(text, width, force=True)
138
+ else:
139
+ return _indent_continuation(str(value), width, force=False)
@@ -3,7 +3,8 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import typing as T
6
- from dataclasses import dataclass, field
6
+ from dataclasses import dataclass
7
+ from dataclasses import field
7
8
  from ..base import RstObj
8
9
 
9
10
 
@@ -13,6 +14,8 @@ class Directive(RstObj):
13
14
  name: str | None = field(default=None)
14
15
 
15
16
  meta_directive_keyword: T.ClassVar[str | None] = None
17
+ #: False for a directive that has no ``:name:`` option, such as ``contents``.
18
+ meta_supports_name: T.ClassVar[bool] = True
16
19
 
17
20
  @property
18
21
  def arg(self): # pragma: no cover
@@ -22,6 +22,9 @@ class Code(RstObj):
22
22
  @dataclass(kw_only=True)
23
23
  class CodeBlockEmpty(Directive):
24
24
  """
25
+ A literal block (``::``). It has no options, so ``class_`` and ``name``
26
+ are not rendered.
27
+
25
28
  Example::
26
29
 
27
30
  code = "your code ..."
@@ -92,7 +95,7 @@ Example::
92
95
 
93
96
  Output::
94
97
 
95
- .. code-block: {}
98
+ .. code-block:: {}
96
99
 
97
100
  your code ...
98
101
  """.strip()
@@ -7,7 +7,8 @@ image related directives.
7
7
  from __future__ import annotations
8
8
 
9
9
  import typing as T
10
- from dataclasses import dataclass, field
10
+ from dataclasses import dataclass
11
+ from dataclasses import field
11
12
  from .base import Directive
12
13
 
13
14
 
@@ -21,9 +22,12 @@ class Image(Directive):
21
22
  :param uri: required.
22
23
  :param height: optional.
23
24
  :param width: optional.
24
- :param scale: optional.
25
+ :param scale: optional, a percentage, rendered as ``:scale: 50%``.
25
26
  :param alt_text: optional.
26
27
  :param align: optional. one of :class:`Image.AlignOptions`.
28
+ ``"top"``, ``"middle"`` and ``"bottom"`` are only valid for an image
29
+ inside a substitution definition; a standalone image accepts
30
+ ``"left"``, ``"center"`` and ``"right"``.
27
31
 
28
32
  Example::
29
33
 
@@ -7,7 +7,8 @@ Other directives.
7
7
  from __future__ import annotations
8
8
 
9
9
  import typing as T
10
- from dataclasses import dataclass, field
10
+ from dataclasses import dataclass
11
+ from dataclasses import field
11
12
  from .base import Directive
12
13
 
13
14
 
@@ -7,7 +7,8 @@ table related directives.
7
7
  from __future__ import annotations
8
8
 
9
9
  import typing as T
10
- from dataclasses import dataclass, field
10
+ from dataclasses import dataclass
11
+ from dataclasses import field
11
12
  from .base import Directive
12
13
 
13
14
 
@@ -7,7 +7,8 @@ table of content directive.
7
7
  from __future__ import annotations
8
8
 
9
9
  import typing as T
10
- from dataclasses import dataclass, field
10
+ from dataclasses import dataclass
11
+ from dataclasses import field
11
12
  from .base import Directive
12
13
 
13
14
 
@@ -22,6 +23,9 @@ class TableOfContent(Directive):
22
23
  :param depth: int, optional.
23
24
  :param local: bool, optional.
24
25
 
26
+ The ``contents`` directive has no ``:name:`` option, so ``name`` is
27
+ not rendered.
28
+
25
29
  :type backlinks: str
26
30
  :param backlinks: optional. one of
27
31
  :attr:`TableOfContent.BacklinksOptions`.
@@ -43,6 +47,7 @@ class TableOfContent(Directive):
43
47
  backlinks: str | None = field(default=None)
44
48
 
45
49
  meta_directive_keyword: T.ClassVar[str] = "contents"
50
+ meta_supports_name: T.ClassVar[bool] = False
46
51
 
47
52
  class BacklinksOptions:
48
53
  """
@@ -6,7 +6,8 @@ Enumerate list.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from dataclasses import dataclass, field
9
+ from dataclasses import dataclass
10
+ from dataclasses import field
10
11
  from ..base import RstObj
11
12
 
12
13
 
@@ -16,8 +16,9 @@ Default `section header line <http://docutils.sourceforge.net/docs/user/rst/quic
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
- import typing as T
20
- from dataclasses import dataclass, field
19
+ import unicodedata
20
+ from dataclasses import dataclass
21
+ from dataclasses import field
21
22
  from ..base import RstObj
22
23
 
23
24
  dash_char_list = " _~"
@@ -39,6 +40,23 @@ def to_label(title: str) -> str:
39
40
  ])
40
41
 
41
42
 
43
+ def _display_width(text: str) -> int:
44
+ """
45
+ The number of columns ``text`` takes on screen, as docutils counts them
46
+ to check a section underline: wide East Asian characters and emoji count
47
+ as two columns, combining characters as zero.
48
+ """
49
+ width = 0
50
+ for char in text:
51
+ if unicodedata.combining(char):
52
+ continue
53
+ if unicodedata.east_asian_width(char) in ("W", "F"):
54
+ width += 2
55
+ else:
56
+ width += 1
57
+ return width
58
+
59
+
42
60
  HEADER_CHAR_MAPPER = {
43
61
  1: "=",
44
62
  2: "-",
@@ -92,7 +110,7 @@ class Header(RstObj):
92
110
  if self.auto_label and (self.ref_label is None):
93
111
  self.ref_label = to_label(self.title)
94
112
 
95
- title_length = len(self.title)
113
+ title_length = _display_width(self.title)
96
114
  if self.bar_length is None:
97
115
  self.bar_length = title_length
98
116
  elif self.bar_length < title_length:
@@ -112,12 +130,14 @@ class HeaderLevel(Header):
112
130
  pass
113
131
 
114
132
 
133
+ # Starts with a newline: autodoc dedents every line after the first, so the
134
+ # literal block must be indented relative to "Example::" on a later line.
115
135
  header_doc_string = """
116
- Example::
136
+ Example::
117
137
 
118
- Header{level}
119
- {bar}
120
- """.strip()
138
+ Header{level}
139
+ {bar}
140
+ """
121
141
 
122
142
 
123
143
  def _build_doc_string(header_level: int) -> str:
@@ -15,6 +15,7 @@ class PathEnum:
15
15
  absolute paths to eliminate current directory dependencies and ensure consistent path
16
16
  resolution across different execution contexts and DevOps workflows.
17
17
  """
18
+
18
19
  @cached_property
19
20
  def dir_home(self):
20
21
  return Path.home()
@@ -24,10 +25,9 @@ class PathEnum:
24
25
 
25
26
  # Source Code
26
27
  dir_package = _dir_here
27
- path_version_py = dir_package / "_version.py"
28
28
  path_pyproject_toml = dir_project_root / "pyproject.toml"
29
29
  path_requirements_txt = dir_project_root / "requirements.txt"
30
- path_authors = dir_project_root / "AUTHORS.txt"
30
+ path_authors = dir_project_root / "AUTHORS.rst"
31
31
  path_license = dir_project_root / "LICENSE.txt"
32
32
  path_release_history = dir_project_root / "release-history.rst"
33
33
 
@@ -0,0 +1,12 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import os
4
+ from jinja2 import Environment
5
+ from jinja2 import FileSystemLoader
6
+ from jinja2 import StrictUndefined
7
+
8
+ # StrictUndefined: a typo in a template raises instead of rendering nothing.
9
+ env = Environment(
10
+ loader=FileSystemLoader(os.path.dirname(__file__)),
11
+ undefined=StrictUndefined,
12
+ )
@@ -0,0 +1,8 @@
1
+ .. {{ obj.meta_directive_keyword }}::{% if obj.arg %} {{ obj.arg }}{% endif %}
2
+ {%- if obj.class_ is not none %}
3
+ {{ tab }}:class: {{ obj.class_ }}
4
+ {%- endif %}
5
+ {%- if obj.name is not none and obj.meta_supports_name %}
6
+ {{ tab }}:name: {{ obj.name }}
7
+ {%- endif %}
8
+ {%- block other %}{% endblock %}