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

154 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''' 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 exclude( objct: _xtnsapi.D ) -> _xtnsapi.D: 

81 ''' Excludes object from docstring updates. ''' 

82 _visitees.add( objct ) 

83 return objct 

84 

85 

86def with_docstring( 

87 *fragments: _xtnsapi.FragmentsArgumentMultivalent, 

88 context: _xtnsapi.ContextArgument = context_default, 

89 introspection: _xtnsapi.IntrospectionArgument = introspection_default, 

90 preserve: _xtnsapi.PreserveArgument = True, 

91 renderer: _xtnsapi.RendererArgument = renderer_default, 

92 table: _xtnsapi.FragmentsTableArgument = __.dictproxy_empty, 

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

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

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

96 _decorate( 

97 objct, 

98 context = context, 

99 introspection = introspection, 

100 preserve = preserve, 

101 renderer = renderer, 

102 fragments = fragments, 

103 table = table ) 

104 return objct 

105 

106 return decorate 

107 

108 

109def _check_module_recursion( 

110 objct: object, /, 

111 introspection: _xtnsapi.IntrospectionControl, 

112 mname: str 

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

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

115 

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

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

118 ''' 

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

120 and __.inspect.ismodule( objct ) 

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

122 return False 

123 

124 

125def _collect_fragments( 

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

127) -> _xtnsapi.Fragments: 

128 ''' Collects docstring fragments from an object. 

129 

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

131 fragments_name from the context. Validates that the fragments are 

132 of the expected types. 

133 ''' 

134 fragments: _xtnsapi.Fragments = ( 

135 # Fragments can come from base class or metaclass. 

136 # We only care about fragments on class itself. 

137 objct.__dict__.get( context.fragments_name, ( ) ) 

138 if __.inspect.isclass( objct ) 

139 else getattr( objct, context.fragments_name, ( ) ) ) 

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

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

142 ): 

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

144 context.notifier( 'error', emessage ) 

145 fragments = ( ) 

146 for fragment in fragments: 

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

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

149 context.notifier( 'error', emessage ) 

150 return fragments 

151 

152 

153def _consider_class_attribute( # noqa: C901,PLR0913 

154 attribute: object, /, 

155 context: _xtnsapi.Context, 

156 introspection: _xtnsapi.IntrospectionControl, 

157 pmname: str, pqname: str, aname: str, 

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

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

160 

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

162 in the documentation process based on introspection targets and 

163 class ownership. Returns the documentable attribute and a flag 

164 indicating whether the surface attribute needs updating. 

165 ''' 

166 if _check_module_recursion( attribute, introspection, pmname ): 

167 return attribute, False 

168 attribute_ = None 

169 update_surface = False 

170 if ( not attribute_ 

171 and introspection.targets & _xtnsapi.IntrospectionTargets.Class 

172 and __.inspect.isclass( attribute ) 

173 ): attribute_ = attribute 

174 if ( not attribute_ 

175 and introspection.targets & _xtnsapi.IntrospectionTargets.Descriptor 

176 ): 

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

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

179 attribute_ = attribute.fget 

180 update_surface = True 

181 # TODO: Apply custom processors from context. 

182 elif __.inspect.isdatadescriptor( attribute ): 

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

184 return None, False 

185 if ( not attribute_ 

186 and introspection.targets & _xtnsapi.IntrospectionTargets.Function 

187 ): 

188 if __.inspect.ismethod( attribute ): 

189 # Methods proxy docstrings from their core functions. 

190 attribute_ = attribute.__func__ 

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

192 attribute_ = attribute 

193 if attribute_: 

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

195 if not mname or mname != pmname: 

196 attribute_ = None 

197 if attribute_: 

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

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

200 attribute_ = None 

201 return attribute_, update_surface 

202 

203 

204def _consider_module_attribute( 

205 attribute: object, /, 

206 context: _xtnsapi.Context, 

207 introspection: _xtnsapi.IntrospectionControl, 

208 pmname: str, aname: str, 

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

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

211 

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

213 in the documentation process based on introspection targets and 

214 module ownership. Returns the documentable attribute and a flag 

215 indicating whether the surface attribute needs updating. 

216 ''' 

217 if _check_module_recursion( attribute, introspection, pmname ): 

218 return attribute, False 

219 attribute_ = None 

220 update_surface = False 

221 if ( not attribute_ 

222 and introspection.targets & _xtnsapi.IntrospectionTargets.Class 

223 and __.inspect.isclass( attribute ) 

224 ): attribute_ = attribute 

225 if ( not attribute_ 

226 and introspection.targets & _xtnsapi.IntrospectionTargets.Function 

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

228 ): attribute_ = attribute 

229 if attribute_: 

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

231 if not mname or mname != pmname: 

232 attribute_ = None 

233 return attribute_, update_surface 

234 

235 

236def _decorate( # noqa: PLR0913 

237 objct: _xtnsapi.Documentable, /, 

238 context: _xtnsapi.Context, 

239 introspection: _xtnsapi.IntrospectionControl, 

240 preserve: bool, 

241 renderer: _xtnsapi.Renderer, 

242 fragments: _xtnsapi.Fragments, 

243 table: _xtnsapi.FragmentsTable, 

244) -> None: 

245 ''' Decorates an object with assembled docstring. 

246 

247 Handles core docstring decoration and potentially recursive decoration 

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

249 Prevents multiple decoration of the same object. 

250 ''' 

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

252 _visitees.add( objct ) 

253 if introspection.targets: 

254 if __.inspect.isclass( objct ): 

255 _decorate_class_attributes( 

256 objct, 

257 context = context, 

258 introspection = introspection, 

259 preserve = preserve, 

260 renderer = renderer, 

261 table = table ) 

262 elif __.inspect.ismodule( objct ): 

263 _decorate_module_attributes( 

264 objct, 

265 context = context, 

266 introspection = introspection, 

267 preserve = preserve, 

268 renderer = renderer, 

269 table = table ) 

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

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

272 fragments_ = _collect_fragments( objct, context, fqname ) 

273 if not fragments_: fragments_ = fragments 

274 _decorate_core( 

275 objct, 

276 context = context, 

277 introspection = introspection, 

278 preserve = preserve, 

279 renderer = renderer, 

280 fragments = fragments_, 

281 table = table ) 

282 

283 

284def _decorate_core( # noqa: PLR0913 

285 objct: _xtnsapi.Documentable, /, 

286 context: _xtnsapi.Context, 

287 introspection: _xtnsapi.IntrospectionControl, 

288 preserve: bool, 

289 renderer: _xtnsapi.Renderer, 

290 fragments: _xtnsapi.Fragments, 

291 table: _xtnsapi.FragmentsTable, 

292) -> None: 

293 ''' Core implementation of docstring decoration. 

294 

295 Assembles a docstring from fragments, existing docstring (if 

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

297 to the object. 

298 ''' 

299 fragments_: list[ str ] = [ ] 

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

301 fragments_.append( context.fragment_rectifier( 

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

303 fragments_.extend( 

304 _process_fragments_argument( context, fragments, table ) ) 

305 if introspection.enable: 

306 cache = _xtnsapi.AnnotationsCache( ) 

307 informations = ( 

308 _xtnsapi.introspect( 

309 objct, 

310 context = context, introspection = introspection, 

311 cache = cache, table = table ) ) 

312 fragments_.append( context.fragment_rectifier( 

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

314 source = _xtnsapi.FragmentSources.Renderer ) ) 

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

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

317 objct.__doc__ = docstring if docstring else None 

318 

319 

320def _decorate_class_attributes( # noqa: PLR0913 

321 objct: type, /, 

322 context: _xtnsapi.Context, 

323 introspection: _xtnsapi.IntrospectionControl, 

324 preserve: bool, 

325 renderer: _xtnsapi.Renderer, 

326 table: _xtnsapi.FragmentsTable, 

327) -> None: 

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

329 

330 Iterates through relevant class attributes, collects fragments, 

331 and applies appropriate docstring decoration to each attribute. 

332 ''' 

333 pmname = objct.__module__ 

334 pqname = objct.__qualname__ 

335 for aname, attribute, surface_attribute in ( 

336 _survey_class_attributes( objct, context, introspection ) 

337 ): 

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

339 introspection_ = _limit_introspection( 

340 attribute, context, introspection, fqname ) 

341 introspection_ = introspection_.evaluate_limits_for( attribute ) 

342 if not introspection_.enable: continue 

343 _decorate( 

344 attribute, 

345 context = context, 

346 introspection = introspection_, 

347 preserve = preserve, 

348 renderer = renderer, 

349 fragments = ( ), 

350 table = table ) 

351 if attribute is not surface_attribute: 

352 surface_attribute.__doc__ = attribute.__doc__ 

353 

354 

355def _decorate_module_attributes( # noqa: PLR0913 

356 module: __.types.ModuleType, /, 

357 context: _xtnsapi.Context, 

358 introspection: _xtnsapi.IntrospectionControl, 

359 preserve: bool, 

360 renderer: _xtnsapi.Renderer, 

361 table: _xtnsapi.FragmentsTable, 

362) -> None: 

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

364 

365 Iterates through relevant module attributes, collects fragments, 

366 and applies appropriate docstring decoration to each attribute. 

367 ''' 

368 pmname = module.__name__ 

369 for aname, attribute, surface_attribute in ( 

370 _survey_module_attributes( module, context, introspection ) 

371 ): 

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

373 introspection_ = _limit_introspection( 

374 attribute, context, introspection, fqname ) 

375 introspection_ = introspection_.evaluate_limits_for( attribute ) 

376 if not introspection_.enable: continue 

377 _decorate( 

378 attribute, 

379 context = context, 

380 introspection = introspection_, 

381 preserve = preserve, 

382 renderer = renderer, 

383 fragments = ( ), 

384 table = table ) 

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

386 surface_attribute.__doc__ = attribute.__doc__ 

387 

388 

389def _limit_introspection( 

390 objct: _xtnsapi.Documentable, /, 

391 context: _xtnsapi.Context, 

392 introspection: _xtnsapi.IntrospectionControl, 

393 fqname: str, 

394) -> _xtnsapi.IntrospectionControl: 

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

396 

397 Returns a new IntrospectionControl that respects the limits 

398 specified by the object being documented. This allows objects 

399 to control how deeply they are introspected. 

400 ''' 

401 limit: _xtnsapi.IntrospectionLimit = ( 

402 getattr( 

403 objct, 

404 context.introspection_limit_name, 

405 _xtnsapi.IntrospectionLimit( ) ) ) 

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

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

408 context.notifier( 'error', emessage ) 

409 return introspection 

410 return introspection.with_limit( limit ) 

411 

412 

413def _process_fragments_argument( 

414 context: _xtnsapi.Context, 

415 fragments: _xtnsapi.Fragments, 

416 table: _xtnsapi.FragmentsTable, 

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

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

419 

420 Converts Doc objects to their documentation strings and resolves 

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

422 rectified fragment strings. 

423 ''' 

424 fragments_: list[ str ] = [ ] 

425 for fragment in fragments: 

426 if isinstance( fragment, _xtnsapi.Doc ): 

427 fragment_r = fragment.documentation 

428 elif isinstance( fragment, str ): 

429 if fragment not in table: 

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

431 context.notifier( 'error', emessage ) 

432 continue 

433 fragment_r = table[ fragment ] 

434 else: 

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

436 context.notifier( 'error', emessage ) 

437 continue 

438 fragments_.append( context.fragment_rectifier( 

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

440 return fragments_ 

441 

442 

443def _survey_class_attributes( 

444 possessor: type, /, 

445 context: _xtnsapi.Context, 

446 introspection: _xtnsapi.IntrospectionControl, 

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

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

449 

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

451 representing documentable attributes of the class. The surface 

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

453 attribute's getter method holds the documentation. 

454 ''' 

455 pmname = possessor.__module__ 

456 pqname = possessor.__qualname__ 

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

458 attribute_, update_surface = ( 

459 _consider_class_attribute( 

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

461 if attribute_ is None: continue 

462 if update_surface: 

463 yield aname, attribute_, attribute 

464 continue 

465 yield aname, attribute_, attribute_ 

466 

467 

468def _survey_module_attributes( 

469 possessor: __.types.ModuleType, /, 

470 context: _xtnsapi.Context, 

471 introspection: _xtnsapi.IntrospectionControl, 

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

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

474 

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

476 representing documentable attributes of the module. The surface 

477 attribute may differ from attribute in cases where the actual 

478 documented object is not directly accessible. 

479 ''' 

480 pmname = possessor.__name__ 

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

482 attribute_, update_surface = ( 

483 _consider_module_attribute( 

484 attribute, context, introspection, pmname, aname ) ) 

485 if attribute_ is None: continue 

486 if update_surface: # pragma: no cover 

487 yield aname, attribute_, attribute 

488 continue 

489 yield aname, attribute_, attribute_