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

151 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-05 22:02 +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 # Fragments can come from base class or metaclass. 

130 # We only care about fragments on class itself. 

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

132 if __.inspect.isclass( objct ) 

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

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

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

136 ): 

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

138 context.notifier( 'error', emessage ) 

139 fragments = ( ) 

140 for fragment in fragments: 

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

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

143 context.notifier( 'error', emessage ) 

144 return fragments 

145 

146 

147def _consider_class_attribute( # noqa: C901,PLR0913 

148 attribute: object, /, 

149 context: _xtnsapi.Context, 

150 introspection: _xtnsapi.IntrospectionControl, 

151 pmname: str, pqname: str, aname: str, 

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

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

154 

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

156 in the documentation process based on introspection targets and 

157 class ownership. Returns the documentable attribute and a flag 

158 indicating whether the surface attribute needs updating. 

159 ''' 

160 if _check_module_recursion( attribute, introspection, pmname ): 

161 return attribute, False 

162 attribute_ = None 

163 update_surface = False 

164 if ( not attribute_ 

165 and introspection.targets & _xtnsapi.IntrospectionTargets.Class 

166 and __.inspect.isclass( attribute ) 

167 ): attribute_ = attribute 

168 if ( not attribute_ 

169 and introspection.targets & _xtnsapi.IntrospectionTargets.Descriptor 

170 ): 

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

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

173 attribute_ = attribute.fget 

174 update_surface = True 

175 # TODO: Apply custom processors from context. 

176 elif __.inspect.isdatadescriptor( attribute ): 

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

178 return None, False 

179 if ( not attribute_ 

180 and introspection.targets & _xtnsapi.IntrospectionTargets.Function 

181 ): 

182 if __.inspect.ismethod( attribute ): 

183 # Methods proxy docstrings from their core functions. 

184 attribute_ = attribute.__func__ 

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

186 attribute_ = attribute 

187 if attribute_: 

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

189 if not mname or mname != pmname: 

190 attribute_ = None 

191 if attribute_: 

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

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

194 attribute_ = None 

195 return attribute_, update_surface 

196 

197 

198def _consider_module_attribute( 

199 attribute: object, /, 

200 context: _xtnsapi.Context, 

201 introspection: _xtnsapi.IntrospectionControl, 

202 pmname: str, aname: str, 

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

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

205 

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

207 in the documentation process based on introspection targets and 

208 module ownership. Returns the documentable attribute and a flag 

209 indicating whether the surface attribute needs updating. 

210 ''' 

211 if _check_module_recursion( attribute, introspection, pmname ): 

212 return attribute, False 

213 attribute_ = None 

214 update_surface = False 

215 if ( not attribute_ 

216 and introspection.targets & _xtnsapi.IntrospectionTargets.Class 

217 and __.inspect.isclass( attribute ) 

218 ): attribute_ = attribute 

219 if ( not attribute_ 

220 and introspection.targets & _xtnsapi.IntrospectionTargets.Function 

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

222 ): attribute_ = attribute 

223 if attribute_: 

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

225 if not mname or mname != pmname: 

226 attribute_ = None 

227 return attribute_, update_surface 

228 

229 

230def _decorate( # noqa: PLR0913 

231 objct: _xtnsapi.Documentable, /, 

232 context: _xtnsapi.Context, 

233 introspection: _xtnsapi.IntrospectionControl, 

234 preserve: bool, 

235 renderer: _xtnsapi.Renderer, 

236 fragments: _xtnsapi.Fragments, 

237 table: _xtnsapi.FragmentsTable, 

238) -> None: 

239 ''' Decorates an object with assembled docstring. 

240 

241 Handles core docstring decoration and potentially recursive decoration 

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

243 Prevents multiple decoration of the same object. 

244 ''' 

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

246 _visitees.add( objct ) 

247 if introspection.targets: 

248 if __.inspect.isclass( objct ): 

249 _decorate_class_attributes( 

250 objct, 

251 context = context, 

252 introspection = introspection, 

253 preserve = preserve, 

254 renderer = renderer, 

255 table = table ) 

256 elif __.inspect.ismodule( objct ): 

257 _decorate_module_attributes( 

258 objct, 

259 context = context, 

260 introspection = introspection, 

261 preserve = preserve, 

262 renderer = renderer, 

263 table = table ) 

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

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

266 fragments_ = _collect_fragments( objct, context, fqname ) 

267 if not fragments_: fragments_ = fragments 

268 _decorate_core( 

269 objct, 

270 context = context, 

271 introspection = introspection, 

272 preserve = preserve, 

273 renderer = renderer, 

274 fragments = fragments_, 

275 table = table ) 

276 

277 

278def _decorate_core( # noqa: PLR0913 

279 objct: _xtnsapi.Documentable, /, 

280 context: _xtnsapi.Context, 

281 introspection: _xtnsapi.IntrospectionControl, 

282 preserve: bool, 

283 renderer: _xtnsapi.Renderer, 

284 fragments: _xtnsapi.Fragments, 

285 table: _xtnsapi.FragmentsTable, 

286) -> None: 

287 ''' Core implementation of docstring decoration. 

288 

289 Assembles a docstring from fragments, existing docstring (if 

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

291 to the object. 

292 ''' 

293 fragments_: list[ str ] = [ ] 

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

295 fragments_.append( context.fragment_rectifier( 

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

297 fragments_.extend( 

298 _process_fragments_argument( context, fragments, table ) ) 

299 if introspection.enable: 

300 cache = _xtnsapi.AnnotationsCache( ) 

301 informations = ( 

302 _xtnsapi.introspect( 

303 objct, 

304 context = context, introspection = introspection, 

305 cache = cache, table = table ) ) 

306 fragments_.append( context.fragment_rectifier( 

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

308 source = _xtnsapi.FragmentSources.Renderer ) ) 

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

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

311 objct.__doc__ = docstring if docstring else None 

312 

313 

314def _decorate_class_attributes( # noqa: PLR0913 

315 objct: type, /, 

316 context: _xtnsapi.Context, 

317 introspection: _xtnsapi.IntrospectionControl, 

318 preserve: bool, 

319 renderer: _xtnsapi.Renderer, 

320 table: _xtnsapi.FragmentsTable, 

321) -> None: 

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

323 

324 Iterates through relevant class attributes, collects fragments, 

325 and applies appropriate docstring decoration to each attribute. 

326 ''' 

327 pmname = objct.__module__ 

328 pqname = objct.__qualname__ 

329 for aname, attribute, surface_attribute in ( 

330 _survey_class_attributes( objct, context, introspection ) 

331 ): 

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

333 introspection_ = _limit_introspection( 

334 attribute, context, introspection, fqname ) 

335 introspection_ = introspection_.evaluate_limits_for( attribute ) 

336 if not introspection_.enable: continue 

337 _decorate( 

338 attribute, 

339 context = context, 

340 introspection = introspection_, 

341 preserve = preserve, 

342 renderer = renderer, 

343 fragments = ( ), 

344 table = table ) 

345 if attribute is not surface_attribute: 

346 surface_attribute.__doc__ = attribute.__doc__ 

347 

348 

349def _decorate_module_attributes( # noqa: PLR0913 

350 module: __.types.ModuleType, /, 

351 context: _xtnsapi.Context, 

352 introspection: _xtnsapi.IntrospectionControl, 

353 preserve: bool, 

354 renderer: _xtnsapi.Renderer, 

355 table: _xtnsapi.FragmentsTable, 

356) -> None: 

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

358 

359 Iterates through relevant module attributes, collects fragments, 

360 and applies appropriate docstring decoration to each attribute. 

361 ''' 

362 pmname = module.__name__ 

363 for aname, attribute, surface_attribute in ( 

364 _survey_module_attributes( module, context, introspection ) 

365 ): 

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

367 introspection_ = _limit_introspection( 

368 attribute, context, introspection, fqname ) 

369 introspection_ = introspection_.evaluate_limits_for( attribute ) 

370 if not introspection_.enable: continue 

371 _decorate( 

372 attribute, 

373 context = context, 

374 introspection = introspection_, 

375 preserve = preserve, 

376 renderer = renderer, 

377 fragments = ( ), 

378 table = table ) 

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

380 surface_attribute.__doc__ = attribute.__doc__ 

381 

382 

383def _limit_introspection( 

384 objct: _xtnsapi.Documentable, /, 

385 context: _xtnsapi.Context, 

386 introspection: _xtnsapi.IntrospectionControl, 

387 fqname: str, 

388) -> _xtnsapi.IntrospectionControl: 

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

390 

391 Returns a new IntrospectionControl that respects the limits 

392 specified by the object being documented. This allows objects 

393 to control how deeply they are introspected. 

394 ''' 

395 limit: _xtnsapi.IntrospectionLimit = ( 

396 getattr( 

397 objct, 

398 context.introspection_limit_name, 

399 _xtnsapi.IntrospectionLimit( ) ) ) 

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

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

402 context.notifier( 'error', emessage ) 

403 return introspection 

404 return introspection.with_limit( limit ) 

405 

406 

407def _process_fragments_argument( 

408 context: _xtnsapi.Context, 

409 fragments: _xtnsapi.Fragments, 

410 table: _xtnsapi.FragmentsTable, 

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

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

413 

414 Converts Doc objects to their documentation strings and resolves 

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

416 rectified fragment strings. 

417 ''' 

418 fragments_: list[ str ] = [ ] 

419 for fragment in fragments: 

420 if isinstance( fragment, _xtnsapi.Doc ): 

421 fragment_r = fragment.documentation 

422 elif isinstance( fragment, str ): 

423 if fragment not in table: 

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

425 context.notifier( 'error', emessage ) 

426 continue 

427 fragment_r = table[ fragment ] 

428 else: 

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

430 context.notifier( 'error', emessage ) 

431 continue 

432 fragments_.append( context.fragment_rectifier( 

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

434 return fragments_ 

435 

436 

437def _survey_class_attributes( 

438 possessor: type, /, 

439 context: _xtnsapi.Context, 

440 introspection: _xtnsapi.IntrospectionControl, 

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

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

443 

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

445 representing documentable attributes of the class. The surface 

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

447 attribute's getter method holds the documentation. 

448 ''' 

449 pmname = possessor.__module__ 

450 pqname = possessor.__qualname__ 

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

452 attribute_, update_surface = ( 

453 _consider_class_attribute( 

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

455 if attribute_ is None: continue 

456 if update_surface: 

457 yield aname, attribute_, attribute 

458 continue 

459 yield aname, attribute_, attribute_ 

460 

461 

462def _survey_module_attributes( 

463 possessor: __.types.ModuleType, /, 

464 context: _xtnsapi.Context, 

465 introspection: _xtnsapi.IntrospectionControl, 

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

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

468 

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

470 representing documentable attributes of the module. The surface 

471 attribute may differ from attribute in cases where the actual 

472 documented object is not directly accessible. 

473 ''' 

474 pmname = possessor.__name__ 

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

476 attribute_, update_surface = ( 

477 _consider_module_attribute( 

478 attribute, context, introspection, pmname, aname ) ) 

479 if attribute_ is None: continue 

480 if update_surface: # pragma: no cover 

481 yield aname, attribute_, attribute 

482 continue 

483 yield aname, attribute_, attribute_