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

139 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2025-07-29 05:16 +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 ] # pragma: no cover 

70 return '<unknown>' 

71 

72 

73def _format_annotation( # noqa: PLR0911 

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, str ): # Cannot do much with unresolved strings. 

82 # TODO? Parse string and try to resolve generic arguments, etc.... 

83 return annotation 

84 if isinstance( annotation, __.typx.ForwardRef ): # Extract string. 

85 return annotation.__forward_arg__ 

86 if isinstance( annotation, list ): 

87 seqstr = ', '.join( 

88 _format_annotation( element, context, style ) 

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

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

91 origin = __.typx.get_origin( annotation ) 

92 if origin is None: 

93 return _qualify_object_name( annotation, context ) 

94 arguments = __.typx.get_args( annotation ) 

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

96 return ' | '.join( 

97 _format_annotation( argument, context, style ) 

98 for argument in arguments ) 

99 oname = _qualify_object_name( origin, context ) 

100 if not arguments: return oname 

101 if origin is __.typx.Literal: 

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

103 else: 

104 argstr = ', '.join( 

105 _format_annotation( argument, context, style ) 

106 for argument in arguments ) 

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

108 

109 

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

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

112 if not description: return '' 

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

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

115 return '\n'.join( lines ) 

116 

117 

118def _produce_fragment_partial( 

119 possessor: __.Documentable, 

120 information: __.InformationBase, 

121 context: __.Context, 

122 style: Style, 

123) -> str: 

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

125 

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

127 ''' 

128 if isinstance( information, __.ArgumentInformation ): 

129 return ( 

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

131 if isinstance( information, __.AttributeInformation ): 

132 return ( 

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

134 if isinstance( information, __.ExceptionInformation ): 

135 return ( 

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

137 if isinstance( information, __.ReturnInformation ): 

138 return ( 

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

140 context.notifier( 

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

142 return '' 

143 

144 

145def _produce_argument_text( 

146 possessor: __.Documentable, 

147 information: __.ArgumentInformation, 

148 context: __.Context, 

149 style: Style, 

150) -> str: 

151 ''' Produces reStructuredText for argument information. 

152 

153 Formats function arguments in Sphinx-compatible reST format, 

154 including parameter descriptions and types. 

155 ''' 

156 annotation = information.annotation 

157 description = _format_description( information.description ) 

158 name = information.name 

159 lines: list[ str ] = [ ] 

160 lines.append( 

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

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

163 if annotation is not __.absent: 

164 typetext = _format_annotation( annotation, context, style ) 

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

166 return '\n'.join( lines ) 

167 

168 

169def _produce_attribute_text( 

170 possessor: __.Documentable, 

171 information: __.AttributeInformation, 

172 context: __.Context, 

173 style: Style, 

174) -> str: 

175 ''' Produces reStructuredText for attribute information. 

176 

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

178 Delegates to special handler for module attributes. 

179 ''' 

180 annotation = information.annotation 

181 match information.association: 

182 case __.AttributeAssociations.Module: 

183 return _produce_module_attribute_text( 

184 possessor, information, context, style ) 

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

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

187 description = _format_description( information.description ) 

188 name = information.name 

189 lines: list[ str ] = [ ] 

190 lines.append( 

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

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

193 if annotation is not __.absent: 

194 typetext = _format_annotation( annotation, context, style ) 

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

196 return '\n'.join( lines ) 

197 

198 

199def _produce_module_attribute_text( 

200 possessor: __.Documentable, 

201 information: __.AttributeInformation, 

202 context: __.Context, 

203 style: Style, 

204) -> str: 

205 ''' Produces reStructuredText for module attribute information. 

206 

207 Formats module attributes in Sphinx-compatible reST format, 

208 with special handling for TypeAlias attributes. 

209 ''' 

210 annotation = information.annotation 

211 description = information.description or '' 

212 name = information.name 

213 match information.default.mode: 

214 case __.ValuationModes.Accept: 

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

216 case __.ValuationModes.Suppress: 

217 value = __.absent 

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

219 value = __.absent 

220 lines: list[ str ] = [ ] 

221 if annotation is __.typx.TypeAlias: 

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

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

224 value_ar = __.reduce_annotation( 

225 value, context, 

226 __.AdjunctsData( ), 

227 __.AnnotationsCache( ) ) 

228 value_s = _format_annotation( value_ar, context, style ) 

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

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

231 else: 

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

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

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

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

236 if annotation is not __.absent: 

237 typetext = _format_annotation( annotation, context, style ) 

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

239 if value is not __.absent: 

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

241 return '\n'.join( lines ) 

242 

243 

244def _produce_exception_text( 

245 possessor: __.Documentable, 

246 information: __.ExceptionInformation, 

247 context: __.Context, 

248 style: Style, 

249) -> str: 

250 ''' Produces reStructuredText for exception information. 

251 

252 Formats exception classes and descriptions in Sphinx-compatible 

253 reST format. Handles union types of exceptions appropriately. 

254 ''' 

255 lines: list[ str ] = [ ] 

256 annotation = information.annotation 

257 description = _format_description( information.description ) 

258 origin = __.typx.get_origin( annotation ) 

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

260 annotations = __.typx.get_args( annotation ) 

261 else: annotations = ( annotation, ) 

262 for annotation_ in annotations: 

263 typetext = _format_annotation( annotation_, context, style ) 

264 lines.append( 

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

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

267 return '\n'.join( lines ) 

268 

269 

270def _produce_return_text( 

271 possessor: __.Documentable, 

272 information: __.ReturnInformation, 

273 context: __.Context, 

274 style: Style, 

275) -> str: 

276 ''' Produces reStructuredText for function return information. 

277 

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

279 Returns empty string for None returns. 

280 ''' 

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

282 description = _format_description( information.description ) 

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

284 lines: list[ str ] = [ ] 

285 if description: 

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

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

288 return '\n'.join( lines ) 

289 

290 

291def _qualify_object_name( # noqa: PLR0911 

292 objct: object, context: __.Context 

293) -> str: 

294 ''' Qualifies an object name for documentation. 

295 

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

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

298 ''' 

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

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

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

302 name = ( 

303 getattr( objct, '__name__', None ) 

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

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

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

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

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

309 return qname 

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

311 return qname 

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

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

314 return name # pragma: no cover 

315 

316 

317def _stylize_delimiter( 

318 style: Style, 

319 delimiters: str, 

320 content: str, 

321 prefix: str = '', 

322) -> str: 

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

324 

325 Formats delimiters around content based on the style setting, 

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

327 ''' 

328 ld = delimiters[ 0 ] 

329 rd = delimiters[ 1 ] 

330 match style: 

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

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