Coverage for sources/dynadoc/renderers/sphinxad.py: 100%
130 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-05-28 04:38 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-05-28 04:38 +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( )
34def produce_fragment(
35 possessor: __.Documentable,
36 informations: __.Informations,
37 context: __.Context,
38 style: Style = Style.Legible,
39) -> str:
40 ''' Produces a reStructuredText docstring fragment.
42 Combines information from object introspection into a formatted
43 docstring fragment suitable for Sphinx Autodoc.
44 '''
45 return '\n'.join(
46 _produce_fragment_partial( possessor, information, context, style )
47 for information in informations )
50_qualident_regex = __.re.compile( r'''^([\w\.]+).*$''' )
51def _extract_qualident( name: str, context: __.Context ) -> str:
52 ''' Extracts a qualified identifier from a string representation.
54 Used to extract the qualified name of an object from its string
55 representation when direct name access is not available.
56 '''
57 extract = _qualident_regex.match( name )
58 if extract is not None: return extract[ 1 ]
59 return '<unknown>'
62def _format_annotation(
63 annotation: __.typx.Any, context: __.Context, style: Style
64) -> str:
65 ''' Formats a type annotation as a string for documentation.
67 Handles various annotation types including unions, generics,
68 and literals. Formats according to the selected style.
69 '''
70 if isinstance( annotation, list ):
71 seqstr = ', '.join(
72 _format_annotation( element, context, style )
73 for element in annotation ) # pyright: ignore[reportUnknownVariableType]
74 return _stylize_delimiter( style, '[]', seqstr )
75 origin = __.typx.get_origin( annotation )
76 if origin is None:
77 return _qualify_object_name( annotation, context )
78 arguments = __.typx.get_args( annotation )
79 if origin in ( __.types.UnionType, __.typx.Union ):
80 return ' | '.join(
81 _format_annotation( argument, context, style )
82 for argument in arguments )
83 oname = _qualify_object_name( origin, context )
84 if not arguments: return oname
85 if origin is __.typx.Literal:
86 argstr = ', '.join( repr( argument ) for argument in arguments )
87 else:
88 argstr = ', '.join(
89 _format_annotation( argument, context, style )
90 for argument in arguments )
91 return _stylize_delimiter( style, '[]', argstr, oname )
94def _produce_fragment_partial(
95 possessor: __.Documentable,
96 information: __.InformationBase,
97 context: __.Context,
98 style: Style,
99) -> str:
100 ''' Produces a docstring fragment for a single piece of information.
102 Dispatches to appropriate producer based on the type of information.
103 '''
104 if isinstance( information, __.ArgumentInformation ):
105 return (
106 _produce_argument_text( possessor, information, context, style ) )
107 if isinstance( information, __.AttributeInformation ):
108 return (
109 _produce_attribute_text( possessor, information, context, style ) )
110 if isinstance( information, __.ExceptionInformation ):
111 return (
112 _produce_exception_text( possessor, information, context, style ) )
113 if isinstance( information, __.ReturnInformation ):
114 return (
115 _produce_return_text( possessor, information, context, style ) )
116 context.notifier(
117 'admonition', f"Unrecognized information: {information!r}" )
118 return ''
121def _produce_argument_text(
122 possessor: __.Documentable,
123 information: __.ArgumentInformation,
124 context: __.Context,
125 style: Style,
126) -> str:
127 ''' Produces reStructuredText for argument information.
129 Formats function arguments in Sphinx-compatible reST format,
130 including parameter descriptions and types.
131 '''
132 annotation = information.annotation
133 description = information.description
134 name = information.name
135 lines: list[ str ] = [ ]
136 lines.append(
137 f":argument {name}: {description}"
138 if description else f":argument {name}:" )
139 if annotation is not __.absent:
140 typetext = _format_annotation( annotation, context, style )
141 lines.append( f":type {information.name}: {typetext}" )
142 return '\n'.join( lines )
145def _produce_attribute_text(
146 possessor: __.Documentable,
147 information: __.AttributeInformation,
148 context: __.Context,
149 style: Style,
150) -> str:
151 ''' Produces reStructuredText for attribute information.
153 Formats class and instance attributes in Sphinx-compatible reST format.
154 Delegates to special handler for module attributes.
155 '''
156 annotation = information.annotation
157 match information.association:
158 case __.AttributeAssociations.Module:
159 return _produce_module_attribute_text(
160 possessor, information, context, style )
161 case __.AttributeAssociations.Class: vlabel = 'cvar'
162 case __.AttributeAssociations.Instance: vlabel = 'ivar'
163 description = information.description
164 name = information.name
165 lines: list[ str ] = [ ]
166 lines.append(
167 f":{vlabel} {name}: {description}"
168 if description else f":{vlabel} {name}:" )
169 if annotation is not __.absent:
170 typetext = _format_annotation( annotation, context, style )
171 lines.append( f":vartype {name}: {typetext}" )
172 return '\n'.join( lines )
175def _produce_module_attribute_text(
176 possessor: __.Documentable,
177 information: __.AttributeInformation,
178 context: __.Context,
179 style: Style,
180) -> str:
181 ''' Produces reStructuredText for module attribute information.
183 Formats module attributes in Sphinx-compatible reST format,
184 with special handling for TypeAlias attributes.
185 '''
186 annotation = information.annotation
187 description = information.description or ''
188 name = information.name
189 match information.default.mode:
190 case __.ValuationModes.Accept:
191 value = getattr( possessor, name, __.absent )
192 case __.ValuationModes.Suppress:
193 value = __.absent
194 case __.ValuationModes.Surrogate: # pragma: no branch
195 value = __.absent
196 lines: list[ str ] = [ ]
197 if annotation is __.typx.TypeAlias:
198 lines.append( f".. py:type:: {name}" )
199 if value is not __.absent: # pragma: no branch
200 value_ar = __.reduce_annotation(
201 value, context,
202 __.AdjunctsData( ),
203 __.AnnotationsCache( ) )
204 value_s = _format_annotation( value_ar, context, style )
205 lines.append( f" :canonical: {value_s}" )
206 if description: lines.extend( [ '', f" {description}" ] )
207 else:
208 # Note: No way to inject data docstring as of 2025-05-11.
209 # Autodoc will read doc comments and pseudo-docstrings,
210 # but we have no means of supplying description via a field.
211 lines.append( f".. py:data:: {name}" )
212 if annotation is not __.absent:
213 typetext = _format_annotation( annotation, context, style )
214 lines.append( f" :type: {typetext}" )
215 if value is not __.absent:
216 lines.append( f" :value: {value!r}" )
217 return '\n'.join( lines )
220def _produce_exception_text(
221 possessor: __.Documentable,
222 information: __.ExceptionInformation,
223 context: __.Context,
224 style: Style,
225) -> str:
226 ''' Produces reStructuredText for exception information.
228 Formats exception classes and descriptions in Sphinx-compatible
229 reST format. Handles union types of exceptions appropriately.
230 '''
231 lines: list[ str ] = [ ]
232 annotation = information.annotation
233 description = information.description
234 origin = __.typx.get_origin( annotation )
235 if origin in ( __.types.UnionType, __.typx.Union ):
236 annotations = __.typx.get_args( annotation )
237 else: annotations = ( annotation, )
238 for annotation_ in annotations:
239 typetext = _format_annotation( annotation_, context, style )
240 lines.append(
241 f":raises {typetext}: {description}"
242 if description else f":raises {typetext}:" )
243 return '\n'.join( lines )
246def _produce_return_text(
247 possessor: __.Documentable,
248 information: __.ReturnInformation,
249 context: __.Context,
250 style: Style,
251) -> str:
252 ''' Produces reStructuredText for function return information.
254 Formats return type and description in Sphinx-compatible reST format.
255 Returns empty string for None returns.
256 '''
257 if information.annotation in ( None, __.types.NoneType ): return ''
258 description = information.description
259 typetext = _format_annotation( information.annotation, context, style )
260 lines: list[ str ] = [ ]
261 if description:
262 lines.append( f":returns: {description}" )
263 lines.append( f":rtype: {typetext}" )
264 return '\n'.join( lines )
267def _qualify_object_name( # noqa: PLR0911
268 objct: object, context: __.Context
269) -> str:
270 ''' Qualifies an object name for documentation.
272 Determines the appropriate fully-qualified name for an object,
273 considering builtin types, module namespaces, and qualname attributes.
274 '''
275 if objct is Ellipsis: return '...'
276 if objct is __.types.NoneType: return 'None'
277 if objct is __.types.ModuleType: return 'types.ModuleType'
278 name = (
279 getattr( objct, '__name__', None )
280 or _extract_qualident( str( objct ), context ) )
281 if name == '<unknown>': return name
282 qname = getattr( objct, '__qualname__', None ) or name
283 name0 = qname.split( '.', maxsplit = 1 )[ 0 ]
284 if name0 in vars( __.builtins ): # int, etc...
285 return qname
286 if context.invoker_globals and name0 in context.invoker_globals:
287 return qname
288 mname = getattr( objct, '__module__', None )
289 if mname: return f"{mname}.{qname}"
290 return name # pragma: no cover
293def _stylize_delimiter(
294 style: Style,
295 delimiters: str,
296 content: str,
297 prefix: str = '',
298) -> str:
299 ''' Stylizes delimiters according to the selected style.
301 Formats delimiters around content based on the style setting,
302 with options for more legible spacing or compact PEP 8 formatting.
303 '''
304 ld = delimiters[ 0 ]
305 rd = delimiters[ 1 ]
306 match style:
307 case Style.Legible: return f"{prefix}{ld} {content} {rd}"
308 case Style.Pep8: return f"{prefix}{ld}{content}{rd}"