Coverage for sources/dynadoc/assembly.py: 100%

151 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''' Docstring assembly and decoration. ''' 

22# TODO? with_docstring_defer 

23# Registers with_docstring partial function in registry. 

24# Registry can be executed after modules are loaded and all string 

25# annotations should be resolvable. 

26 

27 

28from . import __ 

29from . import factories as _factories 

30from . import renderers as _renderers 

31from . import xtnsapi as _xtnsapi 

32 

33 

34_visitees: __.weakref.WeakSet[ _xtnsapi.Documentable ] = __.weakref.WeakSet( ) 

35 

36 

37context_default: __.typx.Annotated[ 

38 _xtnsapi.Context, 

39 _xtnsapi.Doc( 

40 ''' Default context for introspection and rendering. ''' ), 

41 _xtnsapi.Fname( 'context' ), 

42 _xtnsapi.Default( mode = _xtnsapi.ValuationModes.Suppress ), 

43] = _factories.produce_context( ) 

44introspection_default: __.typx.Annotated[ 

45 _xtnsapi.IntrospectionControl, 

46 _xtnsapi.Doc( ''' Default introspection control. ''' ), 

47 _xtnsapi.Fname( 'introspection' ), 

48 _xtnsapi.Default( mode = _xtnsapi.ValuationModes.Suppress ), 

49] = _xtnsapi.IntrospectionControl( ) 

50renderer_default: __.typx.Annotated[ 

51 _xtnsapi.Renderer, 

52 _xtnsapi.Doc( ''' Default renderer for docstring fragments. ''' ), 

53 _xtnsapi.Fname( 'renderer' ), 

54 _xtnsapi.Default( mode = _xtnsapi.ValuationModes.Suppress ), 

55] = _renderers.sphinxad.produce_fragment 

56 

57 

58def assign_module_docstring( # noqa: PLR0913 

59 module: _xtnsapi.Module, /, 

60 *fragments: _xtnsapi.FragmentsArgumentMultivalent, 

61 context: _xtnsapi.ContextArgument = context_default, 

62 introspection: _xtnsapi.IntrospectionArgument = introspection_default, 

63 preserve: _xtnsapi.PreserveArgument = True, 

64 renderer: _xtnsapi.RendererArgument = renderer_default, 

65 table: _xtnsapi.FragmentsTableArgument = __.dictproxy_empty, 

66) -> None: 

67 ''' Assembles docstring from fragments and assigns it to module. ''' 

68 if isinstance( module, str ): 

69 module = __.sys.modules[ module ] 

70 _decorate( 

71 module, 

72 context = context, 

73 introspection = introspection, 

74 preserve = preserve, 

75 renderer = renderer, 

76 fragments = fragments, 

77 table = table ) 

78 

79 

80def with_docstring( 

81 *fragments: _xtnsapi.FragmentsArgumentMultivalent, 

82 context: _xtnsapi.ContextArgument = context_default, 

83 introspection: _xtnsapi.IntrospectionArgument = introspection_default, 

84 preserve: _xtnsapi.PreserveArgument = True, 

85 renderer: _xtnsapi.RendererArgument = renderer_default, 

86 table: _xtnsapi.FragmentsTableArgument = __.dictproxy_empty, 

87) -> _xtnsapi.Decorator[ _xtnsapi.D ]: 

88 ''' Assembles docstring from fragments and decorates object with it. ''' 

89 def decorate( objct: _xtnsapi.D ) -> _xtnsapi.D: 

90 _decorate( 

91 objct, 

92 context = context, 

93 introspection = introspection, 

94 preserve = preserve, 

95 renderer = renderer, 

96 fragments = fragments, 

97 table = table ) 

98 return objct 

99 

100 return decorate 

101 

102 

103def _check_module_recursion( 

104 objct: object, /, 

105 introspection: _xtnsapi.IntrospectionControl, 

106 mname: str 

107) -> __.typx.TypeIs[ __.types.ModuleType ]: 

108 ''' Checks if a module should be recursively documented. 

109 

110 Returns True if the object is a module that should be recursively 

111 documented based on the introspection control and module name prefix. 

112 ''' 

113 if ( introspection.targets & _xtnsapi.IntrospectionTargets.Module 

114 and __.inspect.ismodule( objct ) 

115 ): return objct.__name__.startswith( f"{mname}." ) 

116 return False 

117 

118 

119def _collect_fragments( 

120 objct: _xtnsapi.Documentable, /, context: _xtnsapi.Context, fqname: str 

121) -> _xtnsapi.Fragments: 

122 ''' Collects docstring fragments from an object. 

123 

124 Retrieves the sequence of fragments stored on the object using the 

125 fragments_name from the context. Validates that the fragments are 

126 of the expected types. 

127 ''' 

128 fragments: _xtnsapi.Fragments = ( 

129 getattr( objct, context.fragments_name, ( ) ) ) 

130 if ( isinstance( fragments, ( bytes, str ) ) 

131 or not isinstance( fragments, __.cabc.Sequence ) 

132 ): 

133 emessage = f"Invalid fragments sequence on {fqname}: {fragments!r}" 

134 context.notifier( 'error', emessage ) 

135 fragments = ( ) 

136 for fragment in fragments: 

137 if not isinstance( fragment, ( str, _xtnsapi.Doc ) ): 

138 emessage = f"Invalid fragment on {fqname}: {fragment!r}" 

139 context.notifier( 'error', emessage ) 

140 return fragments 

141 

142 

143def _consider_class_attribute( # noqa: C901,PLR0913 

144 attribute: object, /, 

145 context: _xtnsapi.Context, 

146 introspection: _xtnsapi.IntrospectionControl, 

147 pmname: str, pqname: str, aname: str, 

148) -> tuple[ __.typx.Optional[ _xtnsapi.Documentable ], bool ]: 

149 ''' Considers whether a class attribute should be documented. 

150 

151 Examines a class attribute to determine if it should be included 

152 in the documentation process based on introspection targets and 

153 class ownership. Returns the documentable attribute and a flag 

154 indicating whether the surface attribute needs updating. 

155 ''' 

156 if _check_module_recursion( attribute, introspection, pmname ): 

157 return attribute, False 

158 attribute_ = None 

159 update_surface = False 

160 if ( not attribute_ 

161 and introspection.targets & _xtnsapi.IntrospectionTargets.Class 

162 and __.inspect.isclass( attribute ) 

163 ): attribute_ = attribute 

164 if ( not attribute_ 

165 and introspection.targets & _xtnsapi.IntrospectionTargets.Descriptor 

166 ): 

167 if isinstance( attribute, property ) and attribute.fget: 

168 # Examine docstring and signature of getter method on property. 

169 attribute_ = attribute.fget 

170 update_surface = True 

171 # TODO: Apply custom processors from context. 

172 elif __.inspect.isdatadescriptor( attribute ): 

173 # Ignore descriptors which we do not know how to handle. 

174 return None, False 

175 if ( not attribute_ 

176 and introspection.targets & _xtnsapi.IntrospectionTargets.Function 

177 ): 

178 if __.inspect.ismethod( attribute ): 

179 # Methods proxy docstrings from their core functions. 

180 attribute_ = attribute.__func__ 

181 elif __.inspect.isfunction( attribute ) and aname != '<lambda>': 

182 attribute_ = attribute 

183 if attribute_: 

184 mname = getattr( attribute_, '__module__', None ) 

185 if not mname or mname != pmname: 

186 attribute_ = None 

187 if attribute_: 

188 qname = getattr( attribute_, '__qualname__', None ) 

189 if not qname or not qname.startswith( f"{pqname}." ): 

190 attribute_ = None 

191 return attribute_, update_surface 

192 

193 

194def _consider_module_attribute( 

195 attribute: object, /, 

196 context: _xtnsapi.Context, 

197 introspection: _xtnsapi.IntrospectionControl, 

198 pmname: str, aname: str, 

199) -> tuple[ __.typx.Optional[ _xtnsapi.Documentable ], bool ]: 

200 ''' Considers whether a module attribute should be documented. 

201 

202 Examines a module attribute to determine if it should be included 

203 in the documentation process based on introspection targets and 

204 module ownership. Returns the documentable attribute and a flag 

205 indicating whether the surface attribute needs updating. 

206 ''' 

207 if _check_module_recursion( attribute, introspection, pmname ): 

208 return attribute, False 

209 attribute_ = None 

210 update_surface = False 

211 if ( not attribute_ 

212 and introspection.targets & _xtnsapi.IntrospectionTargets.Class 

213 and __.inspect.isclass( attribute ) 

214 ): attribute_ = attribute 

215 if ( not attribute_ 

216 and introspection.targets & _xtnsapi.IntrospectionTargets.Function 

217 and __.inspect.isfunction( attribute ) and aname != '<lambda>' 

218 ): attribute_ = attribute 

219 if attribute_: 

220 mname = getattr( attribute_, '__module__', None ) 

221 if not mname or mname != pmname: 

222 attribute_ = None 

223 return attribute_, update_surface 

224 

225 

226def _decorate( # noqa: PLR0913 

227 objct: _xtnsapi.Documentable, /, 

228 context: _xtnsapi.Context, 

229 introspection: _xtnsapi.IntrospectionControl, 

230 preserve: bool, 

231 renderer: _xtnsapi.Renderer, 

232 fragments: _xtnsapi.Fragments, 

233 table: _xtnsapi.FragmentsTable, 

234) -> None: 

235 ''' Decorates an object with assembled docstring. 

236 

237 Handles core docstring decoration and potentially recursive decoration 

238 of the object's attributes based on introspection control settings. 

239 Prevents multiple decoration of the same object. 

240 ''' 

241 if objct in _visitees: return # Prevent multiple decoration. 

242 _visitees.add( objct ) 

243 if introspection.targets: 

244 if __.inspect.isclass( objct ): 

245 _decorate_class_attributes( 

246 objct, 

247 context = context, 

248 introspection = introspection, 

249 preserve = preserve, 

250 renderer = renderer, 

251 table = table ) 

252 elif __.inspect.ismodule( objct ): 

253 _decorate_module_attributes( 

254 objct, 

255 context = context, 

256 introspection = introspection, 

257 preserve = preserve, 

258 renderer = renderer, 

259 table = table ) 

260 if __.inspect.ismodule( objct ): fqname = objct.__name__ 

261 else: fqname = f"{objct.__module__}.{objct.__qualname__}" 

262 fragments_ = _collect_fragments( objct, context, fqname ) 

263 if not fragments_: fragments_ = fragments 

264 _decorate_core( 

265 objct, 

266 context = context, 

267 introspection = introspection, 

268 preserve = preserve, 

269 renderer = renderer, 

270 fragments = fragments_, 

271 table = table ) 

272 

273 

274def _decorate_core( # noqa: PLR0913 

275 objct: _xtnsapi.Documentable, /, 

276 context: _xtnsapi.Context, 

277 introspection: _xtnsapi.IntrospectionControl, 

278 preserve: bool, 

279 renderer: _xtnsapi.Renderer, 

280 fragments: _xtnsapi.Fragments, 

281 table: _xtnsapi.FragmentsTable, 

282) -> None: 

283 ''' Core implementation of docstring decoration. 

284 

285 Assembles a docstring from fragments, existing docstring (if 

286 preserved), and introspection results. Assigns the assembled docstring 

287 to the object. 

288 ''' 

289 fragments_: list[ str ] = [ ] 

290 if preserve and ( fragment := getattr( objct, '__doc__', None ) ): 

291 fragments_.append( context.fragment_rectifier( 

292 fragment, source = _xtnsapi.FragmentSources.Docstring ) ) 

293 fragments_.extend( 

294 _process_fragments_argument( context, fragments, table ) ) 

295 if introspection.enable: 

296 cache = _xtnsapi.AnnotationsCache( ) 

297 informations = ( 

298 _xtnsapi.introspect( 

299 objct, 

300 context = context, introspection = introspection, 

301 cache = cache, table = table ) ) 

302 fragments_.append( context.fragment_rectifier( 

303 renderer( objct, informations, context = context ), 

304 source = _xtnsapi.FragmentSources.Renderer ) ) 

305 docstring = '\n\n'.join( 

306 fragment for fragment in filter( None, fragments_ ) ).rstrip( ) 

307 objct.__doc__ = docstring if docstring else None 

308 

309 

310def _decorate_class_attributes( # noqa: PLR0913 

311 objct: type, /, 

312 context: _xtnsapi.Context, 

313 introspection: _xtnsapi.IntrospectionControl, 

314 preserve: bool, 

315 renderer: _xtnsapi.Renderer, 

316 table: _xtnsapi.FragmentsTable, 

317) -> None: 

318 ''' Decorates attributes of a class with assembled docstrings. 

319 

320 Iterates through relevant class attributes, collects fragments, 

321 and applies appropriate docstring decoration to each attribute. 

322 ''' 

323 pmname = objct.__module__ 

324 pqname = objct.__qualname__ 

325 for aname, attribute, surface_attribute in ( 

326 _survey_class_attributes( objct, context, introspection ) 

327 ): 

328 fqname = f"{pmname}.{pqname}.{aname}" 

329 introspection_ = _limit_introspection( 

330 attribute, context, introspection, fqname ) 

331 introspection_ = introspection_.evaluate_limits_for( attribute ) 

332 if not introspection_.enable: continue 

333 _decorate( 

334 attribute, 

335 context = context, 

336 introspection = introspection_, 

337 preserve = preserve, 

338 renderer = renderer, 

339 fragments = ( ), 

340 table = table ) 

341 if attribute is not surface_attribute: 

342 surface_attribute.__doc__ = attribute.__doc__ 

343 

344 

345def _decorate_module_attributes( # noqa: PLR0913 

346 module: __.types.ModuleType, /, 

347 context: _xtnsapi.Context, 

348 introspection: _xtnsapi.IntrospectionControl, 

349 preserve: bool, 

350 renderer: _xtnsapi.Renderer, 

351 table: _xtnsapi.FragmentsTable, 

352) -> None: 

353 ''' Decorates attributes of a module with assembled docstrings. 

354 

355 Iterates through relevant module attributes, collects fragments, 

356 and applies appropriate docstring decoration to each attribute. 

357 ''' 

358 pmname = module.__name__ 

359 for aname, attribute, surface_attribute in ( 

360 _survey_module_attributes( module, context, introspection ) 

361 ): 

362 fqname = f"{pmname}.{aname}" 

363 introspection_ = _limit_introspection( 

364 attribute, context, introspection, fqname ) 

365 introspection_ = introspection_.evaluate_limits_for( attribute ) 

366 if not introspection_.enable: continue 

367 _decorate( 

368 attribute, 

369 context = context, 

370 introspection = introspection_, 

371 preserve = preserve, 

372 renderer = renderer, 

373 fragments = ( ), 

374 table = table ) 

375 if attribute is not surface_attribute: # pragma: no cover 

376 surface_attribute.__doc__ = attribute.__doc__ 

377 

378 

379def _limit_introspection( 

380 objct: _xtnsapi.Documentable, /, 

381 context: _xtnsapi.Context, 

382 introspection: _xtnsapi.IntrospectionControl, 

383 fqname: str, 

384) -> _xtnsapi.IntrospectionControl: 

385 ''' Limits introspection based on object-specific constraints. 

386 

387 Returns a new IntrospectionControl that respects the limits 

388 specified by the object being documented. This allows objects 

389 to control how deeply they are introspected. 

390 ''' 

391 limit: _xtnsapi.IntrospectionLimit = ( 

392 getattr( 

393 objct, 

394 context.introspection_limit_name, 

395 _xtnsapi.IntrospectionLimit( ) ) ) 

396 if not isinstance( limit, _xtnsapi.IntrospectionLimit ): 

397 emessage = f"Invalid introspection limit on {fqname}: {limit!r}" 

398 context.notifier( 'error', emessage ) 

399 return introspection 

400 return introspection.with_limit( limit ) 

401 

402 

403def _process_fragments_argument( 

404 context: _xtnsapi.Context, 

405 fragments: _xtnsapi.Fragments, 

406 table: _xtnsapi.FragmentsTable, 

407) -> __.cabc.Sequence[ str ]: 

408 ''' Processes fragments argument into a sequence of string fragments. 

409 

410 Converts Doc objects to their documentation strings and resolves 

411 string references to the fragments table. Returns a sequence of 

412 rectified fragment strings. 

413 ''' 

414 fragments_: list[ str ] = [ ] 

415 for fragment in fragments: 

416 if isinstance( fragment, _xtnsapi.Doc ): 

417 fragment_r = fragment.documentation 

418 elif isinstance( fragment, str ): 

419 if fragment not in table: 

420 emessage = f"Fragment '{fragment}' not in provided table." 

421 context.notifier( 'error', emessage ) 

422 continue 

423 fragment_r = table[ fragment ] 

424 else: 

425 emessage = f"Fragment {fragment!r} is invalid. Must be Doc or str." 

426 context.notifier( 'error', emessage ) 

427 continue 

428 fragments_.append( context.fragment_rectifier( 

429 fragment_r, source = _xtnsapi.FragmentSources.Argument ) ) 

430 return fragments_ 

431 

432 

433def _survey_class_attributes( 

434 possessor: type, /, 

435 context: _xtnsapi.Context, 

436 introspection: _xtnsapi.IntrospectionControl, 

437) -> __.cabc.Iterator[ tuple[ str, _xtnsapi.Documentable, object ] ]: 

438 ''' Surveys attributes of a class for documentation. 

439 

440 Yields a sequence of (name, attribute, surface_attribute) tuples 

441 representing documentable attributes of the class. The surface 

442 attribute may differ from attribute in cases like properties where the 

443 attribute's getter method holds the documentation. 

444 ''' 

445 pmname = possessor.__module__ 

446 pqname = possessor.__qualname__ 

447 for aname, attribute in __.inspect.getmembers( possessor ): 

448 attribute_, update_surface = ( 

449 _consider_class_attribute( 

450 attribute, context, introspection, pmname, pqname, aname ) ) 

451 if attribute_ is None: continue 

452 if update_surface: 

453 yield aname, attribute_, attribute 

454 continue 

455 yield aname, attribute_, attribute_ 

456 

457 

458def _survey_module_attributes( 

459 possessor: __.types.ModuleType, /, 

460 context: _xtnsapi.Context, 

461 introspection: _xtnsapi.IntrospectionControl, 

462) -> __.cabc.Iterator[ tuple[ str, _xtnsapi.Documentable, object ] ]: 

463 ''' Surveys attributes of a module for documentation. 

464 

465 Yields a sequence of (name, attribute, surface_attribute) tuples 

466 representing documentable attributes of the module. The surface 

467 attribute may differ from attribute in cases where the actual 

468 documented object is not directly accessible. 

469 ''' 

470 pmname = possessor.__name__ 

471 for aname, attribute in __.inspect.getmembers( possessor ): 

472 attribute_, update_surface = ( 

473 _consider_module_attribute( 

474 attribute, context, introspection, pmname, aname ) ) 

475 if attribute_ is None: continue 

476 if update_surface: # pragma: no cover 

477 yield aname, attribute_, attribute 

478 continue 

479 yield aname, attribute_, attribute_