Skip to content

Commit 5a33aac

Browse files
juiwenchenubmarco
authored andcommitted
init impl
1 parent 133cb8c commit 5a33aac

6 files changed

Lines changed: 413 additions & 2 deletions

File tree

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/bridge.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
"""Covert the generated JSON file created by CodeLinks anaylse to need-extend in RST."""
2+
3+
from collections import deque
4+
from dataclasses import MISSING, dataclass, field, fields
5+
import json
6+
from os import linesep
7+
from pathlib import Path
8+
from string import Template
9+
from typing import Any, TypedDict, cast
10+
11+
from jsonschema import ValidationError, validate
12+
13+
from sphinx_codelinks.analyse.models import MarkedContentType, SourceMap
14+
from sphinx_codelinks.logger import logger
15+
16+
NEEDEXTEND_TEMPLATE = Template(""".. needextend:: $need_id
17+
:$remote_url_field: $remote_url
18+
19+
""")
20+
21+
22+
@dataclass
23+
class MarkedContentSchema:
24+
@classmethod
25+
def field_names(cls) -> set[str]:
26+
return {item.name for item in fields(cls)}
27+
28+
filepath: str = field(
29+
metadata={"schema": {"type": "string"}},
30+
)
31+
"""filepath where the marked content is located."""
32+
33+
remote_url: str | None = field(
34+
metadata={"schema": {"type": "string", "nullable": True}}
35+
)
36+
"""remote url which can be directed to the the marked content."""
37+
38+
source_map: SourceMap = field(
39+
metadata={
40+
"schema": {
41+
"type": "object",
42+
"properties": {
43+
"row": {"type": "integer"},
44+
"column": {"type": "integer"},
45+
},
46+
"required": ["row", "column"],
47+
"additionalProperties": False,
48+
}
49+
}
50+
)
51+
"""Coordinate of the marked content in a file"""
52+
53+
tagged_scope: str | None = field(
54+
metadata={"schema": {"type": "string", "nullable": True}}
55+
)
56+
"""The scoped tagged by the marked content"""
57+
58+
type: MarkedContentType = field(
59+
metadata={
60+
"schema": {
61+
"type": "string",
62+
"enum": [key.value for key in MarkedContentType],
63+
}
64+
}
65+
)
66+
"""Type of the marked content."""
67+
68+
marker: str | None = field(
69+
default=None, metadata={"schema": {"type": "string", "nullable": True}}
70+
)
71+
"""Marker of the marked content."""
72+
73+
need_ids: list[str] | None = field(
74+
default=None,
75+
metadata={"schema": {"type": "array", "items": {"type": "string"}}},
76+
)
77+
"""Need id refs which is associated to the need items in the documentation."""
78+
need: dict[str, str | list[str]] | None = field(
79+
default=None,
80+
metadata={
81+
"schema": {
82+
"type": "object",
83+
"properties": {"title": {"type": "string"}, "id": {"title": "string"}},
84+
"required": ["title", "id"],
85+
"additionalProperties": True,
86+
}
87+
},
88+
)
89+
"""Definition of a need item"""
90+
91+
rst: str | None = field(
92+
default=None, metadata={"schema": {"type": "string", "nullable": True}}
93+
)
94+
"""Extracted rst text."""
95+
96+
@classmethod
97+
def get_schema(cls, name: str) -> dict[str, Any] | None: # type: ignore[explicit-any]
98+
_field = next(_field for _field in fields(cls) if _field.name is name)
99+
if _field.metadata is not MISSING and "schema" in _field.metadata:
100+
return cast(dict[str, Any], _field.metadata["schema"]) # type: ignore[explicit-any]
101+
return None
102+
103+
def check_schema(self) -> list[str]:
104+
errors = []
105+
for _field_name in self.field_names():
106+
schema = self.get_schema(_field_name)
107+
value = getattr(self, _field_name)
108+
try:
109+
validate(instance=value, schema=schema) # type: ignore[arg-type] # validate has no type
110+
except ValidationError as e:
111+
errors.append(
112+
f"Schema validation error in field '{_field_name}': {e.message}"
113+
)
114+
return errors
115+
116+
def check_conditional_required_fields(self) -> list[str]:
117+
errors = []
118+
if self.type == MarkedContentType.need.value and not self.need:
119+
errors.append(
120+
"Need definition is required for marked content of type 'need'"
121+
)
122+
elif self.type == MarkedContentType.need_id_refs.value:
123+
if not self.marker:
124+
errors.append(
125+
"Marker is required for marked content of type 'need_id_refs'"
126+
)
127+
if not self.need_ids:
128+
errors.append(
129+
"Need id refs are required for marked content of type 'need_id_refs'"
130+
)
131+
elif self.type == MarkedContentType.need.value and not self.need_ids:
132+
errors.append(
133+
"Need id refs are required for marked content of type 'need_id_refs'"
134+
)
135+
elif self.type == MarkedContentType.rst.value and not self.rst:
136+
errors.append("RST text is required for marked content of type 'rst'")
137+
return errors
138+
139+
def check_loaded_objs(self) -> list[str]:
140+
return self.check_schema() + self.check_conditional_required_fields()
141+
142+
143+
class MarkedObjType(TypedDict):
144+
filepath: str
145+
remote_url: str | None
146+
source_map: SourceMap
147+
tagged_scope: str | None
148+
type: MarkedContentType
149+
need_ids: list[str] | None
150+
need: dict[str, str | list[str]] | None
151+
rst: str | None
152+
153+
154+
def convert_makred_content(
155+
jsonpath: Path, outdir: Path, remote_url_field: str = "remote_url"
156+
) -> None:
157+
"""Convert marked objects extracted by anaylse CLI to needextend in RST"""
158+
with jsonpath.open("r") as f:
159+
marked_objs = json.load(f)
160+
161+
warnings = []
162+
163+
need_id_refs = [
164+
obj
165+
for obj in marked_objs
166+
if obj["type"] == MarkedContentType.need_id_refs.value
167+
]
168+
169+
for obj in need_id_refs:
170+
schema = MarkedContentSchema(**obj)
171+
obj_warnings: deque[str] = deque()
172+
obj_warnings.extend(schema.check_schema())
173+
obj_warnings.extend(schema.check_conditional_required_fields())
174+
if obj_warnings:
175+
obj_warnings.appendleft(f"Marked object {obj} has the following warnings:")
176+
warnings.extend(list(obj_warnings))
177+
178+
if warnings:
179+
logger.warning(
180+
f"Marked objects have the following warnings: {linesep.join(warnings)}"
181+
)
182+
183+
needextend_texts: list[str] = []
184+
for obj in need_id_refs:
185+
for need_id in obj["need_ids"]:
186+
needextend_text = NEEDEXTEND_TEMPLATE.safe_substitute(
187+
need_id=need_id,
188+
remote_url_field=remote_url_field,
189+
remote_url=obj[remote_url_field],
190+
)
191+
needextend_texts.append(needextend_text)
192+
193+
needextend_path = outdir / "needextend.rst"
194+
with needextend_path.open("w") as f:
195+
f.writelines(needextend_texts)

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()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
get_oneline_needs = true
2+
get_rst = true
13
[source_discover]
24
src_dir = "../../data"
35
gitignore = false

0 commit comments

Comments
 (0)