Coverage for sources/dynadoc/renderers/sphinxad.py: 100%
136 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-05-30 03:09 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-05-30 03:09 +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 ]
70 return '<unknown>'
73def _format_annotation(
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, list ):
82 seqstr = ', '.join(
83 _format_annotation( element, context, style )
84 for element in annotation ) # pyright: ignore[reportUnknownVariableType]
85 return _stylize_delimiter( style, '[]', seqstr )
86 origin = __.typx.get_origin( annotation )
87 if origin is None:
88 return _qualify_object_name( annotation, context )
89 arguments = __.typx.get_args( annotation )
90 if origin in ( __.types.UnionType, __.typx.Union ):
91 return ' | '.join(
92 _format_annotation( argument, context, style )
93 for argument in arguments )
94 oname = _qualify_object_name( origin, context )
95 if not arguments: return oname
96 if origin is __.typx.Literal:
97 argstr = ', '.join( repr( argument ) for argument in arguments )
98 else:
99 argstr = ', '.join(
100 _format_annotation( argument, context, style )
101 for argument in arguments )
102 return _stylize_delimiter( style, '[]', argstr, oname )
105def _format_description( description: __.typx.Optional[ str ] ) -> str:
106 ''' Ensures that multiline descriptions render correctly. '''
107 if not description: return ''
108 lines = description.split( '\n' )
109 lines[ 1 : ] = [ f" {line}" for line in lines[ 1 : ] ]
110 return '\n'.join( lines )
113def _produce_fragment_partial(
114 possessor: __.Documentable,
115 information: __.InformationBase,
116 context: __.Context,
117 style: Style,
118) -> str:
119 ''' Produces a docstring fragment for a single piece of information.
121 Dispatches to appropriate producer based on the type of information.
122 '''
123 if isinstance( information, __.ArgumentInformation ):
124 return (
125 _produce_argument_text( possessor, information, context, style ) )
126 if isinstance( information, __.AttributeInformation ):
127 return (
128 _produce_attribute_text( possessor, information, context, style ) )
129 if isinstance( information, __.ExceptionInformation ):
130 return (
131 _produce_exception_text( possessor, information, context, style ) )
132 if isinstance( information, __.ReturnInformation ):
133 return (
134 _produce_return_text( possessor, information, context, style ) )
135 context.notifier(
136 'admonition', f"Unrecognized information: {information!r}" )
137 return ''
140def _produce_argument_text(
141 possessor: __.Documentable,
142 information: __.ArgumentInformation,
143 context: __.Context,
144 style: Style,
145) -> str:
146 ''' Produces reStructuredText for argument information.
148 Formats function arguments in Sphinx-compatible reST format,
149 including parameter descriptions and types.
150 '''
151 annotation = information.annotation
152 description = _format_description( information.description )
153 name = information.name
154 lines: list[ str ] = [ ]
155 lines.append(
156 f":argument {name}: {description}"
157 if description else f":argument {name}:" )
158 if annotation is not __.absent:
159 typetext = _format_annotation( annotation, context, style )
160 lines.append( f":type {information.name}: {typetext}" )
161 return '\n'.join( lines )
164def _produce_attribute_text(
165 possessor: __.Documentable,
166 information: __.AttributeInformation,
167 context: __.Context,
168 style: Style,
169) -> str:
170 ''' Produces reStructuredText for attribute information.
172 Formats class and instance attributes in Sphinx-compatible reST format.
173 Delegates to special handler for module attributes.
174 '''
175 annotation = information.annotation
176 match information.association:
177 case __.AttributeAssociations.Module:
178 return _produce_module_attribute_text(
179 possessor, information, context, style )
180 case __.AttributeAssociations.Class: vlabel = 'cvar'
181 case __.AttributeAssociations.Instance: vlabel = 'ivar'
182 description = _format_description( information.description )
183 name = information.name
184 lines: list[ str ] = [ ]
185 lines.append(
186 f":{vlabel} {name}: {description}"
187 if description else f":{vlabel} {name}:" )
188 if annotation is not __.absent:
189 typetext = _format_annotation( annotation, context, style )
190 lines.append( f":vartype {name}: {typetext}" )
191 return '\n'.join( lines )
194def _produce_module_attribute_text(
195 possessor: __.Documentable,
196 information: __.AttributeInformation,
197 context: __.Context,
198 style: Style,
199) -> str:
200 ''' Produces reStructuredText for module attribute information.
202 Formats module attributes in Sphinx-compatible reST format,
203 with special handling for TypeAlias attributes.
204 '''
205 annotation = information.annotation
206 description = information.description or ''
207 name = information.name
208 match information.default.mode:
209 case __.ValuationModes.Accept:
210 value = getattr( possessor, name, __.absent )
211 case __.ValuationModes.Suppress:
212 value = __.absent
213 case __.ValuationModes.Surrogate: # pragma: no branch
214 value = __.absent
215 lines: list[ str ] = [ ]
216 if annotation is __.typx.TypeAlias:
217 lines.append( f".. py:type:: {name}" )
218 if value is not __.absent: # pragma: no branch
219 value_ar = __.reduce_annotation(
220 value, context,
221 __.AdjunctsData( ),
222 __.AnnotationsCache( ) )
223 value_s = _format_annotation( value_ar, context, style )
224 lines.append( f" :canonical: {value_s}" )
225 if description: lines.extend( [ '', f" {description}" ] )
226 else:
227 # Note: No way to inject data docstring as of 2025-05-11.
228 # Autodoc will read doc comments and pseudo-docstrings,
229 # but we have no means of supplying description via a field.
230 lines.append( f".. py:data:: {name}" )
231 if annotation is not __.absent:
232 typetext = _format_annotation( annotation, context, style )
233 lines.append( f" :type: {typetext}" )
234 if value is not __.absent:
235 lines.append( f" :value: {value!r}" )
236 return '\n'.join( lines )
239def _produce_exception_text(
240 possessor: __.Documentable,
241 information: __.ExceptionInformation,
242 context: __.Context,
243 style: Style,
244) -> str:
245 ''' Produces reStructuredText for exception information.
247 Formats exception classes and descriptions in Sphinx-compatible
248 reST format. Handles union types of exceptions appropriately.
249 '''
250 lines: list[ str ] = [ ]
251 annotation = information.annotation
252 description = _format_description( information.description )
253 origin = __.typx.get_origin( annotation )
254 if origin in ( __.types.UnionType, __.typx.Union ):
255 annotations = __.typx.get_args( annotation )
256 else: annotations = ( annotation, )
257 for annotation_ in annotations:
258 typetext = _format_annotation( annotation_, context, style )
259 lines.append(
260 f":raises {typetext}: {description}"
261 if description else f":raises {typetext}:" )
262 return '\n'.join( lines )
265def _produce_return_text(
266 possessor: __.Documentable,
267 information: __.ReturnInformation,
268 context: __.Context,
269 style: Style,
270) -> str:
271 ''' Produces reStructuredText for function return information.
273 Formats return type and description in Sphinx-compatible reST format.
274 Returns empty string for None returns.
275 '''
276 if information.annotation in ( None, __.types.NoneType ): return ''
277 description = _format_description( information.description )
278 typetext = _format_annotation( information.annotation, context, style )
279 lines: list[ str ] = [ ]
280 if description:
281 lines.append( f":returns: {description}" )
282 lines.append( f":rtype: {typetext}" )
283 return '\n'.join( lines )
286def _qualify_object_name( # noqa: PLR0911
287 objct: object, context: __.Context
288) -> str:
289 ''' Qualifies an object name for documentation.
291 Determines the appropriate fully-qualified name for an object,
292 considering builtin types, module namespaces, and qualname attributes.
293 '''
294 if objct is Ellipsis: return '...'
295 if objct is __.types.NoneType: return 'None'
296 if objct is __.types.ModuleType: return 'types.ModuleType'
297 name = (
298 getattr( objct, '__name__', None )
299 or _extract_qualident( str( objct ), context ) )
300 if name == '<unknown>': return name
301 qname = getattr( objct, '__qualname__', None ) or name
302 name0 = qname.split( '.', maxsplit = 1 )[ 0 ]
303 if name0 in vars( __.builtins ): # int, etc...
304 return qname
305 if context.invoker_globals and name0 in context.invoker_globals:
306 return qname
307 mname = getattr( objct, '__module__', None )
308 if mname: return f"{mname}.{qname}"
309 return name # pragma: no cover
312def _stylize_delimiter(
313 style: Style,
314 delimiters: str,
315 content: str,
316 prefix: str = '',
317) -> str:
318 ''' Stylizes delimiters according to the selected style.
320 Formats delimiters around content based on the style setting,
321 with options for more legible spacing or compact PEP 8 formatting.
322 '''
323 ld = delimiters[ 0 ]
324 rd = delimiters[ 1 ]
325 match style:
326 case Style.Legible: return f"{prefix}{ld} {content} {rd}"
327 case Style.Pep8: return f"{prefix}{ld}{content}{rd}"