Coverage for sources/dynadoc/renderers/sphinxad.py: 100%

136 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-02 23:49 +0000

1# vim: set filetype=python fileencoding=utf-8: 

2# -*- coding: utf-8 -*- 

3 

4#============================================================================# 

5# # 

6# Licensed under the Apache License, Version 2.0 (the "License"); # 

7# you may not use this file except in compliance with the License. # 

8# You may obtain a copy of the License at # 

9# # 

10# http://www.apache.org/licenses/LICENSE-2.0 # 

11# # 

12# Unless required by applicable law or agreed to in writing, software # 

13# distributed under the License is distributed on an "AS IS" BASIS, # 

14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

15# See the License for the specific language governing permissions and # 

16# limitations under the License. # 

17# # 

18#============================================================================# 

19 

20 

21''' Sphinx Autodoc reStructuredText renderers. ''' 

22 

23 

24from . import __ 

25 

26 

27class Style( __.enum.Enum ): 

28 ''' Style of formatter output. ''' 

29 

30 Legible = __.enum.auto( ) 

31 Pep8 = __.enum.auto( ) 

32 

33 

34StyleArgument: __.typx.TypeAlias = __.typx.Annotated[ 

35 Style, 

36 __.Doc( 

37 ''' Output style for renderer. 

38 

39 Legible: Extra space padding inside of delimiters. 

40 Pep8: As the name implies. 

41 ''' ), 

42] 

43 

44 

45def produce_fragment( 

46 possessor: __.PossessorArgument, 

47 informations: __.InformationsArgument, 

48 context: __.ContextArgument, 

49 style: StyleArgument = Style.Legible, 

50) -> __.RendererReturnValue: 

51 ''' Produces a reStructuredText docstring fragment. 

52 

53 Combines information from object introspection into a formatted 

54 docstring fragment suitable for Sphinx Autodoc. 

55 ''' 

56 return '\n'.join( 

57 _produce_fragment_partial( possessor, information, context, style ) 

58 for information in informations ) 

59 

60 

61_qualident_regex = __.re.compile( r'''^([\w\.]+).*$''' ) 

62def _extract_qualident( name: str, context: __.Context ) -> str: 

63 ''' Extracts a qualified identifier from a string representation. 

64 

65 Used to extract the qualified name of an object from its string 

66 representation when direct name access is not available. 

67 ''' 

68 extract = _qualident_regex.match( name ) 

69 if extract is not None: return extract[ 1 ] 

70 return '<unknown>' 

71 

72 

73def _format_annotation( 

74 annotation: __.typx.Any, context: __.Context, style: Style 

75) -> str: 

76 ''' Formats a type annotation as a string for documentation. 

77 

78 Handles various annotation types including unions, generics, 

79 and literals. Formats according to the selected style. 

80 ''' 

81 if isinstance( annotation, list ): 

82 seqstr = ', '.join( 

83 _format_annotation( element, context, style ) 

84 for element in annotation ) # pyright: ignore[reportUnknownVariableType] 

85 return _stylize_delimiter( style, '[]', seqstr ) 

86 origin = __.typx.get_origin( annotation ) 

87 if origin is None: 

88 return _qualify_object_name( annotation, context ) 

89 arguments = __.typx.get_args( annotation ) 

90 if origin in ( __.types.UnionType, __.typx.Union ): 

91 return ' | '.join( 

92 _format_annotation( argument, context, style ) 

93 for argument in arguments ) 

94 oname = _qualify_object_name( origin, context ) 

95 if not arguments: return oname 

96 if origin is __.typx.Literal: 

97 argstr = ', '.join( repr( argument ) for argument in arguments ) 

98 else: 

99 argstr = ', '.join( 

100 _format_annotation( argument, context, style ) 

101 for argument in arguments ) 

102 return _stylize_delimiter( style, '[]', argstr, oname ) 

103 

104 

105def _format_description( description: __.typx.Optional[ str ] ) -> str: 

106 ''' Ensures that multiline descriptions render correctly. ''' 

107 if not description: return '' 

108 lines = description.split( '\n' ) 

109 lines[ 1 : ] = [ f" {line}" for line in lines[ 1 : ] ] 

110 return '\n'.join( lines ) 

111 

112 

113def _produce_fragment_partial( 

114 possessor: __.Documentable, 

115 information: __.InformationBase, 

116 context: __.Context, 

117 style: Style, 

118) -> str: 

119 ''' Produces a docstring fragment for a single piece of information. 

120 

121 Dispatches to appropriate producer based on the type of information. 

122 ''' 

123 if isinstance( information, __.ArgumentInformation ): 

124 return ( 

125 _produce_argument_text( possessor, information, context, style ) ) 

126 if isinstance( information, __.AttributeInformation ): 

127 return ( 

128 _produce_attribute_text( possessor, information, context, style ) ) 

129 if isinstance( information, __.ExceptionInformation ): 

130 return ( 

131 _produce_exception_text( possessor, information, context, style ) ) 

132 if isinstance( information, __.ReturnInformation ): 

133 return ( 

134 _produce_return_text( possessor, information, context, style ) ) 

135 context.notifier( 

136 'admonition', f"Unrecognized information: {information!r}" ) 

137 return '' 

138 

139 

140def _produce_argument_text( 

141 possessor: __.Documentable, 

142 information: __.ArgumentInformation, 

143 context: __.Context, 

144 style: Style, 

145) -> str: 

146 ''' Produces reStructuredText for argument information. 

147 

148 Formats function arguments in Sphinx-compatible reST format, 

149 including parameter descriptions and types. 

150 ''' 

151 annotation = information.annotation 

152 description = _format_description( information.description ) 

153 name = information.name 

154 lines: list[ str ] = [ ] 

155 lines.append( 

156 f":argument {name}: {description}" 

157 if description else f":argument {name}:" ) 

158 if annotation is not __.absent: 

159 typetext = _format_annotation( annotation, context, style ) 

160 lines.append( f":type {information.name}: {typetext}" ) 

161 return '\n'.join( lines ) 

162 

163 

164def _produce_attribute_text( 

165 possessor: __.Documentable, 

166 information: __.AttributeInformation, 

167 context: __.Context, 

168 style: Style, 

169) -> str: 

170 ''' Produces reStructuredText for attribute information. 

171 

172 Formats class and instance attributes in Sphinx-compatible reST format. 

173 Delegates to special handler for module attributes. 

174 ''' 

175 annotation = information.annotation 

176 match information.association: 

177 case __.AttributeAssociations.Module: 

178 return _produce_module_attribute_text( 

179 possessor, information, context, style ) 

180 case __.AttributeAssociations.Class: vlabel = 'cvar' 

181 case __.AttributeAssociations.Instance: vlabel = 'ivar' 

182 description = _format_description( information.description ) 

183 name = information.name 

184 lines: list[ str ] = [ ] 

185 lines.append( 

186 f":{vlabel} {name}: {description}" 

187 if description else f":{vlabel} {name}:" ) 

188 if annotation is not __.absent: 

189 typetext = _format_annotation( annotation, context, style ) 

190 lines.append( f":vartype {name}: {typetext}" ) 

191 return '\n'.join( lines ) 

192 

193 

194def _produce_module_attribute_text( 

195 possessor: __.Documentable, 

196 information: __.AttributeInformation, 

197 context: __.Context, 

198 style: Style, 

199) -> str: 

200 ''' Produces reStructuredText for module attribute information. 

201 

202 Formats module attributes in Sphinx-compatible reST format, 

203 with special handling for TypeAlias attributes. 

204 ''' 

205 annotation = information.annotation 

206 description = information.description or '' 

207 name = information.name 

208 match information.default.mode: 

209 case __.ValuationModes.Accept: 

210 value = getattr( possessor, name, __.absent ) 

211 case __.ValuationModes.Suppress: 

212 value = __.absent 

213 case __.ValuationModes.Surrogate: # pragma: no branch 

214 value = __.absent 

215 lines: list[ str ] = [ ] 

216 if annotation is __.typx.TypeAlias: 

217 lines.append( f".. py:type:: {name}" ) 

218 if value is not __.absent: # pragma: no branch 

219 value_ar = __.reduce_annotation( 

220 value, context, 

221 __.AdjunctsData( ), 

222 __.AnnotationsCache( ) ) 

223 value_s = _format_annotation( value_ar, context, style ) 

224 lines.append( f" :canonical: {value_s}" ) 

225 if description: lines.extend( [ '', f" {description}" ] ) 

226 else: 

227 # Note: No way to inject data docstring as of 2025-05-11. 

228 # Autodoc will read doc comments and pseudo-docstrings, 

229 # but we have no means of supplying description via a field. 

230 lines.append( f".. py:data:: {name}" ) 

231 if annotation is not __.absent: 

232 typetext = _format_annotation( annotation, context, style ) 

233 lines.append( f" :type: {typetext}" ) 

234 if value is not __.absent: 

235 lines.append( f" :value: {value!r}" ) 

236 return '\n'.join( lines ) 

237 

238 

239def _produce_exception_text( 

240 possessor: __.Documentable, 

241 information: __.ExceptionInformation, 

242 context: __.Context, 

243 style: Style, 

244) -> str: 

245 ''' Produces reStructuredText for exception information. 

246 

247 Formats exception classes and descriptions in Sphinx-compatible 

248 reST format. Handles union types of exceptions appropriately. 

249 ''' 

250 lines: list[ str ] = [ ] 

251 annotation = information.annotation 

252 description = _format_description( information.description ) 

253 origin = __.typx.get_origin( annotation ) 

254 if origin in ( __.types.UnionType, __.typx.Union ): 

255 annotations = __.typx.get_args( annotation ) 

256 else: annotations = ( annotation, ) 

257 for annotation_ in annotations: 

258 typetext = _format_annotation( annotation_, context, style ) 

259 lines.append( 

260 f":raises {typetext}: {description}" 

261 if description else f":raises {typetext}:" ) 

262 return '\n'.join( lines ) 

263 

264 

265def _produce_return_text( 

266 possessor: __.Documentable, 

267 information: __.ReturnInformation, 

268 context: __.Context, 

269 style: Style, 

270) -> str: 

271 ''' Produces reStructuredText for function return information. 

272 

273 Formats return type and description in Sphinx-compatible reST format. 

274 Returns empty string for None returns. 

275 ''' 

276 if information.annotation in ( None, __.types.NoneType ): return '' 

277 description = _format_description( information.description ) 

278 typetext = _format_annotation( information.annotation, context, style ) 

279 lines: list[ str ] = [ ] 

280 if description: 

281 lines.append( f":returns: {description}" ) 

282 lines.append( f":rtype: {typetext}" ) 

283 return '\n'.join( lines ) 

284 

285 

286def _qualify_object_name( # noqa: PLR0911 

287 objct: object, context: __.Context 

288) -> str: 

289 ''' Qualifies an object name for documentation. 

290 

291 Determines the appropriate fully-qualified name for an object, 

292 considering builtin types, module namespaces, and qualname attributes. 

293 ''' 

294 if objct is Ellipsis: return '...' 

295 if objct is __.types.NoneType: return 'None' 

296 if objct is __.types.ModuleType: return 'types.ModuleType' 

297 name = ( 

298 getattr( objct, '__name__', None ) 

299 or _extract_qualident( str( objct ), context ) ) 

300 if name == '<unknown>': return name 

301 qname = getattr( objct, '__qualname__', None ) or name 

302 name0 = qname.split( '.', maxsplit = 1 )[ 0 ] 

303 if name0 in vars( __.builtins ): # int, etc... 

304 return qname 

305 if context.invoker_globals and name0 in context.invoker_globals: 

306 return qname 

307 mname = getattr( objct, '__module__', None ) 

308 if mname: return f"{mname}.{qname}" 

309 return name # pragma: no cover 

310 

311 

312def _stylize_delimiter( 

313 style: Style, 

314 delimiters: str, 

315 content: str, 

316 prefix: str = '', 

317) -> str: 

318 ''' Stylizes delimiters according to the selected style. 

319 

320 Formats delimiters around content based on the style setting, 

321 with options for more legible spacing or compact PEP 8 formatting. 

322 ''' 

323 ld = delimiters[ 0 ] 

324 rd = delimiters[ 1 ] 

325 match style: 

326 case Style.Legible: return f"{prefix}{ld} {content} {rd}" 

327 case Style.Pep8: return f"{prefix}{ld}{content}{rd}"