sphinx-autoapi 3.3.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
autoapi/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Sphinx AutoAPI"""
2
+
3
+ from .extension import setup
4
+
5
+ __all__ = ("setup",)
6
+ __version__ = "3.3.3"
7
+ __version_info__ = (3, 3, 3)
@@ -0,0 +1,684 @@
1
+ from __future__ import annotations
2
+
3
+ import builtins
4
+ from collections.abc import Iterable
5
+ import itertools
6
+ import re
7
+ import sys
8
+ from typing import Any, NamedTuple
9
+
10
+ import astroid
11
+ import astroid.nodes
12
+
13
+
14
+ if sys.version_info < (3, 10): # PY310
15
+ from stdlib_list import in_stdlib
16
+ else:
17
+
18
+ def in_stdlib(module_name: str) -> bool:
19
+ return module_name in sys.stdlib_module_names
20
+
21
+
22
+ class ArgInfo(NamedTuple):
23
+ prefix: str | None
24
+ name: str | None
25
+ annotation: str | None
26
+ default_value: str | None
27
+
28
+
29
+ def resolve_import_alias(
30
+ name: str, import_names: Iterable[tuple[str, str | None]]
31
+ ) -> str:
32
+ """Resolve a name from an aliased import to its original name.
33
+
34
+ Args:
35
+ name: The potentially aliased name to resolve.
36
+ import_names: The pairs of original names and aliases from the import.
37
+
38
+ Returns:
39
+ The original name.
40
+ """
41
+ resolved_name = name
42
+
43
+ for import_name, imported_as in import_names:
44
+ if import_name == name:
45
+ break
46
+ if imported_as == name:
47
+ resolved_name = import_name
48
+ break
49
+
50
+ return resolved_name
51
+
52
+
53
+ def get_full_import_name(import_from: astroid.nodes.ImportFrom, name: str) -> str:
54
+ """Get the full path of a name from a ``from x import y`` statement.
55
+
56
+ Args:
57
+ import_from: The astroid node to resolve the name of.
58
+ name: The short name or alias of what was imported.
59
+ This is ``y`` in ``from x import y``
60
+ and ``z`` in ``from x import y as z``.
61
+
62
+ Returns:
63
+ str: The full import path of the name.
64
+ """
65
+ partial_basename = resolve_import_alias(name, import_from.names)
66
+
67
+ module_name = import_from.modname
68
+ if import_from.level:
69
+ module = import_from.root()
70
+ assert isinstance(module, astroid.nodes.Module)
71
+ module_name = module.relative_to_absolute_name(
72
+ import_from.modname, level=import_from.level
73
+ )
74
+
75
+ return f"{module_name}.{partial_basename}"
76
+
77
+
78
+ def resolve_qualname(node: astroid.nodes.NodeNG, basename: str) -> str:
79
+ """Resolve where a node is defined to get its fully qualified name.
80
+
81
+ Args:
82
+ node: The node representing the base name.
83
+ basename: The partial base name to resolve.
84
+
85
+ Returns:
86
+ The fully resolved base name.
87
+ """
88
+ full_basename = basename
89
+
90
+ top_level_name = re.sub(r"\(.*\)", "", basename).split(".", 1)[0]
91
+ if isinstance(node, astroid.nodes.LocalsDictNodeNG):
92
+ lookup_node = node
93
+ else:
94
+ lookup_node = node.scope()
95
+
96
+ assigns = lookup_node.lookup(top_level_name)[1]
97
+
98
+ for assignment in assigns:
99
+ if isinstance(assignment, astroid.nodes.ImportFrom):
100
+ import_name = get_full_import_name(assignment, top_level_name)
101
+ full_basename = basename.replace(top_level_name, import_name, 1)
102
+ break
103
+ if isinstance(assignment, astroid.nodes.Import):
104
+ import_name = resolve_import_alias(top_level_name, assignment.names)
105
+ full_basename = basename.replace(top_level_name, import_name, 1)
106
+ break
107
+ if isinstance(assignment, astroid.nodes.ClassDef):
108
+ full_basename = assignment.qname()
109
+ break
110
+ if isinstance(assignment, astroid.nodes.AssignName):
111
+ full_basename = f"{assignment.scope().qname()}.{assignment.name}"
112
+
113
+ if isinstance(node, astroid.nodes.Call):
114
+ full_basename = re.sub(r"\(.*\)", "()", full_basename)
115
+
116
+ if full_basename.startswith("builtins."):
117
+ return full_basename[len("builtins.") :]
118
+
119
+ if full_basename.startswith("__builtin__."):
120
+ return full_basename[len("__builtin__.") :]
121
+
122
+ return full_basename
123
+
124
+
125
+ def get_full_basenames(node: astroid.nodes.ClassDef) -> Iterable[str]:
126
+ """Resolve the partial names of a class' bases to fully qualified names.
127
+
128
+ Args:
129
+ node: The class definition node to resolve the bases of.
130
+
131
+ Returns:
132
+ The fully qualified names.
133
+ """
134
+ for base in node.bases:
135
+ yield _resolve_annotation(base)
136
+
137
+
138
+ def _get_const_value(node: astroid.nodes.NodeNG) -> str | None:
139
+ if isinstance(node, astroid.nodes.Const):
140
+ if isinstance(node.value, str) and "\n" in node.value:
141
+ return f'"""{node.value}"""'
142
+
143
+ class NotConstException(Exception):
144
+ pass
145
+
146
+ def _inner(node: astroid.nodes.NodeNG) -> Any:
147
+ if isinstance(node, (astroid.nodes.List, astroid.nodes.Tuple)):
148
+ new_value = []
149
+ for element in node.elts:
150
+ new_value.append(_inner(element))
151
+
152
+ if isinstance(node, astroid.nodes.Tuple):
153
+ return tuple(new_value)
154
+
155
+ return new_value
156
+ elif isinstance(node, astroid.nodes.Const):
157
+ # Don't allow multi-line strings inside a data structure.
158
+ if isinstance(node.value, str) and "\n" in node.value:
159
+ raise NotConstException()
160
+
161
+ return node.value
162
+
163
+ raise NotConstException()
164
+
165
+ try:
166
+ result = _inner(node)
167
+ except NotConstException:
168
+ return None
169
+
170
+ return repr(result)
171
+
172
+
173
+ def get_assign_value(
174
+ node: astroid.nodes.Assign | astroid.nodes.AnnAssign,
175
+ ) -> tuple[str, str | None] | None:
176
+ """Get the name and value of the assignment of the given node.
177
+
178
+ Assignments to multiple names are ignored, as per PEP 257.
179
+
180
+ Args:
181
+ node: The node to get the assignment value from.
182
+
183
+ Returns:
184
+ The name that is assigned to, and the string representation of
185
+ the value assigned to the name (if it can be converted).
186
+ """
187
+ try:
188
+ targets = node.targets
189
+ except AttributeError:
190
+ targets = [node.target]
191
+
192
+ if len(targets) == 1:
193
+ target = targets[0]
194
+ if isinstance(target, astroid.nodes.AssignName):
195
+ name = target.name
196
+ elif isinstance(target, astroid.nodes.AssignAttr):
197
+ name = target.attrname
198
+ else:
199
+ return None
200
+ return (name, _get_const_value(node.value))
201
+
202
+ return None
203
+
204
+
205
+ def get_assign_annotation(
206
+ node: astroid.nodes.Assign | astroid.nodes.AnnAssign,
207
+ ) -> str | None:
208
+ """Get the type annotation of the assignment of the given node.
209
+
210
+ Args:
211
+ node: The node to get the annotation for.
212
+
213
+ Returns:
214
+ The type annotation as a string, or None if one does not exist.
215
+ """
216
+ annotation_node = None
217
+ try:
218
+ annotation_node = node.annotation
219
+ except AttributeError:
220
+ annotation_node = node.type_annotation
221
+
222
+ return format_annotation(annotation_node)
223
+
224
+
225
+ def is_decorated_with_property(node: astroid.nodes.FunctionDef) -> bool:
226
+ """Check if the function is decorated as a property.
227
+
228
+ Args:
229
+ node: The node to check.
230
+
231
+ Returns:
232
+ True if the function is a property, False otherwise.
233
+ """
234
+ if not node.decorators:
235
+ return False
236
+
237
+ for decorator in node.decorators.nodes:
238
+ if not isinstance(decorator, astroid.nodes.Name):
239
+ continue
240
+
241
+ try:
242
+ if _is_property_decorator(decorator):
243
+ return True
244
+ except astroid.InferenceError:
245
+ pass
246
+
247
+ return False
248
+
249
+
250
+ def _is_property_decorator(decorator: astroid.nodes.Name) -> bool:
251
+ def _is_property_class(class_node: astroid.nodes.ClassDef) -> bool:
252
+ return (
253
+ class_node.name == "property"
254
+ and class_node.root().name == builtins.__name__
255
+ ) or (
256
+ class_node.name == "cached_property"
257
+ and class_node.root().name == "functools"
258
+ )
259
+
260
+ for inferred in decorator.infer():
261
+ if not isinstance(inferred, astroid.nodes.ClassDef):
262
+ continue
263
+
264
+ if _is_property_class(inferred):
265
+ return True
266
+
267
+ if any(_is_property_class(ancestor) for ancestor in inferred.ancestors()):
268
+ return True
269
+
270
+ return False
271
+
272
+
273
+ def is_decorated_with_property_setter(node: astroid.nodes.FunctionDef) -> bool:
274
+ """Check if the function is decorated as a property setter.
275
+
276
+ Args:
277
+ node: The node to check.
278
+
279
+ Returns:
280
+ True if the function is a property setter, False otherwise.
281
+ """
282
+ if not node.decorators:
283
+ return False
284
+
285
+ for decorator in node.decorators.nodes:
286
+ if (
287
+ isinstance(decorator, astroid.nodes.Attribute)
288
+ and decorator.attrname == "setter"
289
+ ):
290
+ return True
291
+
292
+ return False
293
+
294
+
295
+ def is_decorated_with_overload(node: astroid.nodes.FunctionDef) -> bool:
296
+ """Check if the function is decorated as an overload definition.
297
+
298
+ Args:
299
+ node: The node to check.
300
+
301
+ Returns:
302
+ True if the function is an overload definition, False otherwise.
303
+ """
304
+ if not node.decorators:
305
+ return False
306
+
307
+ for decorator in node.decorators.nodes:
308
+ if not isinstance(decorator, (astroid.nodes.Name, astroid.nodes.Attribute)):
309
+ continue
310
+
311
+ try:
312
+ if _is_overload_decorator(decorator):
313
+ return True
314
+ except astroid.InferenceError:
315
+ pass
316
+
317
+ return False
318
+
319
+
320
+ def _is_overload_decorator(
321
+ decorator: astroid.nodes.Name | astroid.nodes.Attribute,
322
+ ) -> bool:
323
+ for inferred in decorator.infer():
324
+ if not isinstance(inferred, astroid.nodes.FunctionDef):
325
+ continue
326
+
327
+ if inferred.name == "overload" and inferred.root().name == "typing":
328
+ return True
329
+
330
+ return False
331
+
332
+
333
+ def is_constructor(node: astroid.nodes.FunctionDef) -> bool:
334
+ """Check if the function is a constructor.
335
+
336
+ Args:
337
+ node: The node to check.
338
+
339
+ Returns:
340
+ True if the function is a constructor, False otherwise.
341
+ """
342
+ return (
343
+ node.parent
344
+ and isinstance(node.parent.scope(), astroid.nodes.ClassDef)
345
+ and node.name == "__init__"
346
+ )
347
+
348
+
349
+ def is_exception(node: astroid.nodes.ClassDef) -> bool:
350
+ """Check if a class is an exception.
351
+
352
+ Args:
353
+ node: The node to check.
354
+
355
+ Returns:
356
+ True if the class is an exception, False otherwise.
357
+ """
358
+ if node.name in ("Exception", "BaseException") and node.root().name == "builtins":
359
+ return True
360
+
361
+ if not hasattr(node, "ancestors"):
362
+ return False
363
+
364
+ return any(is_exception(parent) for parent in node.ancestors(recurs=True))
365
+
366
+
367
+ def is_local_import_from(node: astroid.nodes.NodeNG, package_name: str) -> bool:
368
+ """Check if a node is an import from the local package.
369
+
370
+ Args:
371
+ node: The node to check.
372
+ package_name: The name of the local package.
373
+
374
+ Returns:
375
+ True if the node is an import from the local package. False otherwise.
376
+ """
377
+ if not isinstance(node, astroid.nodes.ImportFrom):
378
+ return False
379
+
380
+ return (
381
+ node.level
382
+ or node.modname == package_name
383
+ or node.modname.startswith(package_name + ".")
384
+ )
385
+
386
+
387
+ def get_module_all(node: astroid.nodes.Module) -> list[str] | None:
388
+ """Get the contents of the ``__all__`` variable from a module.
389
+
390
+ Args:
391
+ node: The module to get ``__all__`` from.
392
+
393
+ Returns:
394
+ The contents of ``__all__`` if defined. Otherwise ``None``.
395
+ """
396
+ all_ = None
397
+
398
+ if "__all__" in node.locals:
399
+ assigned = next(node.igetattr("__all__"))
400
+ if assigned is not astroid.Uninferable:
401
+ all_ = []
402
+ for elt in getattr(assigned, "elts", ()):
403
+ try:
404
+ elt_name = next(elt.infer())
405
+ except astroid.InferenceError:
406
+ continue
407
+
408
+ if elt_name is astroid.Uninferable:
409
+ continue
410
+
411
+ if isinstance(elt_name, astroid.nodes.Const) and isinstance(
412
+ elt_name.value, str
413
+ ):
414
+ all_.append(elt_name.value)
415
+
416
+ return all_
417
+
418
+
419
+ def _is_ellipsis(node: astroid.nodes.NodeNG) -> bool:
420
+ return isinstance(node, astroid.nodes.Const) and node.value == Ellipsis
421
+
422
+
423
+ def merge_annotations(
424
+ annotations: Iterable[astroid.nodes.NodeNG],
425
+ comment_annotations: Iterable[astroid.nodes.NodeNG],
426
+ ) -> Iterable[astroid.nodes.NodeNG | None]:
427
+ for ann, comment_ann in itertools.zip_longest(annotations, comment_annotations):
428
+ if ann and not _is_ellipsis(ann):
429
+ yield ann
430
+ elif comment_ann and not _is_ellipsis(comment_ann):
431
+ yield comment_ann
432
+ else:
433
+ yield None
434
+
435
+
436
+ def _resolve_annotation(annotation: astroid.nodes.NodeNG) -> str:
437
+ resolved: str
438
+
439
+ if isinstance(annotation, astroid.nodes.Const):
440
+ resolved = resolve_qualname(annotation, str(annotation.value))
441
+ elif isinstance(annotation, astroid.nodes.Name):
442
+ resolved = resolve_qualname(annotation, annotation.name)
443
+ elif isinstance(annotation, astroid.nodes.Attribute):
444
+ resolved = resolve_qualname(annotation, annotation.as_string())
445
+ elif isinstance(annotation, astroid.nodes.Subscript):
446
+ value = _resolve_annotation(annotation.value)
447
+ slice_node = annotation.slice
448
+ # astroid.Index was removed in astroid v3
449
+ if hasattr(astroid.nodes, "Index") and isinstance(
450
+ slice_node, astroid.nodes.Index
451
+ ):
452
+ slice_node = slice_node.value
453
+ if value == "Literal":
454
+ if isinstance(slice_node, astroid.nodes.Tuple):
455
+ elts = slice_node.elts
456
+ else:
457
+ elts = [slice_node]
458
+ slice_ = ", ".join(
459
+ (
460
+ elt.as_string()
461
+ if isinstance(elt, astroid.nodes.Const)
462
+ else _resolve_annotation(elt)
463
+ )
464
+ for elt in elts
465
+ )
466
+ elif isinstance(slice_node, astroid.nodes.Tuple):
467
+ slice_ = ", ".join(_resolve_annotation(elt) for elt in slice_node.elts)
468
+ else:
469
+ slice_ = _resolve_annotation(slice_node)
470
+ resolved = f"{value}[{slice_}]"
471
+ elif isinstance(annotation, astroid.nodes.Tuple):
472
+ resolved = (
473
+ "(" + ", ".join(_resolve_annotation(elt) for elt in annotation.elts) + ")"
474
+ )
475
+ elif isinstance(annotation, astroid.nodes.List):
476
+ resolved = (
477
+ "[" + ", ".join(_resolve_annotation(elt) for elt in annotation.elts) + "]"
478
+ )
479
+ elif isinstance(annotation, astroid.nodes.BinOp) and annotation.op == "|":
480
+ left = _resolve_annotation(annotation.left)
481
+ right = _resolve_annotation(annotation.right)
482
+ resolved = f"{left} | {right}"
483
+ else:
484
+ resolved = annotation.as_string()
485
+
486
+ if resolved.startswith("typing."):
487
+ return resolved[len("typing.") :]
488
+
489
+ # Sphinx is capable of linking anything in the same module
490
+ # without needing a fully qualified path.
491
+ module_prefix = annotation.root().name + "."
492
+ if resolved.startswith(module_prefix):
493
+ return resolved[len(module_prefix) :]
494
+
495
+ return resolved
496
+
497
+
498
+ def format_annotation(annotation: astroid.nodes.NodeNG | None) -> str | None:
499
+ if annotation:
500
+ return _resolve_annotation(annotation)
501
+
502
+ return annotation
503
+
504
+
505
+ def _iter_args(
506
+ args: list[astroid.nodes.AssignName],
507
+ annotations: list[astroid.nodes.NodeNG | None],
508
+ defaults: list[astroid.nodes.NodeNG],
509
+ ) -> Iterable[tuple[str, str | None, str | None]]:
510
+ default_offset = len(args) - len(defaults)
511
+ packed = itertools.zip_longest(args, annotations)
512
+ for i, (arg, annotation) in enumerate(packed):
513
+ default = None
514
+ if defaults is not None and i >= default_offset:
515
+ if defaults[i - default_offset] is not None:
516
+ default = defaults[i - default_offset].as_string()
517
+
518
+ name = arg.name
519
+ if isinstance(arg, astroid.nodes.Tuple):
520
+ argument_names = ", ".join(x.name for x in arg.elts)
521
+ name = f"({argument_names})"
522
+
523
+ yield (name, format_annotation(annotation), default)
524
+
525
+
526
+ def get_args_info(args_node: astroid.nodes.Arguments) -> list[ArgInfo]:
527
+ result: list[ArgInfo] = []
528
+ positional_only_defaults = []
529
+ positional_or_keyword_defaults = args_node.defaults
530
+ if args_node.defaults:
531
+ args = args_node.args or []
532
+ positional_or_keyword_defaults = args_node.defaults[-len(args) :]
533
+ positional_only_defaults = args_node.defaults[
534
+ : len(args_node.defaults) - len(args)
535
+ ]
536
+
537
+ plain_annotations = args_node.annotations or ()
538
+
539
+ func_comment_annotations = args_node.parent.type_comment_args or []
540
+ if args_node.parent.type in ("method", "classmethod"):
541
+ func_comment_annotations = [None] + func_comment_annotations
542
+
543
+ comment_annotations = args_node.type_comment_posonlyargs
544
+ comment_annotations += args_node.type_comment_args or []
545
+ comment_annotations += args_node.type_comment_kwonlyargs
546
+ annotations = list(
547
+ merge_annotations(
548
+ plain_annotations,
549
+ merge_annotations(func_comment_annotations, comment_annotations),
550
+ )
551
+ )
552
+ annotation_offset = 0
553
+
554
+ if args_node.posonlyargs:
555
+ posonlyargs_annotations = args_node.posonlyargs_annotations
556
+ if not any(args_node.posonlyargs_annotations):
557
+ num_args = len(args_node.posonlyargs)
558
+ posonlyargs_annotations = annotations[
559
+ annotation_offset : annotation_offset + num_args
560
+ ]
561
+
562
+ for arg, annotation, default in _iter_args(
563
+ args_node.posonlyargs, posonlyargs_annotations, positional_only_defaults
564
+ ):
565
+ result.append(ArgInfo(None, arg, annotation, default))
566
+
567
+ result.append(ArgInfo("/", None, None, None))
568
+
569
+ if not any(args_node.posonlyargs_annotations):
570
+ annotation_offset += num_args
571
+
572
+ if args_node.args:
573
+ num_args = len(args_node.args)
574
+ for arg, annotation, default in _iter_args(
575
+ args_node.args,
576
+ annotations[annotation_offset : annotation_offset + num_args],
577
+ positional_or_keyword_defaults,
578
+ ):
579
+ result.append(ArgInfo(None, arg, annotation, default))
580
+
581
+ annotation_offset += num_args
582
+
583
+ if args_node.vararg:
584
+ annotation = None
585
+ if args_node.varargannotation:
586
+ annotation = format_annotation(args_node.varargannotation)
587
+ elif len(annotations) > annotation_offset and annotations[annotation_offset]:
588
+ annotation = format_annotation(annotations[annotation_offset])
589
+ annotation_offset += 1
590
+ result.append(ArgInfo("*", args_node.vararg, annotation, None))
591
+
592
+ if args_node.kwonlyargs:
593
+ if not args_node.vararg:
594
+ result.append(ArgInfo("*", None, None, None))
595
+
596
+ kwonlyargs_annotations = args_node.kwonlyargs_annotations
597
+ if not any(args_node.kwonlyargs_annotations):
598
+ num_args = len(args_node.kwonlyargs)
599
+ kwonlyargs_annotations = annotations[
600
+ annotation_offset : annotation_offset + num_args
601
+ ]
602
+
603
+ for arg, annotation, default in _iter_args(
604
+ args_node.kwonlyargs,
605
+ kwonlyargs_annotations,
606
+ args_node.kw_defaults,
607
+ ):
608
+ result.append(ArgInfo(None, arg, annotation, default))
609
+
610
+ if not any(args_node.kwonlyargs_annotations):
611
+ annotation_offset += num_args
612
+
613
+ if args_node.kwarg:
614
+ annotation = None
615
+ if args_node.kwargannotation:
616
+ annotation = format_annotation(args_node.kwargannotation)
617
+ elif len(annotations) > annotation_offset and annotations[annotation_offset]:
618
+ annotation = format_annotation(annotations[annotation_offset])
619
+ annotation_offset += 1
620
+ result.append(ArgInfo("**", args_node.kwarg, annotation, None))
621
+
622
+ if args_node.parent.type in ("method", "classmethod") and result:
623
+ result.pop(0)
624
+
625
+ return result
626
+
627
+
628
+ def get_return_annotation(node: astroid.nodes.FunctionDef) -> str | None:
629
+ """Get the return annotation of a node.
630
+
631
+ Args:
632
+ node: The node to get the return annotation for.
633
+
634
+ Returns:
635
+ The return annotation of the given node.
636
+ """
637
+ return_annotation = None
638
+
639
+ if node.returns:
640
+ return_annotation = format_annotation(node.returns)
641
+ elif node.type_comment_returns:
642
+ return_annotation = format_annotation(node.type_comment_returns)
643
+
644
+ return return_annotation
645
+
646
+
647
+ def get_class_docstring(node: astroid.nodes.ClassDef) -> str:
648
+ """Get the docstring of a node, using a parent docstring if needed.
649
+
650
+ Args:
651
+ node: The node to get a docstring for.
652
+
653
+ Returns:
654
+ The docstring of the class, or the empty string if no docstring
655
+ was found or defined.
656
+ """
657
+ doc = node.doc_node.value if node.doc_node else ""
658
+
659
+ if not doc:
660
+ for base in node.ancestors():
661
+ if base.qname() in (
662
+ "__builtins__.object",
663
+ "builtins.object",
664
+ "builtins.type",
665
+ ):
666
+ continue
667
+ if base.doc_node is not None:
668
+ return base.doc_node.value
669
+
670
+ return doc
671
+
672
+
673
+ def is_abstract_class(node: astroid.nodes.ClassDef) -> bool:
674
+ metaclass = node.metaclass()
675
+ if metaclass and metaclass.name == "ABCMeta":
676
+ return True
677
+
678
+ if "abc.ABC" in node.basenames:
679
+ return True
680
+
681
+ if any(method.is_abstract(pass_is_abstract=False) for method in node.methods()):
682
+ return True
683
+
684
+ return False