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

130 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-05-25 22:29 +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 

34def produce_fragment( 

35 possessor: __.Documentable, 

36 informations: __.Informations, 

37 context: __.Context, 

38 style: Style = Style.Legible, 

39) -> str: 

40 ''' Produces a reStructuredText docstring fragment. 

41 

42 Combines information from object introspection into a formatted 

43 docstring fragment suitable for Sphinx Autodoc. 

44 ''' 

45 return '\n'.join( 

46 _produce_fragment_partial( possessor, information, context, style ) 

47 for information in informations ) 

48 

49 

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

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

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

53 

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

55 representation when direct name access is not available. 

56 ''' 

57 extract = _qualident_regex.match( name ) 

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

59 return '<unknown>' 

60 

61 

62def _format_annotation( 

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

64) -> str: 

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

66 

67 Handles various annotation types including unions, generics, 

68 and literals. Formats according to the selected style. 

69 ''' 

70 if isinstance( annotation, list ): 

71 seqstr = ', '.join( 

72 _format_annotation( element, context, style ) 

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

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

75 origin = __.typx.get_origin( annotation ) 

76 if origin is None: 

77 return _qualify_object_name( annotation, context ) 

78 arguments = __.typx.get_args( annotation ) 

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

80 return ' | '.join( 

81 _format_annotation( argument, context, style ) 

82 for argument in arguments ) 

83 oname = _qualify_object_name( origin, context ) 

84 if not arguments: return oname 

85 if origin is __.typx.Literal: 

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

87 else: 

88 argstr = ', '.join( 

89 _format_annotation( argument, context, style ) 

90 for argument in arguments ) 

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

92 

93 

94def _produce_fragment_partial( 

95 possessor: __.Documentable, 

96 information: __.InformationBase, 

97 context: __.Context, 

98 style: Style, 

99) -> str: 

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

101 

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

103 ''' 

104 if isinstance( information, __.ArgumentInformation ): 

105 return ( 

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

107 if isinstance( information, __.AttributeInformation ): 

108 return ( 

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

110 if isinstance( information, __.ExceptionInformation ): 

111 return ( 

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

113 if isinstance( information, __.ReturnInformation ): 

114 return ( 

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

116 context.notifier( 

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

118 return '' 

119 

120 

121def _produce_argument_text( 

122 possessor: __.Documentable, 

123 information: __.ArgumentInformation, 

124 context: __.Context, 

125 style: Style, 

126) -> str: 

127 ''' Produces reStructuredText for argument information. 

128 

129 Formats function arguments in Sphinx-compatible reST format, 

130 including parameter descriptions and types. 

131 ''' 

132 annotation = information.annotation 

133 description = information.description 

134 name = information.name 

135 lines: list[ str ] = [ ] 

136 lines.append( 

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

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

139 if annotation is not __.absent: 

140 typetext = _format_annotation( annotation, context, style ) 

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

142 return '\n'.join( lines ) 

143 

144 

145def _produce_attribute_text( 

146 possessor: __.Documentable, 

147 information: __.AttributeInformation, 

148 context: __.Context, 

149 style: Style, 

150) -> str: 

151 ''' Produces reStructuredText for attribute information. 

152 

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

154 Delegates to special handler for module attributes. 

155 ''' 

156 annotation = information.annotation 

157 match information.association: 

158 case __.AttributeAssociations.Module: 

159 return _produce_module_attribute_text( 

160 possessor, information, context, style ) 

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

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

163 description = information.description 

164 name = information.name 

165 lines: list[ str ] = [ ] 

166 lines.append( 

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

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

169 if annotation is not __.absent: 

170 typetext = _format_annotation( annotation, context, style ) 

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

172 return '\n'.join( lines ) 

173 

174 

175def _produce_module_attribute_text( 

176 possessor: __.Documentable, 

177 information: __.AttributeInformation, 

178 context: __.Context, 

179 style: Style, 

180) -> str: 

181 ''' Produces reStructuredText for module attribute information. 

182 

183 Formats module attributes in Sphinx-compatible reST format, 

184 with special handling for TypeAlias attributes. 

185 ''' 

186 annotation = information.annotation 

187 description = information.description or '' 

188 name = information.name 

189 match information.default.mode: 

190 case __.ValuationModes.Accept: 

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

192 case __.ValuationModes.Suppress: 

193 value = __.absent 

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

195 value = __.absent 

196 lines: list[ str ] = [ ] 

197 if annotation is __.typx.TypeAlias: 

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

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

200 value_ar = __.reduce_annotation( 

201 value, context, 

202 __.AdjunctsData( ), 

203 __.AnnotationsCache( ) ) 

204 value_s = _format_annotation( value_ar, context, style ) 

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

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

207 else: 

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

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

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

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

212 if annotation is not __.absent: 

213 typetext = _format_annotation( annotation, context, style ) 

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

215 if value is not __.absent: 

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

217 return '\n'.join( lines ) 

218 

219 

220def _produce_exception_text( 

221 possessor: __.Documentable, 

222 information: __.ExceptionInformation, 

223 context: __.Context, 

224 style: Style, 

225) -> str: 

226 ''' Produces reStructuredText for exception information. 

227 

228 Formats exception classes and descriptions in Sphinx-compatible 

229 reST format. Handles union types of exceptions appropriately. 

230 ''' 

231 lines: list[ str ] = [ ] 

232 annotation = information.annotation 

233 description = information.description 

234 origin = __.typx.get_origin( annotation ) 

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

236 annotations = __.typx.get_args( annotation ) 

237 else: annotations = ( annotation, ) 

238 for annotation_ in annotations: 

239 typetext = _format_annotation( annotation_, context, style ) 

240 lines.append( 

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

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

243 return '\n'.join( lines ) 

244 

245 

246def _produce_return_text( 

247 possessor: __.Documentable, 

248 information: __.ReturnInformation, 

249 context: __.Context, 

250 style: Style, 

251) -> str: 

252 ''' Produces reStructuredText for function return information. 

253 

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

255 Returns empty string for None returns. 

256 ''' 

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

258 description = information.description 

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

260 lines: list[ str ] = [ ] 

261 if description: 

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

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

264 return '\n'.join( lines ) 

265 

266 

267def _qualify_object_name( # noqa: PLR0911 

268 objct: object, context: __.Context 

269) -> str: 

270 ''' Qualifies an object name for documentation. 

271 

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

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

274 ''' 

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

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

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

278 name = ( 

279 getattr( objct, '__name__', None ) 

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

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

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

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

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

285 return qname 

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

287 return qname 

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

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

290 return name # pragma: no cover 

291 

292 

293def _stylize_delimiter( 

294 style: Style, 

295 delimiters: str, 

296 content: str, 

297 prefix: str = '', 

298) -> str: 

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

300 

301 Formats delimiters around content based on the style setting, 

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

303 ''' 

304 ld = delimiters[ 0 ] 

305 rd = delimiters[ 1 ] 

306 match style: 

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

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