Coverage for sources/dynadoc/renderers/sphinxad.py: 100%
139 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-13 01:33 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-13 01:33 +0000
1# vim: set filetype=python fileencoding=utf-8:
2# -*- coding: utf-8 -*-
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#============================================================================#
21''' Sphinx Autodoc reStructuredText renderers. '''
24from . import __
27class Style( __.enum.Enum ):
28 ''' Style of formatter output. '''
30 Legible = __.enum.auto( )
31 Pep8 = __.enum.auto( )
34StyleArgument: __.typx.TypeAlias = __.typx.Annotated[
35 Style,
36 __.Doc(
37 ''' Output style for renderer.
39 Legible: Extra space padding inside of delimiters.
40 Pep8: As the name implies.
41 ''' ),
42]
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.
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 )
61_qualident_regex = __.re.compile( r'''^([\w\.]+).*$''' )
62def _extract_qualident( name: str, context: __.Context ) -> str:
63 ''' Extracts a qualified identifier from a string representation.
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>'
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.
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 )
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 )
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.
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 ''
145def _produce_argument_text(
146 possessor: __.Documentable,
147 information: __.ArgumentInformation,
148 context: __.Context,
149 style: Style,
150) -> str:
151 ''' Produces reStructuredText for argument information.
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 )
169def _produce_attribute_text(
170 possessor: __.Documentable,
171 information: __.AttributeInformation,
172 context: __.Context,
173 style: Style,
174) -> str:
175 ''' Produces reStructuredText for attribute information.
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 )
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.
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 )
244def _produce_exception_text(
245 possessor: __.Documentable,
246 information: __.ExceptionInformation,
247 context: __.Context,
248 style: Style,
249) -> str:
250 ''' Produces reStructuredText for exception information.
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 )
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.
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 )
291def _qualify_object_name( # noqa: PLR0911
292 objct: object, context: __.Context
293) -> str:
294 ''' Qualifies an object name for documentation.
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
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.
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}"