Skip to content

Commit bd1fae9

Browse files
authored
✨ Write needextend rst from need-id-refs markers (#13)
- Added new module needextend_write, which consume the output of analyse and generate the corresponding rst with needextend directive - Added write rst CLI which use needextend_write - Added a Sphinx demo project for the whole workflow. Simply run rye run demo:clean to have documentation ready
1 parent 28337c9 commit bd1fae9

14 files changed

Lines changed: 767 additions & 10 deletions

File tree

pyproject.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,21 @@ codelinks = "sphinx_codelinks.cmd:app"
7676
"docs:rm" = "rm -rf docs/_build/html"
7777
"docs" = "sphinx-build -nW --keep-going -b html -T -c docs docs/source docs/_build/html"
7878
"docs:clean" = { chain = ["docs:rm", "docs"] }
79+
# needextend demo
80+
"analyse" = "codelinks analyse tests/data/configs/minimum_config.toml"
81+
"analyse:rm" = "rm -rf output/marked_content.json"
82+
"write" = "codelinks write rst output/marked_content.json --outpath tests/data/needextend_demo/needextend.rst"
83+
"write:rm" = "rm -rf tests/data/needextend_demo/needextend.rst"
84+
"demo:rm" = "rm -rf tests/data/needextend_demo/_build"
85+
"demo:build" = "sphinx-build -nW --keep-going -b html -T -c tests/data/needextend_demo tests/data/needextend_demo tests/data/needextend_demo/_build/html"
86+
"demo:clean" = { chain = [
87+
"demo:rm",
88+
"analyse:rm",
89+
"write:rm",
90+
"analyse",
91+
"write",
92+
"demo:build",
93+
] }
7994

8095
[tool.ruff.lint]
8196
extend-select = [

src/sphinx_codelinks/analyse/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88

99
class MarkedContentType(str, Enum):
10-
need = ("need",)
11-
need_id_refs = ("need-id-refs",)
10+
need = "need"
11+
need_id_refs = "need-id-refs"
1212
rst = "rst"
1313

1414

src/sphinx_codelinks/cmd.py

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from collections import deque
2+
import json
23
from os import linesep
34
from pathlib import Path
45
import tomllib
5-
from typing import Annotated, cast
6+
from typing import Annotated, TypeAlias, cast
67

78
import typer
89

@@ -13,6 +14,8 @@
1314
CodeLinksProjectConfigType,
1415
generate_project_configs,
1516
)
17+
from sphinx_codelinks.logger import logger
18+
from sphinx_codelinks.needextend_write import MarkedObjType, convert_marked_content
1619
from sphinx_codelinks.source_discover.config import (
1720
CommentType,
1821
SourceDiscoverConfig,
@@ -23,6 +26,33 @@
2326
app = typer.Typer(
2427
no_args_is_help=True, context_settings={"help_option_names": ["-h", "--help"]}
2528
)
29+
write_app = typer.Typer(
30+
help="Export marked content to other formats", no_args_is_help=True
31+
)
32+
app.add_typer(write_app, name="write", rich_help_panel="Sub-menus")
33+
34+
OptVerbose: TypeAlias = Annotated[ # noqa: UP040 # has to be TypeAlias
35+
bool,
36+
typer.Option(
37+
...,
38+
"-v",
39+
"--verbose",
40+
is_flag=True,
41+
help="Show debug information",
42+
rich_help_panel="Logging",
43+
),
44+
]
45+
OptQuiet: TypeAlias = Annotated[ # noqa: UP040 # has to be TypeAlias
46+
bool,
47+
typer.Option(
48+
...,
49+
"-q",
50+
"--quiet",
51+
is_flag=True,
52+
help="Only show errors and warnings",
53+
rich_help_panel="Logging",
54+
),
55+
]
2656

2757

2858
@app.command(no_args_is_help=True)
@@ -190,6 +220,79 @@ def discover(
190220
typer.echo(file_path)
191221

192222

223+
@write_app.command("rst", no_args_is_help=True)
224+
def write_rst( # noqa: PLR0913 # for CLI, so it takes as many as it requires
225+
jsonpath: Annotated[
226+
Path,
227+
typer.Argument(
228+
...,
229+
help="Path of the JSON file which contains the extracted markers",
230+
show_default=False,
231+
dir_okay=False,
232+
file_okay=True,
233+
exists=True,
234+
resolve_path=True,
235+
),
236+
],
237+
outpath: Annotated[
238+
Path,
239+
typer.Option(
240+
"--outpath",
241+
"-o",
242+
help="The output path for generated rst file",
243+
show_default=True,
244+
dir_okay=False,
245+
file_okay=True,
246+
exists=False,
247+
),
248+
] = Path("needextend.rst"),
249+
remote_url_field: Annotated[
250+
str,
251+
typer.Option(
252+
"--remote-url-field",
253+
"-r",
254+
help="The field name for the remote url",
255+
show_default=True,
256+
),
257+
] = "remote_url", # to show default value in this CLI
258+
title: Annotated[
259+
str | None,
260+
typer.Option(
261+
"--title",
262+
"-t",
263+
help="Give the title to the generated RST file",
264+
show_default=True,
265+
),
266+
] = None, # to show default value in this CLI
267+
verbose: OptVerbose = False,
268+
quiet: OptQuiet = False,
269+
) -> None:
270+
"""Generate needextend.rst from the extracted obj in JSON."""
271+
logger.configure(verbose, quiet)
272+
try:
273+
with jsonpath.open("r") as f:
274+
marked_content = json.load(f)
275+
except Exception as e:
276+
raise typer.BadParameter(
277+
f"Failed to load marked content from {jsonpath}: {e}"
278+
) from e
279+
280+
marked_objs: list[MarkedObjType] = [
281+
obj for objs in marked_content.values() for obj in objs
282+
]
283+
284+
needextend_texts, errors = convert_marked_content(
285+
marked_objs, remote_url_field, title
286+
)
287+
if errors:
288+
raise typer.BadParameter(
289+
f"Errors occurred during conversion: {linesep.join(errors)}"
290+
)
291+
with outpath.open("w") as f:
292+
f.writelines(needextend_texts)
293+
typer.echo(f"Generated {outpath}")
294+
295+
193296
def load_config_from_toml(toml_file: Path) -> CodeLinksConfigType:
194297
try:
195298
with toml_file.open("rb") as f:

src/sphinx_codelinks/logger.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from rich.console import Console
2+
from rich.text import Text
3+
import typer
4+
5+
6+
class Logger:
7+
__slots__ = ("console", "err_console", "quiet", "verbose")
8+
9+
def __init__(self, *, verbose: bool = False, quiet: bool = False) -> None:
10+
self.verbose = verbose
11+
self.quiet = quiet
12+
self.console = Console()
13+
self.err_console = Console(stderr=True)
14+
15+
def configure(self, verbose: bool = False, quiet: bool = False) -> None:
16+
self.verbose = verbose
17+
self.quiet = quiet
18+
19+
def debug(
20+
self,
21+
*msg: str | Text,
22+
style: str | None = typer.colors.BRIGHT_BLACK,
23+
highlight: bool = False,
24+
markup: bool = False,
25+
console: Console | None = None,
26+
) -> None:
27+
"""Print a debug message.
28+
29+
Will only be shown if verbose mode is enabled and not in quiet mode.
30+
"""
31+
if self.verbose and not self.quiet:
32+
(console or self.console).print(
33+
*msg, style=style, highlight=highlight, markup=markup
34+
)
35+
36+
def info(
37+
self,
38+
*msg: str | Text,
39+
style: str | None = None,
40+
highlight: bool = False,
41+
markup: bool = False,
42+
no_wrap: bool | None = None,
43+
console: Console | None = None,
44+
) -> None:
45+
"""Print an informational message.
46+
47+
Will be suppressed in quiet mode.
48+
"""
49+
if not self.quiet:
50+
(console or self.console).print(
51+
*msg, style=style, highlight=highlight, markup=markup, no_wrap=no_wrap
52+
)
53+
54+
def result(
55+
self,
56+
*msg: str | Text,
57+
style: str | None = None,
58+
highlight: bool = False,
59+
markup: bool = False,
60+
console: Console | None = None,
61+
) -> None:
62+
"""Print a result message, like info but ignores quiet mode."""
63+
(console or self.console).print(
64+
*msg, style=style, highlight=highlight, markup=markup
65+
)
66+
67+
def warning(
68+
self,
69+
*msg: str | Text,
70+
style: str | None = typer.colors.YELLOW,
71+
highlight: bool = False,
72+
markup: bool = False,
73+
console: Console | None = None,
74+
) -> None:
75+
"""Print a warning message."""
76+
(console or self.console).print(
77+
*msg, style=style, highlight=highlight, markup=markup
78+
)
79+
80+
def error(
81+
self,
82+
*msg: str | Text,
83+
style: str | None = typer.colors.RED,
84+
highlight: bool = False,
85+
markup: bool = False,
86+
console: Console | None = None,
87+
) -> None:
88+
"""Print an error message."""
89+
(console or self.err_console).print(
90+
*msg, style=style, highlight=highlight, markup=markup
91+
)
92+
93+
94+
logger = Logger()

0 commit comments

Comments
 (0)