Coverage for src/shellman/tags.py: 61.01%
243 statements
« prev ^ index » next coverage.py v7.3.0, created at 2023-09-03 19:58 +0200
« prev ^ index » next coverage.py v7.3.0, created at 2023-09-03 19:58 +0200
1"""Section module.
3This module contains the Section class.
4"""
6from __future__ import annotations
8import re
9from dataclasses import dataclass
10from functools import cached_property
11from typing import TYPE_CHECKING, Sequence
13if TYPE_CHECKING:
14 from shellman.reader import DocLine
17class Tag:
18 """Base class for tags."""
20 @classmethod
21 def from_lines(cls, lines: Sequence[DocLine]) -> Tag:
22 """Parse a sequence of lines into a tag instance.
24 Parameters:
25 lines: The sequence of lines to parse.
27 Returns:
28 A tag instance.
29 """
30 raise NotImplementedError
33@dataclass
34class TextTag(Tag):
35 """A simple tag holding text only."""
37 text: str
38 """The tag's text."""
40 @classmethod
41 def from_lines(cls, lines: Sequence[DocLine]) -> TextTag: # noqa: D102
42 return cls(text="\n".join(line.value for line in lines))
45@dataclass
46class NameDescTag(Tag):
47 """A tag holding a name and a description."""
49 name: str
50 """The tag name."""
51 description: str
52 """The tag description."""
54 @classmethod
55 def from_lines(cls, lines: Sequence[DocLine]) -> EnvTag: # noqa: D102
56 name, description = "", []
57 for line in lines:
58 if line.tag == "env":
59 split = line.value.split(" ", 1)
60 if len(split) > 1:
61 name = split[0]
62 description.append(split[1])
63 else:
64 name = split[0]
65 else:
66 description.append(line.value)
67 return EnvTag(name=name, description="\n".join(description))
70@dataclass
71class AuthorTag(TextTag):
72 """A tag representing an author."""
75@dataclass
76class BugTag(TextTag):
77 """A tag representing a bug note."""
80@dataclass
81class BriefTag(TextTag):
82 """A tag representing a summary."""
85@dataclass
86class CaveatTag(TextTag):
87 """A tag representing caveats."""
90@dataclass
91class CopyrightTag(TextTag):
92 """A tag representing copyright information."""
95@dataclass
96class DateTag(TextTag):
97 """A tag representing a date."""
100@dataclass
101class DescTag(TextTag):
102 """A tag representing a description."""
105@dataclass
106class EnvTag(NameDescTag):
107 """A tag representing an environment variable used by the script."""
110@dataclass
111class ErrorTag(TextTag):
112 """A tag representing a known error."""
115@dataclass
116class ExampleTag(Tag):
117 """A tag representing a code/shell example."""
119 brief: str
120 """The example's summary."""
121 code: str
122 """The example's code."""
123 code_lang: str
124 """The example's language."""
125 description: str
126 """The example's description."""
128 @classmethod
129 def from_lines(cls, lines: Sequence[DocLine]) -> ExampleTag: # noqa: D102
130 brief, code, description = [], [], []
131 code_lang = ""
132 current = None
133 for line in lines:
134 if line.tag == "example":
135 if line.value:
136 brief.append(line.value)
137 current = "brief"
138 elif line.tag == "example-code":
139 if line.value:
140 code_lang = line.value
141 current = "code"
142 elif line.tag == "example-description":
143 if line.value:
144 description.append(line.value)
145 current = "description"
146 elif current == "brief":
147 brief.append(line.value)
148 elif current == "code":
149 code.append(line.value)
150 elif current == "description":
151 description.append(line.value)
153 return ExampleTag(
154 brief="\n".join(brief),
155 code="\n".join(code),
156 code_lang=code_lang,
157 description="\n".join(description),
158 )
161@dataclass
162class ExitTag(Tag):
163 """A tag representing an exit code."""
165 code: str
166 """The exit code."""
167 description: str
168 """The code description."""
170 @classmethod
171 def from_lines(cls, lines: Sequence[DocLine]) -> ExitTag: # noqa: D102
172 code, description = "", []
173 for line in lines:
174 if line.tag == "exit":
175 split = line.value.split(" ", 1)
176 if len(split) > 1:
177 code = split[0]
178 description.append(split[1])
179 else:
180 code = split[0]
181 else:
182 description.append(line.value)
183 return ExitTag(code=code, description="\n".join(description))
186@dataclass
187class FileTag(NameDescTag):
188 """A tag representing a file used by a script."""
191@dataclass
192class FunctionTag(Tag):
193 """A tag representing a shell function."""
195 prototype: str
196 """The function's prototype."""
197 brief: str
198 """The function's summary."""
199 description: str
200 """The function's description."""
201 arguments: Sequence[str]
202 """The function's arguments."""
203 preconditions: Sequence[str]
204 """The function's preconditions."""
205 return_codes: Sequence[str]
206 """The function's return codes."""
207 seealso: Sequence[str]
208 """The function's "see also" information."""
209 stderr: Sequence[str]
210 """The function's standard error."""
211 stdin: Sequence[str]
212 """The function's standard input."""
213 stdout: Sequence[str]
214 """The function's standard output."""
216 @classmethod
217 def from_lines(cls, lines: Sequence[DocLine]) -> FunctionTag: # noqa: D102
218 brief = ""
219 prototype = ""
220 description = []
221 arguments = []
222 return_codes = []
223 preconditions = []
224 seealso = []
225 stderr = []
226 stdin = []
227 stdout = []
228 for line in lines:
229 if line.tag == "function":
230 prototype = line.value
231 elif line.tag == "function-brief":
232 brief = line.value
233 elif line.tag == "function-description":
234 description.append(line.value)
235 elif line.tag == "function-argument":
236 arguments.append(line.value)
237 elif line.tag == "function-precondition":
238 preconditions.append(line.value)
239 elif line.tag == "function-return":
240 return_codes.append(line.value)
241 elif line.tag == "function-seealso":
242 seealso.append(line.value)
243 elif line.tag == "function-stderr":
244 stderr.append(line.value)
245 elif line.tag == "function-stdin":
246 stdin.append(line.value)
247 elif line.tag == "function-stdout":
248 stdout.append(line.value)
249 else:
250 description.append(line.value)
252 return FunctionTag(
253 prototype=prototype,
254 brief=brief,
255 description="\n".join(description),
256 arguments=arguments,
257 preconditions=preconditions,
258 return_codes=return_codes,
259 seealso=seealso,
260 stderr=stderr,
261 stdin=stdin,
262 stdout=stdout,
263 )
266@dataclass
267class HistoryTag(TextTag):
268 """A tag representing a script's history."""
271@dataclass
272class LicenseTag(TextTag):
273 """A tag representing a license."""
276@dataclass
277class NoteTag(TextTag):
278 """A tag representing a note."""
281@dataclass
282class OptionTag(Tag):
283 """A tag representing a command-line option."""
285 short: str
286 """The option short flag."""
287 long: str
288 """The option long flag."""
289 positional: str
290 """The option positional arguments."""
291 default: str
292 """The option default value."""
293 group: str
294 """The option group."""
295 description: str
296 """The option description."""
298 @cached_property
299 def signature(self) -> str:
300 """The signature of the option."""
301 sign = ""
302 if self.short: 302 ↛ 308line 302 didn't jump to line 308, because the condition on line 302 was never false
303 sign = self.short
304 if self.long: 304 ↛ 306line 304 didn't jump to line 306, because the condition on line 304 was never false
305 sign += ", "
306 elif self.positional:
307 sign += " "
308 if self.long: 308 ↛ 312line 308 didn't jump to line 312, because the condition on line 308 was never false
309 if not self.short: 309 ↛ 310line 309 didn't jump to line 310, because the condition on line 309 was never true
310 sign += " "
311 sign += self.long + " "
312 if self.positional: 312 ↛ 313line 312 didn't jump to line 313, because the condition on line 312 was never true
313 sign += self.positional
314 return sign
316 @classmethod
317 def from_lines(cls, lines: Sequence[DocLine]) -> OptionTag: # noqa: D102
318 short, long, positional, default, group = "", "", "", "", ""
319 description = []
320 for line in lines:
321 if line.tag == "option":
322 search = re.search(
323 r"^(?P<short>-\w)?(?:, )?(?P<long>--[\w-]+)? ?(?P<positional>.+)?",
324 line.value,
325 )
326 if search: 326 ↛ 329line 326 didn't jump to line 329, because the condition on line 326 was never false
327 short, long, positional = search.groups(default="")
328 else:
329 positional = line.value
330 elif line.tag == "option-default": 330 ↛ 331line 330 didn't jump to line 331, because the condition on line 330 was never true
331 default = line.value
332 elif line.tag == "option-group": 332 ↛ 333line 332 didn't jump to line 333, because the condition on line 332 was never true
333 group = line.value
334 else:
335 description.append(line.value)
336 return OptionTag(
337 short=short,
338 long=long,
339 positional=positional,
340 default=default,
341 group=group,
342 description="\n".join(description),
343 )
346@dataclass
347class SeealsoTag(TextTag):
348 """A tag representing "See Also" information."""
351@dataclass
352class StderrTag(TextTag):
353 """A tag representing the standard error of a script/function."""
356@dataclass
357class StdinTag(TextTag):
358 """A tag representing the standard input of a script/function."""
361@dataclass
362class StdoutTag(TextTag):
363 """A tag representing the standard output of a script/function."""
366@dataclass
367class UsageTag(Tag):
368 """A tag representing the command-line usage of a script."""
370 program: str
371 """The program name."""
372 command: str
373 """The command-line usage."""
375 @classmethod
376 def from_lines(cls, lines: Sequence[DocLine]) -> UsageTag: # noqa: D102
377 program, command = "", ""
378 split = lines[0].value.split(" ", 1)
379 if len(split) > 1: 379 ↛ 382line 379 didn't jump to line 382, because the condition on line 379 was never false
380 program, command = split
381 else:
382 program = split[0]
383 if len(lines) > 1: 383 ↛ 384line 383 didn't jump to line 384, because the condition on line 383 was never true
384 command = command + "\n" + "\n".join(line.value for line in lines[1:])
385 return UsageTag(program=program, command=command)
388@dataclass
389class VersionTag(TextTag):
390 """A tag representing a version."""
393TAGS: dict[str | None, type[Tag]] = {
394 None: TextTag,
395 "author": AuthorTag,
396 "bug": BugTag,
397 "brief": BriefTag,
398 "caveat": CaveatTag,
399 "copyright": CopyrightTag,
400 "date": DateTag,
401 "desc": DescTag,
402 "env": EnvTag,
403 "error": ErrorTag,
404 "example": ExampleTag,
405 "exit": ExitTag,
406 "file": FileTag,
407 "function": FunctionTag,
408 "history": HistoryTag,
409 "license": LicenseTag,
410 "note": NoteTag,
411 "option": OptionTag,
412 "seealso": SeealsoTag,
413 "stderr": StderrTag,
414 "stdin": StdinTag,
415 "stdout": StdoutTag,
416 "usage": UsageTag,
417 "version": VersionTag,
418}