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

1"""Section module. 

2 

3This module contains the Section class. 

4""" 

5 

6from __future__ import annotations 

7 

8import re 

9from dataclasses import dataclass 

10from functools import cached_property 

11from typing import TYPE_CHECKING, Sequence 

12 

13if TYPE_CHECKING: 

14 from shellman.reader import DocLine 

15 

16 

17class Tag: 

18 """Base class for tags.""" 

19 

20 @classmethod 

21 def from_lines(cls, lines: Sequence[DocLine]) -> Tag: 

22 """Parse a sequence of lines into a tag instance. 

23 

24 Parameters: 

25 lines: The sequence of lines to parse. 

26 

27 Returns: 

28 A tag instance. 

29 """ 

30 raise NotImplementedError 

31 

32 

33@dataclass 

34class TextTag(Tag): 

35 """A simple tag holding text only.""" 

36 

37 text: str 

38 """The tag's text.""" 

39 

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)) 

43 

44 

45@dataclass 

46class NameDescTag(Tag): 

47 """A tag holding a name and a description.""" 

48 

49 name: str 

50 """The tag name.""" 

51 description: str 

52 """The tag description.""" 

53 

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)) 

68 

69 

70@dataclass 

71class AuthorTag(TextTag): 

72 """A tag representing an author.""" 

73 

74 

75@dataclass 

76class BugTag(TextTag): 

77 """A tag representing a bug note.""" 

78 

79 

80@dataclass 

81class BriefTag(TextTag): 

82 """A tag representing a summary.""" 

83 

84 

85@dataclass 

86class CaveatTag(TextTag): 

87 """A tag representing caveats.""" 

88 

89 

90@dataclass 

91class CopyrightTag(TextTag): 

92 """A tag representing copyright information.""" 

93 

94 

95@dataclass 

96class DateTag(TextTag): 

97 """A tag representing a date.""" 

98 

99 

100@dataclass 

101class DescTag(TextTag): 

102 """A tag representing a description.""" 

103 

104 

105@dataclass 

106class EnvTag(NameDescTag): 

107 """A tag representing an environment variable used by the script.""" 

108 

109 

110@dataclass 

111class ErrorTag(TextTag): 

112 """A tag representing a known error.""" 

113 

114 

115@dataclass 

116class ExampleTag(Tag): 

117 """A tag representing a code/shell example.""" 

118 

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.""" 

127 

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) 

152 

153 return ExampleTag( 

154 brief="\n".join(brief), 

155 code="\n".join(code), 

156 code_lang=code_lang, 

157 description="\n".join(description), 

158 ) 

159 

160 

161@dataclass 

162class ExitTag(Tag): 

163 """A tag representing an exit code.""" 

164 

165 code: str 

166 """The exit code.""" 

167 description: str 

168 """The code description.""" 

169 

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)) 

184 

185 

186@dataclass 

187class FileTag(NameDescTag): 

188 """A tag representing a file used by a script.""" 

189 

190 

191@dataclass 

192class FunctionTag(Tag): 

193 """A tag representing a shell function.""" 

194 

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.""" 

215 

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) 

251 

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 ) 

264 

265 

266@dataclass 

267class HistoryTag(TextTag): 

268 """A tag representing a script's history.""" 

269 

270 

271@dataclass 

272class LicenseTag(TextTag): 

273 """A tag representing a license.""" 

274 

275 

276@dataclass 

277class NoteTag(TextTag): 

278 """A tag representing a note.""" 

279 

280 

281@dataclass 

282class OptionTag(Tag): 

283 """A tag representing a command-line option.""" 

284 

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.""" 

297 

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 

315 

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 ) 

344 

345 

346@dataclass 

347class SeealsoTag(TextTag): 

348 """A tag representing "See Also" information.""" 

349 

350 

351@dataclass 

352class StderrTag(TextTag): 

353 """A tag representing the standard error of a script/function.""" 

354 

355 

356@dataclass 

357class StdinTag(TextTag): 

358 """A tag representing the standard input of a script/function.""" 

359 

360 

361@dataclass 

362class StdoutTag(TextTag): 

363 """A tag representing the standard output of a script/function.""" 

364 

365 

366@dataclass 

367class UsageTag(Tag): 

368 """A tag representing the command-line usage of a script.""" 

369 

370 program: str 

371 """The program name.""" 

372 command: str 

373 """The command-line usage.""" 

374 

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) 

386 

387 

388@dataclass 

389class VersionTag(TextTag): 

390 """A tag representing a version.""" 

391 

392 

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}